From b23009a8f3c8a4aa8ba3a961f1c715e58b6d1a92 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 3 Jun 2014 17:26:57 -0700 Subject: [PATCH 01/45] fold classes work --- src/gutter-component.coffee | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 72581cd0a..9b1cbd4e9 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -91,7 +91,7 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) if @hasLineNumberNode(id) - @updateLineNumberNode(id, screenRow) + @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" @@ -131,7 +131,10 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - "
#{innerHTML}
" + classes = "line-number" + classes += ' foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) + classes += ' folded' if @props.editor.isFoldedAtBufferRow(bufferRow) + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped @@ -143,11 +146,16 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, screenRow) -> + updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) -> + node = @lineNumberNodesById[lineNumberId] + + @toggleClass node, 'foldable', not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) + @toggleClass node, 'folded', @props.editor.isFoldedAtBufferRow(bufferRow) + unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props - @lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeightInPixels + 'px' - @lineNumberNodesById[lineNumberId].dataset.screenRow = screenRow + node.style.top = screenRow * lineHeightInPixels + 'px' + node.dataset.screenRow = screenRow @screenRowsByLineNumberId[lineNumberId] = screenRow @lineNumberIdsByScreenRow[screenRow] = lineNumberId @@ -156,3 +164,7 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] + + toggleClass: (node, klass, condition) -> + if condition then node.classList.add(klass) else node.classList.remove(klass) + From 77d269c6d90509f9148fce49c0b71c6500450980 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 3 Jun 2014 18:05:30 -0700 Subject: [PATCH 02/45] Beginning of decorations --- spec/editor-spec.coffee | 12 +++++++++++- src/display-buffer.coffee | 25 +++++++++++++++++++++++++ src/editor.coffee | 9 +++++++++ src/gutter-component.coffee | 9 +++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 704d062fb..4ba1e82de 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1622,7 +1622,7 @@ describe "Editor", -> editor.setCursorBufferPosition([9,2]) editor.insertNewline() expect(editor.lineForBufferRow(10)).toBe ' };' - + describe ".backspace()", -> describe "when there is a single cursor", -> changeScreenRangeHandler = null @@ -3205,3 +3205,13 @@ describe "Editor", -> editor.pageUp() expect(editor.getScrollTop()).toBe 0 + + fdescribe "decorations", -> + it "can add decorations", -> + decoration = {type: 'gutter-class', class: 'one'} + editor.addDecorationForBufferRow(2, decoration) + editor.addDecorationForBufferRow(2, decoration) + + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toHaveLength 1 + expect(decorations).toContain decoration diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 579863ec2..2f924acf0 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -43,6 +43,7 @@ class DisplayBuffer extends Model @charWidthsByScope = {} @markers = {} @foldsByMarkerId = {} + @decorations = {} @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) @subscribe @tokenizedBuffer, 'grammar-changed', (grammar) => @emit 'grammar-changed', grammar @@ -718,6 +719,30 @@ class DisplayBuffer extends Model rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) + decorationsForBufferRow: (bufferRow) -> + @decorations[bufferRow] ? [] + + addDecorationForBufferRow: (bufferRow, decoration) -> + @decorations[bufferRow] ?= [] + for current in @decorations[bufferRow] + return if _.isEqual(current, decoration) + @decorations[bufferRow].push(decoration) + @emit 'decoration-changed', {bufferRow, add: decoration} + + removeDecorationForBufferRow: (bufferRow, decoration) -> + return unless @decorations[bufferRow] + + removed = @findDecorationsForBufferRow(bufferRow, decoration) + @decorations[bufferRow] = _.without(@decorations, removed) + @emit 'decoration-changed', {bufferRow, remove: removed} + + findDecorationsForBufferRow: (bufferRow, options) -> + return unless @decorations[bufferRow] + (dec for dec in @decorations[bufferRow] when _.isEqual(options, _.pick(decoration, _.keys(options)))) + + addGutterClassForMarker: (bufferRow) -> + removeGutterClassForMarker: (bufferRow) -> + # Retrieves a {DisplayBufferMarker} based on its id. # # id - A {Number} representing a marker id diff --git a/src/editor.coffee b/src/editor.coffee index b676130e4..8b062de8d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1057,6 +1057,15 @@ class Editor extends Model selection.insertText(fn(text)) selection.setBufferRange(range) + decorationsForBufferRow: (bufferRow) -> + @displayBuffer.decorationsForBufferRow(bufferRow) + + addDecorationForBufferRow: (bufferRow, decoration) -> + @displayBuffer.addDecorationForBufferRow(bufferRow, decoration) + + removeDecorationForBufferRow: (bufferRow, decoration) -> + @displayBuffer.removeDecorationForBufferRow(bufferRow, decoration) + # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> @displayBuffer.getMarker(id) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 9b1cbd4e9..836ebd467 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -27,6 +27,13 @@ GutterComponent = React.createClass componentDidMount: -> @appendDummyLineNumber() + @subscribeToEditor() + + componentWillUnmount: -> + @unsubscribe() + + subscribeToEditor: -> + @subscribe @props.editor, 'decoration-changed', @onDecorationChanged # Only update the gutter if the visible row range has changed or if a # non-zero-delta change to the screen lines has occurred within the current @@ -168,3 +175,5 @@ GutterComponent = React.createClass toggleClass: (node, klass, condition) -> if condition then node.classList.add(klass) else node.classList.remove(klass) + onDecorationChanged: (change) -> + @forceUpdate() From 142eedd7055f068b124ac659f92f6d36bb9d8fc2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 3 Jun 2014 18:44:35 -0700 Subject: [PATCH 03/45] Renders decoration changes. --- spec/editor-spec.coffee | 6 +++++- src/display-buffer.coffee | 10 ++++++---- src/editor.coffee | 1 + src/gutter-component.coffee | 12 +++++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 4ba1e82de..a2477e078 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -3207,7 +3207,7 @@ describe "Editor", -> expect(editor.getScrollTop()).toBe 0 fdescribe "decorations", -> - it "can add decorations", -> + it "can add and remove decorations", -> decoration = {type: 'gutter-class', class: 'one'} editor.addDecorationForBufferRow(2, decoration) editor.addDecorationForBufferRow(2, decoration) @@ -3215,3 +3215,7 @@ describe "Editor", -> decorations = editor.decorationsForBufferRow(2) expect(decorations).toHaveLength 1 expect(decorations).toContain decoration + + editor.removeDecorationForBufferRow(2, decoration) + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toHaveLength 0 diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 2f924acf0..4ee37c5df 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -727,18 +727,20 @@ class DisplayBuffer extends Model for current in @decorations[bufferRow] return if _.isEqual(current, decoration) @decorations[bufferRow].push(decoration) - @emit 'decoration-changed', {bufferRow, add: decoration} + @emit 'decoration-changed', {bufferRow, decoration, action: 'add'} removeDecorationForBufferRow: (bufferRow, decoration) -> return unless @decorations[bufferRow] removed = @findDecorationsForBufferRow(bufferRow, decoration) - @decorations[bufferRow] = _.without(@decorations, removed) - @emit 'decoration-changed', {bufferRow, remove: removed} + @decorations[bufferRow] = _.without(@decorations[bufferRow], removed...) + + for decoration in removed + @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} findDecorationsForBufferRow: (bufferRow, options) -> return unless @decorations[bufferRow] - (dec for dec in @decorations[bufferRow] when _.isEqual(options, _.pick(decoration, _.keys(options)))) + (dec for dec in @decorations[bufferRow] when _.isEqual(options, _.pick(dec, _.keys(options)))) addGutterClassForMarker: (bufferRow) -> removeGutterClassForMarker: (bufferRow) -> diff --git a/src/editor.coffee b/src/editor.coffee index 8b062de8d..94701e15b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,6 +214,7 @@ class Editor extends Model @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() @subscribe @displayBuffer, 'tokenized', => @handleTokenization() @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... + @subscribe @displayBuffer, "decoration-changed", (e) => @emit 'decoration-changed', e getViewClass: -> if atom.config.get('core.useReactEditor') diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 836ebd467..54850906d 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -24,6 +24,7 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberIdsByScreenRow = {} @screenRowsByLineNumberId = {} + @decoratorUpdates = {} componentDidMount: -> @appendDummyLineNumber() @@ -141,6 +142,7 @@ GutterComponent = React.createClass classes = "line-number" classes += ' foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) classes += ' folded' if @props.editor.isFoldedAtBufferRow(bufferRow) + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> @@ -159,6 +161,11 @@ GutterComponent = React.createClass @toggleClass node, 'foldable', not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) @toggleClass node, 'folded', @props.editor.isFoldedAtBufferRow(bufferRow) + if @decoratorUpdates[bufferRow]? + for change in @decoratorUpdates[bufferRow] + node.classList[change.action](change.decoration.class) + delete @decoratorUpdates[bufferRow] + unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props node.style.top = screenRow * lineHeightInPixels + 'px' @@ -176,4 +183,7 @@ GutterComponent = React.createClass if condition then node.classList.add(klass) else node.classList.remove(klass) onDecorationChanged: (change) -> - @forceUpdate() + if change.decoration.type == 'gutter-class' + @decoratorUpdates[change.bufferRow] ?= [] + @decoratorUpdates[change.bufferRow].push change + @forceUpdate() From eb59196c0236d7438c8446b5cbee0766016483d6 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 09:53:22 -0700 Subject: [PATCH 04/45] Rendering decorations works well. Also specs. --- spec/editor-component-spec.coffee | 61 +++++++++++++++++++++++++++++++ src/display-buffer.coffee | 6 ++- src/editor.coffee | 4 +- src/gutter-component.coffee | 22 +++++++---- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index b9c469829..8e151f193 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -302,6 +302,67 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" expect(gutterNode.offsetWidth).toBe initialGutterWidth + fdescribe "when decorations are used", -> + it "renders the gutter-class decorations", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureScrollView() + + expect(component.lineNumberNodeForScreenRow(9)).toBeFalsy() + + editor.addDecorationForBufferRow(9, {type: 'gutter-class', class: 'fancy-class'}) + editor.addDecorationForBufferRow(9, {type: 'someother-type', class: 'nope-class'}) + + verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + expect(component.lineNumberNodeForScreenRow(9).classList.contains('fancy-class')).toBeTruthy() + expect(component.lineNumberNodeForScreenRow(9).classList.contains('nope-class')).toBeFalsy() + + it "handles updates to gutter-class decorations", -> + editor.addDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) + editor.addDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + + expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeTruthy() + expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() + + editor.removeDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) + editor.removeDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + + expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeFalsy() + expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() + + it "handles softWrap decorations", -> + editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) + editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) + + editor.setSoftWrap(true) + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 30 * charWidth + 'px' + component.measureScrollView() + + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeTruthy() + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeTruthy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeTruthy() + + # should remove the wrapped decorations + editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) + editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me'}) + + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeFalsy() + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeFalsy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeFalsy() + + # should add them back when the nodes are not recreated + editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) + editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) + + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeTruthy() + expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeTruthy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() + expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeTruthy() + describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> cursor1 = editor.getCursor() diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 4ee37c5df..de5b08860 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -719,8 +719,10 @@ class DisplayBuffer extends Model rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) - decorationsForBufferRow: (bufferRow) -> - @decorations[bufferRow] ? [] + decorationsForBufferRow: (bufferRow, decorationType) -> + decorations = @decorations[bufferRow] ? [] + decorations = (dec for dec in decorations when dec.type == decorationType) if decorationType? + decorations addDecorationForBufferRow: (bufferRow, decoration) -> @decorations[bufferRow] ?= [] diff --git a/src/editor.coffee b/src/editor.coffee index 94701e15b..177d34d0a 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1058,8 +1058,8 @@ class Editor extends Model selection.insertText(fn(text)) selection.setBufferRange(range) - decorationsForBufferRow: (bufferRow) -> - @displayBuffer.decorationsForBufferRow(bufferRow) + decorationsForBufferRow: (bufferRow, decorationType) -> + @displayBuffer.decorationsForBufferRow(bufferRow, decorationType) addDecorationForBufferRow: (bufferRow, decoration) -> @displayBuffer.addDecorationForBufferRow(bufferRow, decoration) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 54850906d..11e5b6e8e 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -9,6 +9,7 @@ module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' mixins: [SubscriberMixin] + decorationType: 'gutter-class' dummyLineNumberNode: null @@ -118,6 +119,7 @@ GutterComponent = React.createClass @lineNumberNodesById[lineNumberId] = lineNumberNode node.appendChild(lineNumberNode) + @decoratorUpdates = {} visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> @@ -139,11 +141,15 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - classes = "line-number" - classes += ' foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) - classes += ' folded' if @props.editor.isFoldedAtBufferRow(bufferRow) + classes = ['line-number'] + classes.push 'foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) + classes.push 'folded' if @props.editor.isFoldedAtBufferRow(bufferRow) - "
#{innerHTML}
" + decorations = @props.editor.decorationsForBufferRow(bufferRow, @decorationType) + for decoration in decorations + classes.push(decoration.class) if not softWrapped or softWrapped and decoration.softWrap + + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped @@ -163,8 +169,10 @@ GutterComponent = React.createClass if @decoratorUpdates[bufferRow]? for change in @decoratorUpdates[bufferRow] - node.classList[change.action](change.decoration.class) - delete @decoratorUpdates[bufferRow] + if change.action == 'add' and (not softWrapped or softWrapped and change.decoration.softWrap) + node.classList.add(change.decoration.class) + else if change.action == 'remove' + node.classList.remove(change.decoration.class) unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props @@ -183,7 +191,7 @@ GutterComponent = React.createClass if condition then node.classList.add(klass) else node.classList.remove(klass) onDecorationChanged: (change) -> - if change.decoration.type == 'gutter-class' + if change.decoration.type == @decorationType @decoratorUpdates[change.bufferRow] ?= [] @decoratorUpdates[change.bufferRow].push change @forceUpdate() From d51894103d6e0e41101dacdf6d6887535052d1ce Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 12:23:37 -0700 Subject: [PATCH 05/45] Debounce the rendering of decorations --- spec/editor-component-spec.coffee | 9 +++++++++ src/gutter-component.coffee | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 8e151f193..4dbfe9848 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -319,19 +319,26 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).classList.contains('nope-class')).toBeFalsy() it "handles updates to gutter-class decorations", -> + gutter = component.refs.gutter + editor.addDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) editor.addDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + advanceClock(gutter.decorationRenderDelay) + expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeTruthy() expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() editor.removeDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) editor.removeDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + advanceClock(gutter.decorationRenderDelay) + expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeFalsy() expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() it "handles softWrap decorations", -> + gutter = component.refs.gutter editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) @@ -348,6 +355,7 @@ describe "EditorComponent", -> # should remove the wrapped decorations editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me'}) + advanceClock(gutter.decorationRenderDelay) expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeFalsy() expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeFalsy() @@ -357,6 +365,7 @@ describe "EditorComponent", -> # should add them back when the nodes are not recreated editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) + advanceClock(gutter.decorationRenderDelay) expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeTruthy() expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeTruthy() diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 11e5b6e8e..00dfa7b1b 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -10,6 +10,7 @@ GutterComponent = React.createClass displayName: 'GutterComponent' mixins: [SubscriberMixin] decorationType: 'gutter-class' + decorationRenderDelay: 100 dummyLineNumberNode: null @@ -194,4 +195,11 @@ GutterComponent = React.createClass if change.decoration.type == @decorationType @decoratorUpdates[change.bufferRow] ?= [] @decoratorUpdates[change.bufferRow].push change + @renderDecorations() + + renderDecorations: -> + clearTimeout(@decorationRenderTimeout) if @decorationRenderTimeout + render = => @forceUpdate() + @decorationRenderTimeout = null + @decorationRenderTimeout = setTimeout(render, @decorationRenderDelay) From 732e23b8ea5c59acf926a17f731c78ed2cc9e21e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 15:23:40 -0700 Subject: [PATCH 06/45] Add initial addDecorationForMarker() --- src/display-buffer.coffee | 39 +++++++++++++++++++++++++++++++++++++-- src/editor.coffee | 3 +++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index de5b08860..1cfbf05ca 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -744,8 +744,43 @@ class DisplayBuffer extends Model return unless @decorations[bufferRow] (dec for dec in @decorations[bufferRow] when _.isEqual(options, _.pick(dec, _.keys(options)))) - addGutterClassForMarker: (bufferRow) -> - removeGutterClassForMarker: (bufferRow) -> + addDecorationForMarker: (marker, decoration) -> + head = marker.getHeadBufferPosition().row + tail = marker.getTailBufferPosition().row + [tail, head] = [head, tail] if head > tail + while head <= tail + @addDecorationForBufferRow(head++, decoration) + + @subscribe marker, 'changed', (e) => + oldHead = e.oldHeadBufferPosition.row + oldTail = e.oldTailBufferPosition.row + newHead = e.newHeadBufferPosition.row + newTail = e.newTailBufferPosition.row + + # swap so head is always <= than tail + [oldTail, oldHead] = [oldHead, oldTail] if oldHead > oldTail + [newTail, newHead] = [newHead, newTail] if newHead > newTail + + # TODO: we could only update the rows that change by tracking an overlap, + # then update only those outside of the overlap. I had something to do + # this, but it's complicated by marker validity. When invlaid, I removed + # all decorations, then when markers becoming valid, some of the + # overlap was not visible. + + while oldHead <= oldTail + @removeDecorationForBufferRow(oldHead, decoration) + oldHead++ + + while e.isValid and newHead <= newTail + @addDecorationForBufferRow(newHead, decoration) + newHead++ + + @subscribe marker, 'destroyed', (e) => + console.log 'destroyed', e + @removeDecorationForMarker(marker, decoration) + + removeDecorationForMarker: (marker, decoration) -> + # TODO: unsubscribe from the change event for the marker + decoration combo # Retrieves a {DisplayBufferMarker} based on its id. # diff --git a/src/editor.coffee b/src/editor.coffee index 177d34d0a..58e5c82a1 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1067,6 +1067,9 @@ class Editor extends Model removeDecorationForBufferRow: (bufferRow, decoration) -> @displayBuffer.removeDecorationForBufferRow(bufferRow, decoration) + addDecorationForMarker: (marker, decoration) -> + @displayBuffer.addDecorationForMarker(marker, decoration) + # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> @displayBuffer.getMarker(id) From 79578e08abab6adf1c5fa26c50bc6628ce1e332b Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 16:36:41 -0700 Subject: [PATCH 07/45] Specs for marker decorations --- spec/editor-component-spec.coffee | 120 ++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 21 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 4dbfe9848..a2c174f8a 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -303,6 +303,12 @@ describe "EditorComponent", -> expect(gutterNode.offsetWidth).toBe initialGutterWidth fdescribe "when decorations are used", -> + {lineHasClass, gutter} = {} + beforeEach -> + gutter = component.refs.gutter + lineHasClass = (screenRow, klass) -> + component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + it "renders the gutter-class decorations", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() @@ -315,30 +321,27 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(component.lineNumberNodeForScreenRow(9).classList.contains('fancy-class')).toBeTruthy() - expect(component.lineNumberNodeForScreenRow(9).classList.contains('nope-class')).toBeFalsy() + expect(lineHasClass(9, 'fancy-class')).toBeTruthy() + expect(lineHasClass(9, 'nope-class')).toBeFalsy() it "handles updates to gutter-class decorations", -> - gutter = component.refs.gutter - editor.addDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) editor.addDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) advanceClock(gutter.decorationRenderDelay) - expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeTruthy() - expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() + expect(lineHasClass(2, 'fancy-class')).toBeTruthy() + expect(lineHasClass(2, 'nope-class')).toBeFalsy() editor.removeDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) editor.removeDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) advanceClock(gutter.decorationRenderDelay) - expect(component.lineNumberNodeForScreenRow(2).classList.contains('fancy-class')).toBeFalsy() - expect(component.lineNumberNodeForScreenRow(2).classList.contains('nope-class')).toBeFalsy() + expect(lineHasClass(2, 'fancy-class')).toBeFalsy() + expect(lineHasClass(2, 'nope-class')).toBeFalsy() it "handles softWrap decorations", -> - gutter = component.refs.gutter editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) @@ -347,30 +350,105 @@ describe "EditorComponent", -> node.style.width = 30 * charWidth + 'px' component.measureScrollView() - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeTruthy() - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeTruthy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeTruthy() + expect(lineHasClass(2, 'no-wrap')).toBeTruthy() + expect(lineHasClass(2, 'wrap-me')).toBeTruthy() + expect(lineHasClass(3, 'no-wrap')).toBeFalsy() + expect(lineHasClass(3, 'wrap-me')).toBeTruthy() # should remove the wrapped decorations editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me'}) advanceClock(gutter.decorationRenderDelay) - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeFalsy() - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeFalsy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeFalsy() + expect(lineHasClass(2, 'no-wrap')).toBeFalsy() + expect(lineHasClass(2, 'wrap-me')).toBeFalsy() + expect(lineHasClass(3, 'no-wrap')).toBeFalsy() + expect(lineHasClass(3, 'wrap-me')).toBeFalsy() # should add them back when the nodes are not recreated editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) advanceClock(gutter.decorationRenderDelay) - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'no-wrap').toBeTruthy() - expect(component.lineNumberNodeForScreenRow(2).classList.contains 'wrap-me').toBeTruthy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'no-wrap').toBeFalsy() - expect(component.lineNumberNodeForScreenRow(3).classList.contains 'wrap-me').toBeTruthy() + expect(lineHasClass(2, 'no-wrap')).toBeTruthy() + expect(lineHasClass(2, 'wrap-me')).toBeTruthy() + expect(lineHasClass(3, 'no-wrap')).toBeFalsy() + expect(lineHasClass(3, 'wrap-me')).toBeTruthy() + + describe "when markers are used", -> + {marker, decoration} = {} + beforeEach -> + marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') + decoration = {type: 'gutter-class', class: 'someclass'} + editor.addDecorationForMarker(marker, decoration) + advanceClock(gutter.decorationRenderDelay) + + it "tracks the marker's movements", -> + expect(lineHasClass(1, 'someclass')).toBeFalsy() + expect(lineHasClass(2, 'someclass')).toBeTruthy() + expect(lineHasClass(3, 'someclass')).toBeTruthy() + expect(lineHasClass(4, 'someclass')).toBeFalsy() + + editor.getBuffer().insert([0, 0], '\n') + advanceClock(gutter.decorationRenderDelay) + + expect(lineHasClass(2, 'someclass')).toBeFalsy() + expect(lineHasClass(3, 'someclass')).toBeTruthy() + expect(lineHasClass(4, 'someclass')).toBeTruthy() + expect(lineHasClass(5, 'someclass')).toBeFalsy() + + editor.getBuffer().deleteRows(0, 1) + advanceClock(gutter.decorationRenderDelay) + + expect(lineHasClass(0, 'someclass')).toBeFalsy() + expect(lineHasClass(1, 'someclass')).toBeTruthy() + expect(lineHasClass(2, 'someclass')).toBeTruthy() + expect(lineHasClass(3, 'someclass')).toBeFalsy() + + it "removes the classes when marker is invalidated", -> + t = editor.getBuffer().getText() + # invalidate this thing + editor.getBuffer().insert([3, 2], 'n') + advanceClock(gutter.decorationRenderDelay) + + expect(marker.isValid()).toBeFalsy() + expect(lineHasClass(1, 'someclass')).toBeFalsy() + expect(lineHasClass(2, 'someclass')).toBeFalsy() + expect(lineHasClass(3, 'someclass')).toBeFalsy() + expect(lineHasClass(4, 'someclass')).toBeFalsy() + + # Back to valid + editor.getBuffer().undo() + advanceClock(gutter.decorationRenderDelay) + + expect(marker.isValid()).toBeTruthy() + expect(lineHasClass(1, 'someclass')).toBeFalsy() + expect(lineHasClass(2, 'someclass')).toBeTruthy() + expect(lineHasClass(3, 'someclass')).toBeTruthy() + expect(lineHasClass(4, 'someclass')).toBeFalsy() + + it "removes the classes and unsubscribes from the marker when decoration is removed", -> + editor.removeDecorationForMarker(marker, decoration) + advanceClock(gutter.decorationRenderDelay) + + expect(lineHasClass(1, 'someclass')).toBeFalsy() + expect(lineHasClass(2, 'someclass')).toBeFalsy() + expect(lineHasClass(3, 'someclass')).toBeFalsy() + expect(lineHasClass(4, 'someclass')).toBeFalsy() + + editor.getBuffer().insert([0, 0], '\n') + advanceClock(gutter.decorationRenderDelay) + expect(lineHasClass(2, 'someclass')).toBeFalsy() + expect(lineHasClass(3, 'someclass')).toBeFalsy() + + it "removes the decoration when the marker is destroyed", -> + marker.destroy() + advanceClock(gutter.decorationRenderDelay) + + expect(lineHasClass(1, 'someclass')).toBeFalsy() + expect(lineHasClass(2, 'someclass')).toBeFalsy() + expect(lineHasClass(3, 'someclass')).toBeFalsy() + expect(lineHasClass(4, 'someclass')).toBeFalsy() describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> From 9ee54801a21cd0e20c97655726140f7cee5d05f7 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 16:37:08 -0700 Subject: [PATCH 08/45] Implement removeDecorationsForMarker --- src/display-buffer.coffee | 32 ++++++++++++++++++++++++++------ src/editor.coffee | 3 +++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 1cfbf05ca..552b5e6be 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -44,6 +44,7 @@ class DisplayBuffer extends Model @markers = {} @foldsByMarkerId = {} @decorations = {} + @decorationMarkerSubscriptions = {} @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) @subscribe @tokenizedBuffer, 'grammar-changed', (grammar) => @emit 'grammar-changed', grammar @@ -740,9 +741,12 @@ class DisplayBuffer extends Model for decoration in removed @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} - findDecorationsForBufferRow: (bufferRow, options) -> + findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] - (dec for dec in @decorations[bufferRow] when _.isEqual(options, _.pick(dec, _.keys(options)))) + (dec for dec in @decorations[bufferRow] when @decorationEqualsPattern(dec, decorationPattern)) + + decorationEqualsPattern: (decoration, decorationPattern) -> + _.isEqual(decorationPattern, _.pick(decoration, _.keys(decorationPattern))) addDecorationForMarker: (marker, decoration) -> head = marker.getHeadBufferPosition().row @@ -751,7 +755,7 @@ class DisplayBuffer extends Model while head <= tail @addDecorationForBufferRow(head++, decoration) - @subscribe marker, 'changed', (e) => + changedSubscription = @subscribe marker, 'changed', (e) => oldHead = e.oldHeadBufferPosition.row oldTail = e.oldTailBufferPosition.row newHead = e.newHeadBufferPosition.row @@ -775,12 +779,28 @@ class DisplayBuffer extends Model @addDecorationForBufferRow(newHead, decoration) newHead++ - @subscribe marker, 'destroyed', (e) => - console.log 'destroyed', e + destroyedSubscription = @subscribe marker, 'destroyed', (e) => @removeDecorationForMarker(marker, decoration) + @decorationMarkerSubscriptions[marker.id] ?= [] + @decorationMarkerSubscriptions[marker.id].push {decoration, changedSubscription, destroyedSubscription} + removeDecorationForMarker: (marker, decoration) -> - # TODO: unsubscribe from the change event for the marker + decoration combo + return unless @decorationMarkerSubscriptions[marker.id] + + head = marker.getHeadBufferPosition().row + tail = marker.getTailBufferPosition().row + [tail, head] = [head, tail] if head > tail + while head <= tail + @removeDecorationForBufferRow(head++, decoration) + + for sub in _.clone(@decorationMarkerSubscriptions[marker.id]) + if @decorationEqualsPattern(sub.decoration, decoration) + sub.changedSubscription.off() + sub.destroyedSubscription.off() + @decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], sub) + + return # Retrieves a {DisplayBufferMarker} based on its id. # diff --git a/src/editor.coffee b/src/editor.coffee index 58e5c82a1..258a94498 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1070,6 +1070,9 @@ class Editor extends Model addDecorationForMarker: (marker, decoration) -> @displayBuffer.addDecorationForMarker(marker, decoration) + removeDecorationForMarker: (marker, decoration) -> + @displayBuffer.removeDecorationForMarker(marker, decoration) + # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> @displayBuffer.getMarker(id) From fa4a6e2d71de9247d90efe9daf7142c7ac1ac264 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 17:06:05 -0700 Subject: [PATCH 09/45] Nof --- spec/editor-component-spec.coffee | 2 +- spec/editor-spec.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index a2c174f8a..0ee26f717 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -302,7 +302,7 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" expect(gutterNode.offsetWidth).toBe initialGutterWidth - fdescribe "when decorations are used", -> + describe "when decorations are used", -> {lineHasClass, gutter} = {} beforeEach -> gutter = component.refs.gutter diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index a2477e078..1a02f027b 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -3206,7 +3206,7 @@ describe "Editor", -> editor.pageUp() expect(editor.getScrollTop()).toBe 0 - fdescribe "decorations", -> + describe "decorations", -> it "can add and remove decorations", -> decoration = {type: 'gutter-class', class: 'one'} editor.addDecorationForBufferRow(2, decoration) From 6ce859774abf38936ed9fc454dde0e0ca54fbef2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 4 Jun 2014 17:54:13 -0700 Subject: [PATCH 10/45] Name changes --- spec/editor-component-spec.coffee | 30 +++++++++++++++--------------- spec/editor-spec.coffee | 8 ++++---- src/display-buffer.coffee | 12 ++++++------ src/editor.coffee | 8 ++++---- src/gutter-component.coffee | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 0ee26f717..4b1620c3b 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -309,14 +309,14 @@ describe "EditorComponent", -> lineHasClass = (screenRow, klass) -> component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - it "renders the gutter-class decorations", -> + it "renders the gutter decorations", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() expect(component.lineNumberNodeForScreenRow(9)).toBeFalsy() - editor.addDecorationForBufferRow(9, {type: 'gutter-class', class: 'fancy-class'}) - editor.addDecorationForBufferRow(9, {type: 'someother-type', class: 'nope-class'}) + editor.addDecorationToBufferRow(9, {type: 'gutter', class: 'fancy-class'}) + editor.addDecorationToBufferRow(9, {type: 'someother-type', class: 'nope-class'}) verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) @@ -324,17 +324,17 @@ describe "EditorComponent", -> expect(lineHasClass(9, 'fancy-class')).toBeTruthy() expect(lineHasClass(9, 'nope-class')).toBeFalsy() - it "handles updates to gutter-class decorations", -> - editor.addDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) - editor.addDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + it "handles updates to gutter decorations", -> + editor.addDecorationToBufferRow(2, {type: 'gutter', class: 'fancy-class'}) + editor.addDecorationToBufferRow(2, {type: 'someother-type', class: 'nope-class'}) advanceClock(gutter.decorationRenderDelay) expect(lineHasClass(2, 'fancy-class')).toBeTruthy() expect(lineHasClass(2, 'nope-class')).toBeFalsy() - editor.removeDecorationForBufferRow(2, {type: 'gutter-class', class: 'fancy-class'}) - editor.removeDecorationForBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + editor.removeDecorationFromBufferRow(2, {type: 'gutter', class: 'fancy-class'}) + editor.removeDecorationFromBufferRow(2, {type: 'someother-type', class: 'nope-class'}) advanceClock(gutter.decorationRenderDelay) @@ -342,8 +342,8 @@ describe "EditorComponent", -> expect(lineHasClass(2, 'nope-class')).toBeFalsy() it "handles softWrap decorations", -> - editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) - editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) + editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'no-wrap'}) + editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'wrap-me', softWrap: true}) editor.setSoftWrap(true) node.style.height = 4.5 * lineHeightInPixels + 'px' @@ -356,8 +356,8 @@ describe "EditorComponent", -> expect(lineHasClass(3, 'wrap-me')).toBeTruthy() # should remove the wrapped decorations - editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) - editor.removeDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me'}) + editor.removeDecorationFromBufferRow(1, {type: 'gutter', class: 'no-wrap'}) + editor.removeDecorationFromBufferRow(1, {type: 'gutter', class: 'wrap-me'}) advanceClock(gutter.decorationRenderDelay) expect(lineHasClass(2, 'no-wrap')).toBeFalsy() @@ -366,8 +366,8 @@ describe "EditorComponent", -> expect(lineHasClass(3, 'wrap-me')).toBeFalsy() # should add them back when the nodes are not recreated - editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'no-wrap'}) - editor.addDecorationForBufferRow(1, {type: 'gutter-class', class: 'wrap-me', softWrap: true}) + editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'no-wrap'}) + editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'wrap-me', softWrap: true}) advanceClock(gutter.decorationRenderDelay) expect(lineHasClass(2, 'no-wrap')).toBeTruthy() @@ -379,7 +379,7 @@ describe "EditorComponent", -> {marker, decoration} = {} beforeEach -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') - decoration = {type: 'gutter-class', class: 'someclass'} + decoration = {type: 'gutter', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) advanceClock(gutter.decorationRenderDelay) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 1a02f027b..121a7bb4a 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -3208,14 +3208,14 @@ describe "Editor", -> describe "decorations", -> it "can add and remove decorations", -> - decoration = {type: 'gutter-class', class: 'one'} - editor.addDecorationForBufferRow(2, decoration) - editor.addDecorationForBufferRow(2, decoration) + decoration = {type: 'gutter', class: 'one'} + editor.addDecorationToBufferRow(2, decoration) + editor.addDecorationToBufferRow(2, decoration) decorations = editor.decorationsForBufferRow(2) expect(decorations).toHaveLength 1 expect(decorations).toContain decoration - editor.removeDecorationForBufferRow(2, decoration) + editor.removeDecorationFromBufferRow(2, decoration) decorations = editor.decorationsForBufferRow(2) expect(decorations).toHaveLength 0 diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 552b5e6be..82211e307 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -725,14 +725,14 @@ class DisplayBuffer extends Model decorations = (dec for dec in decorations when dec.type == decorationType) if decorationType? decorations - addDecorationForBufferRow: (bufferRow, decoration) -> + addDecorationToBufferRow: (bufferRow, decoration) -> @decorations[bufferRow] ?= [] for current in @decorations[bufferRow] return if _.isEqual(current, decoration) @decorations[bufferRow].push(decoration) @emit 'decoration-changed', {bufferRow, decoration, action: 'add'} - removeDecorationForBufferRow: (bufferRow, decoration) -> + removeDecorationFromBufferRow: (bufferRow, decoration) -> return unless @decorations[bufferRow] removed = @findDecorationsForBufferRow(bufferRow, decoration) @@ -753,7 +753,7 @@ class DisplayBuffer extends Model tail = marker.getTailBufferPosition().row [tail, head] = [head, tail] if head > tail while head <= tail - @addDecorationForBufferRow(head++, decoration) + @addDecorationToBufferRow(head++, decoration) changedSubscription = @subscribe marker, 'changed', (e) => oldHead = e.oldHeadBufferPosition.row @@ -772,11 +772,11 @@ class DisplayBuffer extends Model # overlap was not visible. while oldHead <= oldTail - @removeDecorationForBufferRow(oldHead, decoration) + @removeDecorationFromBufferRow(oldHead, decoration) oldHead++ while e.isValid and newHead <= newTail - @addDecorationForBufferRow(newHead, decoration) + @addDecorationToBufferRow(newHead, decoration) newHead++ destroyedSubscription = @subscribe marker, 'destroyed', (e) => @@ -792,7 +792,7 @@ class DisplayBuffer extends Model tail = marker.getTailBufferPosition().row [tail, head] = [head, tail] if head > tail while head <= tail - @removeDecorationForBufferRow(head++, decoration) + @removeDecorationFromBufferRow(head++, decoration) for sub in _.clone(@decorationMarkerSubscriptions[marker.id]) if @decorationEqualsPattern(sub.decoration, decoration) diff --git a/src/editor.coffee b/src/editor.coffee index 258a94498..31211397f 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1061,11 +1061,11 @@ class Editor extends Model decorationsForBufferRow: (bufferRow, decorationType) -> @displayBuffer.decorationsForBufferRow(bufferRow, decorationType) - addDecorationForBufferRow: (bufferRow, decoration) -> - @displayBuffer.addDecorationForBufferRow(bufferRow, decoration) + addDecorationToBufferRow: (bufferRow, decoration) -> + @displayBuffer.addDecorationToBufferRow(bufferRow, decoration) - removeDecorationForBufferRow: (bufferRow, decoration) -> - @displayBuffer.removeDecorationForBufferRow(bufferRow, decoration) + removeDecorationFromBufferRow: (bufferRow, decoration) -> + @displayBuffer.removeDecorationFromBufferRow(bufferRow, decoration) addDecorationForMarker: (marker, decoration) -> @displayBuffer.addDecorationForMarker(marker, decoration) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 00dfa7b1b..e21e5e9b4 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -9,7 +9,7 @@ module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' mixins: [SubscriberMixin] - decorationType: 'gutter-class' + decorationType: 'gutter' decorationRenderDelay: 100 dummyLineNumberNode: null From 5bae58eeb1d952eff922391e7f5c529a07109ea7 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 10:50:57 -0700 Subject: [PATCH 11/45] Clean up specs based on feedback. --- spec/editor-component-spec.coffee | 178 +++++++++++++++--------------- 1 file changed, 88 insertions(+), 90 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 4b1620c3b..68e460628 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -303,79 +303,80 @@ describe "EditorComponent", -> expect(gutterNode.offsetWidth).toBe initialGutterWidth describe "when decorations are used", -> - {lineHasClass, gutter} = {} + {lineNumberHasClass, gutter} = {} beforeEach -> - gutter = component.refs.gutter - lineHasClass = (screenRow, klass) -> + {gutter} = component.refs + lineNumberHasClass = (screenRow, klass) -> component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - it "renders the gutter decorations", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + describe "when decorations are applied to buffer rows", -> + it "renders line number classes based on the decorations on their buffer row", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureScrollView() - expect(component.lineNumberNodeForScreenRow(9)).toBeFalsy() + expect(component.lineNumberNodeForScreenRow(9)).not.toBeDefined() - editor.addDecorationToBufferRow(9, {type: 'gutter', class: 'fancy-class'}) - editor.addDecorationToBufferRow(9, {type: 'someother-type', class: 'nope-class'}) + editor.addDecorationToBufferRow(9, type: 'gutter', class: 'fancy-class') + editor.addDecorationToBufferRow(9, type: 'someother-type', class: 'nope-class') - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(lineHasClass(9, 'fancy-class')).toBeTruthy() - expect(lineHasClass(9, 'nope-class')).toBeFalsy() + expect(lineNumberHasClass(9, 'fancy-class')).toBe true + expect(lineNumberHasClass(9, 'nope-class')).toBe false - it "handles updates to gutter decorations", -> - editor.addDecorationToBufferRow(2, {type: 'gutter', class: 'fancy-class'}) - editor.addDecorationToBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + it "handles updates to gutter decorations", -> + editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') + editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') - advanceClock(gutter.decorationRenderDelay) + advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'fancy-class')).toBeTruthy() - expect(lineHasClass(2, 'nope-class')).toBeFalsy() + expect(lineNumberHasClass(2, 'fancy-class')).toBe true + expect(lineNumberHasClass(2, 'nope-class')).toBe false - editor.removeDecorationFromBufferRow(2, {type: 'gutter', class: 'fancy-class'}) - editor.removeDecorationFromBufferRow(2, {type: 'someother-type', class: 'nope-class'}) + editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') + editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') - advanceClock(gutter.decorationRenderDelay) + advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'fancy-class')).toBeFalsy() - expect(lineHasClass(2, 'nope-class')).toBeFalsy() + expect(lineNumberHasClass(2, 'fancy-class')).toBe false + expect(lineNumberHasClass(2, 'nope-class')).toBe false - it "handles softWrap decorations", -> - editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'no-wrap'}) - editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'wrap-me', softWrap: true}) + it "renders decorations on soft-wrapped line numbers when softWrap is true", -> + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) - editor.setSoftWrap(true) - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 30 * charWidth + 'px' - component.measureScrollView() + editor.setSoftWrap(true) + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 30 * charWidth + 'px' + component.measureScrollView() - expect(lineHasClass(2, 'no-wrap')).toBeTruthy() - expect(lineHasClass(2, 'wrap-me')).toBeTruthy() - expect(lineHasClass(3, 'no-wrap')).toBeFalsy() - expect(lineHasClass(3, 'wrap-me')).toBeTruthy() + expect(lineNumberHasClass(2, 'no-wrap')).toBe true + expect(lineNumberHasClass(2, 'wrap-me')).toBe true + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe true - # should remove the wrapped decorations - editor.removeDecorationFromBufferRow(1, {type: 'gutter', class: 'no-wrap'}) - editor.removeDecorationFromBufferRow(1, {type: 'gutter', class: 'wrap-me'}) - advanceClock(gutter.decorationRenderDelay) + # should remove the wrapped decorations + editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me') + advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'no-wrap')).toBeFalsy() - expect(lineHasClass(2, 'wrap-me')).toBeFalsy() - expect(lineHasClass(3, 'no-wrap')).toBeFalsy() - expect(lineHasClass(3, 'wrap-me')).toBeFalsy() + expect(lineNumberHasClass(2, 'no-wrap')).toBe false + expect(lineNumberHasClass(2, 'wrap-me')).toBe false + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe false - # should add them back when the nodes are not recreated - editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'no-wrap'}) - editor.addDecorationToBufferRow(1, {type: 'gutter', class: 'wrap-me', softWrap: true}) - advanceClock(gutter.decorationRenderDelay) + # should add them back when the nodes are not recreated + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) + advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'no-wrap')).toBeTruthy() - expect(lineHasClass(2, 'wrap-me')).toBeTruthy() - expect(lineHasClass(3, 'no-wrap')).toBeFalsy() - expect(lineHasClass(3, 'wrap-me')).toBeTruthy() + expect(lineNumberHasClass(2, 'no-wrap')).toBe true + expect(lineNumberHasClass(2, 'wrap-me')).toBe true + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe true - describe "when markers are used", -> + describe "when decorations are applied to markers", -> {marker, decoration} = {} beforeEach -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') @@ -383,72 +384,69 @@ describe "EditorComponent", -> editor.addDecorationForMarker(marker, decoration) advanceClock(gutter.decorationRenderDelay) - it "tracks the marker's movements", -> - expect(lineHasClass(1, 'someclass')).toBeFalsy() - expect(lineHasClass(2, 'someclass')).toBeTruthy() - expect(lineHasClass(3, 'someclass')).toBeTruthy() - expect(lineHasClass(4, 'someclass')).toBeFalsy() + it "updates line number classes when the marker moves", -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'someclass')).toBeFalsy() - expect(lineHasClass(3, 'someclass')).toBeTruthy() - expect(lineHasClass(4, 'someclass')).toBeTruthy() - expect(lineHasClass(5, 'someclass')).toBeFalsy() + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe true + expect(lineNumberHasClass(5, 'someclass')).toBe false editor.getBuffer().deleteRows(0, 1) advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(0, 'someclass')).toBeFalsy() - expect(lineHasClass(1, 'someclass')).toBeTruthy() - expect(lineHasClass(2, 'someclass')).toBeTruthy() - expect(lineHasClass(3, 'someclass')).toBeFalsy() + expect(lineNumberHasClass(0, 'someclass')).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe true + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe false - it "removes the classes when marker is invalidated", -> - t = editor.getBuffer().getText() - # invalidate this thing + it "removes line number classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') advanceClock(gutter.decorationRenderDelay) - expect(marker.isValid()).toBeFalsy() - expect(lineHasClass(1, 'someclass')).toBeFalsy() - expect(lineHasClass(2, 'someclass')).toBeFalsy() - expect(lineHasClass(3, 'someclass')).toBeFalsy() - expect(lineHasClass(4, 'someclass')).toBeFalsy() + expect(marker.isValid()).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false - # Back to valid editor.getBuffer().undo() advanceClock(gutter.decorationRenderDelay) - expect(marker.isValid()).toBeTruthy() - expect(lineHasClass(1, 'someclass')).toBeFalsy() - expect(lineHasClass(2, 'someclass')).toBeTruthy() - expect(lineHasClass(3, 'someclass')).toBeTruthy() - expect(lineHasClass(4, 'someclass')).toBeFalsy() + expect(marker.isValid()).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(1, 'someclass')).toBeFalsy() - expect(lineHasClass(2, 'someclass')).toBeFalsy() - expect(lineHasClass(3, 'someclass')).toBeFalsy() - expect(lineHasClass(4, 'someclass')).toBeFalsy() + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(2, 'someclass')).toBeFalsy() - expect(lineHasClass(3, 'someclass')).toBeFalsy() + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false - it "removes the decoration when the marker is destroyed", -> + it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() advanceClock(gutter.decorationRenderDelay) - expect(lineHasClass(1, 'someclass')).toBeFalsy() - expect(lineHasClass(2, 'someclass')).toBeFalsy() - expect(lineHasClass(3, 'someclass')).toBeFalsy() - expect(lineHasClass(4, 'someclass')).toBeFalsy() + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> From 3ef91c61d905f094ba2f6e16af966e37b04a3dea Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:19:58 -0700 Subject: [PATCH 12/45] Add api for getStartBufferPosition and related fns --- src/display-buffer-marker.coffee | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/display-buffer-marker.coffee b/src/display-buffer-marker.coffee index c3a902b99..e2324f6b7 100644 --- a/src/display-buffer-marker.coffee +++ b/src/display-buffer-marker.coffee @@ -111,6 +111,34 @@ class DisplayBufferMarker setTailBufferPosition: (bufferPosition) -> @bufferMarker.setTailPosition(bufferPosition) + # Retrieves the screen position of the marker's start. This will always be + # less than or equal to the result of {DisplayBufferMarker::getEndScreenPosition}. + # + # Returns a {Point}. + getStartScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true) + + # Retrieves the buffer position of the marker's start. This will always be + # less than or equal to the result of {DisplayBufferMarker::getEndBufferPosition}. + # + # Returns a {Point}. + getStartBufferPosition: -> + @bufferMarker.getStartPosition() + + # Retrieves the screen position of the marker's end. This will always be + # greater than or equal to the result of {DisplayBufferMarker::getStartScreenPosition}. + # + # Returns a {Point}. + getEndScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true) + + # Retrieves the buffer position of the marker's end. This will always be + # greater than or equal to the result of {DisplayBufferMarker::getStartBufferPosition}. + # + # Returns a {Point}. + getEndBufferPosition: -> + @bufferMarker.getEndPosition() + # Sets the marker's tail to the same position as the marker's head. # # This only works if there isn't already a tail position. From 9e86d5f5f140b64c8be5c08f0cdcde63f1d48a0d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:24:34 -0700 Subject: [PATCH 13/45] :lipstick: Clean up based on feedback --- src/display-buffer.coffee | 56 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 82211e307..16e63a611 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -743,27 +743,26 @@ class DisplayBuffer extends Model findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] - (dec for dec in @decorations[bufferRow] when @decorationEqualsPattern(dec, decorationPattern)) + decoration for decoration in @decorations[bufferRow] when @decorationMatchesPattern(decoration, decorationPattern) - decorationEqualsPattern: (decoration, decorationPattern) -> + decorationMatchesPattern: (decoration, decorationPattern) -> _.isEqual(decorationPattern, _.pick(decoration, _.keys(decorationPattern))) addDecorationForMarker: (marker, decoration) -> - head = marker.getHeadBufferPosition().row - tail = marker.getTailBufferPosition().row - [tail, head] = [head, tail] if head > tail - while head <= tail - @addDecorationToBufferRow(head++, decoration) + startRow = marker.getStartBufferPosition().row + endRow = marker.getEndBufferPosition().row + while startRow <= endRow + @addDecorationToBufferRow(startRow++, decoration) changedSubscription = @subscribe marker, 'changed', (e) => - oldHead = e.oldHeadBufferPosition.row - oldTail = e.oldTailBufferPosition.row - newHead = e.newHeadBufferPosition.row - newTail = e.newTailBufferPosition.row + oldStartRow = e.oldHeadBufferPosition.row + oldEndRow = e.oldTailBufferPosition.row + newStartRow = e.newHeadBufferPosition.row + newEndRow = e.newTailBufferPosition.row # swap so head is always <= than tail - [oldTail, oldHead] = [oldHead, oldTail] if oldHead > oldTail - [newTail, newHead] = [newHead, newTail] if newHead > newTail + [oldEndRow, oldStartRow] = [oldStartRow, oldEndRow] if oldStartRow > oldEndRow + [newEndRow, newStartRow] = [newStartRow, newEndRow] if newStartRow > newEndRow # TODO: we could only update the rows that change by tracking an overlap, # then update only those outside of the overlap. I had something to do @@ -771,13 +770,11 @@ class DisplayBuffer extends Model # all decorations, then when markers becoming valid, some of the # overlap was not visible. - while oldHead <= oldTail - @removeDecorationFromBufferRow(oldHead, decoration) - oldHead++ + while oldStartRow <= oldEndRow + @removeDecorationFromBufferRow(oldStartRow++, decoration) - while e.isValid and newHead <= newTail - @addDecorationToBufferRow(newHead, decoration) - newHead++ + while e.isValid and newStartRow <= newEndRow + @addDecorationToBufferRow(newStartRow++, decoration) destroyedSubscription = @subscribe marker, 'destroyed', (e) => @removeDecorationForMarker(marker, decoration) @@ -786,19 +783,18 @@ class DisplayBuffer extends Model @decorationMarkerSubscriptions[marker.id].push {decoration, changedSubscription, destroyedSubscription} removeDecorationForMarker: (marker, decoration) -> - return unless @decorationMarkerSubscriptions[marker.id] + return unless @decorationMarkerSubscriptions[marker.id]? - head = marker.getHeadBufferPosition().row - tail = marker.getTailBufferPosition().row - [tail, head] = [head, tail] if head > tail - while head <= tail - @removeDecorationFromBufferRow(head++, decoration) + startRow = marker.getStartBufferPosition().row + endRow = marker.getEndBufferPosition().row + while startRow <= endRow + @removeDecorationFromBufferRow(startRow++, decoration) - for sub in _.clone(@decorationMarkerSubscriptions[marker.id]) - if @decorationEqualsPattern(sub.decoration, decoration) - sub.changedSubscription.off() - sub.destroyedSubscription.off() - @decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], sub) + for subscription in _.clone(@decorationMarkerSubscriptions[marker.id]) + if @decorationMatchesPattern(subscription.decoration, decoration) + subscription.changedSubscription.off() + subscription.destroyedSubscription.off() + @decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], subscription) return From ef6ca3853dbcccdd5b57d28213c18e53c002a04b Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:34:16 -0700 Subject: [PATCH 14/45] :lipstick: --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 16e63a611..11741d3d5 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -722,7 +722,7 @@ class DisplayBuffer extends Model decorationsForBufferRow: (bufferRow, decorationType) -> decorations = @decorations[bufferRow] ? [] - decorations = (dec for dec in decorations when dec.type == decorationType) if decorationType? + decorations = (dec for dec in decorations when dec.type is decorationType) if decorationType? decorations addDecorationToBufferRow: (bufferRow, decoration) -> From a229d696d54467eb521deba7daba18c556bd089d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:34:50 -0700 Subject: [PATCH 15/45] Add `addDecorationForBufferRowRange` and related remove --- src/display-buffer.coffee | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 11741d3d5..950315dea 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -741,6 +741,14 @@ class DisplayBuffer extends Model for decoration in removed @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} + addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + while startBufferRow <= endBufferRow + @addDecorationToBufferRow(startBufferRow++, decoration) + + removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + while startBufferRow <= endBufferRow + @removeDecorationFromBufferRow(startBufferRow++, decoration) + findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] decoration for decoration in @decorations[bufferRow] when @decorationMatchesPattern(decoration, decorationPattern) @@ -751,8 +759,7 @@ class DisplayBuffer extends Model addDecorationForMarker: (marker, decoration) -> startRow = marker.getStartBufferPosition().row endRow = marker.getEndBufferPosition().row - while startRow <= endRow - @addDecorationToBufferRow(startRow++, decoration) + @addDecorationToBufferRowRange(startRow, endRow, decoration) changedSubscription = @subscribe marker, 'changed', (e) => oldStartRow = e.oldHeadBufferPosition.row @@ -770,11 +777,8 @@ class DisplayBuffer extends Model # all decorations, then when markers becoming valid, some of the # overlap was not visible. - while oldStartRow <= oldEndRow - @removeDecorationFromBufferRow(oldStartRow++, decoration) - - while e.isValid and newStartRow <= newEndRow - @addDecorationToBufferRow(newStartRow++, decoration) + @removeDecorationFromBufferRowRange(oldStartRow, oldEndRow, decoration) + @addDecorationToBufferRowRange(newStartRow, newEndRow, decoration) if e.isValid destroyedSubscription = @subscribe marker, 'destroyed', (e) => @removeDecorationForMarker(marker, decoration) From 86d7a45a7841991c7ca52da9b25cc6f1ea3af800 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:36:13 -0700 Subject: [PATCH 16/45] Remove the comment about overlap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out it’s already dealing with overlap by not emitting events when there is an overlap. --- src/display-buffer.coffee | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 950315dea..fd448c4fb 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -771,12 +771,6 @@ class DisplayBuffer extends Model [oldEndRow, oldStartRow] = [oldStartRow, oldEndRow] if oldStartRow > oldEndRow [newEndRow, newStartRow] = [newStartRow, newEndRow] if newStartRow > newEndRow - # TODO: we could only update the rows that change by tracking an overlap, - # then update only those outside of the overlap. I had something to do - # this, but it's complicated by marker validity. When invlaid, I removed - # all decorations, then when markers becoming valid, some of the - # overlap was not visible. - @removeDecorationFromBufferRowRange(oldStartRow, oldEndRow, decoration) @addDecorationToBufferRowRange(newStartRow, newEndRow, decoration) if e.isValid From a72f11594da3c425e6e73ef57fb1fe67e0dc54f0 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 11:50:49 -0700 Subject: [PATCH 17/45] :lipstick: remove decoratorType instance var --- src/gutter-component.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index e21e5e9b4..db3cfd5fe 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -9,7 +9,6 @@ module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' mixins: [SubscriberMixin] - decorationType: 'gutter' decorationRenderDelay: 100 dummyLineNumberNode: null @@ -146,7 +145,7 @@ GutterComponent = React.createClass classes.push 'foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) classes.push 'folded' if @props.editor.isFoldedAtBufferRow(bufferRow) - decorations = @props.editor.decorationsForBufferRow(bufferRow, @decorationType) + decorations = @props.editor.decorationsForBufferRow(bufferRow, 'gutter') for decoration in decorations classes.push(decoration.class) if not softWrapped or softWrapped and decoration.softWrap @@ -192,7 +191,7 @@ GutterComponent = React.createClass if condition then node.classList.add(klass) else node.classList.remove(klass) onDecorationChanged: (change) -> - if change.decoration.type == @decorationType + if change.decoration.type is 'gutter' @decoratorUpdates[change.bufferRow] ?= [] @decoratorUpdates[change.bufferRow].push change @renderDecorations() From a13990155f4e532298c8a4650250b26c4d8b231c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 12:32:45 -0700 Subject: [PATCH 18/45] Use setImmediate rather than setTimeout --- spec/editor-component-spec.coffee | 133 ++++++++++++++++-------------- src/gutter-component.coffee | 7 +- 2 files changed, 75 insertions(+), 65 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 68e460628..011091f62 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -329,18 +329,18 @@ describe "EditorComponent", -> editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') - advanceClock(gutter.decorationRenderDelay) + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'fancy-class')).toBe true + expect(lineNumberHasClass(2, 'nope-class')).toBe false - expect(lineNumberHasClass(2, 'fancy-class')).toBe true - expect(lineNumberHasClass(2, 'nope-class')).toBe false + editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') + editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') - editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') - editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') - - advanceClock(gutter.decorationRenderDelay) - - expect(lineNumberHasClass(2, 'fancy-class')).toBe false - expect(lineNumberHasClass(2, 'nope-class')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'fancy-class')).toBe false + expect(lineNumberHasClass(2, 'nope-class')).toBe false it "renders decorations on soft-wrapped line numbers when softWrap is true", -> editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') @@ -359,22 +359,24 @@ describe "EditorComponent", -> # should remove the wrapped decorations editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap') editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me') - advanceClock(gutter.decorationRenderDelay) - expect(lineNumberHasClass(2, 'no-wrap')).toBe false - expect(lineNumberHasClass(2, 'wrap-me')).toBe false - expect(lineNumberHasClass(3, 'no-wrap')).toBe false - expect(lineNumberHasClass(3, 'wrap-me')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'no-wrap')).toBe false + expect(lineNumberHasClass(2, 'wrap-me')).toBe false + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe false - # should add them back when the nodes are not recreated - editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') - editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) - advanceClock(gutter.decorationRenderDelay) + # should add them back when the nodes are not recreated + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) - expect(lineNumberHasClass(2, 'no-wrap')).toBe true - expect(lineNumberHasClass(2, 'wrap-me')).toBe true - expect(lineNumberHasClass(3, 'no-wrap')).toBe false - expect(lineNumberHasClass(3, 'wrap-me')).toBe true + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'no-wrap')).toBe true + expect(lineNumberHasClass(2, 'wrap-me')).toBe true + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe true describe "when decorations are applied to markers", -> {marker, decoration} = {} @@ -382,7 +384,7 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') decoration = {type: 'gutter', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) - advanceClock(gutter.decorationRenderDelay) + waitsFor -> not gutter.decorationRenderImmediate? it "updates line number classes when the marker moves", -> expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -391,62 +393,71 @@ describe "EditorComponent", -> expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') - advanceClock(gutter.decorationRenderDelay) - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe true - expect(lineNumberHasClass(4, 'someclass')).toBe true - expect(lineNumberHasClass(5, 'someclass')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe true + expect(lineNumberHasClass(5, 'someclass')).toBe false - editor.getBuffer().deleteRows(0, 1) - advanceClock(gutter.decorationRenderDelay) + editor.getBuffer().deleteRows(0, 1) - expect(lineNumberHasClass(0, 'someclass')).toBe false - expect(lineNumberHasClass(1, 'someclass')).toBe true - expect(lineNumberHasClass(2, 'someclass')).toBe true - expect(lineNumberHasClass(3, 'someclass')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(0, 'someclass')).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe true + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe false it "removes line number classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - advanceClock(gutter.decorationRenderDelay) - expect(marker.isValid()).toBe false - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> - editor.getBuffer().undo() - advanceClock(gutter.decorationRenderDelay) + expect(marker.isValid()).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false - expect(marker.isValid()).toBe true - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe true - expect(lineNumberHasClass(3, 'someclass')).toBe true - expect(lineNumberHasClass(4, 'someclass')).toBe false + editor.getBuffer().undo() + + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(marker.isValid()).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) - advanceClock(gutter.decorationRenderDelay) - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') - advanceClock(gutter.decorationRenderDelay) - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false + + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() - advanceClock(gutter.decorationRenderDelay) - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false + waitsFor -> not gutter.decorationRenderImmediate? + runs -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index db3cfd5fe..84920399e 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -9,7 +9,6 @@ module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' mixins: [SubscriberMixin] - decorationRenderDelay: 100 dummyLineNumberNode: null @@ -197,8 +196,8 @@ GutterComponent = React.createClass @renderDecorations() renderDecorations: -> - clearTimeout(@decorationRenderTimeout) if @decorationRenderTimeout + clearImmediate(@decorationRenderImmediate) if @decorationRenderImmediate render = => @forceUpdate() - @decorationRenderTimeout = null - @decorationRenderTimeout = setTimeout(render, @decorationRenderDelay) + @decorationRenderImmediate = null + @decorationRenderImmediate = setImmediate(render) From 235180cf03c79d8d254420e708ec8168f8fda276 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 14:03:55 -0700 Subject: [PATCH 19/45] Add specs for fold rendering --- spec/editor-component-spec.coffee | 44 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 011091f62..561cd6565 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -228,6 +228,12 @@ describe "EditorComponent", -> [node] describe "gutter rendering", -> + {lineNumberHasClass, gutter} = {} + beforeEach -> + {gutter} = component.refs + lineNumberHasClass = (screenRow, klass) -> + component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() @@ -302,13 +308,37 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" expect(gutterNode.offsetWidth).toBe initialGutterWidth - describe "when decorations are used", -> - {lineNumberHasClass, gutter} = {} - beforeEach -> - {gutter} = component.refs - lineNumberHasClass = (screenRow, klass) -> - component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + describe "rendering fold decorations", -> + it "adds the foldable class to line numbers when the line is foldable", -> + expect(lineNumberHasClass(0, 'foldable')).toBe true + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe false + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe true + expect(lineNumberHasClass(5, 'foldable')).toBe false + it "updates the foldable class on the correct line numbers when the foldable positions change", -> + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(0, 'foldable')).toBe false + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe true + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe false + expect(lineNumberHasClass(5, 'foldable')).toBe true + expect(lineNumberHasClass(6, 'foldable')).toBe false + + it "adds, updates and removes the folded class on the correct line number nodes", -> + editor.foldBufferRow(4) + expect(lineNumberHasClass(4, 'folded')).toBe true + + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(4, 'folded')).toBe false + expect(lineNumberHasClass(5, 'folded')).toBe true + + editor.unfoldBufferRow(5) + expect(lineNumberHasClass(5, 'folded')).toBe false + + describe "when decorations are used", -> describe "when decorations are applied to buffer rows", -> it "renders line number classes based on the decorations on their buffer row", -> node.style.height = 4.5 * lineHeightInPixels + 'px' @@ -325,7 +355,7 @@ describe "EditorComponent", -> expect(lineNumberHasClass(9, 'fancy-class')).toBe true expect(lineNumberHasClass(9, 'nope-class')).toBe false - it "handles updates to gutter decorations", -> + it "renders updates to gutter decorations", -> editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') From 1b8be75a764a07d18a232014aa5f6d335a9b536e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 14:15:39 -0700 Subject: [PATCH 20/45] Add specs for the editor interface for decorations in ranges and associated with markers --- spec/editor-spec.coffee | 49 ++++++++++++++++++++++++++++++++++++++++- src/editor.coffee | 6 +++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 121a7bb4a..5a7fffe3a 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -3207,8 +3207,11 @@ describe "Editor", -> expect(editor.getScrollTop()).toBe 0 describe "decorations", -> - it "can add and remove decorations", -> + decoration = null + beforeEach -> decoration = {type: 'gutter', class: 'one'} + + it "can add decorations to buffer rows and remove them", -> editor.addDecorationToBufferRow(2, decoration) editor.addDecorationToBufferRow(2, decoration) @@ -3219,3 +3222,47 @@ describe "Editor", -> editor.removeDecorationFromBufferRow(2, decoration) decorations = editor.decorationsForBufferRow(2) expect(decorations).toHaveLength 0 + + it "can add decorations to buffer row ranges and remove them", -> + editor.addDecorationToBufferRowRange(2, 4, decoration) + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + + editor.removeDecorationFromBufferRowRange(3, 5, decoration) + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + + it "can add decorations associated with markers and remove them", -> + marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') + + editor.addDecorationForMarker(marker, decoration) + expect(editor.decorationsForBufferRow 1).not.toContain decoration + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + + editor.getBuffer().insert([0, 0], '\n') + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.getBuffer().insert([4, 2], 'n') + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.getBuffer().undo() + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.removeDecorationForMarker(marker, decoration) + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration diff --git a/src/editor.coffee b/src/editor.coffee index 31211397f..9879a477b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1067,6 +1067,12 @@ class Editor extends Model removeDecorationFromBufferRow: (bufferRow, decoration) -> @displayBuffer.removeDecorationFromBufferRow(bufferRow, decoration) + addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) + + removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) + addDecorationForMarker: (marker, decoration) -> @displayBuffer.addDecorationForMarker(marker, decoration) From 5cd8f5952f54337b22744e6cc45afb93fab43b85 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 17:56:39 -0700 Subject: [PATCH 21/45] Make editor push decorator updates to the gutter --- spec/editor-component-spec.coffee | 24 +++++----- src/editor-component.coffee | 23 +++++++++- src/gutter-component.coffee | 76 ++++++++++++------------------- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 561cd6565..d164fb0e7 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -359,7 +359,7 @@ describe "EditorComponent", -> editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'fancy-class')).toBe true expect(lineNumberHasClass(2, 'nope-class')).toBe false @@ -367,7 +367,7 @@ describe "EditorComponent", -> editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'fancy-class')).toBe false expect(lineNumberHasClass(2, 'nope-class')).toBe false @@ -390,7 +390,7 @@ describe "EditorComponent", -> editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap') editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'no-wrap')).toBe false expect(lineNumberHasClass(2, 'wrap-me')).toBe false @@ -401,7 +401,7 @@ describe "EditorComponent", -> editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'no-wrap')).toBe true expect(lineNumberHasClass(2, 'wrap-me')).toBe true @@ -414,7 +414,7 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') decoration = {type: 'gutter', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? it "updates line number classes when the marker moves", -> expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -424,7 +424,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'someclass')).toBe false expect(lineNumberHasClass(3, 'someclass')).toBe true @@ -433,7 +433,7 @@ describe "EditorComponent", -> editor.getBuffer().deleteRows(0, 1) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(0, 'someclass')).toBe false expect(lineNumberHasClass(1, 'someclass')).toBe true @@ -443,7 +443,7 @@ describe "EditorComponent", -> it "removes line number classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(marker.isValid()).toBe false @@ -454,7 +454,7 @@ describe "EditorComponent", -> editor.getBuffer().undo() - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(marker.isValid()).toBe true expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -465,7 +465,7 @@ describe "EditorComponent", -> it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(1, 'someclass')).toBe false expect(lineNumberHasClass(2, 'someclass')).toBe false @@ -474,7 +474,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(2, 'someclass')).toBe false expect(lineNumberHasClass(3, 'someclass')).toBe false @@ -482,7 +482,7 @@ describe "EditorComponent", -> it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() - waitsFor -> not gutter.decorationRenderImmediate? + waitsFor -> not component.decorationChangedImmediate? runs -> expect(lineNumberHasClass(1, 'someclass')).toBe false expect(lineNumberHasClass(2, 'someclass')).toBe false diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1d1006290..1810cf711 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -49,6 +49,7 @@ EditorComponent = React.createClass [renderedStartRow, renderedEndRow] = renderedRowRange cursorScreenRanges = @getCursorScreenRanges(renderedRowRange) selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange) + decorations = @getGutterDecorations(renderedRowRange) scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -71,7 +72,8 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, - scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow + scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow, + decorations } div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, @@ -225,6 +227,19 @@ EditorComponent = React.createClass selectionScreenRanges + getGutterDecorations: (renderedRowRange) -> + {editor} = @props + [renderedStartRow, renderedEndRow] = renderedRowRange + + bufferRows = editor.bufferRowsForScreenRows(renderedStartRow, renderedEndRow - 1) + + decorations = {} + for bufferRow in bufferRows + decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter') + decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow) + decorations[bufferRow].push {class: 'folded'} if editor.isFoldedAtBufferRow(bufferRow) + decorations + observeEditor: -> {editor} = @props @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted @@ -233,6 +248,7 @@ EditorComponent = React.createClass @subscribe editor, 'cursors-moved', @onCursorsMoved @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor, 'selection-added', @onSelectionAdded + @subscribe editor, 'decoration-changed', @onDecorationChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -510,6 +526,11 @@ EditorComponent = React.createClass onCursorsMoved: -> @cursorsMoved = true + onDecorationChanged: -> + @decorationChangedImmediate = setImmediate => + @requestUpdate() + @decorationChangedImmediate = null + selectToMousePositionUntilMouseUp: (event) -> {editor} = @props dragging = false diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 84920399e..5dff8609d 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,3 +1,4 @@ +_ = require 'underscore-plus' React = require 'react-atom-fork' {div} = require 'reactionary-atom-fork' {isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' @@ -24,17 +25,10 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberIdsByScreenRow = {} @screenRowsByLineNumberId = {} - @decoratorUpdates = {} + @previousDecorations = {} componentDidMount: -> @appendDummyLineNumber() - @subscribeToEditor() - - componentWillUnmount: -> - @unsubscribe() - - subscribeToEditor: -> - @subscribe @props.editor, 'decoration-changed', @onDecorationChanged # Only update the gutter if the visible row range has changed or if a # non-zero-delta change to the screen lines has occurred within the current @@ -44,10 +38,12 @@ GutterComponent = React.createClass 'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow' ) - {renderedRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges, decorations} = newProps for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless _.isEqual(@previousDecorations, decorations) + false componentDidUpdate: (oldProps) -> @@ -78,7 +74,7 @@ GutterComponent = React.createClass @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props + {editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props [startRow, endRow] = renderedRowRange newLineNumberIds = null @@ -99,12 +95,12 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) if @hasLineNumberNode(id) - @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0) + @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow]) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" newLineNumberIds.push(id) - newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow) + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow]) @screenRowsByLineNumberId[id] = screenRow @lineNumberIdsByScreenRow[screenRow] = id @@ -118,7 +114,7 @@ GutterComponent = React.createClass @lineNumberNodesById[lineNumberId] = lineNumberNode node.appendChild(lineNumberNode) - @decoratorUpdates = {} + @previousDecorations = decorations visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> @@ -132,7 +128,7 @@ GutterComponent = React.createClass delete @screenRowsByLineNumberId[lineNumberId] node.removeChild(lineNumberNode) - buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) -> if screenRow? {lineHeightInPixels} = @props style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;" @@ -140,15 +136,13 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - classes = ['line-number'] - classes.push 'foldable' if not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) - classes.push 'folded' if @props.editor.isFoldedAtBufferRow(bufferRow) + classes = '' + if decorations? + for decoration in decorations + classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap + classes += 'line-number' - decorations = @props.editor.decorationsForBufferRow(bufferRow, 'gutter') - for decoration in decorations - classes.push(decoration.class) if not softWrapped or softWrapped and decoration.softWrap - - "
#{innerHTML}
" + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped @@ -160,18 +154,17 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) -> + updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) -> node = @lineNumberNodesById[lineNumberId] + previousDecorations = @previousDecorations[bufferRow] - @toggleClass node, 'foldable', not softWrapped and @props.editor.isFoldableAtBufferRow(bufferRow) - @toggleClass node, 'folded', @props.editor.isFoldedAtBufferRow(bufferRow) + if previousDecorations? + for decoration in previousDecorations + node.classList.remove(decoration.class) if not contains(decorations, decoration) - if @decoratorUpdates[bufferRow]? - for change in @decoratorUpdates[bufferRow] - if change.action == 'add' and (not softWrapped or softWrapped and change.decoration.softWrap) - node.classList.add(change.decoration.class) - else if change.action == 'remove' - node.classList.remove(change.decoration.class) + for decoration in decorations + if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap) + node.classList.add(decoration.class) unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props @@ -186,18 +179,9 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] - toggleClass: (node, klass, condition) -> - if condition then node.classList.add(klass) else node.classList.remove(klass) - - onDecorationChanged: (change) -> - if change.decoration.type is 'gutter' - @decoratorUpdates[change.bufferRow] ?= [] - @decoratorUpdates[change.bufferRow].push change - @renderDecorations() - - renderDecorations: -> - clearImmediate(@decorationRenderImmediate) if @decorationRenderImmediate - render = => - @forceUpdate() - @decorationRenderImmediate = null - @decorationRenderImmediate = setImmediate(render) +# Created because underscore uses === not _.isEqual, which we need +contains = (array, target) -> + return false unless array? + for object in array + return true if _.isEqual(object, target) + false From da5bf6c74cc17432e64346af5102cb09cfac0a83 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 18:12:36 -0700 Subject: [PATCH 22/45] Defensive on the decorations --- src/gutter-component.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 5dff8609d..822d43393 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -162,9 +162,10 @@ GutterComponent = React.createClass for decoration in previousDecorations node.classList.remove(decoration.class) if not contains(decorations, decoration) - for decoration in decorations - if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap) - node.classList.add(decoration.class) + if decorations? + for decoration in decorations + if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap) + node.classList.add(decoration.class) unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props From 02594e3f7ac1fb18b08f22ddc9c9bb9779391ad9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 5 Jun 2014 18:13:07 -0700 Subject: [PATCH 23/45] :lipstick: Use for loops --- src/display-buffer.coffee | 17 +++++++++++------ src/editor.coffee | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index fd448c4fb..e729ebdb3 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -725,6 +725,12 @@ class DisplayBuffer extends Model decorations = (dec for dec in decorations when dec.type is decorationType) if decorationType? decorations + decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> + decorations = {} + for bufferRow in [startBufferRow..endBufferRow] + decorations[bufferRow] = @decorationsForBufferRow(bufferRow, decorationType) + decorations + addDecorationToBufferRow: (bufferRow, decoration) -> @decorations[bufferRow] ?= [] for current in @decorations[bufferRow] @@ -742,12 +748,12 @@ class DisplayBuffer extends Model @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - while startBufferRow <= endBufferRow - @addDecorationToBufferRow(startBufferRow++, decoration) + for bufferRow in [startBufferRow..endBufferRow] + @addDecorationToBufferRow(bufferRow, decoration) removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - while startBufferRow <= endBufferRow - @removeDecorationFromBufferRow(startBufferRow++, decoration) + for bufferRow in [startBufferRow..endBufferRow] + @removeDecorationFromBufferRow(bufferRow, decoration) findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] @@ -785,8 +791,7 @@ class DisplayBuffer extends Model startRow = marker.getStartBufferPosition().row endRow = marker.getEndBufferPosition().row - while startRow <= endRow - @removeDecorationFromBufferRow(startRow++, decoration) + @removeDecorationFromBufferRowRange(startRow, endRow, decoration) for subscription in _.clone(@decorationMarkerSubscriptions[marker.id]) if @decorationMatchesPattern(subscription.decoration, decoration) diff --git a/src/editor.coffee b/src/editor.coffee index 9879a477b..86e7a758b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1061,6 +1061,9 @@ class Editor extends Model decorationsForBufferRow: (bufferRow, decorationType) -> @displayBuffer.decorationsForBufferRow(bufferRow, decorationType) + decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> + @displayBuffer.decorationsForBufferRowRange(startBufferRow, endBufferRow, decorationType) + addDecorationToBufferRow: (bufferRow, decoration) -> @displayBuffer.addDecorationToBufferRow(bufferRow, decoration) From 8e1e5a3760b92975e5c138637779aaf6d0eaa9d2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 12:23:12 -0700 Subject: [PATCH 24/45] Add ability to click the fold icons --- spec/editor-component-spec.coffee | 89 ++++++++++++++++++++++--------- src/editor-component.coffee | 15 +++++- src/gutter-component.coffee | 4 +- 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d164fb0e7..ea23d083a 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -232,7 +232,10 @@ describe "EditorComponent", -> beforeEach -> {gutter} = component.refs lineNumberHasClass = (screenRow, klass) -> - component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + if screenRow.classList? + screenRow.classList.contains(klass) + else + component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' @@ -308,35 +311,69 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" expect(gutterNode.offsetWidth).toBe initialGutterWidth - describe "rendering fold decorations", -> - it "adds the foldable class to line numbers when the line is foldable", -> - expect(lineNumberHasClass(0, 'foldable')).toBe true - expect(lineNumberHasClass(1, 'foldable')).toBe true - expect(lineNumberHasClass(2, 'foldable')).toBe false - expect(lineNumberHasClass(3, 'foldable')).toBe false - expect(lineNumberHasClass(4, 'foldable')).toBe true - expect(lineNumberHasClass(5, 'foldable')).toBe false + describe "fold decorations", -> + describe "rendering fold decorations", -> + it "adds the foldable class to line numbers when the line is foldable", -> + expect(lineNumberHasClass(0, 'foldable')).toBe true + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe false + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe true + expect(lineNumberHasClass(5, 'foldable')).toBe false - it "updates the foldable class on the correct line numbers when the foldable positions change", -> - editor.getBuffer().insert([0, 0], '\n') - expect(lineNumberHasClass(0, 'foldable')).toBe false - expect(lineNumberHasClass(1, 'foldable')).toBe true - expect(lineNumberHasClass(2, 'foldable')).toBe true - expect(lineNumberHasClass(3, 'foldable')).toBe false - expect(lineNumberHasClass(4, 'foldable')).toBe false - expect(lineNumberHasClass(5, 'foldable')).toBe true - expect(lineNumberHasClass(6, 'foldable')).toBe false + it "updates the foldable class on the correct line numbers when the foldable positions change", -> + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(0, 'foldable')).toBe false + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe true + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe false + expect(lineNumberHasClass(5, 'foldable')).toBe true + expect(lineNumberHasClass(6, 'foldable')).toBe false - it "adds, updates and removes the folded class on the correct line number nodes", -> - editor.foldBufferRow(4) - expect(lineNumberHasClass(4, 'folded')).toBe true + it "adds, updates and removes the folded class on the correct line number nodes", -> + editor.foldBufferRow(4) + expect(lineNumberHasClass(4, 'folded')).toBe true - editor.getBuffer().insert([0, 0], '\n') - expect(lineNumberHasClass(4, 'folded')).toBe false - expect(lineNumberHasClass(5, 'folded')).toBe true + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(4, 'folded')).toBe false + expect(lineNumberHasClass(5, 'folded')).toBe true - editor.unfoldBufferRow(5) - expect(lineNumberHasClass(5, 'folded')).toBe false + editor.unfoldBufferRow(5) + expect(lineNumberHasClass(5, 'folded')).toBe false + + describe "mouse interactions with fold indicators", -> + [gutterNode] = [] + + buildClickEvent = (target) -> + # FIXME: I could not get the simulated event to set the target. I tried several things, and was unable to get it set. + # buildMouseEvent('click', {target}) + {target, type: 'click', bubbles: true, cancelable: true} + + beforeEach -> + gutterNode = node.querySelector('.gutter') + + it "folds and unfolds the block represented by the fold indicator when clicked", -> + lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 + expect(lineNumberHasClass(lineNumber, 'folded')).toBe false + + target = lineNumber.querySelector('.icon-right') + # target.dispatchEvent(buildClickEvent(target)) + component.onClickGutter(buildClickEvent(target)) + lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 + expect(lineNumberHasClass(lineNumber, 'folded')).toBe true + + target = lineNumber.querySelector('.icon-right') + # target.dispatchEvent(buildClickEvent(target)) + component.onClickGutter(buildClickEvent(target)) + lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 + expect(lineNumberHasClass(lineNumber, 'folded')).toBe false + + it "does not fold when the line number node is clicked", -> + lineNumber = gutterNode.querySelectorAll('.line-number')[2] + component.onClickGutter(buildClickEvent(lineNumber)) + lineNumber = gutterNode.querySelectorAll('.line-number')[2] + expect(lineNumberHasClass(lineNumber, 'folded')).toBe false describe "when decorations are used", -> describe "when decorations are applied to buffer rows", -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1810cf711..d278c66c6 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -71,7 +71,7 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, + ref: 'gutter', onClick: @onClickGutter, editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow, decorations } @@ -479,6 +479,19 @@ EditorComponent = React.createClass @selectToMousePositionUntilMouseUp(event) + onClickGutter: (event) -> + console.log 'gutter click', event + {editor} = @props + {target} = event + lineNumber = target.parentNode + + if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') + bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) + if lineNumber.classList.contains('folded') + editor.unfoldBufferRow(bufferRow) + else + editor.foldBufferRow(bufferRow) + onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 822d43393..c9e8c601b 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -14,9 +14,9 @@ GutterComponent = React.createClass dummyLineNumberNode: null render: -> - {scrollHeight, scrollTop} = @props + {scrollHeight, scrollTop, onClick} = @props - div className: 'gutter', + div className: 'gutter', onClick: onClick, div className: 'line-numbers', ref: 'lineNumbers', style: height: scrollHeight WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" From d9e731c84a65d6873f670d2f61b934bc2c5c61ac Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 13:07:47 -0700 Subject: [PATCH 25/45] Update styles on the foldable icons --- static/editor.less | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/static/editor.less b/static/editor.less index 4898967cc..8773962ee 100644 --- a/static/editor.less +++ b/static/editor.less @@ -69,11 +69,13 @@ .gutter { .line-number { white-space: nowrap; - padding: 0 .5em; + padding-left: .5em; .icon-right { - padding: 0; - padding-left: .1em; + padding: 0 .4em; + &:before { + text-align: center; + } } } } @@ -121,7 +123,7 @@ visibility: hidden; padding-left: .1em; padding-right: .5em; - opacity: .7; + opacity: .6; } .editor .gutter:hover .line-number.foldable .icon-right { From e7bd8026d2bf6a8d3e5647cd3bd1293bccf1a6be Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 13:33:43 -0700 Subject: [PATCH 26/45] Deprecate old class functions --- src/react-editor-view.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index aeceb4186..2852e5685 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,4 +1,5 @@ {View, $} = require 'space-pen' +Grim = require 'Grim' React = require 'react-atom-fork' EditorComponent = require './editor-component' {defaults} = require 'underscore-plus' @@ -53,9 +54,11 @@ class ReactEditorView extends View @gutter = $(node).find('.gutter') @gutter.removeClassFromAllLines = (klass) => + Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::removeDecorationFromBufferRow()` and related functions' @gutter.find('.line-number').removeClass(klass) @gutter.addClassToLine = (bufferRow, klass) => + Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::addDecorationToBufferRow()` and related functions' lines = @gutter.find("[data-buffer-row='#{bufferRow}']") lines.addClass(klass) lines.length > 0 From 65ab436da2a7b66e8ddf2382d651bf8dd638b0ac Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 14:13:57 -0700 Subject: [PATCH 27/45] API docs --- src/editor.coffee | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/editor.coffee b/src/editor.coffee index 86e7a758b..64bacc92b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1058,27 +1058,103 @@ class Editor extends Model selection.insertText(fn(text)) selection.setBufferRange(range) + # Public: Get all the decorations for a buffer row. + # + # bufferRow - the {int} buffer row + # decorationType - the {String} decoration type to filter by eg. 'gutter' + # + # Returns an {Array} of decorations in the form `[{type: 'gutter', class: 'someclass'}, ...]` + # Returns an empty array when no decorations are found decorationsForBufferRow: (bufferRow, decorationType) -> @displayBuffer.decorationsForBufferRow(bufferRow, decorationType) + # Public: Get all the decorations for a range of buffer rows (inclusive) + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decorationType - the {String} decoration type to filter by eg. 'gutter' + # + # Returns an {Object} of decorations in the form `{23: [{type: 'gutter', class: 'someclass'}, ...], 24: [...]}` + # Returns an {Object} with keyed with all buffer rows in the range containing empty {Array}s when no decorations are found decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> @displayBuffer.decorationsForBufferRowRange(startBufferRow, endBufferRow, decorationType) + # Public: Adds a decoration to a buffer row. For example, use to mark a gutter + # line number with a class by using the form `{type: 'gutter', class: 'linter-error'}` + # + # bufferRow - the {int} buffer row + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing addDecorationToBufferRow: (bufferRow, decoration) -> @displayBuffer.addDecorationToBufferRow(bufferRow, decoration) + # Public: Removes a decoration from a buffer row. + # + # ```coffee + # editor.removeDecorationFromBufferRow(2, {type: 'gutter', class: 'linter-error'}) + # ``` + # + # All decorations matching a pattern will be removed. For example, you might + # have decorations with a namespace like this attached to a row: + # + # ```coffee + # [ + # {type: 'gutter', namespace: 'myns', class: 'something'}, + # {type: 'gutter', namespace: 'myns', class: 'something-else'} + # ] + # ``` + # + # You can remove both with: + # + # ```coffee + # editor.removeDecorationFromBufferRow(2, {namespace: 'myns'}) + # ``` + # + # bufferRow - the {int} buffer row + # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns an {Array} of the removed decorations removeDecorationFromBufferRow: (bufferRow, decoration) -> @displayBuffer.removeDecorationFromBufferRow(bufferRow, decoration) + # Public: Adds a decoration to line numbers in a buffer row range + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) + # Public: Removes a decoration from line numbers in a buffer row range + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) + # Public: Adds a decoration that tracks a {Marker}. When the marker moves, + # is invalidated, or is destroyed, the decoration will be updated to reflect the marker's state. + # + # marker - the {Marker} you want this decoration to follow + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing addDecorationForMarker: (marker, decoration) -> @displayBuffer.addDecorationForMarker(marker, decoration) + # Public: Removes all decorations associated with a {Marker} that match a + # `decorationPattern` and stop tracking the {Marker}. + # + # marker - the {Marker} to detach from + # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing removeDecorationForMarker: (marker, decoration) -> @displayBuffer.removeDecorationForMarker(marker, decoration) From e8594ccec4cd3287e222c796a99f849ba501c646 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 14:15:11 -0700 Subject: [PATCH 28/45] :lipstick: Change var names for consistency --- src/display-buffer.coffee | 13 ++++++++----- src/editor.coffee | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index e729ebdb3..6ab46b431 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -738,22 +738,25 @@ class DisplayBuffer extends Model @decorations[bufferRow].push(decoration) @emit 'decoration-changed', {bufferRow, decoration, action: 'add'} - removeDecorationFromBufferRow: (bufferRow, decoration) -> + removeDecorationFromBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] - removed = @findDecorationsForBufferRow(bufferRow, decoration) + removed = @findDecorationsForBufferRow(bufferRow, decorationPattern) @decorations[bufferRow] = _.without(@decorations[bufferRow], removed...) for decoration in removed @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} + removed addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> for bufferRow in [startBufferRow..endBufferRow] @addDecorationToBufferRow(bufferRow, decoration) + return removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> for bufferRow in [startBufferRow..endBufferRow] @removeDecorationFromBufferRow(bufferRow, decoration) + return findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] @@ -786,15 +789,15 @@ class DisplayBuffer extends Model @decorationMarkerSubscriptions[marker.id] ?= [] @decorationMarkerSubscriptions[marker.id].push {decoration, changedSubscription, destroyedSubscription} - removeDecorationForMarker: (marker, decoration) -> + removeDecorationForMarker: (marker, decorationPattern) -> return unless @decorationMarkerSubscriptions[marker.id]? startRow = marker.getStartBufferPosition().row endRow = marker.getEndBufferPosition().row - @removeDecorationFromBufferRowRange(startRow, endRow, decoration) + @removeDecorationFromBufferRowRange(startRow, endRow, decorationPattern) for subscription in _.clone(@decorationMarkerSubscriptions[marker.id]) - if @decorationMatchesPattern(subscription.decoration, decoration) + if @decorationMatchesPattern(subscription.decoration, decorationPattern) subscription.changedSubscription.off() subscription.destroyedSubscription.off() @decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], subscription) diff --git a/src/editor.coffee b/src/editor.coffee index 64bacc92b..f01d1ec46 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1115,8 +1115,8 @@ class Editor extends Model # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` # # Returns an {Array} of the removed decorations - removeDecorationFromBufferRow: (bufferRow, decoration) -> - @displayBuffer.removeDecorationFromBufferRow(bufferRow, decoration) + removeDecorationFromBufferRow: (bufferRow, decorationPattern) -> + @displayBuffer.removeDecorationFromBufferRow(bufferRow, decorationPattern) # Public: Adds a decoration to line numbers in a buffer row range # @@ -1155,8 +1155,8 @@ class Editor extends Model # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` # # Returns nothing - removeDecorationForMarker: (marker, decoration) -> - @displayBuffer.removeDecorationForMarker(marker, decoration) + removeDecorationForMarker: (marker, decorationPattern) -> + @displayBuffer.removeDecorationForMarker(marker, decorationPattern) # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> From 049531e49599dc8b3c8fcbb8f448dc69ff8fd853 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 14:18:33 -0700 Subject: [PATCH 29/45] Add comment --- src/display-buffer.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 6ab46b431..b805ec4f0 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -758,6 +758,12 @@ class DisplayBuffer extends Model @removeDecorationFromBufferRow(bufferRow, decoration) return + # Finds all decorations on a buffer row that match a `decorationPattern` + # + # bufferRow - the {int} buffer row + # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns an {Array} of the matching decorations findDecorationsForBufferRow: (bufferRow, decorationPattern) -> return unless @decorations[bufferRow] decoration for decoration in @decorations[bufferRow] when @decorationMatchesPattern(decoration, decorationPattern) From f30641da44c3e6f544f9184bd180570a4b5b52bc Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 14:18:45 -0700 Subject: [PATCH 30/45] :lipstick: Remove log line. --- src/editor-component.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index d278c66c6..189e3954a 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -480,7 +480,6 @@ EditorComponent = React.createClass @selectToMousePositionUntilMouseUp(event) onClickGutter: (event) -> - console.log 'gutter click', event {editor} = @props {target} = event lineNumber = target.parentNode From a8df77243c75603535000eefc3468e6ed019f9e4 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 14:28:27 -0700 Subject: [PATCH 31/45] Fix spec I changed the width of the gutter in b0af7cfc12729e9ef1320c0b178cc024bc0e60cc 16 characters is still within the break range of the word 'wraps' --- spec/editor-component-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index ea23d083a..3668f83f6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -165,7 +165,7 @@ describe "EditorComponent", -> beforeEach -> editor.setText "a line that wraps " editor.setSoftWrap(true) - node.style.width = 15 * charWidth + 'px' + node.style.width = 16 * charWidth + 'px' component.measureScrollView() it "doesn't show end of line invisibles at the end of wrapped lines", -> From 346b6007ca6a88c73fc659985db8a48cb52bc1cd Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 15:44:59 -0700 Subject: [PATCH 32/45] Allow for typeless decorations that apply to everything MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you are querying for `type: ‘gutter’` it will return the typeless decorations as well. --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b805ec4f0..8024edcc5 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -722,7 +722,7 @@ class DisplayBuffer extends Model decorationsForBufferRow: (bufferRow, decorationType) -> decorations = @decorations[bufferRow] ? [] - decorations = (dec for dec in decorations when dec.type is decorationType) if decorationType? + decorations = (dec for dec in decorations when not dec.type? or dec.type is decorationType) if decorationType? decorations decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> From 312901ff68d758da23d91a718481966ac3edef76 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 15:45:55 -0700 Subject: [PATCH 33/45] Use decorations for folds. They are more efficient when re-rendering. --- src/display-buffer.coffee | 2 ++ src/editor-component.coffee | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 8024edcc5..b5b6f1725 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1053,6 +1053,8 @@ class DisplayBuffer extends Model @emit 'marker-created', @getMarker(marker.id) createFoldForMarker: (marker) -> + bufferMarker = new DisplayBufferMarker({bufferMarker: marker, displayBuffer: this}) + @addDecorationForMarker(bufferMarker, type: 'gutter', class: 'folded') new Fold(this, marker) foldForMarker: (marker) -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 189e3954a..aa2a96629 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -237,7 +237,6 @@ EditorComponent = React.createClass for bufferRow in bufferRows decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter') decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow) - decorations[bufferRow].push {class: 'folded'} if editor.isFoldedAtBufferRow(bufferRow) decorations observeEditor: -> From 13be8d5139e05146a836fc51309f2936bfbb175c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 15:46:13 -0700 Subject: [PATCH 34/45] Add a cursor-line decoration to the gutter --- src/editor.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor.coffee b/src/editor.coffee index f01d1ec46..e2e314a4e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1263,6 +1263,7 @@ class Editor extends Model addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker) @cursors.push(cursor) + @addDecorationForMarker(marker, {class: 'cursor-line'}) @emit 'cursor-added', cursor cursor From 1a1ed56419836d507bfd51efcf8dfe1faf1e3f32 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 16:40:29 -0700 Subject: [PATCH 35/45] Oh man. Render only once! --- src/editor-component.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index aa2a96629..cb5275ebd 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -538,6 +538,7 @@ EditorComponent = React.createClass @cursorsMoved = true onDecorationChanged: -> + return if @decorationChangedImmediate? @decorationChangedImmediate = setImmediate => @requestUpdate() @decorationChangedImmediate = null From 31b4b7a372170df84465bffeb14fdf7cb1bcee7d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 6 Jun 2014 16:41:15 -0700 Subject: [PATCH 36/45] Speed up decoration removal and use less temp objects. --- src/display-buffer.coffee | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b5b6f1725..c8b3ed97c 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -739,15 +739,23 @@ class DisplayBuffer extends Model @emit 'decoration-changed', {bufferRow, decoration, action: 'add'} removeDecorationFromBufferRow: (bufferRow, decorationPattern) -> - return unless @decorations[bufferRow] + return unless decorations = @decorations[bufferRow] - removed = @findDecorationsForBufferRow(bufferRow, decorationPattern) - @decorations[bufferRow] = _.without(@decorations[bufferRow], removed...) + removed = [] + i = decorations.length - 1 + while i >= 0 + if @decorationMatchesPattern(decorations[i], decorationPattern) + removed.push decorations[i] + decorations.splice(i, 1) + i-- + + delete @decorations[bufferRow] unless @decorations[bufferRow]? for decoration in removed @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} removed + addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> for bufferRow in [startBufferRow..endBufferRow] @addDecorationToBufferRow(bufferRow, decoration) @@ -758,18 +766,11 @@ class DisplayBuffer extends Model @removeDecorationFromBufferRow(bufferRow, decoration) return - # Finds all decorations on a buffer row that match a `decorationPattern` - # - # bufferRow - the {int} buffer row - # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` - # - # Returns an {Array} of the matching decorations - findDecorationsForBufferRow: (bufferRow, decorationPattern) -> - return unless @decorations[bufferRow] - decoration for decoration in @decorations[bufferRow] when @decorationMatchesPattern(decoration, decorationPattern) - decorationMatchesPattern: (decoration, decorationPattern) -> - _.isEqual(decorationPattern, _.pick(decoration, _.keys(decorationPattern))) + return false unless decoration? and decorationPattern? + for key, value of decorationPattern + return false if decoration[key] != value + true addDecorationForMarker: (marker, decoration) -> startRow = marker.getStartBufferPosition().row From b5532ee4a370dc4af327613bd35558932805aee8 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 13:56:23 -0700 Subject: [PATCH 37/45] :lipstick: spec Use the event system to click on the gutter --- spec/editor-component-spec.coffee | 52 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 3668f83f6..336326fd9 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -228,14 +228,12 @@ describe "EditorComponent", -> [node] describe "gutter rendering", -> - {lineNumberHasClass, gutter} = {} + [lineNumberHasClass, gutter] = [] + beforeEach -> {gutter} = component.refs lineNumberHasClass = (screenRow, klass) -> - if screenRow.classList? - screenRow.classList.contains(klass) - else - component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' @@ -346,34 +344,29 @@ describe "EditorComponent", -> [gutterNode] = [] buildClickEvent = (target) -> - # FIXME: I could not get the simulated event to set the target. I tried several things, and was unable to get it set. - # buildMouseEvent('click', {target}) - {target, type: 'click', bubbles: true, cancelable: true} + buildMouseEvent('click', {target}) beforeEach -> gutterNode = node.querySelector('.gutter') it "folds and unfolds the block represented by the fold indicator when clicked", -> - lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 - expect(lineNumberHasClass(lineNumber, 'folded')).toBe false + expect(lineNumberHasClass(1, 'folded')).toBe false + lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') - # target.dispatchEvent(buildClickEvent(target)) - component.onClickGutter(buildClickEvent(target)) - lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 - expect(lineNumberHasClass(lineNumber, 'folded')).toBe true + target.dispatchEvent(buildClickEvent(target)) + expect(lineNumberHasClass(1, 'folded')).toBe true + + lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') - # target.dispatchEvent(buildClickEvent(target)) - component.onClickGutter(buildClickEvent(target)) - lineNumber = gutterNode.querySelectorAll('.line-number')[2] # bufferRow 1 - expect(lineNumberHasClass(lineNumber, 'folded')).toBe false + + target.dispatchEvent(buildClickEvent(target)) + expect(lineNumberHasClass(1, 'folded')).toBe false it "does not fold when the line number node is clicked", -> - lineNumber = gutterNode.querySelectorAll('.line-number')[2] - component.onClickGutter(buildClickEvent(lineNumber)) - lineNumber = gutterNode.querySelectorAll('.line-number')[2] - expect(lineNumberHasClass(lineNumber, 'folded')).toBe false + component.onClickGutter(buildClickEvent(component.lineNumberNodeForScreenRow(1))) + expect(lineNumberHasClass(1, 'folded')).toBe false describe "when decorations are used", -> describe "when decorations are applied to buffer rows", -> @@ -854,12 +847,6 @@ describe "EditorComponent", -> clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() {clientX, clientY} - buildMouseEvent = (type, properties...) -> - properties = extend({bubbles: true, cancelable: true}, properties...) - event = new MouseEvent(type, properties) - Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? - event - describe "focus handling", -> inputNode = null @@ -1164,3 +1151,12 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) wrapperView.show() expect(node.querySelector('.cursor').style['-webkit-transform']).toBe "translate3d(#{9 * charWidth}px, 0px, 0px)" + + buildMouseEvent = (type, properties...) -> + properties = extend({bubbles: true, cancelable: true}, properties...) + event = new MouseEvent(type, properties) + Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? + if properties.target? + Object.defineProperty(event, 'target', get: -> properties.target) + Object.defineProperty(event, 'srcObject', get: -> properties.target) + event From bae625a89445aba72fbea13732c37b6e73466739 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 14:03:53 -0700 Subject: [PATCH 38/45] Add spec for when lines become foldable --- spec/editor-component-spec.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 336326fd9..b39f657b6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -329,6 +329,15 @@ describe "EditorComponent", -> expect(lineNumberHasClass(5, 'foldable')).toBe true expect(lineNumberHasClass(6, 'foldable')).toBe false + it "updates the foldable class on a line number that becomes foldable", -> + expect(lineNumberHasClass(11, 'foldable')).toBe false + + editor.getBuffer().insert([11, 44], '\n fold me') + expect(lineNumberHasClass(11, 'foldable')).toBe true + + editor.undo() + expect(lineNumberHasClass(11, 'foldable')).toBe false + it "adds, updates and removes the folded class on the correct line number nodes", -> editor.foldBufferRow(4) expect(lineNumberHasClass(4, 'folded')).toBe true From e59f242f19123df2dea9b3dd5cf8a76073219126 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 14:11:41 -0700 Subject: [PATCH 39/45] Move click gutter into the GutterComponent --- spec/editor-component-spec.coffee | 3 ++- src/editor-component.coffee | 14 +------------- src/gutter-component.coffee | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index b39f657b6..f04f8f8c8 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -374,7 +374,8 @@ describe "EditorComponent", -> expect(lineNumberHasClass(1, 'folded')).toBe false it "does not fold when the line number node is clicked", -> - component.onClickGutter(buildClickEvent(component.lineNumberNodeForScreenRow(1))) + lineNumber = component.lineNumberNodeForScreenRow(1) + lineNumber.dispatchEvent(buildClickEvent(lineNumber)) expect(lineNumberHasClass(1, 'folded')).toBe false describe "when decorations are used", -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index cb5275ebd..48faf84a7 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -71,7 +71,7 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - ref: 'gutter', onClick: @onClickGutter, editor, renderedRowRange, maxLineNumberDigits, scrollTop, + ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow, decorations } @@ -478,18 +478,6 @@ EditorComponent = React.createClass @selectToMousePositionUntilMouseUp(event) - onClickGutter: (event) -> - {editor} = @props - {target} = event - lineNumber = target.parentNode - - if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') - bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) - if lineNumber.classList.contains('folded') - editor.unfoldBufferRow(bufferRow) - else - editor.foldBufferRow(bufferRow) - onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index c9e8c601b..87f96dd76 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -14,9 +14,9 @@ GutterComponent = React.createClass dummyLineNumberNode: null render: -> - {scrollHeight, scrollTop, onClick} = @props + {scrollHeight, scrollTop} = @props - div className: 'gutter', onClick: onClick, + div className: 'gutter', onClick: @onClick, div className: 'line-numbers', ref: 'lineNumbers', style: height: scrollHeight WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" @@ -180,6 +180,18 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] + onClick: (event) -> + {editor} = @props + {target} = event + lineNumber = target.parentNode + + if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') + bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) + if lineNumber.classList.contains('folded') + editor.unfoldBufferRow(bufferRow) + else + editor.foldBufferRow(bufferRow) + # Created because underscore uses === not _.isEqual, which we need contains = (array, target) -> return false unless array? From ad522e6ab1c0d5f4545ea1af756a67fb42d2ee21 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 14:40:59 -0700 Subject: [PATCH 40/45] Move setImmediate into requestUpdate; Batch updates --- src/editor-component.coffee | 9 ++++----- src/editor.coffee | 6 ++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 48faf84a7..6eb5c319c 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -177,7 +177,9 @@ EditorComponent = React.createClass if @batchingUpdates @updateRequested = true else - @forceUpdate() + @willUpdate ?= setImmediate => + @forceUpdate() + @willUpdate = null getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props @@ -526,10 +528,7 @@ EditorComponent = React.createClass @cursorsMoved = true onDecorationChanged: -> - return if @decorationChangedImmediate? - @decorationChangedImmediate = setImmediate => - @requestUpdate() - @decorationChangedImmediate = null + @requestUpdate() selectToMousePositionUntilMouseUp: (event) -> {editor} = @props diff --git a/src/editor.coffee b/src/editor.coffee index e2e314a4e..d5f4bfc21 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1126,7 +1126,8 @@ class Editor extends Model # # Returns nothing addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) + @batchUpdates => + @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) # Public: Removes a decoration from line numbers in a buffer row range # @@ -1136,7 +1137,8 @@ class Editor extends Model # # Returns nothing removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) + @batchUpdates => + @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) # Public: Adds a decoration that tracks a {Marker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect the marker's state. From 6c609cb7d2afc8062c6744ac80a1f40eacfc27b6 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 14:45:43 -0700 Subject: [PATCH 41/45] Revert "Move setImmediate into requestUpdate; Batch updates" This reverts commit ad522e6ab1c0d5f4545ea1af756a67fb42d2ee21. --- src/editor-component.coffee | 9 +++++---- src/editor.coffee | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 6eb5c319c..48faf84a7 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -177,9 +177,7 @@ EditorComponent = React.createClass if @batchingUpdates @updateRequested = true else - @willUpdate ?= setImmediate => - @forceUpdate() - @willUpdate = null + @forceUpdate() getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props @@ -528,7 +526,10 @@ EditorComponent = React.createClass @cursorsMoved = true onDecorationChanged: -> - @requestUpdate() + return if @decorationChangedImmediate? + @decorationChangedImmediate = setImmediate => + @requestUpdate() + @decorationChangedImmediate = null selectToMousePositionUntilMouseUp: (event) -> {editor} = @props diff --git a/src/editor.coffee b/src/editor.coffee index d5f4bfc21..e2e314a4e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1126,8 +1126,7 @@ class Editor extends Model # # Returns nothing addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - @batchUpdates => - @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) + @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) # Public: Removes a decoration from line numbers in a buffer row range # @@ -1137,8 +1136,7 @@ class Editor extends Model # # Returns nothing removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> - @batchUpdates => - @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) + @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) # Public: Adds a decoration that tracks a {Marker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect the marker's state. From 2087426afc1fb9e9ea7e7dbdb0c2c9c2cb757102 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 15:05:43 -0700 Subject: [PATCH 42/45] Specs for decorationsForBufferRow --- spec/editor-spec.coffee | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 5a7fffe3a..ec0bdd76e 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -3266,3 +3266,53 @@ describe "Editor", -> expect(editor.decorationsForBufferRow 3).not.toContain decoration expect(editor.decorationsForBufferRow 4).not.toContain decoration expect(editor.decorationsForBufferRow 5).not.toContain decoration + + describe "decorationsForBufferRow", -> + one = {type: 'one', class: 'one'} + two = {type: 'two', class: 'two'} + typeless = {class: 'typeless'} + + beforeEach -> + editor.addDecorationToBufferRow(2, one) + editor.addDecorationToBufferRow(2, two) + editor.addDecorationToBufferRow(2, typeless) + + it "returns all decorations with no decorationType specified", -> + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toContain one + expect(decorations).toContain two + expect(decorations).toContain typeless + + it "returns typeless decorations with all decorationTypes", -> + decorations = editor.decorationsForBufferRow(2, 'one') + expect(decorations).toContain one + expect(decorations).not.toContain two + expect(decorations).toContain typeless + + describe "decorationsForBufferRowRange", -> + one = {type: 'one', class: 'one'} + two = {type: 'two', class: 'two'} + typeless = {class: 'typeless'} + + it "returns an object of decorations based on the decorationType", -> + editor.addDecorationToBufferRow(2, one) + editor.addDecorationToBufferRow(3, one) + editor.addDecorationToBufferRow(5, one) + + editor.addDecorationToBufferRow(3, two) + editor.addDecorationToBufferRow(4, two) + + editor.addDecorationToBufferRow(3, typeless) + editor.addDecorationToBufferRow(5, typeless) + + decorations = editor.decorationsForBufferRowRange(2, 5, 'one') + expect(decorations[2]).toContain one + + expect(decorations[3]).toContain one + expect(decorations[3]).not.toContain two + expect(decorations[3]).toContain typeless + + expect(decorations[4]).toHaveLength 0 + + expect(decorations[5]).toContain one + expect(decorations[5]).toContain typeless From dc6836dc2dff6eb1ae6981c944a133cb43f67c8c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 15:18:12 -0700 Subject: [PATCH 43/45] Add specs for cursor-line decorations --- spec/editor-component-spec.coffee | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index f04f8f8c8..764fafd60 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -378,6 +378,47 @@ describe "EditorComponent", -> lineNumber.dispatchEvent(buildClickEvent(lineNumber)) expect(lineNumberHasClass(1, 'folded')).toBe false + describe "cursor-line decorations", -> + cursor = null + beforeEach -> + cursor = editor.getCursor() + + it "modifies the cursor-line decoration when the cursor moves", -> + cursor.setScreenPosition([0, 0]) + expect(lineNumberHasClass(0, 'cursor-line')).toBe true + + cursor.setScreenPosition([1, 0]) + expect(lineNumberHasClass(0, 'cursor-line')).toBe false + expect(lineNumberHasClass(1, 'cursor-line')).toBe true + + it "updates cursor-line decorations for multiple cursors", -> + cursor.setScreenPosition([2, 0]) + cursor2 = editor.addCursorAtScreenPosition([8, 0]) + cursor3 = editor.addCursorAtScreenPosition([10, 0]) + + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe true + expect(lineNumberHasClass(10, 'cursor-line')).toBe true + + cursor2.destroy() + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe false + expect(lineNumberHasClass(10, 'cursor-line')).toBe true + + cursor3.destroy() + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe false + expect(lineNumberHasClass(10, 'cursor-line')).toBe false + + it "adds cursor-line decorations to multiple lines when a selection is performed", -> + cursor.setScreenPosition([1, 0]) + editor.selectDown(2) + expect(lineNumberHasClass(0, 'cursor-line')).toBe false + expect(lineNumberHasClass(1, 'cursor-line')).toBe true + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(3, 'cursor-line')).toBe true + expect(lineNumberHasClass(4, 'cursor-line')).toBe false + describe "when decorations are used", -> describe "when decorations are applied to buffer rows", -> it "renders line number classes based on the decorations on their buffer row", -> From 756347a716ce5e7ea0b2230e44ae4fabdeb7d2c9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 15:45:32 -0700 Subject: [PATCH 44/45] Add `has-selection` class to the editor div when there is a selection --- spec/editor-component-spec.coffee | 16 ++++++++++++++++ src/editor-component.coffee | 2 ++ 2 files changed, 18 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 764fafd60..cf3e0a06e 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -916,6 +916,22 @@ describe "EditorComponent", -> inputNode.blur() expect(node.classList.contains('is-focused')).toBe false + describe "selection handling", -> + cursor = null + + beforeEach -> + cursor = editor.getCursor() + cursor.setScreenPosition([0, 0]) + + it "adds the 'has-selection' class to the editor when there is a selection", -> + expect(node.classList.contains('has-selection')).toBe false + + editor.selectDown() + expect(node.classList.contains('has-selection')).toBe true + + cursor.moveDown() + expect(node.classList.contains('has-selection')).toBe false + describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 48faf84a7..591e9379e 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -43,6 +43,7 @@ EditorComponent = React.createClass {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length invisibles = if showInvisibles then @state.invisibles else {} + hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() if @isMounted() renderedRowRange = @getRenderedRowRange() @@ -68,6 +69,7 @@ EditorComponent = React.createClass className = 'editor-contents editor-colors' className += ' is-focused' if focused + className += ' has-selection' if hasSelection div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { From 5db163a328aad18d023ffebb336da5b7b0d8bc6f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 16:01:29 -0700 Subject: [PATCH 45/45] :lipstick: --- src/editor-component.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 591e9379e..a0f731264 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -528,8 +528,7 @@ EditorComponent = React.createClass @cursorsMoved = true onDecorationChanged: -> - return if @decorationChangedImmediate? - @decorationChangedImmediate = setImmediate => + @decorationChangedImmediate ?= setImmediate => @requestUpdate() @decorationChangedImmediate = null