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 //
// @@ -2236,45 +2239,25 @@ class TextEditor { //
// ``` // * __overlay__: Positions the view associated with the given item at the head - // or tail of the given `DisplayMarker`. - // * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter - // decorations are created by calling {Gutter::decorateMarker} on the - // desired `Gutter` instance. + // or tail of the given `DisplayMarker`, depending on the `position` property. + // * __gutter__: Tracks a {DisplayMarker} in a {Gutter}. Gutter decorations are created + // by calling {Gutter::decorateMarker} on the desired `Gutter` instance. // * __block__: Positions the view associated with the given item before or - // after the row of the given `TextEditorMarker`. + // after the row of the given {DisplayMarker}, depending on the `position` property. + // Block decorations at the same screen row are ordered by their `order` property. + // * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations + // are created for the same marker, their class strings and style objects are combined + // into a single cursor. This decoration type may be used to style existing cursors + // by passing in their markers or to render artificial cursors that don't actaully + // exist in the model by passing a marker that isn't associated with a real cursor. // // ## Arguments // // * `marker` A {DisplayMarker} you want this decoration to follow. // * `decorationParams` An {Object} representing the decoration e.g. // `{type: 'line-number', class: 'linter-error'}` - // * `type` There are several supported decoration types. The behavior of the - // types are as follows: - // * `line` Adds the given `class` to the lines overlapping the rows - // spanned by the `DisplayMarker`. - // * `line-number` Adds the given `class` to the line numbers overlapping - // the rows spanned by the `DisplayMarker`. - // * `text` Injects spans into all text overlapping the marked range, - // then adds the given `class` or `style` properties to these spans. - // Use this to manipulate the foreground color or styling of text in - // a given range. - // * `highlight` Creates an absolutely-positioned `.highlight` div - // containing nested divs to cover the marked region. For example, this - // is used to implement selections. - // * `overlay` Positions the view associated with the given item at the - // head or tail of the given `DisplayMarker`, depending on the `position` - // property. - // * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling - // {Gutter::decorateMarker} on the desired `Gutter` instance. - // * `block` Positions the view associated with the given item before or - // after the row of the given `TextEditorMarker`, depending on the `position` - // property. - // * `cursor` Renders a cursor at the head of the given marker. If multiple - // decorations are created for the same marker, their class strings and - // style objects are combined into a single cursor. You can use this - // decoration type to style existing cursors by passing in their markers - // or render artificial cursors that don't actually exist in the model - // by passing a marker that isn't actually associated with a cursor. + // * `type` Determines the behavior and appearance of this {Decoration}. Supported decoration types + // and their uses are listed above. // * `class` This CSS class will be applied to the decorated line number, // line, text spans, highlight regions, cursors, or overlay. // * `style` An {Object} containing CSS style properties to apply to the @@ -2300,12 +2283,15 @@ class TextEditor { // Controls where the view is positioned relative to the `TextEditorMarker`. // Values can be `'head'` (the default) or `'tail'` for overlay decorations, and // `'before'` (the default) or `'after'` for block decorations. + // * `order` (optional) Only applicable to decorations of type `block`. Controls + // where the view is positioned relative to other block decorations at the + // same screen row. If unspecified, block decorations render oldest to newest. // * `avoidOverflow` (optional) Only applicable to decorations of type // `overlay`. Determines whether the decoration adjusts its horizontal or // vertical position to remain fully visible when it would otherwise // overflow the editor. Defaults to `true`. // - // Returns a {Decoration} object + // Returns the created {Decoration} object. decorateMarker (marker, decorationParams) { return this.decorationManager.decorateMarker(marker, decorationParams) }