diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index bc33382d7..db82b5077 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -409,6 +409,64 @@ describe('TextEditorComponent', () => { expect(lineNumberNodeForScreenRow(component, 8).classList.contains('a')).toBe(true) expect(lineNumberNodeForScreenRow(component, 8).classList.contains('b')).toBe(true) }) + + it('honors the onlyEmpty and onlyNonEmpty decoration options', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenPosition([1, 0]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a', onlyEmpty: true}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'b', onlyNonEmpty: true}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'c'}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(false) + expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('b')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + + marker.setScreenRange([[1, 0], [2, 4]]) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('c')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 2).classList.contains('c')).toBe(true) + }) + + it('honors the onlyHead option', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 4], [3, 4]]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a', onlyHead: true}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + expect(lineNumberNodeForScreenRow(component, 1).classList.contains('a')).toBe(false) + expect(lineNumberNodeForScreenRow(component, 3).classList.contains('a')).toBe(true) + }) + + it('only decorates the last row of non-empty ranges that end at column 0 if omitEmptyLastRow is false', async () => { + const {component, element, editor} = buildComponent() + const marker = editor.markScreenRange([[1, 0], [3, 0]]) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'a'}) + editor.decorateMarker(marker, {type: ['line', 'line-number'], class: 'b', omitEmptyLastRow: false}) + await component.getNextUpdatePromise() + + expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(true) + expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(false) + + expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(true) + expect(lineNodeForScreenRow(component, 3).classList.contains('b')).toBe(true) + }) }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 1f6db370f..06e3d5abd 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -424,30 +424,48 @@ class TextEditorComponent { const reversed = marker.isReversed() for (let i = 0, length = decorations.length; i < decorations.length; i++) { const decoration = decorations[i] - this.addToDecorationsToRender(decoration.type, decoration, screenRange, reversed) + this.addDecorationToRender(decoration.type, decoration, screenRange, reversed) } }) } - addToDecorationsToRender (type, decoration, screenRange, reversed) { + addDecorationToRender (type, decoration, screenRange, reversed) { if (Array.isArray(type)) { for (let i = 0, length = type.length; i < length; i++) { - this.addToDecorationsToRender(type[i], decoration, screenRange, reversed) + this.addDecorationToRender(type[i], decoration, screenRange, reversed) } } else { switch (type) { - case 'line-number': - for (let row = screenRange.start.row; row <= screenRange.end.row; row++) { - const currentClassName = this.decorationsToRender.lineNumbers.get(row) - const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class - this.decorationsToRender.lineNumbers.set(row, newClassName) - } - break case 'line': - for (let row = screenRange.start.row; row <= screenRange.end.row; row++) { - const currentClassName = this.decorationsToRender.lines.get(row) + case 'line-number': + const decorationsByRow = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers + + let omitLastRow = false + if (screenRange.isEmpty()) { + if (decoration.onlyNonEmpty) return + } else { + if (decoration.onlyEmpty) return + if (decoration.omitEmptyLastRow !== false) { + omitLastRow = screenRange.end.column === 0 + } + } + + let startRow = screenRange.start.row + let endRow = screenRange.end.row + + if (decoration.onlyHead) { + if (reversed) { + endRow = startRow + } else { + startRow = endRow + } + } + + for (let row = startRow; row <= endRow; row++) { + if (omitLastRow && row === endRow) break + const currentClassName = decorationsByRow.get(row) const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class - this.decorationsToRender.lines.set(row, newClassName) + decorationsByRow.set(row, newClassName) } break } diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a94b6b0b1..540e1d2fd 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1752,16 +1752,20 @@ class TextEditor extends Model # line, highlight, or overlay. # * `item` (optional) An {HTMLElement} or a model {Object} with a # corresponding view registered. Only applicable to the `gutter`, - # `overlay` and `block` types. + # `overlay` and `block` decoration types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to # the head of the `DisplayMarker`. Only applicable to the `line` and - # `line-number` types. + # `line-number` decoration types. # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, - # `line`, and `line-number` types. + # `line`, and `line-number` decoration types. # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied # if the associated `DisplayMarker` is non-empty. Only applicable to the - # `gutter`, `line`, and `line-number` types. + # `gutter`, `line`, and `line-number` decoration types. + # * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied + # to the last row of a non-empty range, even if it ends at column 0. + # Defaults to `true`. Only applicable to the `gutter`, `line`, and + # `line-number` decoration types. # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. # Controls where the view is positioned relative to the `TextEditorMarker`. # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and