From aa2868efbf05f9ac9d72ffee4d0372ce7ad52216 Mon Sep 17 00:00:00 2001 From: batjko Date: Mon, 9 Jun 2014 14:39:00 +0100 Subject: [PATCH 01/70] Classic copy/paste added to Linux and Win32 Ctrl+Insert: Copy Shift+Insert: Paste --- keymaps/linux.cson | 2 ++ keymaps/win32.cson | 2 ++ 2 files changed, 4 insertions(+) diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 374742256..dbb879efc 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -29,6 +29,8 @@ 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' + 'ctrl-insert': 'core:copy' + 'shift-insert': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 2e94af6ba..9a46e7933 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -31,6 +31,8 @@ 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' + 'ctrl-insert': 'core:copy' + 'shift-insert': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' From a3e85d67581e616fc7eb74a5a1c335a957355314 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 09:17:27 -0700 Subject: [PATCH 02/70] Upgrade to first-mate 1.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7c48de18b..22e545f81 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "coffeestack": "0.7.0", "delegato": "^1", "emissary": "^1.2.1", - "first-mate": "^1.6.1", + "first-mate": "^1.7", "fs-plus": "^2.2.3", "fstream": "0.1.24", "fuzzaldrin": "^1.1", From 7ae6cba3372284218a371872981c5c9b403c3be4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 09:32:06 -0700 Subject: [PATCH 03/70] Upgrade to tree-view@0.98 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 22e545f81..8f61a2344 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "symbols-view": "0.55.0", "tabs": "0.41.0", "timecop": "0.19.0", - "tree-view": "0.97.0", + "tree-view": "0.98.0", "update-package-dependencies": "0.6.0", "welcome": "0.16.0", "whitespace": "0.22.0", From d0639393caa6523115dd3fac066480f44b3ed49c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Mon, 9 Jun 2014 09:52:11 -0700 Subject: [PATCH 04/70] 0.103.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f61a2344..de42b1da6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.102.0", + "version": "0.103.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { From 6bf97f7a1ae72b62454ce2c5ed0f7dc721a26c4b Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 10:57:31 -0700 Subject: [PATCH 05/70] Upgrade to find-and-replace@0.114.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de42b1da6..7b85cef46 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.31.0", "exception-reporting": "0.18.0", "feedback": "0.33.0", - "find-and-replace": "0.113.0", + "find-and-replace": "0.114.0", "fuzzy-finder": "0.54.0", "git-diff": "0.28.0", "go-to-line": "0.22.0", From 389b5c7891e9b621bd09a869ab389ca8fc9db2db Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 11:00:01 -0700 Subject: [PATCH 06/70] Remove script/install-cli The CommandInstaller class now has native module dependencies that are compiled with apm so invoking it directly from node is no longer possible. This can still be done using the grunt install task. Closes #2555 --- script/install-cli | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100755 script/install-cli diff --git a/script/install-cli b/script/install-cli deleted file mode 100755 index c24835852..000000000 --- a/script/install-cli +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env coffee - -path = require 'path' -CommandInstaller = require '../src/command-installer' - -callback = (error) -> - console.warn error.message if error? - -CommandInstaller.installAtomCommand(path.resolve(__dirname, '..'), callback) -CommandInstaller.installApmCommand(path.resolve(__dirname, '..'), callback) From 8cf9e1990b5690e186c1b7600b7b5547c43dd428 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 Jun 2014 13:47:36 -0600 Subject: [PATCH 07/70] Add ReactEditorView::getModel shim --- src/react-editor-view.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 132bb82c2..aeceb4186 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -16,6 +16,8 @@ class ReactEditorView extends View getEditor: -> @editor + getModel: -> @editor + Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels() Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth() Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] From d8d378e0832d52d505bc92367b61a32bc010911a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 Jun 2014 13:58:09 -0600 Subject: [PATCH 08/70] Revert "Upgrade space-pen now that problem with 3.2.4 is fixed" This reverts commit 7dcafb44f15e21d3928d6d1eec172e6970b5f5bb. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b85cef46..8852c4591 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "season": "^1.0.2", "semver": "1.1.4", "serializable": "^1", - "space-pen": "3.3.0", + "space-pen": "3.2.0", "temp": "0.5.0", "text-buffer": "^2.2.2", "theorist": "^1", From 6a9faee1099df4245671340a349779d4e490a36a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 14:01:14 -0700 Subject: [PATCH 09/70] Prepare 0.104 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8852c4591..06cb564e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.103.0", + "version": "0.104.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { From b23009a8f3c8a4aa8ba3a961f1c715e58b6d1a92 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 3 Jun 2014 17:26:57 -0700 Subject: [PATCH 10/70] 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 11/70] 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 12/70] 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 13/70] 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 14/70] 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 15/70] 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 16/70] 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 17/70] 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 18/70] 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 19/70] 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 20/70] 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 21/70] 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 22/70] :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 23/70] :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 24/70] 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 25/70] 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 26/70] :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 27/70] 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 28/70] 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 29/70] 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 30/70] 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 31/70] 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 32/70] :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 33/70] 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 34/70] 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 35/70] 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 36/70] 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 37/70] :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 38/70] 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 39/70] :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 40/70] 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 41/70] 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 42/70] 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 43/70] 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 44/70] 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 45/70] 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 46/70] :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 47/70] 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 48/70] 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 49/70] 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 50/70] 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 b703ca0ebf458a4acc6b70b5946df6987843d9ac Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 15:04:32 -0700 Subject: [PATCH 51/70] Upgrade to markdown-preview@0.76 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06cb564e3..761057fcc 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "image-view": "0.34.0", "keybinding-resolver": "0.18.0", "link": "0.22.0", - "markdown-preview": "0.74.0", + "markdown-preview": "0.76.0", "metrics": "0.32.0", "open-on-github": "0.28.0", "package-generator": "0.30.0", From 2087426afc1fb9e9ea7e7dbdb0c2c9c2cb757102 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 15:05:43 -0700 Subject: [PATCH 52/70] 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 53/70] 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 4b02d3ff61b988054871b4a9918991dd4dcda5a7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 15:33:47 -0700 Subject: [PATCH 54/70] Upgrade to tree-view@0.99 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 761057fcc..d7f6d8db8 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "symbols-view": "0.55.0", "tabs": "0.41.0", "timecop": "0.19.0", - "tree-view": "0.98.0", + "tree-view": "0.99.0", "update-package-dependencies": "0.6.0", "welcome": "0.16.0", "whitespace": "0.22.0", From 756347a716ce5e7ea0b2230e44ae4fabdeb7d2c9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 15:45:32 -0700 Subject: [PATCH 55/70] 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 4d12e025e40bb1bf981f3c892bd4dd7002393289 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 15:50:26 -0700 Subject: [PATCH 56/70] Upgrade to archive-view@0.32 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7f6d8db8..e00eb3d13 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "base16-tomorrow-dark-theme": "0.16.0", "solarized-dark-syntax": "0.17.0", "solarized-light-syntax": "0.8.0", - "archive-view": "0.31.0", + "archive-view": "0.32.0", "autocomplete": "0.28.0", "autoflow": "0.17.0", "autosave": "0.13.0", From 5db163a328aad18d023ffebb336da5b7b0d8bc6f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 16:01:29 -0700 Subject: [PATCH 57/70] :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 From 326542644d077d55459a6bf7fbb0eeceeba7feda Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 9 Jun 2014 16:33:27 -0700 Subject: [PATCH 58/70] Upgrade git-diff@0.29.0 bookmarks@0.23.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e00eb3d13..b4b8b821b 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "autoflow": "0.17.0", "autosave": "0.13.0", "background-tips": "0.14.0", - "bookmarks": "0.22.0", + "bookmarks": "0.23.0", "bracket-matcher": "0.43.0", "command-palette": "0.21.0", "deprecation-cop": "0.6.0", @@ -83,7 +83,7 @@ "feedback": "0.33.0", "find-and-replace": "0.114.0", "fuzzy-finder": "0.54.0", - "git-diff": "0.28.0", + "git-diff": "0.29.0", "go-to-line": "0.22.0", "grammar-selector": "0.27.0", "image-view": "0.34.0", From 3d28a04ffa00da7c2dc64a637cb6600730d6779f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 16:43:30 -0700 Subject: [PATCH 59/70] Upgrade to command-palette@0.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4b8b821b..f4ba6b145 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "background-tips": "0.14.0", "bookmarks": "0.23.0", "bracket-matcher": "0.43.0", - "command-palette": "0.21.0", + "command-palette": "0.22.0", "deprecation-cop": "0.6.0", "dev-live-reload": "0.31.0", "exception-reporting": "0.18.0", From 24a463b18d983b8e6f0a9e756269e6fd6e89b573 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 9 Jun 2014 17:08:57 -0700 Subject: [PATCH 60/70] Upgrade to find-and-replace@0.115 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4ba6b145..a7bee6ee9 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.31.0", "exception-reporting": "0.18.0", "feedback": "0.33.0", - "find-and-replace": "0.114.0", + "find-and-replace": "0.115.0", "fuzzy-finder": "0.54.0", "git-diff": "0.29.0", "go-to-line": "0.22.0", From e762efa97ee7dfdad4dc414cd0c97ff5492fb44a Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 10 Jun 2014 09:00:04 -0700 Subject: [PATCH 61/70] Upgrade to settings-view@0.120.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a7bee6ee9..1cbb89d2f 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "open-on-github": "0.28.0", "package-generator": "0.30.0", "release-notes": "0.32.0", - "settings-view": "0.119.0", + "settings-view": "0.120.0", "snippets": "0.45.0", "spell-check": "0.36.0", "status-bar": "0.40.0", From faada2de306e9fe1f33e055344d287e1da14861c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 09:17:37 -0700 Subject: [PATCH 62/70] :memo: Mention build-essential package Closes #2384 --- docs/build-instructions/linux.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index dfe3c51b2..1a59149e7 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -5,6 +5,8 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ## Requirements * OS with 64-bit or 32-bit architecture + * C++ build toolchain + * on Ubuntu/Debian: `sudo apt-get install build-essential` * [node.js](http://nodejs.org/download/) v0.10.x * [npm](http://www.npmjs.org/) v1.4.x * libgnome-keyring-dev @@ -15,7 +17,6 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * This command may require `sudo` depending on how you have [configured npm](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint-elementary-os). - ## Instructions ```sh From 636a8a82860f4f0184e88786f965801d7bdd39b3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 09:18:29 -0700 Subject: [PATCH 63/70] :memo: :lipstick: --- docs/build-instructions/linux.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 1a59149e7..32a126363 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -5,8 +5,8 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ## Requirements * OS with 64-bit or 32-bit architecture - * C++ build toolchain - * on Ubuntu/Debian: `sudo apt-get install build-essential` + * C++ toolchain + * on Ubuntu/Debian: `sudo apt-get install build-essential` * [node.js](http://nodejs.org/download/) v0.10.x * [npm](http://www.npmjs.org/) v1.4.x * libgnome-keyring-dev @@ -54,4 +54,5 @@ and restart Atom. If Atom now works fine, you can make this setting permanent: See also https://github.com/atom/atom/issues/2082. ### Linux build error reports in atom/atom -* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) to get a list of reports about build errors on Linux. +* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) + to get a list of reports about build errors on Linux. From 153faefaf907873c53cdae9ec1cb009c60b01893 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 09:26:06 -0700 Subject: [PATCH 64/70] :memo: Link to node.js wiki install instructions Closes #2340 --- docs/build-instructions/linux.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 32a126363..68f0374de 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -8,7 +8,11 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * C++ toolchain * on Ubuntu/Debian: `sudo apt-get install build-essential` * [node.js](http://nodejs.org/download/) v0.10.x - * [npm](http://www.npmjs.org/) v1.4.x + * [Ubuntu/Debian/Mint instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint-elementary-os) + * [Fedora instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#fedora) + * [npm](http://www.npmjs.org/) v1.4.x + * `npm` comes with node.js so no explicit installation is needed here. + * You can check `npm` 1.4 or above is installed by running `npm -v`. * libgnome-keyring-dev * on Ubuntu/Debian: `sudo apt-get install libgnome-keyring-dev` * on Fedora: `sudo yum --assumeyes install libgnome-keyring-devel` From 634c995a71d29f25282c772ae8b1ed28a8aa9281 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 11:04:04 -0700 Subject: [PATCH 65/70] Verify npm version This will end up in log output when people report build issues and now the bootstrap will error when the npm version is <1.4. --- script/utils/verify-requirements.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index 4a638be41..0f240b3d8 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -11,8 +11,15 @@ module.exports = function(cb) { return; } - verifyPython27(function(error, pythonSuccessMessage) { - cb(error, (nodeSuccessMessage + "\n" + pythonSuccessMessage).trim()); + verifyNpm(function(error, npmSuccessMessage) { + if (error) { + cb(error); + return; + } + + verifyPython27(function(error, pythonSuccessMessage) { + cb(error, (nodeSuccessMessage + "\n" + npmSuccessMessage + "\n" + pythonSuccessMessage).trim()); + }); }); }); @@ -32,6 +39,23 @@ function verifyNode(cb) { } } +function verifyNpm(cb) { + childProcess.execFile('npm', ['-v'], { env: process.env }, function(err, stdout) { + if (err) + return cb("npm 1.4 is required to build Atom. An error (" + err + ") occured when checking the version."); + + var npmVersion = stdout ? stdout.trim() : ''; + var versionArray = npmVersion.split('.'); + var npmMajorVersion = +versionArray[0] || 0; + var npmMinorVersion = +versionArray[1] || 0; + if (npmMajorVersion === 1 && npmMinorVersion < 4) + cb("npm v1.4+ is required to build Atom."); + else + cb(null, "npm: v" + npmVersion); + }); +} + + function verifyPython27(cb) { if (process.platform == 'win32') { if (!pythonExecutable) { From 562a047b9eb3fe71166fa3afb3afa0fb1ae2cb45 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 10 Jun 2014 12:13:28 -0600 Subject: [PATCH 66/70] Only requestUpdate when scrolling stops if component is still mounted Fixes #2566 --- src/editor-component.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index a0f731264..1717cebe0 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -518,6 +518,8 @@ EditorComponent = React.createClass @onStoppedScrollingAfterDelay() onStoppedScrolling: -> + return unless @isMounted() + @scrollingVertically = false @mouseWheelScreenRow = null @requestUpdate() From fe088ba16a35b8f3faf584f576bb32becb21a752 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 11:45:11 -0700 Subject: [PATCH 67/70] Use local npm version when available --- script/utils/verify-requirements.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index 0f240b3d8..148fa2384 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -40,7 +40,12 @@ function verifyNode(cb) { } function verifyNpm(cb) { - childProcess.execFile('npm', ['-v'], { env: process.env }, function(err, stdout) { + var localNpmPath = path.resolve(__dirname, '..', '..', 'build', 'node_modules', '.bin', 'npm'); + if (process.platform === 'win32') + localNpmPath += ".cmd"; + var npmCommand = fs.existsSync(localNpmPath) ? localNpmPath : 'npm'; + + childProcess.execFile(npmCommand, ['-v'], { env: process.env }, function(err, stdout) { if (err) return cb("npm 1.4 is required to build Atom. An error (" + err + ") occured when checking the version."); From a8186b15cf6112c8ad4a3f9b39438ceb68b91b09 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 10 Jun 2014 13:07:29 -0700 Subject: [PATCH 68/70] Upgrade to bracket-matcher@0.44.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1cbb89d2f..c5a3d419c 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "autosave": "0.13.0", "background-tips": "0.14.0", "bookmarks": "0.23.0", - "bracket-matcher": "0.43.0", + "bracket-matcher": "0.44.0", "command-palette": "0.22.0", "deprecation-cop": "0.6.0", "dev-live-reload": "0.31.0", From f5a70da6f0b9cdefcdf6df7fdcbbb4b9fade9b33 Mon Sep 17 00:00:00 2001 From: James R Sconfitto Date: Tue, 10 Jun 2014 17:26:57 -0400 Subject: [PATCH 69/70] :checkered_flag: Use "npm.cmd" to verify npm --- script/utils/verify-requirements.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index 148fa2384..fa65a339e 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -43,7 +43,10 @@ function verifyNpm(cb) { var localNpmPath = path.resolve(__dirname, '..', '..', 'build', 'node_modules', '.bin', 'npm'); if (process.platform === 'win32') localNpmPath += ".cmd"; + var npmCommand = fs.existsSync(localNpmPath) ? localNpmPath : 'npm'; + if (npmCommand === 'npm' && process.platform === 'win32') + npmCommand += ".cmd"; childProcess.execFile(npmCommand, ['-v'], { env: process.env }, function(err, stdout) { if (err) From 8523df58b0b4dfd97cae4063fd6075d6082af025 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 10 Jun 2014 14:36:02 -0700 Subject: [PATCH 70/70] Upgrade to image-view@0.35 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5a3d419c..99b6ba586 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "git-diff": "0.29.0", "go-to-line": "0.22.0", "grammar-selector": "0.27.0", - "image-view": "0.34.0", + "image-view": "0.35.0", "keybinding-resolver": "0.18.0", "link": "0.22.0", "markdown-preview": "0.76.0",