diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee index 056d8bd83..02073a3a2 100644 --- a/spec/decoration-manager-spec.coffee +++ b/spec/decoration-manager-spec.coffee @@ -65,7 +65,9 @@ describe "DecorationManager", -> {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] expect(oldProperties).toEqual decorationProperties - expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'} + expect(newProperties.type).toBe 'line-number' + expect(newProperties.gutterName).toBe 'line-number' + expect(newProperties.class).toBe 'two' describe "::getDecorations(properties)", -> it "returns decorations matching the given optional properties", -> diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 2db53db53..dbad4a3e7 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2638,7 +2638,71 @@ describe('TextEditorComponent', () => { expect(editor.getCursorScreenPosition()).toEqual([0, 0]) }) - function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position, invalidate}) { + it('uses the order property to control the order of block decorations at the same screen row', async () => { + const editor = buildEditor({autoHeight: false}) + const {component, element} = buildComponent({editor}) + element.style.height = 10 * component.getLineHeight() + horizontalScrollbarHeight + 'px' + await component.getNextUpdatePromise() + + // Order parameters that differ from creation order; that collide; and that are not provided. + const [beforeItems, beforeDecorations] = [30, 20, undefined, 20, 10, undefined].map(order => { + return createBlockDecorationAtScreenRow(editor, 2, {height: 10, position: 'before', order}) + }).reduce((lists, result) => { + lists[0].push(result.item) + lists[1].push(result.decoration) + return lists + }, [[], []]) + + const [afterItems, afterDecorations] = [undefined, 1, 6, undefined, 6, 2].map(order => { + return createBlockDecorationAtScreenRow(editor, 2, {height: 10, position: 'after', order}) + }).reduce((lists, result) => { + lists[0].push(result.item) + lists[1].push(result.decoration) + return lists + }, [[], []]) + + await component.getNextUpdatePromise() + + expect(beforeItems[4].previousSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(beforeItems[4].nextSibling).toBe(beforeItems[1]) + expect(beforeItems[1].nextSibling).toBe(beforeItems[3]) + expect(beforeItems[3].nextSibling).toBe(beforeItems[0]) + expect(beforeItems[0].nextSibling).toBe(beforeItems[2]) + expect(beforeItems[2].nextSibling).toBe(beforeItems[5]) + expect(beforeItems[5].nextSibling).toBe(lineNodeForScreenRow(component, 2)) + expect(afterItems[1].previousSibling).toBe(lineNodeForScreenRow(component, 2)) + expect(afterItems[1].nextSibling).toBe(afterItems[5]) + expect(afterItems[5].nextSibling).toBe(afterItems[2]) + expect(afterItems[2].nextSibling).toBe(afterItems[4]) + expect(afterItems[4].nextSibling).toBe(afterItems[0]) + expect(afterItems[0].nextSibling).toBe(afterItems[3]) + + // Create a decoration somewhere else and move it to the same screen row as the existing decorations + const {item: later, decoration} = createBlockDecorationAtScreenRow(editor, 4, {height: 20, position: 'after', order: 3}) + await component.getNextUpdatePromise() + expect(later.previousSibling).toBe(lineNodeForScreenRow(component, 4)) + expect(later.nextSibling).toBe(lineNodeForScreenRow(component, 5)) + + decoration.getMarker().setHeadScreenPosition([2, 0]) + await component.getNextUpdatePromise() + expect(later.previousSibling).toBe(afterItems[5]) + expect(later.nextSibling).toBe(afterItems[2]) + + // Move a decoration away from its screen row and ensure the rest maintain their order + beforeDecorations[3].getMarker().setHeadScreenPosition([5, 0]) + await component.getNextUpdatePromise() + expect(beforeItems[3].previousSibling).toBe(lineNodeForScreenRow(component, 4)) + expect(beforeItems[3].nextSibling).toBe(lineNodeForScreenRow(component, 5)) + + expect(beforeItems[4].previousSibling).toBe(lineNodeForScreenRow(component, 1)) + expect(beforeItems[4].nextSibling).toBe(beforeItems[1]) + expect(beforeItems[1].nextSibling).toBe(beforeItems[0]) + expect(beforeItems[0].nextSibling).toBe(beforeItems[2]) + expect(beforeItems[2].nextSibling).toBe(beforeItems[5]) + expect(beforeItems[5].nextSibling).toBe(lineNodeForScreenRow(component, 2)) + }); + + function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position, order, invalidate}) { const marker = editor.markScreenPosition([screenRow, 0], {invalidate: invalidate || 'never'}) const item = document.createElement('div') item.style.height = height + 'px' @@ -2646,7 +2710,7 @@ describe('TextEditorComponent', () => { if (marginTop != null) item.style.marginTop = marginTop + 'px' if (marginBottom != null) item.style.marginBottom = marginBottom + 'px' item.style.width = 30 + 'px' - const decoration = editor.decorateMarker(marker, {type: 'block', item, position}) + const decoration = editor.decorateMarker(marker, {type: 'block', item, position, order}) return {item, decoration, marker} } diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 587e1b523..908ea8082 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -6861,7 +6861,7 @@ describe('TextEditor', () => { const marker = editor.markBufferRange([[2, 4], [6, 8]]) const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual({ - properties: {type: 'highlight', class: 'foo'}, + properties: {id: decoration.id, order: Infinity, type: 'highlight', class: 'foo'}, screenRange: marker.getScreenRange(), bufferRange: marker.getBufferRange(), rangeIsReversed: false diff --git a/src/decoration.js b/src/decoration.js index 69bbcaa19..989e48588 100644 --- a/src/decoration.js +++ b/src/decoration.js @@ -3,12 +3,17 @@ const {Emitter} = require('event-kit') let idCounter = 0 const nextId = () => idCounter++ -// Applies changes to a decorationsParam {Object} to make it possible to -// differentiate decorations on custom gutters versus the line-number gutter. -const translateDecorationParamsOldToNew = function (decorationParams) { - if (decorationParams.type === 'line-number') { +const normalizeDecorationProperties = function (decoration, decorationParams) { + decorationParams.id = decoration.id + + if (decorationParams.type === 'line-number' && decorationParams.gutterName == null) { decorationParams.gutterName = 'line-number' } + + if (decorationParams.order == null) { + decorationParams.order = Infinity + } + return decorationParams } @@ -164,7 +169,7 @@ class Decoration { setProperties (newProperties) { if (this.destroyed) { return } const oldProperties = this.properties - this.properties = translateDecorationParamsOldToNew(newProperties) + this.properties = normalizeDecorationProperties(this, newProperties) if (newProperties.type != null) { this.decorationManager.decorationDidChangeType(this) } diff --git a/src/text-editor-component.js b/src/text-editor-component.js index e41a1c739..9da3db137 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1191,6 +1191,10 @@ class TextEditorComponent { decorationsByScreenLine.set(screenLine.id, decorations) } decorations.push(decoration) + + // Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors + // their creation sequence. + decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id) } addTextDecorationToRender (decoration, screenRange, marker) { @@ -3862,15 +3866,24 @@ class LinesTileComponent { if (blockDecorations) { blockDecorations.forEach((newDecorations, screenLineId) => { - var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null - for (var i = 0; i < newDecorations.length; i++) { - var newDecoration = newDecorations[i] - if (oldDecorations && oldDecorations.includes(newDecoration)) continue + const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null + const lineNode = lineComponentsByScreenLineId.get(screenLineId).element + let lastAfter = lineNode + + for (let i = 0; i < newDecorations.length; i++) { + const newDecoration = newDecorations[i] + const element = TextEditor.viewForItem(newDecoration.item) + + if (oldDecorations && oldDecorations.includes(newDecoration)) { + if (newDecoration.position === 'after') { + lastAfter = element + } + continue + } - var element = TextEditor.viewForItem(newDecoration.item) - var lineNode = lineComponentsByScreenLineId.get(screenLineId).element if (newDecoration.position === 'after') { - this.element.insertBefore(element, lineNode.nextSibling) + this.element.insertBefore(element, lastAfter.nextSibling) + lastAfter = element } else { this.element.insertBefore(element, lineNode) } diff --git a/src/text-editor.js b/src/text-editor.js index eed26d91c..fe14e562c 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -2221,14 +2221,17 @@ class TextEditor { // // The following are the supported decorations types: // - // * __line__: Adds your CSS `class` to the line nodes within the range - // marked by the marker - // * __line-number__: Adds your CSS `class` to the line number nodes within the - // range marked by the marker - // * __highlight__: Adds a new highlight div to the editor surrounding the - // range marked by the marker. When the user selects text, the selection is - // visualized with a highlight decoration internally. The structure of this - // highlight will be + // * __line__: Adds the given CSS `class` to the lines overlapping the rows + // spanned by the marker. + // * __line-number__: Adds the given CSS `class` to the line numbers overlapping + // the rows spanned by the marker + // * __text__: Injects spans into all text overlapping the marked range, then adds + // the given `class` or `style` to these spans. Use this to manipulate the foreground + // color or styling of text in a range. + // * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor + // containing nested divs that cover the marked region. For example, when the user + // selects text, the selection is implemented with a highlight decoration. The structure + // of this highlight will be: // ```html //