From 111b5d1fbe5d9dcc054d063a7dd6702d87c23cef Mon Sep 17 00:00:00 2001 From: Kirill Nikitin Date: Mon, 12 May 2014 02:40:49 +0400 Subject: [PATCH 001/119] :lipstick: Deprecate backspaceToBeginningOf{Word,Line} Rename functions `backspaceToBeginningOfWord` to `deleteToBeginningOfWord` and `backspaceToBeginningOfLine to `deleteToBeginningOfLine`. Rename commands `editor:backspace-to-beginning-of-word` to `delete-to-beginning-of-word` and `editor:backspace-to-beginning-of-line` to `editor:delete-to-beginning-of-line`. Fix #1791 --- docs/advanced/keymaps.md | 6 +++--- keymaps/darwin.cson | 8 ++++---- keymaps/emacs.cson | 2 +- keymaps/linux.cson | 2 +- keymaps/win32.cson | 4 ++-- spec/editor-spec.coffee | 20 ++++++++++---------- src/editor-component.coffee | 4 ++-- src/editor-view.coffee | 4 ++-- src/editor.coffee | 22 ++++++++++++++++------ src/selection.coffee | 14 ++++++++++++-- 10 files changed, 53 insertions(+), 33 deletions(-) diff --git a/docs/advanced/keymaps.md b/docs/advanced/keymaps.md index 723369253..5b7c5392d 100644 --- a/docs/advanced/keymaps.md +++ b/docs/advanced/keymaps.md @@ -10,8 +10,8 @@ keystrokes pass through elements with the class `.editor`: ```coffee '.editor': - 'cmd-delete': 'editor:backspace-to-beginning-of-line' - 'alt-backspace': 'editor:backspace-to-beginning-of-word' + 'cmd-delete': 'editor:delete-to-beginning-of-line' + 'alt-backspace': 'editor:delete-to-beginning-of-word' 'ctrl-A': 'editor:select-to-first-character-of-line' 'ctrl-shift-e': 'editor:select-to-end-of-line' 'cmd-left': 'editor:move-to-first-character-of-line' @@ -24,7 +24,7 @@ keystrokes pass through elements with the class `.editor`: Beneath the first selector are several bindings, mapping specific *keystroke patterns* to *commands*. When an element with the `.editor` class is focused and `cmd-delete` is pressed, an custom DOM event called -`editor:backspace-to-beginning-of-line` is emitted on the `.editor` element. +`editor:delete-to-beginning-of-line` is emitted on the `.editor` element. The second selector group also targets editors, but only if they don't have the `.mini` class. In this example, the commands for code folding don't really make diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 221dcfe09..cf37a5e8f 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -103,16 +103,16 @@ 'alt-shift-right': 'editor:select-to-end-of-word' # Apple Specific - 'cmd-backspace': 'editor:backspace-to-beginning-of-line' - 'cmd-shift-backspace': 'editor:backspace-to-beginning-of-line' - 'cmd-delete': 'editor:backspace-to-beginning-of-line' + 'cmd-backspace': 'editor:delete-to-beginning-of-line' + 'cmd-shift-backspace': 'editor:delete-to-beginning-of-line' + 'cmd-delete': 'editor:delete-to-beginning-of-line' 'ctrl-A': 'editor:select-to-first-character-of-line' 'ctrl-E': 'editor:select-to-end-of-line' 'cmd-left': 'editor:move-to-first-character-of-line' 'cmd-right': 'editor:move-to-end-of-screen-line' 'cmd-shift-left': 'editor:select-to-first-character-of-line' 'cmd-shift-right': 'editor:select-to-end-of-line' - 'alt-backspace': 'editor:backspace-to-beginning-of-word' + 'alt-backspace': 'editor:delete-to-beginning-of-word' 'alt-delete': 'editor:delete-to-end-of-word' 'ctrl-a': 'editor:move-to-beginning-of-line' 'ctrl-e': 'editor:move-to-end-of-line' diff --git a/keymaps/emacs.cson b/keymaps/emacs.cson index 11ce62987..78463671c 100644 --- a/keymaps/emacs.cson +++ b/keymaps/emacs.cson @@ -3,5 +3,5 @@ 'alt-F': 'editor:select-to-end-of-word' 'alt-b': 'editor:move-to-beginning-of-word' 'alt-B': 'editor:select-to-beginning-of-word' - 'alt-h': 'editor:backspace-to-beginning-of-word' + 'alt-h': 'editor:delete-to-beginning-of-word' 'alt-d': 'editor:delete-to-end-of-word' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 474829da3..8200d663e 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -65,7 +65,7 @@ 'ctrl-right': 'editor:move-to-end-of-word' 'ctrl-shift-left': 'editor:select-to-beginning-of-word' 'ctrl-shift-right': 'editor:select-to-end-of-word' - 'ctrl-backspace': 'editor:backspace-to-beginning-of-word' + 'ctrl-backspace': 'editor:delete-to-beginning-of-word' 'ctrl-delete': 'editor:delete-to-end-of-word' # Sublime Parity diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 91545a99c..d0f9cf421 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -63,8 +63,8 @@ 'ctrl-right': 'editor:move-to-end-of-word' 'ctrl-shift-left': 'editor:select-to-beginning-of-word' 'ctrl-shift-right': 'editor:select-to-end-of-word' - 'ctrl-backspace': 'editor:backspace-to-beginning-of-word' - 'ctrl-delete': 'editor:backspace-to-beginning-of-word' + 'ctrl-backspace': 'editor:delete-to-beginning-of-word' + 'ctrl-delete': 'editor:delete-to-beginning-of-word' # Sublime Parity 'ctrl-a': 'core:select-all' diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index d7f214f22..d1e33e3b8 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1731,26 +1731,26 @@ describe "Editor", -> editor.backspace() expect(editor.lineForBufferRow(0)).toBe 'var = () {' - describe ".backspaceToBeginningOfWord()", -> + describe ".deleteToBeginningOfWord()", -> describe "when no text is selected", -> it "deletes all text between the cursor and the beginning of the word", -> editor.setCursorBufferPosition([1, 24]) editor.addCursorAtBufferPosition([3, 5]) [cursor1, cursor2] = editor.getCursors() - editor.backspaceToBeginningOfWord() + editor.deleteToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 22] expect(cursor2.getBufferPosition()).toEqual [3, 4] - editor.backspaceToBeginningOfWord() + editor.deleteToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 21] expect(cursor2.getBufferPosition()).toEqual [2, 39] - editor.backspaceToBeginningOfWord() + editor.deleteToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 13] @@ -1758,24 +1758,24 @@ describe "Editor", -> editor.setText(' var sort') editor.setCursorBufferPosition([0, 2]) - editor.backspaceToBeginningOfWord() + editor.deleteToBeginningOfWord() expect(buffer.lineForRow(0)).toBe 'var sort' describe "when text is selected", -> it "deletes only selected text", -> editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.backspaceToBeginningOfWord() + editor.deleteToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - describe ".backspaceToBeginningOfLine()", -> + describe ".deleteToToBeginningOfLine()", -> describe "when no text is selected", -> it "deletes all text between the cursor and the beginning of the line", -> editor.setCursorBufferPosition([1, 24]) editor.addCursorAtBufferPosition([2, 5]) [cursor1, cursor2] = editor.getCursors() - editor.backspaceToBeginningOfLine() + editor.deleteToToBeginningOfLine() expect(buffer.lineForRow(1)).toBe 'ems) {' expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' expect(cursor1.getBufferPosition()).toEqual [1, 0] @@ -1784,13 +1784,13 @@ describe "Editor", -> describe "when at the beginning of the line", -> it "deletes the newline", -> editor.setCursorBufferPosition([2]) - editor.backspaceToBeginningOfLine() + editor.deleteToToBeginningOfLine() expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' describe "when text is selected", -> it "still deletes all text to begginning of the line", -> editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.backspaceToBeginningOfLine() + editor.deleteToToBeginningOfLine() expect(buffer.lineForRow(1)).toBe 'ems) {' expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' diff --git a/src/editor-component.coffee b/src/editor-component.coffee index a4a2eadef..556fe5e17 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -174,8 +174,8 @@ EditorComponent = React.createClass 'editor:move-to-previous-word': => editor.moveCursorToPreviousWord() 'editor:select-word': => editor.selectWord() 'editor:consolidate-selections': @consolidateSelections - 'editor:backspace-to-beginning-of-word': => editor.backspaceToBeginningOfWord() - 'editor:backspace-to-beginning-of-line': => editor.backspaceToBeginningOfLine() + 'editor:delete-to-beginning-of-word': => editor.deleteToBeginningOfWord() + 'editor:delete-to-beginning-of-line': => editor.deleteToBeginningOfLine() 'editor:delete-to-end-of-word': => editor.deleteToEndOfWord() 'editor:delete-line': => editor.deleteLine() 'editor:cut-to-end-of-line': => editor.cutToEndOfLine() diff --git a/src/editor-view.coffee b/src/editor-view.coffee index 087181371..4e8d2443f 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -154,8 +154,8 @@ class EditorView extends View 'editor:move-to-previous-word': => @editor.moveCursorToPreviousWord() 'editor:select-word': => @editor.selectWord() 'editor:consolidate-selections': (event) => @consolidateSelections(event) - 'editor:backspace-to-beginning-of-word': => @editor.backspaceToBeginningOfWord() - 'editor:backspace-to-beginning-of-line': => @editor.backspaceToBeginningOfLine() + 'editor:delete-to-beginning-of-word': => @editor.deleteToBeginningOfWord() + 'editor:delete-to-beginning-of-line': => @editor.deleteToBeginningOfLine() 'editor:delete-to-end-of-word': => @editor.deleteToEndOfWord() 'editor:delete-line': => @editor.deleteLine() 'editor:cut-to-end-of-line': => @editor.cutToEndOfLine() diff --git a/src/editor.coffee b/src/editor.coffee index 63c09cca3..28880a946 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -109,8 +109,8 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # - {::insertNewlineAbove} # - {::insertNewlineBelow} # - {::backspace} -# - {::backspaceToBeginningOfWord} -# - {::backspaceToBeginningOfLine} +# - {::deleteToBeginningOfWord} +# - {::deleteToBeginningOfLine} # - {::delete} # - {::deleteToEndOfWord} # - {::deleteLine} @@ -658,17 +658,27 @@ class Editor extends Model backspace: -> @mutateSelectedText (selection) -> selection.backspace() + # Deprecated: Use {::deleteToBeginningOfWord} instead. + backspaceToBeginningOfWord: -> + deprecate("Use Editor::deleteToBeginningOfWord() instead") + @deleteToBeginningOfWord() + + # Deprecated: Use {::deleteToBeginningOfLine} instead. + backspaceToBeginningOfLine: -> + deprecate("Use Editor::deleteToBeginningOfLine() instead") + @deleteToBeginningOfLine() + # Public: For each selection, if the selection is empty, delete all characters # of the containing word that precede the cursor. Otherwise delete the # selected text. - backspaceToBeginningOfWord: -> - @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() + deleteToBeginningOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() # Public: For each selection, if the selection is empty, delete all characters # of the containing line that precede the cursor. Otherwise delete the # selected text. - backspaceToBeginningOfLine: -> - @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine() + deleteToBeginningOfLine: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() # Public: For each selection, if the selection is empty, delete the character # preceding the cursor. Otherwise delete the selected text. diff --git a/src/selection.coffee b/src/selection.coffee index efaf25d7a..204b5c3fe 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -382,15 +382,25 @@ class Selection extends Model @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow()) @deleteSelectedText() + # Deprecated: Use {::deleteToBeginningOfWord} instead. + backspaceToBeginningOfWord: -> + deprecate("Use Selection::deleteToBeginningOfWord() instead") + @deleteToBeginningOfWord() + + # Deprecated: Use {::deleteToBeginningOfLine} instead. + backspaceToBeginningOfLine: -> + deprecate("Use Selection::deleteToBeginningOfLine() instead") + @deleteToBeginningOfLine() + # Public: Removes from the start of the selection to the beginning of the # current word if the selection is empty otherwise it deletes the selection. - backspaceToBeginningOfWord: -> + deleteToBeginningOfWord: -> @selectToBeginningOfWord() if @isEmpty() @deleteSelectedText() # Public: Removes from the beginning of the line which the selection begins on # all the way through to the end of the selection. - backspaceToBeginningOfLine: -> + deleteToBeginningOfLine: -> if @isEmpty() and @cursor.isAtBeginningOfLine() @selectLeft() else From 6c4d1be0041ead8e22edbaafeab0ef49d1e53dc8 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Tue, 13 May 2014 03:05:31 +0300 Subject: [PATCH 002/119] :memo: Add more Emoji to the contributing guide Suggest Emoji for: - Fixing something on Mac OS - Fixing a bug - Burning whitespace - Fixing the CI build - Adding tests - Refactoring - Security --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1bd76263..753529cd7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,13 @@ in the proper package's repository. * :non-potable_water: `:non-potable_water:` when plugging memory leaks * :memo: `:memo:` when writing docs * :penguin: `:penguin:` when fixing something on Linux + * :green_apple: `:green_apple:` when fixing something on Mac OS + * :bug: `:bug:` when fixing a bug + * :fire: whitespace `:fire: whitespace` when removing whitespace + * :green_heart: `:green_heart:` when fixing the CI build + * :white_check_mark: `:white_check_mark:` when adding tests + * :recycle: `:recycle:` when refactoring (or removing code, moving around etc.) + * :guardsman: `:guardsman:` when dealing with security ## CoffeeScript Styleguide From 952c96d03baf3cc7e9b2597485e54c52a83bffa5 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Thu, 15 May 2014 20:10:40 +0300 Subject: [PATCH 003/119] Use :apple: for Mac OS specific bugs --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 753529cd7..97199b4ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ in the proper package's repository. * :non-potable_water: `:non-potable_water:` when plugging memory leaks * :memo: `:memo:` when writing docs * :penguin: `:penguin:` when fixing something on Linux - * :green_apple: `:green_apple:` when fixing something on Mac OS + * :apple: `:apple:` when fixing something on Mac OS * :bug: `:bug:` when fixing a bug * :fire: whitespace `:fire: whitespace` when removing whitespace * :green_heart: `:green_heart:` when fixing the CI build From 6102143faf652ef6e2b15ccf45bb7461ed0d4e10 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Thu, 15 May 2014 21:50:06 +0300 Subject: [PATCH 004/119] Use :fire: emoji for general deleting of code --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97199b4ec..6f8e14b9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ in the proper package's repository. * :penguin: `:penguin:` when fixing something on Linux * :apple: `:apple:` when fixing something on Mac OS * :bug: `:bug:` when fixing a bug - * :fire: whitespace `:fire: whitespace` when removing whitespace + * :fire: `:fire:` when removing code or files * :green_heart: `:green_heart:` when fixing the CI build * :white_check_mark: `:white_check_mark:` when adding tests * :recycle: `:recycle:` when refactoring (or removing code, moving around etc.) From ba8bd801733f682beb71622eaaeb1168e0a1e759 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Thu, 15 May 2014 22:02:29 +0300 Subject: [PATCH 005/119] Remove :recycle: from the emoji in CONTRIBUTING.md --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f8e14b9e..305327066 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,6 @@ in the proper package's repository. * :fire: `:fire:` when removing code or files * :green_heart: `:green_heart:` when fixing the CI build * :white_check_mark: `:white_check_mark:` when adding tests - * :recycle: `:recycle:` when refactoring (or removing code, moving around etc.) * :guardsman: `:guardsman:` when dealing with security ## CoffeeScript Styleguide From 1c1c3617e95af70dfe0004861efe4bde7e21d226 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 14:40:16 -0700 Subject: [PATCH 006/119] Upgrade to find-and-replace@0.102.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4254e3746..b26a14c91 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.30.0", "exception-reporting": "0.17.0", "feedback": "0.33.0", - "find-and-replace": "0.101.0", + "find-and-replace": "0.102.0", "fuzzy-finder": "0.50.0", "git-diff": "0.28.0", "go-to-line": "0.19.0", From 4ff5f96fd41db0bd63d1f94987f9449cd6f3d9e3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 14:41:33 -0700 Subject: [PATCH 007/119] Upgrade to go-to-line@0.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b26a14c91..79377292c 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "find-and-replace": "0.102.0", "fuzzy-finder": "0.50.0", "git-diff": "0.28.0", - "go-to-line": "0.19.0", + "go-to-line": "0.20.0", "grammar-selector": "0.26.0", "image-view": "0.33.0", "keybinding-resolver": "0.17.0", From d7c98cb39442d36da8ddaf3f746710e6d861067b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 14:44:36 -0700 Subject: [PATCH 008/119] Upgrade to language-sass@0.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79377292c..fc2dfd813 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "language-python": "0.15.0", "language-ruby": "0.24.0", "language-ruby-on-rails": "0.14.0", - "language-sass": "0.10.0", + "language-sass": "0.11.0", "language-shellscript": "0.8.0", "language-source": "0.7.0", "language-sql": "0.8.0", From cc1e6e2a1fd1846470f86b72a49613e5833571f3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 14:48:24 -0700 Subject: [PATCH 009/119] Upgrade to find-and-replace@0.103.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc2dfd813..6c3062928 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.30.0", "exception-reporting": "0.17.0", "feedback": "0.33.0", - "find-and-replace": "0.102.0", + "find-and-replace": "0.103.0", "fuzzy-finder": "0.50.0", "git-diff": "0.28.0", "go-to-line": "0.20.0", From 56af2ca4d7acbe84afa2b3815f56be70baf98bfd Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 15:10:01 -0700 Subject: [PATCH 010/119] Upgrade to bracket-matcher@0.34.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c3062928..40c763ac0 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "autosave": "0.13.0", "background-tips": "0.13.0", "bookmarks": "0.22.0", - "bracket-matcher": "0.33.0", + "bracket-matcher": "0.34.0", "command-palette": "0.21.0", "deprecation-cop": "0.5.0", "dev-live-reload": "0.30.0", From d5458c1865d809adbbb63bc8e30fcfd9ca84144a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 15:14:51 -0700 Subject: [PATCH 011/119] Upgrade to fuzzy-finder@0.51.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40c763ac0..48be1885e 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "exception-reporting": "0.17.0", "feedback": "0.33.0", "find-and-replace": "0.103.0", - "fuzzy-finder": "0.50.0", + "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", "go-to-line": "0.20.0", "grammar-selector": "0.26.0", From 71155abf57466e825a75fd38fd4f736f159d99ac Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 15:19:54 -0700 Subject: [PATCH 012/119] Upgrade to find-and-replace@0.104.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48be1885e..eb5af2547 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.30.0", "exception-reporting": "0.17.0", "feedback": "0.33.0", - "find-and-replace": "0.103.0", + "find-and-replace": "0.104.0", "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", "go-to-line": "0.20.0", From fc9a11959c8ddc9b184d2e86f9566a97286c83ed Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 15:33:37 -0700 Subject: [PATCH 013/119] Upgrade to language-html@0.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb5af2547..18d032220 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "language-gfm": "0.37.0", "language-git": "0.9.0", "language-go": "0.11.0", - "language-html": "0.21.0", + "language-html": "0.22.0", "language-hyperlink": "0.9.0", "language-java": "0.10.0", "language-javascript": "0.26.0", From e3dbd412e18589fb37fb90b5f50874e103636d4d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 15 May 2014 15:34:50 -0700 Subject: [PATCH 014/119] Upgrade to find-and-replace@0.105.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18d032220..d61422347 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.30.0", "exception-reporting": "0.17.0", "feedback": "0.33.0", - "find-and-replace": "0.104.0", + "find-and-replace": "0.105.0", "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", "go-to-line": "0.20.0", From e3302b3f73e967cc850d24dad54975803e0f71ac Mon Sep 17 00:00:00 2001 From: "David Y. Ross" Date: Tue, 13 May 2014 17:38:43 -0700 Subject: [PATCH 015/119] hide the cursor with cursor-hidden class rather than element.style --- src/cursor-view.coffee | 6 +++++- static/editor.less | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee index 857e5e137..a3d67264b 100644 --- a/src/cursor-view.coffee +++ b/src/cursor-view.coffee @@ -78,7 +78,11 @@ class CursorView extends View setVisible: (visible) -> unless @visible is visible @visible = visible - @toggle(@visible) + hiddenCursor = 'hidden-cursor' + if visible + @removeClass hiddenCursor + else + @addClass hiddenCursor stopBlinking: -> @constructor.stopBlinking(this) if @blinking diff --git a/static/editor.less b/static/editor.less index 4c2964650..f70e7c3d0 100644 --- a/static/editor.less +++ b/static/editor.less @@ -226,6 +226,10 @@ visibility: visible; } +.cursor.hidden-cursor { + display: none; +} + .editor .hidden-input { padding: 0; border: 0; From ce668e7139a30e356c141537a1d529a075845017 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 08:33:28 -0600 Subject: [PATCH 016/119] Fix subscription leak when ~/.atom/styles.less is present running specs --- spec/atom-spec.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index d1ee823ed..826d78b5e 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -510,20 +510,22 @@ describe "the `atom` global", -> # enabling of theme pack = atom.packages.enablePackage(packageName) - activatedPackages = null - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length > 0 + waitsFor (done) -> + atom.themes.once 'reloaded', done runs -> - expect(activatedPackages).toContain(pack) + expect(atom.packages.getActivePackages()).toContain pack expect(atom.config.get('core.themes')).toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName # disabling of theme pack = atom.packages.disablePackage(packageName) - activatedPackages = atom.packages.getActivePackages() - expect(activatedPackages).not.toContain(pack) + + waitsFor (done) -> + atom.themes.once 'reloaded', done + + runs -> + expect(atom.packages.getActivePackages()).not.toContain(pack) expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName From 7a4a85cb20848bf7b86bfe38007db0ad2939d1a5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 09:00:01 -0600 Subject: [PATCH 017/119] Fix failures running config specs locally --- spec/config-spec.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 95ef05760..d857a3b42 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -5,6 +5,10 @@ fs = require 'fs-plus' describe "Config", -> dotAtomPath = path.join(temp.dir, 'dot-atom-dir') + dotAtomPath = null + + beforeEach -> + dotAtomPath = temp.path('dot-atom-dir') describe ".get(keyPath)", -> it "allows a key path's value to be read", -> @@ -258,8 +262,10 @@ describe "Config", -> describe ".initializeConfigDirectory()", -> beforeEach -> + if fs.existsSync(dotAtomPath) + fs.removeSync(dotAtomPath) + atom.config.configDirPath = dotAtomPath - expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() afterEach -> fs.removeSync(dotAtomPath) From f2c7d171bf53f24d7c963b7153188e9a2cc04a8f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 09:14:21 -0600 Subject: [PATCH 018/119] Fix another subscription leakage associated with theme manager specs --- spec/theme-manager-spec.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 831b34ee6..dbb1ce672 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -286,5 +286,10 @@ describe "ThemeManager", -> runs -> spyOn(console, 'warn') expect(-> atom.config.set('core.themes', ['atom-light-ui', 'theme-really-does-not-exist'])).not.toThrow() + + waitsFor (done) -> + themeManager.once 'reloaded', done + + runs -> expect(console.warn.callCount).toBe 1 expect(console.warn.argsForCall[0][0].length).toBeGreaterThan 0 From e537080b64a06200f7642f2a811158621d8d9211 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 08:36:02 -0700 Subject: [PATCH 019/119] Upgrade to autocomplete@0.28.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d61422347..af3b2d743 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "solarized-dark-syntax": "0.14.0", "solarized-light-syntax": "0.7.0", "archive-view": "0.31.0", - "autocomplete": "0.27.0", + "autocomplete": "0.28.0", "autoflow": "0.17.0", "autosave": "0.13.0", "background-tips": "0.13.0", From 635af7f8382484f7b669b1276d47bfef708214d7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 09:27:16 -0700 Subject: [PATCH 020/119] Upgrade to apm 0.54.0 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 59a334e2b..bb15222bc 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.53.0" + "atom-package-manager": "0.54.0" } } From b0e91f8b33d316f8b1808dc3102025ea29bd7882 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 09:47:44 -0700 Subject: [PATCH 021/119] Upgrade to tree-view@0.93.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af3b2d743..bad5438d0 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "symbols-view": "0.52.0", "tabs": "0.39.0", "timecop": "0.18.0", - "tree-view": "0.92.0", + "tree-view": "0.93.0", "update-package-dependencies": "0.6.0", "welcome": "0.14.0", "whitespace": "0.22.0", From 5e2181e665187df58b57676ee583f7f1586f23f2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 10:05:00 -0700 Subject: [PATCH 022/119] :penguin: Add keybindings for select-to-top/bottom ctrl-shift-home/end are now mapped Closes #2251 --- keymaps/linux.cson | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 2f3df3dd7..08b91d081 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -69,6 +69,8 @@ 'ctrl-delete': 'editor:delete-to-end-of-word' 'ctrl-home': 'core:move-to-top' 'ctrl-end': 'core:move-to-bottom' + 'cmd-shift-home': 'core:select-to-top' + 'cmd-shift-end': 'core:select-to-bottom' # Sublime Parity 'ctrl-a': 'core:select-all' From b5bff9f8b8d31fafc4949f8708de608acf7497c6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 10:38:48 -0700 Subject: [PATCH 023/119] Upgrade to language-python@0.16.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bad5438d0..97637d65b 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "language-perl": "0.8.0", "language-php": "0.14.0", "language-property-list": "0.7.0", - "language-python": "0.15.0", + "language-python": "0.16.0", "language-ruby": "0.24.0", "language-ruby-on-rails": "0.14.0", "language-sass": "0.11.0", From fc2830bacba72b1abb42f9a1767b2eb9aa13af02 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 11:53:40 -0700 Subject: [PATCH 024/119] Use div for test workspace element --- src/menu-manager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index c21a5ad16..cd24a3503 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -58,7 +58,7 @@ class MenuManager testBody = document.createElement('body') testBody.classList.add(@classesForElement(document.body)...) - testWorkspace = document.createElement('body') + testWorkspace = document.createElement('div') workspaceClasses = @classesForElement(document.body.querySelector('.workspace')) workspaceClasses = ['workspace'] if workspaceClasses.length is 0 testWorkspace.classList.add(workspaceClasses...) From 628ea72943bbf9c5caf4434c142cb8394d4a6077 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 11:57:04 -0700 Subject: [PATCH 025/119] Check if selector matches parents of test element Previously a menu for a keybinding with a .workspace selector would not display the shortcut because the selector wasn't matching the test editor element directly. Now the parent elements of the test editor are checked as well. Closes #2089 --- src/menu-manager.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index cd24a3503..b7ed27ab3 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -69,7 +69,12 @@ class MenuManager @testEditor.classList.add('editor') testWorkspace.appendChild(@testEditor) - @testEditor.webkitMatchesSelector(selector) + element = @testEditor + while element + return true if element.webkitMatchesSelector(selector) + element = element.parentElement + + false # Public: Refreshes the currently visible menu. update: -> From 8918eb4758c33fc5feee194d5ad064376c153052 Mon Sep 17 00:00:00 2001 From: Kirill Nikitin Date: Fri, 16 May 2014 23:05:05 +0400 Subject: [PATCH 026/119] Bug #1791 Fix typo in example group name and function names. --- spec/editor-spec.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index a64339154..6b37d6f86 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1768,14 +1768,14 @@ describe "Editor", -> expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - describe ".deleteToToBeginningOfLine()", -> + describe ".deleteToBeginningOfLine()", -> describe "when no text is selected", -> it "deletes all text between the cursor and the beginning of the line", -> editor.setCursorBufferPosition([1, 24]) editor.addCursorAtBufferPosition([2, 5]) [cursor1, cursor2] = editor.getCursors() - editor.deleteToToBeginningOfLine() + editor.deleteToBeginningOfLine() expect(buffer.lineForRow(1)).toBe 'ems) {' expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' expect(cursor1.getBufferPosition()).toEqual [1, 0] @@ -1784,13 +1784,13 @@ describe "Editor", -> describe "when at the beginning of the line", -> it "deletes the newline", -> editor.setCursorBufferPosition([2]) - editor.deleteToToBeginningOfLine() + editor.deleteToBeginningOfLine() expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' describe "when text is selected", -> it "still deletes all text to begginning of the line", -> editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToToBeginningOfLine() + editor.deleteToBeginningOfLine() expect(buffer.lineForRow(1)).toBe 'ems) {' expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' From 7f442d045bf14fe3e132ee20c94630c92191bde2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 12:54:22 -0700 Subject: [PATCH 027/119] Check for errors in script/mkdeb Refs #2129 --- script/mkdeb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/mkdeb b/script/mkdeb index 0bcc84926..aadaa7ab5 100755 --- a/script/mkdeb +++ b/script/mkdeb @@ -1,6 +1,8 @@ #!/bin/bash # mkdeb version control-file-path deb-file-path +set -e + SCRIPT=`readlink -f "$0"` ROOT=`readlink -f $(dirname $SCRIPT)/..` cd $ROOT From bff396ab1a65b1c3f175aa0f4f870bfaedf3de63 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 13:00:46 -0700 Subject: [PATCH 028/119] Upgrade to language-xml@0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97637d65b..c5087d56a 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "language-text": "0.6.0", "language-todo": "0.10.0", "language-toml": "0.12.0", - "language-xml": "0.12.0", + "language-xml": "0.13.0", "language-yaml": "0.6.0" }, "private": true, From 6cae6981d84246b1db2d44cba0c2883f8cdd3fc9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 13:29:39 -0700 Subject: [PATCH 029/119] Recommend cloning to shallow path Prevents path length issues --- docs/build-instructions/windows.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 4168f98dd..fc4320c9a 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -14,12 +14,12 @@ ## Instructions ```bat - cd C:\Users\\github - git clone https://github.com/atom/atom/ + cd C:\ + git clone https://github.com/atom/atom cd atom script\build ``` - + ## Why do I have to use GitHub for Windows? Can't I just use my existing Git? You totally can! GitHub for Windows's Git Shell just takes less work to set up. You need to have Posix tools in your `%PATH%` (i.e. `grep`, `sed`, et al.), which isn't the default configuration when you install Git. To fix this, you probably need to fiddle with your system PATH. @@ -39,4 +39,4 @@ If your Visual Studio is in a non-standard location, and you get the error `You Example: - vs2010Path = "H:/VS2010/Common7/IDE" \ No newline at end of file + vs2010Path = "H:/VS2010/Common7/IDE" From 10e609ba27233e12af0508cb9eaa128965803d0a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 13:31:58 -0700 Subject: [PATCH 030/119] Upgrade to tabs@0.40.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5087d56a..c06b5083f 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "status-bar": "0.40.0", "styleguide": "0.29.0", "symbols-view": "0.52.0", - "tabs": "0.39.0", + "tabs": "0.40.0", "timecop": "0.18.0", "tree-view": "0.93.0", "update-package-dependencies": "0.6.0", From 3f1ce617a72f261a91c6f0790908e6d54962af9f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:05:44 -0600 Subject: [PATCH 031/119] Try to fix flaky spec --- spec/atom-spec.coffee | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 826d78b5e..d2e6392af 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -510,22 +510,20 @@ describe "the `atom` global", -> # enabling of theme pack = atom.packages.enablePackage(packageName) - waitsFor (done) -> - atom.themes.once 'reloaded', done + waitsFor -> + pack in atom.packages.getActivePackages() runs -> - expect(atom.packages.getActivePackages()).toContain pack expect(atom.config.get('core.themes')).toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName # disabling of theme pack = atom.packages.disablePackage(packageName) - waitsFor (done) -> - atom.themes.once 'reloaded', done + waitsFor -> + not (pack in atom.packages.getActivePackages()) runs -> - expect(atom.packages.getActivePackages()).not.toContain(pack) expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName From b8ac8516fecd80c116714dd1346732f962a17ea6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 May 2014 21:21:59 -0600 Subject: [PATCH 032/119] Don't preserve rows when scrolling --- src/editor-component.coffee | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index a4a2eadef..ee1e9e629 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -20,7 +20,6 @@ EditorComponent = React.createClass batchingUpdates: false updateRequested: false cursorsMoved: false - preservedRowRange: null scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -32,7 +31,7 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - renderedRowRange = @getRenderedRowRange() + renderedRowRange = editor.getVisibleRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -92,13 +91,6 @@ EditorComponent = React.createClass height: horizontalScrollbarHeight width: verticalScrollbarWidth - getRenderedRowRange: -> - renderedRowRange = @props.editor.getVisibleRowRange() - if @preservedRowRange? - renderedRowRange[0] = Math.min(@preservedRowRange[0], renderedRowRange[0]) - renderedRowRange[1] = Math.max(@preservedRowRange[1], renderedRowRange[1]) - renderedRowRange - getInitialState: -> {} getDefaultProps: -> @@ -357,13 +349,6 @@ EditorComponent = React.createClass # if the editor's content and dimensions require them to be visible. @requestUpdate() - clearPreservedRowRange: -> - @preservedRowRange = null - @scrollingVertically = false - @requestUpdate() - - clearPreservedRowRangeAfterDelay: null # Created lazily - onBatchedUpdatesStarted: -> @batchingUpdates = true @@ -384,10 +369,7 @@ EditorComponent = React.createClass @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) onScrollTopChanged: -> - @preservedRowRange = @getRenderedRowRange() @scrollingVertically = true - @clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200) - @clearPreservedRowRangeAfterDelay() @requestUpdate() onSelectionRemoved: (selection) -> From 308960309d9fe9e0db263363b196421936cffedf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 8 May 2014 21:22:23 -0600 Subject: [PATCH 033/119] Overdraw lines to discourage Blink from repainting the entire editor --- src/display-buffer.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 98225df6e..6adb0f9f7 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,6 +34,7 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 + lineOverdraw: 8 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -241,7 +242,9 @@ class DisplayBuffer extends Model heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeight()) - endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines)) + endRow = Math.min(@getLineCount(), startRow + heightInLines + @lineOverdraw) + startRow = Math.max(0, startRow - @lineOverdraw) + [startRow, endRow] intersectsVisibleRowRange: (startRow, endRow) -> From 0ae8765a8a068e2577382619cfc6d38a2537271d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 12:16:06 -0600 Subject: [PATCH 034/119] Update scroll position directly on mousewheel events Previously, we were updating the scrollbars and relying on an async scroll events to fire. But updating the scrollbars is expensive, so this updates the model directly when the next animation frame fires instead. --- src/editor-component.coffee | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ee1e9e629..2c01aff3d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -24,6 +24,8 @@ EditorComponent = React.createClass gutterWidth: 0 refreshingScrollbars: false measuringScrollbars: true + pendingVerticalScrollDelta: 0 + pendingHorizontalScrollDelta: 0 render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state @@ -311,14 +313,24 @@ EditorComponent = React.createClass @pendingScrollLeft = null onMouseWheel: (event) -> + event.preventDefault() + + animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 + # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) - @refs.horizontalScrollbar.getDOMNode().scrollLeft -= wheelDeltaX + @pendingHorizontalScrollDelta -= wheelDeltaX else - @refs.verticalScrollbar.getDOMNode().scrollTop -= wheelDeltaY + @pendingVerticalScrollDelta -= wheelDeltaY - event.preventDefault() + unless animationFramePending + requestAnimationFrame => + {editor} = @props + editor.setScrollTop(editor.getScrollTop() + @pendingVerticalScrollDelta) + editor.setScrollLeft(editor.getScrollLeft() + @pendingHorizontalScrollDelta) + @pendingVerticalScrollDelta = 0 + @pendingHorizontalScrollDelta = 0 onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) From 9f2c8c175611f055403fcb348469c54374be77bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 12:42:13 -0600 Subject: [PATCH 035/119] Measure characters in new lines when vertically scrolling stops --- src/editor-component.coffee | 8 ++++++++ src/lines-component.coffee | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 2c01aff3d..97074ba9f 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -383,6 +383,14 @@ EditorComponent = React.createClass onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() + @stopScrollingAfterDelay ?= debounce(@onStoppedScrolling, 100) + @stopScrollingAfterDelay() + + onStoppedScrolling: -> + @scrollingVertically = false + @requestUpdate() + + stopScrollingAfterDelay: null # created lazily onSelectionRemoved: (selection) -> {editor} = @props diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 844670f32..555695ad8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -28,7 +28,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide', 'scrollingVertically') {renderedRowRange, pendingChanges} = newProps for change in pendingChanges From bf9f8597a76dde2265fb322a110a95f4c3ae4831 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 9 May 2014 16:42:51 -0600 Subject: [PATCH 036/119] Give each line its own layer on the GPU --- src/display-buffer.coffee | 4 +++- src/editor-component.coffee | 5 +++-- src/editor-scroll-view-component.coffee | 11 +++-------- src/editor.coffee | 1 + src/lines-component.coffee | 25 ++++++++++++++++--------- static/editor.less | 4 ++++ 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 6adb0f9f7..5075b44da 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,7 +34,7 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 - lineOverdraw: 8 + lineOverdraw: 4 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -247,6 +247,8 @@ class DisplayBuffer extends Model [startRow, endRow] + getLineOverdraw: -> @lineOverdraw + intersectsVisibleRowRange: (startRow, endRow) -> [visibleStart, visibleEnd] = @getVisibleRowRange() not (endRow <= visibleStart or visibleEnd <= startRow) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 97074ba9f..bd48ed2fc 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -34,6 +34,7 @@ EditorComponent = React.createClass if @isMounted() renderedRowRange = editor.getVisibleRowRange() + lineOverdraw = editor.getLineOverdraw() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -56,8 +57,8 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - scrollHeight, scrollWidth, lineHeight: lineHeightInPixels, - renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved, + lineHeight: lineHeightInPixels, renderedRowRange, lineOverdraw, @pendingChanges + scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index bc04b41f3..e361802a6 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,18 +17,13 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props + {renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - contentStyle = - height: scrollHeight - minWidth: scrollWidth - WebkitTransform: "translate3d(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px, 0)" - div className: 'scroll-view', InputComponent ref: 'input' @@ -38,11 +33,11 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown, + div className: 'scroll-view-content', style: {top: -lineOverdraw * lineHeight}, onMouseDown: @onMouseDown, CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, pendingChanges, scrollingVertically + renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', SelectionsComponent({editor}) diff --git a/src/editor.coffee b/src/editor.coffee index 467f13e32..a7e71262e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1883,6 +1883,7 @@ class Editor extends Model getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth) getVisibleRowRange: -> @displayBuffer.getVisibleRowRange() + getLineOverdraw: -> @displayBuffer.getLineOverdraw() intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 555695ad8..316dd41f8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,12 +12,20 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange + firstVisibleRow = Math.floor(scrollTop / lineHeight) + + + offset = -scrollTop % lineHeight + + if firstVisibleRow < lineOverdraw + offset += (lineOverdraw - firstVisibleRow) * lineHeight lines = - for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1) - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i}) + for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) + screenRow = startRow + index + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, offset, screenRow}) div {className: 'lines'}, lines @@ -28,7 +36,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') {renderedRowRange, pendingChanges} = newProps for change in pendingChanges @@ -103,11 +111,10 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {screenRow, lineHeight} = @props + {index, screenRow, offset, lineHeight} = @props - style = - top: screenRow * lineHeight - position: 'absolute' + top = index * lineHeight + offset + style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -137,4 +144,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'offset') diff --git a/static/editor.less b/static/editor.less index f70e7c3d0..b259d7652 100644 --- a/static/editor.less +++ b/static/editor.less @@ -14,6 +14,10 @@ .lines { z-index: -1; + + .line { + position: absolute; + } } .horizontal-scrollbar { From 7d8256d343b2106a04aa52ab9265b6a658813270 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 13:32:50 -0600 Subject: [PATCH 037/119] Drop lineOverdraw and scroll-view-content div --- src/display-buffer.coffee | 6 +----- src/editor-component.coffee | 3 +-- src/editor-scroll-view-component.coffee | 19 +++++++++---------- src/editor.coffee | 1 - src/lines-component.coffee | 18 ++++++------------ 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 5075b44da..5c6a05d1e 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -34,7 +34,6 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 horizontalScrollbarHeight: 15 verticalScrollbarWidth: 15 - lineOverdraw: 4 constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) -> super @@ -242,13 +241,10 @@ class DisplayBuffer extends Model heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeight()) - endRow = Math.min(@getLineCount(), startRow + heightInLines + @lineOverdraw) - startRow = Math.max(0, startRow - @lineOverdraw) + endRow = Math.min(@getLineCount(), startRow + heightInLines) [startRow, endRow] - getLineOverdraw: -> @lineOverdraw - intersectsVisibleRowRange: (startRow, endRow) -> [visibleStart, visibleEnd] = @getVisibleRowRange() not (endRow <= visibleStart or visibleEnd <= startRow) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index bd48ed2fc..8f89707a8 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -34,7 +34,6 @@ EditorComponent = React.createClass if @isMounted() renderedRowRange = editor.getVisibleRowRange() - lineOverdraw = editor.getLineOverdraw() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -57,7 +56,7 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, renderedRowRange, lineOverdraw, @pendingChanges + lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index e361802a6..88faf8f3c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,14 +17,14 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - div className: 'scroll-view', + div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent ref: 'input' className: 'hidden-input' @@ -33,14 +33,13 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content', style: {top: -lineOverdraw * lineHeight}, onMouseDown: @onMouseDown, - CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, lineOverdraw, pendingChanges, scrollTop, scrollLeft, scrollingVertically - } - div className: 'underlayer', - SelectionsComponent({editor}) + CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + } + div className: 'underlayer', + SelectionsComponent({editor}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/editor.coffee b/src/editor.coffee index a7e71262e..467f13e32 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1883,7 +1883,6 @@ class Editor extends Model getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth) getVisibleRowRange: -> @displayBuffer.getVisibleRowRange() - getLineOverdraw: -> @displayBuffer.getLineOverdraw() intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 316dd41f8..cc4ace194 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,20 +12,14 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, scrollTop, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange - firstVisibleRow = Math.floor(scrollTop / lineHeight) - - - offset = -scrollTop % lineHeight - - if firstVisibleRow < lineOverdraw - offset += (lineOverdraw - firstVisibleRow) * lineHeight + scrollOffset = -scrollTop % lineHeight lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, offset, screenRow}) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, scrollOffset, screenRow}) div {className: 'lines'}, lines @@ -111,9 +105,9 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {index, screenRow, offset, lineHeight} = @props + {index, screenRow, scrollOffset, lineHeight} = @props - top = index * lineHeight + offset + top = index * lineHeight + scrollOffset style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -144,4 +138,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'offset') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'scrollOffset') From e3d1a6aef8f485b834d73bc3def8ce847cf90b7d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 14:26:23 -0600 Subject: [PATCH 038/119] Render each line number on its own layer --- src/gutter-component.coffee | 61 ++++++++++++++++--------------------- static/editor.less | 6 +--- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 16903d5fd..c05f117b2 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,36 +11,29 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> + if @isMounted() + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = renderedRowRange + scrollOffset = -scrollTop % lineHeight + maxLineNumberDigits = editor.getLineCount().toString().length + + wrapCount = 0 + lineNumbers = + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + lineNumber = '•' + key = "#{bufferRow + 1}-#{++wrapCount}" + else + lastBufferRow = bufferRow + wrapCount = 0 + lineNumber = "#{bufferRow + 1}" + key = lineNumber + + LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + div className: 'gutter', - @renderLineNumbers() if @isMounted() - - renderLineNumbers: -> - {editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight} = @props - [startRow, endRow] = renderedRowRange - charWidth = editor.getDefaultCharWidth() - lineHeight = editor.getLineHeight() - style = - width: charWidth * (maxLineNumberDigits + 1.5) - height: scrollHeight - WebkitTransform: "translate3d(0, #{-scrollTop}px, 0)" - - lineNumbers = [] - tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1) - tokenizedLines.push({id: 0}) if tokenizedLines.length is 0 - for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - else - lastBufferRow = bufferRow - lineNumber = (bufferRow + 1).toString() - - key = tokenizedLines[i].id - screenRow = startRow + i - lineNumbers.push(LineNumberComponent({key, lineNumber, maxLineNumberDigits, bufferRow, screenRow, lineHeight})) - lastBufferRow = bufferRow - - div className: 'line-numbers', style: style, - lineNumbers + div className: 'line-numbers', + lineNumbers # 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 @@ -65,12 +58,10 @@ LineNumberComponent = React.createClass displayName: 'LineNumberComponent' render: -> - {bufferRow, screenRow, lineHeight} = @props + {index, lineHeight, scrollOffset} = @props div - className: "line-number line-number-#{bufferRow}" - style: {top: screenRow * lineHeight} - 'data-buffer-row': bufferRow - 'data-screen-row': screenRow + className: "line-number" + style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} dangerouslySetInnerHTML: {__html: @buildInnerHTML()} buildInnerHTML: -> @@ -84,4 +75,4 @@ LineNumberComponent = React.createClass iconDivHTML: '
' shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow', 'maxLineNumberDigits') + not isEqualForProperties(newProps, @props, 'index', 'lineHeight', 'scrollOffset') diff --git a/static/editor.less b/static/editor.less index b259d7652..9ce364108 100644 --- a/static/editor.less +++ b/static/editor.less @@ -57,14 +57,10 @@ } .gutter { - padding-left: 0.5em; - padding-right: 0.5em; + width: 100px; .line-number { position: absolute; - left: 0; - right: 0; - padding: 0; white-space: nowrap; .icon-right { From a36163ce86968446393d4cd7d1acf19b849be7d4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 15:30:52 -0600 Subject: [PATCH 039/119] Manually set the gutter width to the width of a line number We need to absolutely position line numbers to minimize repaints, but the gutter needs to be wide enough to show them. --- src/editor-component.coffee | 2 +- src/gutter-component.coffee | 59 ++++++++++++++++++++++++------------- static/editor.less | 3 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 8f89707a8..784a557c5 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -51,7 +51,7 @@ EditorComponent = React.createClass GutterComponent { editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, - onWidthChanged: @onGutterWidthChanged + width: @gutterWidth, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index c05f117b2..2f180badf 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,29 +11,46 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - if @isMounted() - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange - scrollOffset = -scrollTop % lineHeight - maxLineNumberDigits = editor.getLineCount().toString().length + {width} = @props - wrapCount = 0 - lineNumbers = - for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - key = "#{bufferRow + 1}-#{++wrapCount}" - else - lastBufferRow = bufferRow - wrapCount = 0 - lineNumber = "#{bufferRow + 1}" - key = lineNumber + div className: 'gutter', style: {width}, + div className: 'line-numbers', ref: 'lineNumbers', + if @isMounted() + @renderLineNumbers() + else + @renderLineNumberForMeasurement() - LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + renderLineNumbers: -> + {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = renderedRowRange + maxLineNumberDigits = @getMaxLineNumberDigits() + scrollOffset = -scrollTop % lineHeight + wrapCount = 0 - div className: 'gutter', - div className: 'line-numbers', - lineNumbers + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + lineNumber = '•' + key = "#{bufferRow + 1}-#{++wrapCount}" + else + lastBufferRow = bufferRow + wrapCount = 0 + lineNumber = "#{bufferRow + 1}" + key = lineNumber + + LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) + + renderLineNumberForMeasurement: -> + LineNumberComponent( + key: 'forMeasurement' + lineNumber: '•' + maxLineNumberDigits: @getMaxLineNumberDigits() + index: 0 + lineHeight: 0 + scrollOffset: 0 + ) + + getMaxLineNumberDigits: -> + @props.editor.getLineCount().toString().length # 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 @@ -49,7 +66,7 @@ GutterComponent = React.createClass componentDidUpdate: (oldProps) -> unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - width = @getDOMNode().offsetWidth + width = @refs.lineNumbers.getDOMNode().firstChild.offsetWidth if width isnt @lastMeasuredWidth @lastMeasuredWidth = width @props.onWidthChanged(width) diff --git a/static/editor.less b/static/editor.less index 9ce364108..613723c28 100644 --- a/static/editor.less +++ b/static/editor.less @@ -57,11 +57,10 @@ } .gutter { - width: 100px; - .line-number { position: absolute; white-space: nowrap; + padding: 0 .5em; .icon-right { padding: 0; From c8e9282557460e380b22a58f718869ef4b8b37df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:12:58 -0600 Subject: [PATCH 040/119] Position cursors as layers relative to the viewport --- src/cursor-component.coffee | 9 +++++++-- src/cursors-component.coffee | 4 ++-- src/editor-scroll-view-component.coffee | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index fcc6c2022..ebd82fa00 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -6,8 +6,13 @@ CursorComponent = React.createClass displayName: 'CursorComponent' render: -> - {top, left, height, width} = @props.cursor.getPixelRect() + {cursor, scrollTop} = @props + {top, left, height, width} = cursor.getPixelRect() + top -= scrollTop + className = 'cursor' className += ' blink-off' if @props.blinkOff - div className: className, style: {top, left, height, width} + WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)" + + div className: className, style: {height, width, WebkitTransform} diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index e3143f1f1..104cb935a 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -13,7 +13,7 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {editor} = @props + {editor, scrollTop} = @props blinkOff = @state.blinkCursorsOff div className: 'cursors', @@ -21,7 +21,7 @@ CursorsComponent = React.createClass for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, blinkOff}) getInitialState: -> blinkCursorsOff: false diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 88faf8f3c..c43d60330 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically From cfc08e8b98e5279367450074f99fa6dbaf5b8698 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:24:52 -0600 Subject: [PATCH 041/119] Allow horizontal scrolling --- src/lines-component.coffee | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index cc4ace194..c51f92bff 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,14 +12,15 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, scrollTop, lineHeight, showIndentGuide} = @props + {editor, renderedRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props [startRow, endRow] = renderedRowRange - scrollOffset = -scrollTop % lineHeight + verticalScrollOffset = -scrollTop % lineHeight + horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, scrollOffset, screenRow}) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow}) div {className: 'lines'}, lines @@ -105,10 +106,11 @@ LineComponent = React.createClass displayName: 'LineComponent' render: -> - {index, screenRow, scrollOffset, lineHeight} = @props + {index, screenRow, verticalScrollOffset, horizontalScrollOffset, lineHeight} = @props - top = index * lineHeight + scrollOffset - style = WebkitTransform: "translate3d(0px, #{top}px, 0px)" + top = index * lineHeight + verticalScrollOffset + left = horizontalScrollOffset + style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} @@ -138,4 +140,4 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'scrollOffset') + not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') From 757ae6de3904f1435a18c852d0427f985898f146 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 10 May 2014 16:58:19 -0600 Subject: [PATCH 042/119] Position selections relative to viewport This is getting closer, but lines still need to be opaque. Multi-line selections will still need to be rendered behind the line layers so they can extend to the edge of the viewport, so this code still has value. --- src/editor-scroll-view-component.coffee | 2 +- src/selection-component.coffee | 9 ++++++++- src/selections-component.coffee | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index c43d60330..6fdf54196 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -39,7 +39,7 @@ EditorScrollViewComponent = React.createClass renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', - SelectionsComponent({editor}) + SelectionsComponent({editor, scrollTop, scrollLeft}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-component.coffee b/src/selection-component.coffee index e3dd22e3a..3e0228079 100644 --- a/src/selection-component.coffee +++ b/src/selection-component.coffee @@ -6,6 +6,13 @@ SelectionComponent = React.createClass displayName: 'SelectionComponent' render: -> + {scrollTop, scrollLeft} = @props + div className: 'selection', for regionRect, i in @props.selection.getRegionRects() - div className: 'region', key: i, style: regionRect + {top, left, right, width, height} = regionRect + top -= scrollTop + left -= scrollLeft + right -= scrollLeft + WebkitTransform = "translate3d(0px, #{top}px, 0px)" + div className: 'region', key: i, style: {left, right, width, height, WebkitTransform} diff --git a/src/selections-component.coffee b/src/selections-component.coffee index 616fc62dd..e3de8e36f 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -7,10 +7,10 @@ SelectionsComponent = React.createClass displayName: 'SelectionsComponent' render: -> - {editor} = @props + {editor, scrollTop, scrollLeft} = @props div className: 'selections', if @isMounted() for selection in editor.getSelections() if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - SelectionComponent({key: selection.id, selection}) + SelectionComponent({key: selection.id, selection, scrollTop, scrollLeft}) From a22480d8572ec9f1dcf77e9b1da7b223cd3a713d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 11:36:54 -0600 Subject: [PATCH 043/119] Don't give lines a negative z-index Removing the z-index makes them accessible via mouse in the inspector. --- static/editor.less | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/editor.less b/static/editor.less index 613723c28..1529c33e2 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,8 +13,6 @@ } .lines { - z-index: -1; - .line { position: absolute; } From 63488997eeefd30ddd5a83a1915e5b155b185b2f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:22:11 -0600 Subject: [PATCH 044/119] Give lines and line numbers an opaque background to support sub-pixel AA Since lines and line numbers are now on the GPU, their text won't be properly anti-aliased on low-resolution displays unless their layers have a solid background. --- src/gutter-component.coffee | 2 +- src/lines-component.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 2f180badf..04bcfa7d6 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -77,7 +77,7 @@ LineNumberComponent = React.createClass render: -> {index, lineHeight, scrollOffset} = @props div - className: "line-number" + className: "line-number editor-colors" style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} dangerouslySetInnerHTML: {__html: @buildInnerHTML()} diff --git a/src/lines-component.coffee b/src/lines-component.coffee index c51f92bff..b1cd995c2 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -112,7 +112,7 @@ LineComponent = React.createClass left = horizontalScrollOffset style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} buildInnerHTML: -> if @props.tokenizedLine.text.length is 0 From 191bc115cff5642c493b3d745671e331450acec0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:22:26 -0600 Subject: [PATCH 045/119] Use explicit descendant selector for styling lines --- static/editor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/editor.less b/static/editor.less index 1529c33e2..9d20f625b 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,7 +13,7 @@ } .lines { - .line { + > .line { position: absolute; } } From 1aee276b4518c322419c2285caf4828a82202f65 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:40:19 -0600 Subject: [PATCH 046/119] Update line rendering specs for new layer scheme --- spec/editor-component-spec.coffee | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 23cd6a2d1..9e6b3505e 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -45,7 +45,7 @@ describe "EditorComponent", -> contentNode.style.width = '' describe "line rendering", -> - it "renders only the currently-visible lines", -> + it "renders only the currently-visible lines, translated relative to the scroll position", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() @@ -57,28 +57,27 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(node.querySelector('.scroll-view-content').style['-webkit-transform']).toBe "translate3d(0px, #{-2.5 * lineHeightInPixels}px, 0)" - lineNodes = node.querySelectorAll('.line') expect(lineNodes.length).toBe 6 - expect(lineNodes[0].offsetTop).toBe 2 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text + expect(lineNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" - it "updates absolute positions of subsequent lines when lines are inserted or removed", -> + it "updates the translation of subsequent lines when lines are inserted or removed", -> editor.getBuffer().deleteRows(0, 1) lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].offsetTop).toBe 0 - expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" editor.getBuffer().insert([0, 0], '\n\n') lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].offsetTop).toBe 0 - expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" describe "when indent guides are enabled", -> beforeEach -> From 8d25da9474a5646a91f73168e4ad89527100a191 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 12:45:58 -0600 Subject: [PATCH 047/119] Update line number rendering specs for new layer scheme --- spec/editor-component-spec.coffee | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 9e6b3505e..140b34dfb 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -143,40 +143,41 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line-number') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe "#{nbsp}1" - expect(lines[5].textContent).toBe "#{nbsp}6" + lineNumberNodes = node.querySelectorAll('.line-number') + expect(lineNumberNodes.length).toBe 6 + expect(lineNumberNodes[0].textContent).toBe "#{nbsp}1" + expect(lineNumberNodes[5].textContent).toBe "#{nbsp}6" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(node.querySelector('.line-numbers').style['-webkit-transform']).toBe "translate3d(0, #{-2.5 * lineHeightInPixels}px, 0)" - lineNumberNodes = node.querySelectorAll('.line-number') expect(lineNumberNodes.length).toBe 6 - expect(lineNumberNodes[0].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[5].offsetTop).toBe 7 * lineHeightInPixels + expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3" + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8" + expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" - it "updates absolute positions of subsequent line numbers when lines are inserted or removed", -> + it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].offsetTop).toBe 0 - expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].offsetTop).toBe 0 - expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels - expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels - expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels - expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels + expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{5 * lineHeightInPixels}px, 0px)" + expect(lineNumberNodes[6].style['-webkit-transform']).toBe "translate3d(0px, #{6 * lineHeightInPixels}px, 0px)" it "renders • characters for soft-wrapped lines", -> editor.setSoftWrap(true) From f3efd7d60be7148c16cd9b9cdccf90b122837b72 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:13:56 -0600 Subject: [PATCH 048/119] Position cursors relative to scrollLeft and fix specs --- spec/editor-component-spec.coffee | 25 +++++++++++-------------- src/cursor-component.coffee | 3 ++- src/cursors-component.coffee | 4 ++-- src/editor-scroll-view-component.coffee | 2 +- static/editor.less | 7 +++++++ 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 140b34dfb..684a0ed25 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -209,19 +209,19 @@ describe "EditorComponent", -> expect(node.textContent).toBe "#{i + 1}" describe "cursor rendering", -> - it "renders the currently visible cursors", -> + it "renders the currently visible cursors, translated relative to the scroll position", -> cursor1 = editor.getCursor() cursor1.setScreenPosition([0, 5]) node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * lineHeightInPixels + 'px' component.measureHeightAndWidth() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels expect(cursorNodes[0].offsetWidth).toBe charWidth - expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" cursor2 = editor.addCursorAtScreenPosition([6, 11]) cursor3 = editor.addCursorAtScreenPosition([4, 10]) @@ -229,25 +229,23 @@ describe "EditorComponent", -> cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth - expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels - expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{10 * charWidth}px, #{4 * lineHeightInPixels}px, 0px)" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + horizontalScrollbarNode.scrollLeft = 3.5 * charWidth + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth - expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels - expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 2.5) * lineHeightInPixels}px, 0px)" cursor3.destroy() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -330,8 +328,7 @@ describe "EditorComponent", -> cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels - expect(cursorNodes[0].offsetLeft).toBe 8 * charWidth + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)" describe "selection rendering", -> scrollViewClientLeft = null diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index ebd82fa00..5dc0eccbf 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -6,9 +6,10 @@ CursorComponent = React.createClass displayName: 'CursorComponent' render: -> - {cursor, scrollTop} = @props + {cursor, scrollTop, scrollLeft} = @props {top, left, height, width} = cursor.getPixelRect() top -= scrollTop + left -= scrollLeft className = 'cursor' className += ' blink-off' if @props.blinkOff diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 104cb935a..6883b10d5 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -13,7 +13,7 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {editor, scrollTop} = @props + {editor, scrollTop, scrollLeft} = @props blinkOff = @state.blinkCursorsOff div className: 'cursors', @@ -21,7 +21,7 @@ CursorsComponent = React.createClass for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, scrollTop, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft, blinkOff}) getInitialState: -> blinkCursorsOff: false diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 6fdf54196..76d0fd11e 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically diff --git a/static/editor.less b/static/editor.less index 9d20f625b..f7e0d8188 100644 --- a/static/editor.less +++ b/static/editor.less @@ -13,11 +13,17 @@ } .lines { + z-index: 0; + > .line { position: absolute; } } + .cursor { + z-index: 1; + } + .horizontal-scrollbar { position: absolute; left: 0; @@ -47,6 +53,7 @@ .scroll-view { overflow: hidden; + z-index: 0; } .scroll-view-content { From d53f97ecfe8dbf3e72592fac57be8e33a506e176 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:18:28 -0600 Subject: [PATCH 049/119] Fix horizontal scrolling spec --- spec/editor-component-spec.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 684a0ed25..22aef3aab 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -524,16 +524,18 @@ describe "EditorComponent", -> editor.setScrollTop(10) expect(verticalScrollbarNode.scrollTop).toBe 10 - it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", -> + it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - scrollViewContentNode = node.querySelector('.scroll-view-content') - expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0)" + lineNodes = node.querySelectorAll('.line') + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) - expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0)" + expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" + expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(-100px, #{4 * lineHeightInPixels}px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> From e44027b18677888233504c8940e5585bb8bb0d65 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:30:21 -0600 Subject: [PATCH 050/119] Fix the height/width of the editor in spec Now that everything is absolutely position, the editor no longer assumes a "natural" height and width. This can be addressed later if we want to allow editors to expand based on their content. --- spec/editor-component-spec.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 22aef3aab..b7afea0e6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -41,6 +41,10 @@ describe "EditorComponent", -> verticalScrollbarNode = node.querySelector('.vertical-scrollbar') horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar') + node.style.height = editor.getLineCount() * lineHeightInPixels + 'px' + node.style.width = '1000px' + component.measureHeightAndWidth() + afterEach -> contentNode.style.width = '' From 01622140e34f65c83c148c11c6c785d3350c5753 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 15:38:12 -0600 Subject: [PATCH 051/119] Rename renderedRowRange to visibleRowRange We only render visible rows now, so this makes more sense. --- src/editor-component.coffee | 6 +++--- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 10 +++++----- src/lines-component.coffee | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 784a557c5..c3b4b37ac 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -33,7 +33,7 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - renderedRowRange = editor.getVisibleRowRange() + visibleRowRange = editor.getVisibleRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -49,14 +49,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, + editor, visibleRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, width: @gutterWidth, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges + lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 76d0fd11e..727a7e11c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically } div className: 'underlayer', SelectionsComponent({editor, scrollTop, scrollLeft}) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 04bcfa7d6..214b1af52 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -21,8 +21,8 @@ GutterComponent = React.createClass @renderLineNumberForMeasurement() renderLineNumbers: -> - {editor, renderedRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange + {editor, visibleRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange maxLineNumberDigits = @getMaxLineNumberDigits() scrollOffset = -scrollTop % lineHeight wrapCount = 0 @@ -56,11 +56,11 @@ GutterComponent = React.createClass # non-zero-delta change to the screen lines has occurred within the current # visible row range. shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize') + return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') - {renderedRowRange, pendingChanges} = newProps + {visibleRowRange, pendingChanges} = newProps for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0 - return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b1cd995c2..c7d6ee3f7 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,8 +12,8 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, renderedRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props - [startRow, endRow] = renderedRowRange + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft @@ -31,11 +31,11 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') - {renderedRowRange, pendingChanges} = newProps + {visibleRowRange, pendingChanges} = newProps for change in pendingChanges - return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false @@ -56,7 +56,7 @@ LinesComponent = React.createClass editor.setDefaultCharWidth(charWidth) measureCharactersInNewLines: -> - [visibleStartRow, visibleEndRow] = @props.renderedRowRange + [visibleStartRow, visibleEndRow] = @props.visibleRowRange node = @getDOMNode() for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) From 7a9278e6a76058a2b446d3520694f5cca86725dc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 16:47:39 -0600 Subject: [PATCH 052/119] Render selection fragments on opaque lines Because lines are opaque on the GPU for sub pixel antialiasing, the lines obscure the selections which were formerly rendered behind the lines. This commit renders selection fragments *on* each opaque line layer so the selections look correct again. Still needs cleanup and optimization. --- src/editor-component.coffee | 22 ++++++++++------------ src/editor-scroll-view-component.coffee | 5 +++-- src/editor.coffee | 3 +++ src/lines-component.coffee | 22 +++++++++++++++++----- src/selection.coffee | 19 +++++++++++++++++++ 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index c3b4b37ac..0dd622579 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -20,6 +20,7 @@ EditorComponent = React.createClass batchingUpdates: false updateRequested: false cursorsMoved: false + selectionChanged: false scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -55,9 +56,9 @@ EditorComponent = React.createClass } EditorScrollViewComponent { - ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide - lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges - scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, + ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, + lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, + scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } @@ -124,6 +125,7 @@ EditorComponent = React.createClass componentDidUpdate: -> @pendingChanges.length = 0 @cursorsMoved = false + @selectionChanged = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars @props.parentView.trigger 'editor:display-updated' @@ -134,9 +136,7 @@ EditorComponent = React.createClass @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved - @subscribe editor, 'selection-screen-range-changed', @requestUpdate - @subscribe editor, 'selection-added', @onSelectionAdded - @subscribe editor, 'selection-removed', @onSelectionAdded + @subscribe editor, 'selection-added selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -376,9 +376,11 @@ EditorComponent = React.createClass @pendingChanges.push(change) @requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events - onSelectionAdded: (selection) -> + onSelectionChanged: (selection) -> {editor} = @props - @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) + if editor.selectionIntersectsVisibleRowRange(selection) + @selectionChanged = true + @requestUpdate() onScrollTopChanged: -> @scrollingVertically = true @@ -392,10 +394,6 @@ EditorComponent = React.createClass stopScrollingAfterDelay: null # created lazily - onSelectionRemoved: (selection) -> - {editor} = @props - @requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection) - onCursorsMoved: -> @cursorsMoved = true diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 727a7e11c..51f62d720 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,8 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged } div className: 'underlayer', SelectionsComponent({editor, scrollTop, scrollLeft}) diff --git a/src/editor.coffee b/src/editor.coffee index 467f13e32..3dc51555d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1259,6 +1259,9 @@ class Editor extends Model # Returns: An {Array} of {Selection}s. getSelections: -> new Array(@selections...) + selectionsForScreenRows: (startRow, endRow) -> + @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) + # Public: Get the most recent {Selection} or the selection at the given # index. # diff --git a/src/lines-component.coffee b/src/lines-component.coffee index c7d6ee3f7..e13133402 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -12,15 +12,17 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide} = @props + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + visibleSelections = editor.selectionsForScreenRows(startRow, endRow - 1) verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow}) + selections = visibleSelections.filter (selection) -> selection.intersectsScreenRow(screenRow) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) div {className: 'lines'}, lines @@ -31,6 +33,7 @@ LinesComponent = React.createClass @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> + return true if newProps.selectionChanged return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') {visibleRowRange, pendingChanges} = newProps @@ -112,9 +115,11 @@ LineComponent = React.createClass left = horizontalScrollOffset style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, + span dangerouslySetInnerHTML: {__html: @buildTokensHTML()} + @renderSelections() - buildInnerHTML: -> + buildTokensHTML: -> if @props.tokenizedLine.text.length is 0 @buildEmptyLineHTML() else @@ -139,5 +144,12 @@ LineComponent = React.createClass else "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" + renderSelections: -> + {selections, screenRow} = @props + for selection in selections + div className: 'selection', key: selection.id, + div className: 'region', style: selection.regionRectForScreenRow(screenRow) + shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') + return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') + newProps.selectionChanged and (newProps.selections.length > 0 or @props.selections.length > 0) diff --git a/src/selection.coffee b/src/selection.coffee index 00cae8b7c..14d218fb0 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -553,6 +553,12 @@ class Selection extends Model intersectsBufferRange: (bufferRange) -> @getBufferRange().intersectsWith(bufferRange) + intersectsScreenRowRange: (startRow, endRow) -> + @getScreenRange().intersectsRowRange(startRow, endRow) + + intersectsScreenRow: (screenRow) -> + @getScreenRange().intersectsRow(screenRow) + # Public: Identifies if a selection intersects with another selection. # # otherSelection - A {Selection} to check against. @@ -626,6 +632,19 @@ class Selection extends Model rects + regionRectForScreenRow: (screenRow) -> + {start, end} = @getScreenRange() + region = {height: @editor.getLineHeight(), top: 0, left: 0} + + if screenRow is start.row + region.left = @editor.pixelPositionForScreenPosition(start).left + + if screenRow is end.row + region.width = @editor.pixelPositionForScreenPosition(end).left - region.left + + region.right = 0 unless region.width? + region + screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange From ce9fe90217e480f30111ba9eddd4ad10eb4206a1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 12 May 2014 20:05:44 -0600 Subject: [PATCH 053/119] Make multi-line selections appear to span the screen with a single div Because lines are opaque and any area of a selection that overlaps a line is actually rendered on the line itself, the screen-spanning background of a multi-line selection can actually be rendered as a single div spanning the entire screen from the first row to the penultimate row of the selection. --- src/editor-scroll-view-component.coffee | 4 +- src/selection-backgrounds-component.coffee | 18 ++++++++ src/selection-component.coffee | 18 -------- src/selection.coffee | 52 +++++----------------- src/selections-component.coffee | 16 ------- 5 files changed, 31 insertions(+), 77 deletions(-) create mode 100644 src/selection-backgrounds-component.coffee delete mode 100644 src/selection-component.coffee delete mode 100644 src/selections-component.coffee diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 51f62d720..817980189 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -5,7 +5,7 @@ React = require 'react' InputComponent = require './input-component' LinesComponent = require './lines-component' CursorsComponent = require './cursors-component' -SelectionsComponent = require './selections-component' +SelectionBackgroundsComponent = require './selection-backgrounds-component' module.exports = EditorScrollViewComponent = React.createClass @@ -40,7 +40,7 @@ EditorScrollViewComponent = React.createClass selectionChanged } div className: 'underlayer', - SelectionsComponent({editor, scrollTop, scrollLeft}) + SelectionBackgroundsComponent({editor, scrollTop, scrollLeft}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee new file mode 100644 index 000000000..62152cc09 --- /dev/null +++ b/src/selection-backgrounds-component.coffee @@ -0,0 +1,18 @@ +React = require 'react' +{div} = require 'reactionary' + +module.exports = +SelectionBackgroundsComponent = React.createClass + displayName: 'SelectionBackgroundsComponent' + + render: -> + {editor, scrollTop} = @props + + div className: 'selections', + if @isMounted() + for selection in editor.getSelections() + if backgroundRect = selection.getBackgroundRect() + {top, left, right, height} = backgroundRect + WebkitTransform = "translate3d(0px, #{top - scrollTop}px, 0px)" + div className: 'selection', key: selection.id, + div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection-component.coffee b/src/selection-component.coffee deleted file mode 100644 index 3e0228079..000000000 --- a/src/selection-component.coffee +++ /dev/null @@ -1,18 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' - -module.exports = -SelectionComponent = React.createClass - displayName: 'SelectionComponent' - - render: -> - {scrollTop, scrollLeft} = @props - - div className: 'selection', - for regionRect, i in @props.selection.getRegionRects() - {top, left, right, width, height} = regionRect - top -= scrollTop - left -= scrollLeft - right -= scrollLeft - WebkitTransform = "translate3d(0px, #{top}px, 0px)" - div className: 'region', key: i, style: {left, right, width, height, WebkitTransform} diff --git a/src/selection.coffee b/src/selection.coffee index 14d218fb0..fdccb0242 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,47 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - # Get the pixel dimensions of rectangular regions that cover selection's area - # on the screen. Used by SelectionComponent for rendering. - getRegionRects: -> - lineHeight = @editor.getLineHeight() - {start, end} = @getScreenRange() - rowCount = end.row - start.row + 1 - startPixelPosition = @editor.pixelPositionForScreenPosition(start) - endPixelPosition = @editor.pixelPositionForScreenPosition(end) - - if rowCount is 1 - # Single line selection - rects = [{ - top: startPixelPosition.top - height: lineHeight - left: startPixelPosition.left - width: endPixelPosition.left - startPixelPosition.left - }] - else - # Multi-line selection - rects = [] - - # First row, extending from selection start to the right side of screen - rects.push { - top: startPixelPosition.top - left: startPixelPosition.left - height: lineHeight - right: 0 - } - if rowCount > 2 - # Middle rows, extending from left side to right side of screen - rects.push { - top: startPixelPosition.top + lineHeight - height: (rowCount - 2) * lineHeight - left: 0 - right: 0 - } - # Last row, extending from left side of screen to selection end - rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left } - - rects - regionRectForScreenRow: (screenRow) -> {start, end} = @getScreenRange() region = {height: @editor.getLineHeight(), top: 0, left: 0} @@ -645,6 +604,17 @@ class Selection extends Model region.right = 0 unless region.width? region + getBackgroundRect: -> + {start, end} = @getScreenRange() + return if start.row is end.row + + lineHeight = @editor.getLineHeight() + height = (end.row - start.row) * lineHeight + top = start.row * lineHeight + left = 0 + right = 0 + {top, left, right, height} + screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange diff --git a/src/selections-component.coffee b/src/selections-component.coffee deleted file mode 100644 index e3de8e36f..000000000 --- a/src/selections-component.coffee +++ /dev/null @@ -1,16 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' -SelectionComponent = require './selection-component' - -module.exports = -SelectionsComponent = React.createClass - displayName: 'SelectionsComponent' - - render: -> - {editor, scrollTop, scrollLeft} = @props - - div className: 'selections', - if @isMounted() - for selection in editor.getSelections() - if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - SelectionComponent({key: selection.id, selection, scrollTop, scrollLeft}) From cbcc30b384797c62dee253200a5b5310c6c9cd98 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:31:56 -0600 Subject: [PATCH 054/119] Don't render empty selections --- src/lines-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e13133402..9f2b926b1 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -21,7 +21,7 @@ LinesComponent = React.createClass lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - selections = visibleSelections.filter (selection) -> selection.intersectsScreenRow(screenRow) + selections = visibleSelections.filter (selection) -> not selection.isEmpty() and selection.intersectsScreenRow(screenRow) LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) div {className: 'lines'}, lines From 9001d34ddf0e5c02de0bfe5aeacf0f4c11a1824b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:32:20 -0600 Subject: [PATCH 055/119] Change selection specs to match new rendering scheme --- spec/editor-component-spec.coffee | 85 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index b7afea0e6..ff6665cfb 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -335,62 +335,57 @@ describe "EditorComponent", -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)" describe "selection rendering", -> - scrollViewClientLeft = null + [scrollViewNode, scrollViewClientLeft] = [] beforeEach -> + scrollViewNode = node.querySelector('.scroll-view') scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left - it "renders 1 region for 1-line selections", -> - # 1-line selection - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - regions = node.querySelectorAll('.selection .region') + describe "for single line selections", -> + it "renders 1 region on the line and no background region", -> + # 1-line selection + editor.setSelectedScreenRange([[1, 6], [1, 10]]) + lineNodes = node.querySelectorAll('.line') + line1Region = lineNodes[1].querySelector('.selection .region') + regionRect = line1Region.getBoundingClientRect() + expect(regionRect.top).toBe 1 * lineHeightInPixels + expect(regionRect.height).toBe 1 * lineHeightInPixels + expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(regionRect.width).toBe 4 * charWidth - expect(regions.length).toBe 1 - regionRect = regions[0].getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(regionRect.width).toBe 4 * charWidth + expect(node.querySelectorAll('.underlayer .selection .region').length).toBe 0 - it "renders 2 regions for 2-line selections", -> - editor.setSelectedScreenRange([[1, 6], [2, 10]]) - regions = node.querySelectorAll('.selection .region') - expect(regions.length).toBe 2 + describe "for multi-line selections", -> + it "renders a region on each line and a full-width background region from the first line to the penultimate line", -> + editor.setSelectedScreenRange([[1, 6], [3, 10]]) - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed + lineNodes = node.querySelectorAll('.line') + region1Rect = lineNodes[1].querySelector('.selection .region').getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewClientLeft + lineNodes[1].offsetWidth - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(region2Rect.width).toBe 10 * charWidth + region2Rect = lineNodes[2].querySelector('.selection .region').getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + expect(region2Rect.width).toBe lineNodes[2].offsetWidth - it "renders 3 regions for selections with more than 2 lines", -> - editor.setSelectedScreenRange([[1, 6], [5, 10]]) - regions = node.querySelectorAll('.selection .region') - expect(regions.length).toBe 3 + region3Rect = lineNodes[3].querySelector('.selection .region').getBoundingClientRect() + expect(region3Rect.top).toBe 3 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBe scrollViewClientLeft + 0 + expect(region3Rect.width).toBe 10 * charWidth - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed + backgroundNodes = node.querySelectorAll('.underlayer .selection .region') + expect(backgroundNodes.length).toBe 1 + backgroundRegionRect = backgroundNodes[0].getBoundingClientRect() - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 3 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(Math.ceil(region2Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 5 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.width).toBe 10 * charWidth + expect(backgroundRegionRect.top).toBe 1 * lineHeightInPixels + expect(backgroundRegionRect.left).toBe scrollViewClientLeft + expect(backgroundRegionRect.width).toBe scrollViewNode.offsetWidth + expect(backgroundRegionRect.height).toBe 2 * lineHeightInPixels it "does not render empty selections", -> expect(editor.getSelection().isEmpty()).toBe true From 0162247bd724d7f20d21864f7dd28989f1c44618 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:49:45 -0600 Subject: [PATCH 056/119] Precompute selection regions for all lines This is easer to reason about and probably more efficient than computing everything on a per-line basis. --- src/lines-component.coffee | 41 +++++++++++++++++++++++++++++--------- src/selection.coffee | 13 ------------ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 9f2b926b1..1ec920d78 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -14,18 +14,41 @@ LinesComponent = React.createClass if @isMounted() {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange - visibleSelections = editor.selectionsForScreenRows(startRow, endRow - 1) + selectionRegionsByScreenRow = @getVisibleSelectionRegions() verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft lines = for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) screenRow = startRow + index - selections = visibleSelections.filter (selection) -> not selection.isEmpty() and selection.intersectsScreenRow(screenRow) - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selections, selectionChanged}) + selectionRegions = selectionRegionsByScreenRow[screenRow] + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selectionRegions}) div {className: 'lines'}, lines + getVisibleSelectionRegions: -> + {editor, visibleRowRange, lineHeight} = @props + [visibleStartRow, visibleEndRow] = visibleRowRange + regions = {} + + for selection in editor.selectionsForScreenRows(visibleStartRow, visibleEndRow - 1) when not selection.isEmpty() + {start, end} = selection.getScreenRange() + + for screenRow in [start.row..end.row] + region = {id: selection.id, top: 0, left: 0, height: lineHeight} + + if screenRow is start.row + region.left = editor.pixelPositionForScreenPosition(start).left + if screenRow is end.row + region.width = editor.pixelPositionForScreenPosition(end).left - region.left + else + region.right = 0 + + regions[screenRow] ?= [] + regions[screenRow].push(region) + + regions + componentWillMount: -> @measuredLines = new WeakSet @@ -145,11 +168,11 @@ LineComponent = React.createClass "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" renderSelections: -> - {selections, screenRow} = @props - for selection in selections - div className: 'selection', key: selection.id, - div className: 'region', style: selection.regionRectForScreenRow(screenRow) + {selectionRegions} = @props + if selectionRegions? + for region in selectionRegions + div className: 'selection', key: region.id, + div className: 'region', style: region shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') - newProps.selectionChanged and (newProps.selections.length > 0 or @props.selections.length > 0) + return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'selectionRegions', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') diff --git a/src/selection.coffee b/src/selection.coffee index fdccb0242..d02eded36 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,19 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - regionRectForScreenRow: (screenRow) -> - {start, end} = @getScreenRange() - region = {height: @editor.getLineHeight(), top: 0, left: 0} - - if screenRow is start.row - region.left = @editor.pixelPositionForScreenPosition(start).left - - if screenRow is end.row - region.width = @editor.pixelPositionForScreenPosition(end).left - region.left - - region.right = 0 unless region.width? - region - getBackgroundRect: -> {start, end} = @getScreenRange() return if start.row is end.row From 9b02055db9c5004d2ccdaf37dc68a765aaa76a4a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 10:59:38 -0600 Subject: [PATCH 057/119] Move selection background region calculation into React component --- src/editor-scroll-view-component.coffee | 2 +- src/selection-backgrounds-component.coffee | 18 ++++++++++++------ src/selection.coffee | 11 ----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 817980189..02e37b388 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -40,7 +40,7 @@ EditorScrollViewComponent = React.createClass selectionChanged } div className: 'underlayer', - SelectionBackgroundsComponent({editor, scrollTop, scrollLeft}) + SelectionBackgroundsComponent({editor, lineHeight, scrollTop}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee index 62152cc09..993007184 100644 --- a/src/selection-backgrounds-component.coffee +++ b/src/selection-backgrounds-component.coffee @@ -6,13 +6,19 @@ SelectionBackgroundsComponent = React.createClass displayName: 'SelectionBackgroundsComponent' render: -> - {editor, scrollTop} = @props + {editor, lineHeight, scrollTop} = @props div className: 'selections', if @isMounted() for selection in editor.getSelections() - if backgroundRect = selection.getBackgroundRect() - {top, left, right, height} = backgroundRect - WebkitTransform = "translate3d(0px, #{top - scrollTop}px, 0px)" - div className: 'selection', key: selection.id, - div className: 'region', style: {left, right, height, WebkitTransform} + {start, end} = selection.getScreenRange() + continue if start.row is end.row + + height = (end.row - start.row) * lineHeight + top = (start.row * lineHeight) - scrollTop + left = 0 + right = 0 + WebkitTransform = "translate3d(0px, #{top}px, 0px)" + + div className: 'selection', key: selection.id, + div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection.coffee b/src/selection.coffee index d02eded36..5d70d9406 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -591,17 +591,6 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - getBackgroundRect: -> - {start, end} = @getScreenRange() - return if start.row is end.row - - lineHeight = @editor.getLineHeight() - height = (end.row - start.row) * lineHeight - top = start.row * lineHeight - left = 0 - right = 0 - {top, left, right, height} - screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange From 070d239f41781fd8f04e73cdc581ba947853f182 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 12:12:04 -0600 Subject: [PATCH 058/119] Blink cursors with a CSS animation Now that they're on their own layer, I don't think it affects the repaint timing when typing on lines (if it ever did). --- spec/editor-component-spec.coffee | 43 ++++++------------------- src/cursor-component.coffee | 6 +--- src/cursors-component.coffee | 20 ++++++------ src/editor-component.coffee | 7 ++-- src/editor-scroll-view-component.coffee | 4 +-- static/editor.less | 12 +++++++ 6 files changed, 36 insertions(+), 56 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index ff6665cfb..a81a1dc68 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -268,44 +268,19 @@ describe "EditorComponent", -> expect(cursorRect.width).toBe rangeRect.width it "blinks cursors when they aren't moving", -> - editor.addCursorAtScreenPosition([1, 0]) - [cursorNode1, cursorNode2] = node.querySelectorAll('.cursor') - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + jasmine.unspy(window, 'setTimeout') - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true + cursorsNode = node.querySelector('.cursors') + expect(cursorsNode.classList.contains('blinking')).toBe true - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false - - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - # Stop blinking immediately when cursors move - advanceClock(component.props.cursorBlinkPeriod / 4) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - # Stop blinking for one full period after moving the cursor + # Stop blinking after moving the cursor editor.moveCursorRight() - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + expect(cursorsNode.classList.contains('blinking')).toBe false - advanceClock(component.props.cursorBlinkResumeDelay / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false - - advanceClock(component.props.cursorBlinkResumeDelay / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe true - expect(cursorNode2.classList.contains('blink-off')).toBe true - - advanceClock(component.props.cursorBlinkPeriod / 2) - expect(cursorNode1.classList.contains('blink-off')).toBe false - expect(cursorNode2.classList.contains('blink-off')).toBe false + # Resume blinking after resume delay passes + waits component.props.cursorBlinkResumeDelay + runs -> + expect(cursorsNode.classList.contains('blinking')).toBe true it "renders the hidden input field at the position of the last cursor if it is on screen", -> inputNode = node.querySelector('.hidden-input') diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee index 5dc0eccbf..36ba33a08 100644 --- a/src/cursor-component.coffee +++ b/src/cursor-component.coffee @@ -10,10 +10,6 @@ CursorComponent = React.createClass {top, left, height, width} = cursor.getPixelRect() top -= scrollTop left -= scrollLeft - - className = 'cursor' - className += ' blink-off' if @props.blinkOff - WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)" - div className: className, style: {height, width, WebkitTransform} + div className: 'cursor', style: {height, width, WebkitTransform} diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 6883b10d5..50d53ad1d 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -4,7 +4,6 @@ React = require 'react' SubscriberMixin = require './subscriber-mixin' CursorComponent = require './cursor-component' - module.exports = CursorsComponent = React.createClass displayName: 'CursorsComponent' @@ -14,21 +13,23 @@ CursorsComponent = React.createClass render: -> {editor, scrollTop, scrollLeft} = @props - blinkOff = @state.blinkCursorsOff + {blinking} = @state - div className: 'cursors', + className = 'cursors' + className += ' blinking' if blinking + + div {className}, if @isMounted() for selection in editor.getSelections() if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) {cursor} = selection - CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft, blinkOff}) + CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft}) getInitialState: -> - blinkCursorsOff: false + blinking: true componentDidMount: -> {editor} = @props - @startBlinkingCursors() componentWillUnmount: -> clearInterval(@cursorBlinkIntervalHandle) @@ -37,14 +38,11 @@ CursorsComponent = React.createClass @pauseCursorBlinking() if cursorsMoved startBlinkingCursors: -> - @cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2) + @setState(blinking: true) if @isMounted() startBlinkingCursorsAfterDelay: null # Created lazily - toggleCursorBlink: -> @setState(blinkCursorsOff: not @state.blinkCursorsOff) - pauseCursorBlinking: -> - @state.blinkCursorsOff = false - clearInterval(@cursorBlinkIntervalHandle) + @state.blinking = false @startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay) @startBlinkingCursorsAfterDelay() diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 0dd622579..bb24726b4 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -30,7 +30,7 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props + {editor, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() @@ -59,7 +59,7 @@ EditorComponent = React.createClass ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } ScrollbarComponent @@ -97,8 +97,7 @@ EditorComponent = React.createClass getInitialState: -> {} getDefaultProps: -> - cursorBlinkPeriod: 800 - cursorBlinkResumeDelay: 200 + cursorBlinkResumeDelay: 100 componentWillMount: -> @pendingChanges = [] diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 02e37b388..6290bc52b 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -18,7 +18,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props - {cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props + {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, diff --git a/static/editor.less b/static/editor.less index f7e0d8188..eb4a57b65 100644 --- a/static/editor.less +++ b/static/editor.less @@ -24,6 +24,18 @@ z-index: 1; } + &.is-focused .cursors.blinking .cursor { + -webkit-animation: blink 0.8s; + -webkit-animation-iteration-count: infinite; + } + + @-webkit-keyframes blink { + 0% { opacity: .7; } + 50% { opacity: .7; } + 51% { opacity: 0; } + 100% { opacity: 0; } + } + .horizontal-scrollbar { position: absolute; left: 0; From f07a832c835325dd3130cec51cb057210d9f2492 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 12:37:55 -0600 Subject: [PATCH 059/119] Sync cursor animations when cursors are added --- src/cursors-component.coffee | 11 ++++++++++- src/editor-component.coffee | 14 ++++++++++++-- src/editor-scroll-view-component.coffee | 4 ++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 50d53ad1d..16213b1b9 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -1,6 +1,6 @@ React = require 'react' {div} = require 'reactionary' -{debounce} = require 'underscore-plus' +{debounce, toArray} = require 'underscore-plus' SubscriberMixin = require './subscriber-mixin' CursorComponent = require './cursor-component' @@ -37,6 +37,9 @@ CursorsComponent = React.createClass componentWillUpdate: ({cursorsMoved}) -> @pauseCursorBlinking() if cursorsMoved + componentDidUpdate: -> + @syncCursorAnimations() if @props.selectionAdded + startBlinkingCursors: -> @setState(blinking: true) if @isMounted() @@ -46,3 +49,9 @@ CursorsComponent = React.createClass @state.blinking = false @startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay) @startBlinkingCursorsAfterDelay() + + syncCursorAnimations: -> + node = @getDOMNode() + cursorNodes = toArray(node.children) + node.removeChild(cursorNode) for cursorNode in cursorNodes + node.appendChild(cursorNode) for cursorNode in cursorNodes diff --git a/src/editor-component.coffee b/src/editor-component.coffee index bb24726b4..408216587 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -21,6 +21,7 @@ EditorComponent = React.createClass updateRequested: false cursorsMoved: false selectionChanged: false + selectionAdded: false scrollingVertically: false gutterWidth: 0 refreshingScrollbars: false @@ -59,7 +60,7 @@ EditorComponent = React.createClass ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred } ScrollbarComponent @@ -125,6 +126,7 @@ EditorComponent = React.createClass @pendingChanges.length = 0 @cursorsMoved = false @selectionChanged = false + @selectionAdded = false @refreshingScrollbars = false @measureScrollbars() if @measuringScrollbars @props.parentView.trigger 'editor:display-updated' @@ -135,7 +137,8 @@ EditorComponent = React.createClass @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved - @subscribe editor, 'selection-added selection-removed selection-screen-range-changed', @onSelectionChanged + @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged + @subscribe editor, 'selection-added', @onSelectionAdded @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -381,6 +384,13 @@ EditorComponent = React.createClass @selectionChanged = true @requestUpdate() + onSelectionAdded: (selection) -> + {editor} = @props + if editor.selectionIntersectsVisibleRowRange(selection) + @selectionChanged = true + @selectionAdded = true + @requestUpdate() + onScrollTopChanged: -> @scrollingVertically = true @requestUpdate() diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 6290bc52b..98ae26fcf 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, selectionAdded} = @props {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -33,7 +33,7 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, cursorBlinkResumeDelay}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, From 8148e4e50db9104f749fa83d30bfc095007b43aa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 14:22:47 -0600 Subject: [PATCH 060/119] Skip selection restoration on our fork of react --- src/input-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input-component.coffee b/src/input-component.coffee index d441c2bce..d569dbd0d 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -10,7 +10,7 @@ InputComponent = React.createClass render: -> {className, style, onFocus, onBlur} = @props - input {className, style, onFocus, onBlur} + input {className, style, onFocus, onBlur, 'data-react-skip-selection-restoration': true} getInitialState: -> {lastChar: ''} From 4f9108980fa3a8690a29bee8e8517ef010269676 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 15:30:53 -0600 Subject: [PATCH 061/119] WIP: Manually update line nodes when scrolling --- src/lines-component.coffee | 79 ++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1ec920d78..dd2e41c7b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -3,6 +3,8 @@ React = require 'react' {debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' {$$} = require 'space-pen' +EditorView = require './editor-view' + DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -11,20 +13,7 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - if @isMounted() - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props - [startRow, endRow] = visibleRowRange - selectionRegionsByScreenRow = @getVisibleSelectionRegions() - verticalScrollOffset = -scrollTop % lineHeight - horizontalScrollOffset = -scrollLeft - - lines = - for tokenizedLine, index in editor.linesForScreenRows(startRow, endRow - 1) - screenRow = startRow + index - selectionRegions = selectionRegionsByScreenRow[screenRow] - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, index, verticalScrollOffset, horizontalScrollOffset, screenRow, selectionRegions}) - - div {className: 'lines'}, lines + div {className: 'lines'} getVisibleSelectionRegions: -> {editor, visibleRowRange, lineHeight} = @props @@ -51,6 +40,7 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet + @lineNodesByLineId = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -66,10 +56,67 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> + @updateRenderedLines() @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically + updateRenderedLines: -> + {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props + [startRow, endRow] = visibleRowRange + verticalScrollOffset = -scrollTop % lineHeight + horizontalScrollOffset = -scrollLeft + + node = @getDOMNode() + + currentLineIds = new Set + lines = editor.linesForScreenRows(startRow, endRow - 1) + for line in lines + currentLineIds.add(line.id.toString()) + + for id, domNode of @lineNodesByLineId + unless currentLineIds.has(id) + delete @lineNodesByLineId[id] + node.removeChild(domNode) + + for line, index in lines + top = (index * lineHeight) + verticalScrollOffset + left = horizontalScrollOffset + screenRow = startRow + index + + if @hasNodeForLine(line.id) + @updateNodeForLine(line, screenRow, top, left) + else + @buildNodeForLine(line, screenRow, top, left) + + hasNodeForLine: (id) -> + @lineNodesByLineId[id]? + + buildNodeForLine: (tokenizedLine, screenRow, top, left) -> + {editor} = @props + {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = tokenizedLine + if fold + attributes = {class: 'fold line', 'fold-id': fold.id} + else + attributes = {class: 'line'} + + invisibles = {} + eolInvisibles = {} + htmlEolInvisibles = [] + indentation = indentLevel + + wrapper = document.createElement('div') + wrapper.innerHTML = EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, indentation, editor}) + lineNode = wrapper.children[0] + lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + + @lineNodesByLineId[tokenizedLine.id] = lineNode + @getDOMNode().appendChild(lineNode) + + updateNodeForLine: (tokenizedLine, screenRow, top, left) -> + lineNode = @lineNodesByLineId[tokenizedLine.id] + lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + measureLineHeightAndCharWidth: -> node = @getDOMNode() node.appendChild(DummyLineNode) @@ -85,9 +132,9 @@ LinesComponent = React.createClass [visibleStartRow, visibleEndRow] = @props.visibleRowRange node = @getDOMNode() - for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) + for tokenizedLine in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) unless @measuredLines.has(tokenizedLine) - lineNode = node.children[i] + lineNode = @lineNodesByLineId[tokenizedLine.id] @measureCharactersInLine(tokenizedLine, lineNode) measureCharactersInLine: (tokenizedLine, lineNode) -> From ea5c5c9e84d4b2e8c2399e569a571b9a6eab3ca8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 20:46:15 -0600 Subject: [PATCH 062/119] Move line HTML generation into lines component --- src/lines-component.coffee | 174 ++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 88 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index dd2e41c7b..37d319d6c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -1,12 +1,13 @@ React = require 'react' {div, span} = require 'reactionary' -{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' +{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' {$$} = require 'space-pen' EditorView = require './editor-view' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +WrapperDiv = document.createElement('div') module.exports = LinesComponent = React.createClass @@ -62,58 +63,105 @@ LinesComponent = React.createClass @measureCharactersInNewLines() unless @props.scrollingVertically updateRenderedLines: -> - {editor, visibleRowRange, scrollTop, scrollLeft, lineHeight, showIndentGuide, selectionChanged} = @props + {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + visibleLines = editor.linesForScreenRows(startRow, endRow - 1) + @removeNonVisibleLineNodes(visibleLines) + @appendOrUpdateVisibleLineNodes(visibleLines) + + removeNonVisibleLineNodes: (visibleLines) -> + visibleLineIds = new Set + visibleLineIds.add(line.id.toString()) for line in visibleLines + node = @getDOMNode() + for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) + delete @lineNodesByLineId[lineId] + node.removeChild(lineNode) + + appendOrUpdateVisibleLineNodes: (visibleLines) -> + {scrollTop, scrollLeft, lineHeight} = @props + newLines = null + newLinesHTML = null verticalScrollOffset = -scrollTop % lineHeight horizontalScrollOffset = -scrollLeft - node = @getDOMNode() - - currentLineIds = new Set - lines = editor.linesForScreenRows(startRow, endRow - 1) - for line in lines - currentLineIds.add(line.id.toString()) - - for id, domNode of @lineNodesByLineId - unless currentLineIds.has(id) - delete @lineNodesByLineId[id] - node.removeChild(domNode) - - for line, index in lines + for line, index in visibleLines top = (index * lineHeight) + verticalScrollOffset left = horizontalScrollOffset - screenRow = startRow + index - if @hasNodeForLine(line.id) - @updateNodeForLine(line, screenRow, top, left) + if @hasLineNode(line.id) + @updateLineNode(line, top, left) else - @buildNodeForLine(line, screenRow, top, left) + newLines ?= [] + newLinesHTML ?= "" + newLines.push(line) + newLinesHTML += @buildLineHTML(line, top, left) - hasNodeForLine: (id) -> - @lineNodesByLineId[id]? + return unless newLines? - buildNodeForLine: (tokenizedLine, screenRow, top, left) -> - {editor} = @props - {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = tokenizedLine - if fold - attributes = {class: 'fold line', 'fold-id': fold.id} + WrapperDiv.innerHTML = newLinesHTML + newLineNodes = toArray(WrapperDiv.children) + node = @getDOMNode() + for line, i in newLines + lineNode = newLineNodes[i] + @lineNodesByLineId[line.id] = lineNode + node.appendChild(lineNode) + + hasLineNode: (lineId) -> + @lineNodesByLineId.hasOwnProperty(lineId) + + buildTranslate3d: (top, left) -> + "translate3d(#{left}px, #{top}px, 0px)" + + buildLineHTML: (line, top, left) -> + {editor, mini, showIndentGuide, invisibles} = @props + {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line + translate3d = @buildTranslate3d(top, left) + line = "
" + + if text is "" + line += " " else - attributes = {class: 'line'} + scopeStack = [] + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + for token in tokens + line += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) + line += token.getValueAsHtml({invisibles, hasIndentGuide}) + line += @popScope(scopeStack) while scopeStack.length > 0 - invisibles = {} - eolInvisibles = {} - htmlEolInvisibles = [] - indentation = indentLevel + # line.push(htmlEolInvisibles) unless text == '' + # line.push("") if fold - wrapper = document.createElement('div') - wrapper.innerHTML = EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, indentation, editor}) - lineNode = wrapper.children[0] - lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + line += "
" + line - @lineNodesByLineId[tokenizedLine.id] = lineNode - @getDOMNode().appendChild(lineNode) + updateScopeStack: (scopeStack, desiredScopes) -> + html = "" - updateNodeForLine: (tokenizedLine, screenRow, top, left) -> + # Find a common prefix + for scope, i in desiredScopes + break unless scopeStack[i]?.scope is desiredScopes[i] + + # Pop scopes until we're at the common prefx + until scopeStack.length is i + html += @popScope(scopeStack) + + # Push onto common prefix until scopeStack equals desiredScopes + for j in [i...desiredScopes.length] + html += @pushScope(scopeStack, desiredScopes[j]) + + html + + popScope: (scopeStack) -> + scopeStack.pop() + "" + + pushScope: (scopeStack, scope) -> + scopeStack.push(scope) + "" + + updateLineNode: (tokenizedLine, top, left) -> lineNode = @lineNodesByLineId[tokenizedLine.id] lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" @@ -173,53 +221,3 @@ LinesComponent = React.createClass clearScopedCharWidths: -> @measuredLines.clear() @props.editor.clearScopedCharWidths() - - -LineComponent = React.createClass - displayName: 'LineComponent' - - render: -> - {index, screenRow, verticalScrollOffset, horizontalScrollOffset, lineHeight} = @props - - top = index * lineHeight + verticalScrollOffset - left = horizontalScrollOffset - style = WebkitTransform: "translate3d(#{left}px, #{top}px, 0px)" - - div className: 'line editor-colors', style: style, 'data-screen-row': screenRow, - span dangerouslySetInnerHTML: {__html: @buildTokensHTML()} - @renderSelections() - - buildTokensHTML: -> - if @props.tokenizedLine.text.length is 0 - @buildEmptyLineHTML() - else - @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree()) - - buildEmptyLineHTML: -> - {showIndentGuide, tokenizedLine} = @props - {indentLevel, tabLength} = tokenizedLine - - if showIndentGuide and indentLevel > 0 - indentSpan = "#{multiplyString(' ', tabLength)}" - multiplyString(indentSpan, indentLevel + 1) - else - " " - - buildScopeTreeHTML: (scopeTree) -> - if scopeTree.children? - html = "" - html += @buildScopeTreeHTML(child) for child in scopeTree.children - html += "" - html - else - "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" - - renderSelections: -> - {selectionRegions} = @props - if selectionRegions? - for region in selectionRegions - div className: 'selection', key: region.id, - div className: 'region', style: region - - shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow', 'selectionRegions', 'index', 'verticalScrollOffset', 'horizontalScrollOffset') From 695f8da3c353114dd4011ecb7d0280d90d614150 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 May 2014 21:12:47 -0600 Subject: [PATCH 063/119] :lipstick: extract buildLineInnerHTML method --- src/lines-component.coffee | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 37d319d6c..3c9168bc8 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -113,28 +113,33 @@ LinesComponent = React.createClass "translate3d(#{left}px, #{top}px, 0px)" buildLineHTML: (line, top, left) -> - {editor, mini, showIndentGuide, invisibles} = @props + {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - line = "
" + lineHTML = "
" if text is "" - line += " " + lineHTML += " " else - scopeStack = [] - firstTrailingWhitespacePosition = text.search(/\s*$/) - lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 - for token in tokens - line += @updateScopeStack(scopeStack, token.scopes) - hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) - line += token.getValueAsHtml({invisibles, hasIndentGuide}) - line += @popScope(scopeStack) while scopeStack.length > 0 + lineHTML += @buildLineInnerHTML(line) - # line.push(htmlEolInvisibles) unless text == '' - # line.push("") if fold + lineHTML += "
" + lineHTML - line += "
" - line + buildLineInnerHTML: (line) -> + {invisibles, mini, showIndentGuide} = @props + {tokens, text} = line + innerHTML = "" + + scopeStack = [] + firstTrailingWhitespacePosition = text.search(/\s*$/) + lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 + for token in tokens + innerHTML += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) + innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide}) + innerHTML += @popScope(scopeStack) while scopeStack.length > 0 + innerHTML updateScopeStack: (scopeStack, desiredScopes) -> html = "" From e9bff37e06ef3263a05931d4e92f682e2a7a8ea5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 08:47:15 -0600 Subject: [PATCH 064/119] Render line numbers manually --- src/gutter-component.coffee | 144 +++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 214b1af52..30a487cf3 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,8 +1,10 @@ React = require 'react' {div} = require 'reactionary' -{isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus' +{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' SubscriberMixin = require './subscriber-mixin' +WrapperDiv = document.createElement('div') + module.exports = GutterComponent = React.createClass displayName: 'GutterComponent' @@ -14,43 +16,10 @@ GutterComponent = React.createClass {width} = @props div className: 'gutter', style: {width}, - div className: 'line-numbers', ref: 'lineNumbers', - if @isMounted() - @renderLineNumbers() - else - @renderLineNumberForMeasurement() + div className: 'line-numbers', ref: 'lineNumbers' - renderLineNumbers: -> - {editor, visibleRowRange, lineOverdraw, scrollTop, lineHeight, showIndentGuide} = @props - [startRow, endRow] = visibleRowRange - maxLineNumberDigits = @getMaxLineNumberDigits() - scrollOffset = -scrollTop % lineHeight - wrapCount = 0 - - for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) - if bufferRow is lastBufferRow - lineNumber = '•' - key = "#{bufferRow + 1}-#{++wrapCount}" - else - lastBufferRow = bufferRow - wrapCount = 0 - lineNumber = "#{bufferRow + 1}" - key = lineNumber - - LineNumberComponent({key, lineNumber, maxLineNumberDigits, index, lineHeight, scrollOffset}) - - renderLineNumberForMeasurement: -> - LineNumberComponent( - key: 'forMeasurement' - lineNumber: '•' - maxLineNumberDigits: @getMaxLineNumberDigits() - index: 0 - lineHeight: 0 - scrollOffset: 0 - ) - - getMaxLineNumberDigits: -> - @props.editor.getLineCount().toString().length + componentWillMount: -> + @lineNumberNodesById = {} # 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 @@ -59,37 +28,94 @@ GutterComponent = React.createClass return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') {visibleRowRange, pendingChanges} = newProps - for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0 + for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start false componentDidUpdate: (oldProps) -> + @updateLineNumbers() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - width = @refs.lineNumbers.getDOMNode().firstChild.offsetWidth - if width isnt @lastMeasuredWidth - @lastMeasuredWidth = width - @props.onWidthChanged(width) + @measureWidth() -LineNumberComponent = React.createClass - displayName: 'LineNumberComponent' + updateLineNumbers: -> + visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() + @removeNonVisibleLineNumberNodes(visibleLineNumberIds) - render: -> - {index, lineHeight, scrollOffset} = @props - div - className: "line-number editor-colors" - style: {WebkitTransform: "translate3d(0px, #{index * lineHeight + scrollOffset}px, 0px)"} - dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + appendOrUpdateVisibleLineNumberNodes: -> + {editor, visibleRowRange, scrollTop, lineHeight} = @props + [startRow, endRow] = visibleRowRange + maxLineNumberDigits = editor.getLineCount().toString().length + verticalScrollOffset = -scrollTop % lineHeight + newLineNumberIds = null + newLineNumbersHTML = null + visibleLineNumberIds = new Set - buildInnerHTML: -> - {lineNumber, maxLineNumberDigits} = @props - if lineNumber.length < maxLineNumberDigits - padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) - padding + lineNumber + @iconDivHTML + wrapCount = 0 + for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + if bufferRow is lastBufferRow + id = "#{bufferRow}-#{wrapCount++}" + else + id = bufferRow.toString() + lastBufferRow = bufferRow + wrapCount = 0 + + visibleLineNumberIds.add(id) + + top = (index * lineHeight) + verticalScrollOffset + + if @hasLineNumberNode(id) + @updateLineNumberNode(id, top) + else + newLineNumberIds ?= [] + newLineNumbersHTML ?= "" + newLineNumberIds.push(id) + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) + + if newLineNumberIds? + WrapperDiv.innerHTML = newLineNumbersHTML + newLineNumberNodes = toArray(WrapperDiv.children) + + node = @refs.lineNumbers.getDOMNode() + for lineNumberId, i in newLineNumberIds + lineNumberNode = newLineNumberNodes[i] + @lineNumberNodesById[lineNumberId] = lineNumberNode + node.appendChild(lineNumberNode) + + visibleLineNumberIds + + removeNonVisibleLineNumberNodes: (visibleLineNumberIds) -> + node = @refs.lineNumbers.getDOMNode() + for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) + delete @lineNumberNodesById[id] + node.removeChild(lineNumberNode) + + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + if softWrapped + lineNumber = "•" else - lineNumber + @iconDivHTML + lineNumber = (bufferRow + 1).toString() - iconDivHTML: '
' + padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) + iconHTML = '
' + innerHTML = padding + lineNumber + iconHTML + translate3d = @buildTranslate3d(top) - shouldComponentUpdate: (newProps) -> - not isEqualForProperties(newProps, @props, 'index', 'lineHeight', 'scrollOffset') + "
#{innerHTML}
" + + updateLineNumberNode: (lineNumberId, top) -> + @lineNumberNodesById[lineNumberId].style['-webkit-transform'] = @buildTranslate3d(top) + + hasLineNumberNode: (lineNumberId) -> + @lineNumberNodesById.hasOwnProperty(lineNumberId) + + buildTranslate3d: (top) -> + "translate3d(0px, #{top}px, 0px)" + + measureWidth: -> + lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild + # return unless lineNumberNode? + + width = lineNumberNode.offsetWidth + if width isnt @lastMeasuredWidth + @props.onWidthChanged(@lastMeasuredWidth = width) From c60e5d90fd84ef17347806b11146241bcf2d83b8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 10:16:29 -0600 Subject: [PATCH 065/119] :lipstick: --- src/lines-component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 3c9168bc8..092bb842c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -57,12 +57,12 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> - @updateRenderedLines() + @updateLines() @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically - updateRenderedLines: -> + updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange visibleLines = editor.linesForScreenRows(startRow, endRow - 1) From 3a2de9c6985fb5c9cf51654be667b78bcf517a7c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 12:35:45 -0600 Subject: [PATCH 066/119] Don't render every line on the GPU Opaque lines are turning out to be a total pain, plus they ruin absolute positioning on the lines div. The slight speed boost isn't seeming worth it anymore. --- src/lines-component.coffee | 39 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 092bb842c..febed6140 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -14,7 +14,14 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - div {className: 'lines'} + if @isMounted() + {editor, scrollTop, scrollLeft} = @props + style = + height: editor.getScrollHeight() + width: editor.getScrollWidth() + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" + + div {className: 'lines editor-colors', style} getVisibleSelectionRegions: -> {editor, visibleRowRange, lineHeight} = @props @@ -65,9 +72,13 @@ LinesComponent = React.createClass updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange + + startRow = Math.max(0, startRow - 8) + endRow = Math.min(editor.getLineCount(), endRow + 8) + visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeNonVisibleLineNodes(visibleLines) - @appendOrUpdateVisibleLineNodes(visibleLines) + @appendOrUpdateVisibleLineNodes(visibleLines, startRow) removeNonVisibleLineNodes: (visibleLines) -> visibleLineIds = new Set @@ -77,24 +88,22 @@ LinesComponent = React.createClass delete @lineNodesByLineId[lineId] node.removeChild(lineNode) - appendOrUpdateVisibleLineNodes: (visibleLines) -> - {scrollTop, scrollLeft, lineHeight} = @props + appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> + {lineHeight} = @props newLines = null newLinesHTML = null - verticalScrollOffset = -scrollTop % lineHeight - horizontalScrollOffset = -scrollLeft for line, index in visibleLines - top = (index * lineHeight) + verticalScrollOffset - left = horizontalScrollOffset + screenRow = startRow + index + top = (screenRow * lineHeight) if @hasLineNode(line.id) - @updateLineNode(line, top, left) + @updateLineNode(line, top) else newLines ?= [] newLinesHTML ?= "" newLines.push(line) - newLinesHTML += @buildLineHTML(line, top, left) + newLinesHTML += @buildLineHTML(line, top) return unless newLines? @@ -109,14 +118,14 @@ LinesComponent = React.createClass hasLineNode: (lineId) -> @lineNodesByLineId.hasOwnProperty(lineId) - buildTranslate3d: (top, left) -> - "translate3d(#{left}px, #{top}px, 0px)" + buildTranslate3d: (top) -> + "translate3d(0px, #{top}px, 0px)" buildLineHTML: (line, top, left) -> {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " @@ -166,9 +175,9 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (tokenizedLine, top, left) -> + updateLineNode: (tokenizedLine, top) -> lineNode = @lineNodesByLineId[tokenizedLine.id] - lineNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)" + lineNode.style.top = top + 'px' measureLineHeightAndCharWidth: -> node = @getDOMNode() From a118cdd32b3e5f01d212ca7383d3191e26bc26a7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 13:50:25 -0600 Subject: [PATCH 067/119] Put selections and lines on the GPU together in sibling divs --- src/editor-component.coffee | 5 +- src/editor-scroll-view-component.coffee | 26 +++++---- src/lines-component.coffee | 31 +---------- src/selection-backgrounds-component.coffee | 24 -------- src/selection-component.coffee | 65 ++++++++++++++++++++++ src/selections-component.coffee | 16 ++++++ static/editor.less | 9 +++ 7 files changed, 110 insertions(+), 66 deletions(-) delete mode 100644 src/selection-backgrounds-component.coffee create mode 100644 src/selection-component.coffee create mode 100644 src/selections-component.coffee diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 408216587..353ad6281 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -59,8 +59,9 @@ EditorComponent = React.createClass EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, - scrollTop, scrollLeft, @scrollingVertically, @cursorsMoved, @selectionChanged, - @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred + scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, + @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, + @onInputFocused, @onInputBlurred } ScrollbarComponent diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 98ae26fcf..934c90cab 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -5,7 +5,7 @@ React = require 'react' InputComponent = require './input-component' LinesComponent = require './lines-component' CursorsComponent = require './cursors-component' -SelectionBackgroundsComponent = require './selection-backgrounds-component' +SelectionsComponent = require './selections-component' module.exports = EditorScrollViewComponent = React.createClass @@ -17,12 +17,16 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, selectionAdded} = @props - {cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props + {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' + contentStyle = + height: scrollHeight + width: scrollWidth + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent @@ -33,14 +37,14 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged - } - div className: 'underlayer', - SelectionBackgroundsComponent({editor, lineHeight, scrollTop}) + div className: 'scroll-view-content editor-colors', style: contentStyle, + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged + } + SelectionsComponent({editor, lineHeight}) componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/lines-component.coffee b/src/lines-component.coffee index febed6140..e8182ecca 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -16,35 +16,8 @@ LinesComponent = React.createClass render: -> if @isMounted() {editor, scrollTop, scrollLeft} = @props - style = - height: editor.getScrollHeight() - width: editor.getScrollWidth() - WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" - div {className: 'lines editor-colors', style} - - getVisibleSelectionRegions: -> - {editor, visibleRowRange, lineHeight} = @props - [visibleStartRow, visibleEndRow] = visibleRowRange - regions = {} - - for selection in editor.selectionsForScreenRows(visibleStartRow, visibleEndRow - 1) when not selection.isEmpty() - {start, end} = selection.getScreenRange() - - for screenRow in [start.row..end.row] - region = {id: selection.id, top: 0, left: 0, height: lineHeight} - - if screenRow is start.row - region.left = editor.pixelPositionForScreenPosition(start).left - if screenRow is end.row - region.width = editor.pixelPositionForScreenPosition(end).left - region.left - else - region.right = 0 - - regions[screenRow] ?= [] - regions[screenRow].push(region) - - regions + div {className: 'lines'} componentWillMount: -> @measuredLines = new WeakSet @@ -125,7 +98,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " diff --git a/src/selection-backgrounds-component.coffee b/src/selection-backgrounds-component.coffee deleted file mode 100644 index 993007184..000000000 --- a/src/selection-backgrounds-component.coffee +++ /dev/null @@ -1,24 +0,0 @@ -React = require 'react' -{div} = require 'reactionary' - -module.exports = -SelectionBackgroundsComponent = React.createClass - displayName: 'SelectionBackgroundsComponent' - - render: -> - {editor, lineHeight, scrollTop} = @props - - div className: 'selections', - if @isMounted() - for selection in editor.getSelections() - {start, end} = selection.getScreenRange() - continue if start.row is end.row - - height = (end.row - start.row) * lineHeight - top = (start.row * lineHeight) - scrollTop - left = 0 - right = 0 - WebkitTransform = "translate3d(0px, #{top}px, 0px)" - - div className: 'selection', key: selection.id, - div className: 'region', style: {left, right, height, WebkitTransform} diff --git a/src/selection-component.coffee b/src/selection-component.coffee new file mode 100644 index 000000000..57eb6d0b8 --- /dev/null +++ b/src/selection-component.coffee @@ -0,0 +1,65 @@ +React = require 'react' +{div} = require 'reactionary' + +module.exports = +SelectionComponent = React.createClass + displayName: 'SelectionComponent' + + render: -> + {editor, selection, lineHeight} = @props + {start, end} = selection.getScreenRange() + rowCount = end.row - start.row + 1 + startPixelPosition = editor.pixelPositionForScreenPosition(start) + endPixelPosition = editor.pixelPositionForScreenPosition(end) + + div className: 'selection', + if rowCount is 1 + @renderSingleLineRegions(startPixelPosition, endPixelPosition) + else + @renderMultiLineRegions(startPixelPosition, endPixelPosition, rowCount) + + renderSingleLineRegions: (startPixelPosition, endPixelPosition) -> + {lineHeight} = @props + + [ + div className: 'region', key: 0, style: + top: startPixelPosition.top + height: lineHeight + left: startPixelPosition.left + width: endPixelPosition.left - startPixelPosition.left + ] + + renderMultiLineRegions: (startPixelPosition, endPixelPosition, rowCount) -> + {lineHeight} = @props + regions = [] + index = 0 + + # First row, extending from selection start to the right side of screen + regions.push( + div className: 'region', key: index++, style: + top: startPixelPosition.top + left: startPixelPosition.left + height: lineHeight + right: 0 + ) + + # Middle rows, extending from left side to right side of screen + if rowCount > 2 + regions.push( + div className: 'region', key: index++, style: + top: startPixelPosition.top + lineHeight + height: (rowCount - 2) * lineHeight + left: 0 + right: 0 + ) + + # Last row, extending from left side of screen to selection end + regions.push( + div className: 'region', key: index, style: + top: endPixelPosition.top + height: lineHeight + left: 0 + width: endPixelPosition.left + ) + + regions diff --git a/src/selections-component.coffee b/src/selections-component.coffee new file mode 100644 index 000000000..ad9ed61e1 --- /dev/null +++ b/src/selections-component.coffee @@ -0,0 +1,16 @@ +React = require 'react' +{div} = require 'reactionary' +SelectionComponent = require './selection-component' + +module.exports = +SelectionsComponent = React.createClass + displayName: 'SelectionsComponent' + + render: -> + {editor, lineHeight} = @props + + div className: 'selections', + if @isMounted() + for selection in editor.getSelections() + if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + SelectionComponent({key: selection.id, selection, editor, lineHeight}) diff --git a/static/editor.less b/static/editor.less index eb4a57b65..f31b44206 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,6 +12,15 @@ z-index: -2; } + .selections { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: -1; + } + .lines { z-index: 0; From d15fd34f7a5bc0171b561757eedf1ce8f80a990d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 18:05:54 -0600 Subject: [PATCH 068/119] Render selections on lines layer; don't put each line number on GPU --- src/editor-scroll-view-component.coffee | 18 ++++++------------ src/gutter-component.coffee | 17 +++++++++++------ src/lines-component.coffee | 12 ++++++++---- src/selections-component.coffee | 5 +++-- static/editor.less | 9 --------- 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 934c90cab..2985910b9 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -23,10 +23,6 @@ EditorScrollViewComponent = React.createClass if @isMounted() inputStyle = @getHiddenInputPosition() inputStyle.WebkitTransform = 'translateZ(0)' - contentStyle = - height: scrollHeight - width: scrollWidth - WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div className: 'scroll-view', onMouseDown: @onMouseDown, InputComponent @@ -37,14 +33,12 @@ EditorScrollViewComponent = React.createClass onFocus: onInputFocused onBlur: onInputBlurred - div className: 'scroll-view-content editor-colors', style: contentStyle, - CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) - LinesComponent { - ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged - } - SelectionsComponent({editor, lineHeight}) + CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) + LinesComponent { + ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, + visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + selectionChanged, scrollHeight, scrollWidth + } componentDidMount: -> @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 30a487cf3..0a68ccd04 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -13,10 +13,12 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - {width} = @props + {width, scrollHeight, scrollTop} = @props div className: 'gutter', style: {width}, - div className: 'line-numbers', ref: 'lineNumbers' + div className: 'line-numbers', ref: 'lineNumbers', style: + height: scrollHeight + WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" componentWillMount: -> @lineNumberNodesById = {} @@ -45,8 +47,10 @@ GutterComponent = React.createClass appendOrUpdateVisibleLineNumberNodes: -> {editor, visibleRowRange, scrollTop, lineHeight} = @props [startRow, endRow] = visibleRowRange + startRow = Math.max(0, startRow - 8) + endRow = Math.min(editor.getLineCount(), endRow + 8) + maxLineNumberDigits = editor.getLineCount().toString().length - verticalScrollOffset = -scrollTop % lineHeight newLineNumberIds = null newLineNumbersHTML = null visibleLineNumberIds = new Set @@ -62,7 +66,8 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - top = (index * lineHeight) + verticalScrollOffset + screenRow = startRow + index + top = screenRow * lineHeight if @hasLineNumberNode(id) @updateLineNumberNode(id, top) @@ -101,10 +106,10 @@ GutterComponent = React.createClass innerHTML = padding + lineNumber + iconHTML translate3d = @buildTranslate3d(top) - "
#{innerHTML}
" + "
#{innerHTML}
" updateLineNumberNode: (lineNumberId, top) -> - @lineNumberNodesById[lineNumberId].style['-webkit-transform'] = @buildTranslate3d(top) + @lineNumberNodesById[lineNumberId].style.top = top + 'px' hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e8182ecca..a7ba5dba6 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -3,7 +3,7 @@ React = require 'react' {debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' {$$} = require 'space-pen' -EditorView = require './editor-view' +SelectionsComponent = require './selections-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -15,9 +15,14 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, scrollTop, scrollLeft} = @props + {editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeight} = @props + style = + height: scrollHeight + width: scrollWidth + WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" - div {className: 'lines'} + div {className: 'lines', style}, + SelectionsComponent({editor, lineHeight}) if @isMounted componentWillMount: -> @measuredLines = new WeakSet @@ -45,7 +50,6 @@ LinesComponent = React.createClass updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) endRow = Math.min(editor.getLineCount(), endRow + 8) diff --git a/src/selections-component.coffee b/src/selections-component.coffee index ad9ed61e1..014600e4f 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -11,6 +11,7 @@ SelectionsComponent = React.createClass div className: 'selections', if @isMounted() - for selection in editor.getSelections() - if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + for selection, index in editor.getSelections() + # Rendering artifacts occur on the lines GPU layer if we remove the last selection + if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) SelectionComponent({key: selection.id, selection, editor, lineHeight}) diff --git a/static/editor.less b/static/editor.less index f31b44206..eb4a57b65 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,15 +12,6 @@ z-index: -2; } - .selections { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: -1; - } - .lines { z-index: 0; From 89bd241a7870f5bdebaca8e97036f52364e5b901 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 18:15:47 -0600 Subject: [PATCH 069/119] Always run react in dev mode for now --- src/editor.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 3dc51555d..bc8b72759 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,10 +214,10 @@ class Editor extends Model @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... getViewClass: -> - if atom.config.get('core.useReactEditor') - require './react-editor-view' - else - require './editor-view' + # if atom.config.get('core.useReactEditor') + require './react-editor-view' + # else + # require './editor-view' destroyed: -> @unsubscribe() From c87bc57f9eae6d2658bf85194a599068a6771687 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 14 May 2014 19:58:50 -0600 Subject: [PATCH 070/119] Don't update top positions of lines/lineNodes unless they have changed --- src/gutter-component.coffee | 7 ++++++- src/lines-component.coffee | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 0a68ccd04..a099936e3 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -22,6 +22,7 @@ GutterComponent = React.createClass componentWillMount: -> @lineNumberNodesById = {} + @lineNumberNodeTopPositions = {} # 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 @@ -76,6 +77,7 @@ GutterComponent = React.createClass newLineNumbersHTML ?= "" newLineNumberIds.push(id) newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) + @lineNumberNodeTopPositions[id] = top if newLineNumberIds? WrapperDiv.innerHTML = newLineNumbersHTML @@ -93,6 +95,7 @@ GutterComponent = React.createClass node = @refs.lineNumbers.getDOMNode() for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) delete @lineNumberNodesById[id] + delete @lineNumberNodeTopPositions[id] node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> @@ -109,7 +112,9 @@ GutterComponent = React.createClass "
#{innerHTML}
" updateLineNumberNode: (lineNumberId, top) -> - @lineNumberNodesById[lineNumberId].style.top = top + 'px' + unless @lineNumberNodeTopPositions[lineNumberId] is top + @lineNumberNodesById[lineNumberId].style.top = top + 'px' + @lineNumberNodeTopPositions[lineNumberId] = top hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index a7ba5dba6..51bae0044 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -27,6 +27,7 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet @lineNodesByLineId = {} + @lineNodeTopPositions = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -63,6 +64,7 @@ LinesComponent = React.createClass node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) delete @lineNodesByLineId[lineId] + delete @lineNodeTopPositions[lineId] node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> @@ -81,6 +83,7 @@ LinesComponent = React.createClass newLinesHTML ?= "" newLines.push(line) newLinesHTML += @buildLineHTML(line, top) + @lineNodeTopPositions[line.id] = top return unless newLines? @@ -152,9 +155,11 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (tokenizedLine, top) -> - lineNode = @lineNodesByLineId[tokenizedLine.id] - lineNode.style.top = top + 'px' + updateLineNode: (line, top) -> + unless @lineNodeTopPositions[line.id] is top + lineNode = @lineNodesByLineId[line.id] + lineNode.style.top = top + 'px' + @lineNodeTopPositions[line.id] = top measureLineHeightAndCharWidth: -> node = @getDOMNode() From 7dfe829fc848d0f5be8b0418537adbd3754a8a03 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 11:00:39 -0600 Subject: [PATCH 071/119] Style lines with inline styles for performance --- src/lines-component.coffee | 2 +- static/editor.less | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 51bae0044..3fe9e741c 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -105,7 +105,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line translate3d = @buildTranslate3d(top, left) - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += " " diff --git a/static/editor.less b/static/editor.less index eb4a57b65..4bd80a1e6 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,14 +12,6 @@ z-index: -2; } - .lines { - z-index: 0; - - > .line { - position: absolute; - } - } - .cursor { z-index: 1; } From 3f01e2f7484eb5b72884aa96a151ee664041c18e Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 15 May 2014 11:50:25 -0600 Subject: [PATCH 072/119] Implement shouldComponentUpdate for SelectionsComponent --- src/lines-component.coffee | 2 +- src/selection-component.coffee | 4 ++-- src/selections-component.coffee | 38 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 3fe9e741c..89d992db3 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -22,7 +22,7 @@ LinesComponent = React.createClass WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" div {className: 'lines', style}, - SelectionsComponent({editor, lineHeight}) if @isMounted + SelectionsComponent({editor, lineHeight}) if @isMounted() componentWillMount: -> @measuredLines = new WeakSet diff --git a/src/selection-component.coffee b/src/selection-component.coffee index 57eb6d0b8..8f4a1f20a 100644 --- a/src/selection-component.coffee +++ b/src/selection-component.coffee @@ -6,8 +6,8 @@ SelectionComponent = React.createClass displayName: 'SelectionComponent' render: -> - {editor, selection, lineHeight} = @props - {start, end} = selection.getScreenRange() + {editor, screenRange, lineHeight} = @props + {start, end} = screenRange rowCount = end.row - start.row + 1 startPixelPosition = editor.pixelPositionForScreenPosition(start) endPixelPosition = editor.pixelPositionForScreenPosition(end) diff --git a/src/selections-component.coffee b/src/selections-component.coffee index 014600e4f..524f7236a 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -7,11 +7,37 @@ SelectionsComponent = React.createClass displayName: 'SelectionsComponent' render: -> + div className: 'selections', @renderSelections() + + renderSelections: -> {editor, lineHeight} = @props - div className: 'selections', - if @isMounted() - for selection, index in editor.getSelections() - # Rendering artifacts occur on the lines GPU layer if we remove the last selection - if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) - SelectionComponent({key: selection.id, selection, editor, lineHeight}) + selectionComponents = [] + for selectionId, screenRange of @selectionRanges + selectionComponents.push(SelectionComponent({key: selectionId, screenRange, editor, lineHeight})) + selectionComponents + + componentWillMount: -> + @selectionRanges = {} + + shouldComponentUpdate: -> + {editor} = @props + oldSelectionRanges = @selectionRanges + newSelectionRanges = {} + @selectionRanges = newSelectionRanges + + for selection, index in editor.getSelections() + # Rendering artifacts occur on the lines GPU layer if we remove the last selection + if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)) + newSelectionRanges[selection.id] = selection.getScreenRange() + + for id, range of newSelectionRanges + if oldSelectionRanges.hasOwnProperty(id) + return true unless range.isEqual(oldSelectionRanges[id]) + else + return true + + for id of oldSelectionRanges + return true unless newSelectionRanges.hasOwnProperty(id) + + false From bc8a1756f3ae8b2b399f5fcf8d90e26ae65228ca Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 14:49:28 -0600 Subject: [PATCH 073/119] Use the .selections layer as the underlayer --- src/react-editor-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 9b91eb384..b9766f099 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -41,7 +41,7 @@ class ReactEditorView extends View node = @component.getDOMNode() - @underlayer = $(node).find('.underlayer') + @underlayer = $(node).find('.selections') @gutter = $(node).find('.gutter') @gutter.removeClassFromAllLines = (klass) => From c5fa2bf12dddfe48ed3d3c24b9dff94ef6bbde22 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 14:53:14 -0600 Subject: [PATCH 074/119] Attach views to .lines instead of defunct .scroll-view-content --- src/react-editor-view.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index b9766f099..e2f3fd3df 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -64,7 +64,8 @@ class ReactEditorView extends View appendToLinesView: (view) -> view.css('position', 'absolute') - @find('.scroll-view-content').prepend(view) + view.css('z-index', 1) + @find('.lines').prepend(view) beforeRemove: -> React.unmountComponentAtNode(@element) From 54cec0a5ff8af4e97d0d7d0403ede1dfea02ef5a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 17:11:54 -0600 Subject: [PATCH 075/119] Hold the gutter's width with a dummy line number --- src/gutter-component.coffee | 39 +++++++++++++++++++++++++++---------- static/editor.less | 1 - 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a099936e3..07cd36a75 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -13,9 +13,9 @@ GutterComponent = React.createClass lastMeasuredWidth: null render: -> - {width, scrollHeight, scrollTop} = @props + {scrollHeight, scrollTop} = @props - div className: 'gutter', style: {width}, + div className: 'gutter', div className: 'line-numbers', ref: 'lineNumbers', style: height: scrollHeight WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" @@ -24,6 +24,9 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberNodeTopPositions = {} + componentDidMount: -> + @appendDummyLineNumber() + # 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 # visible row range. @@ -37,21 +40,31 @@ GutterComponent = React.createClass false componentDidUpdate: (oldProps) -> + @updateDummyLineNumber() if oldProps.maxLineNumberDigits isnt @props.maxLineNumberDigits + @measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') @updateLineNumbers() - unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') - @measureWidth() + + # This dummy line number element holds the gutter to the appropriate width, + # since the real line numbers are absolutely positioned for performance reasons. + appendDummyLineNumber: -> + {maxLineNumberDigits} = @props + WrapperDiv.innerHTML = @buildLineNumberHTML(0, false, maxLineNumberDigits) + @dummyLineNumberNode = WrapperDiv.children[0] + @refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode) + + updateDummyLineNumber: -> + WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() @removeNonVisibleLineNumberNodes(visibleLineNumberIds) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight} = @props + {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits} = @props [startRow, endRow] = visibleRowRange startRow = Math.max(0, startRow - 8) endRow = Math.min(editor.getLineCount(), endRow + 8) - maxLineNumberDigits = editor.getLineCount().toString().length newLineNumberIds = null newLineNumbersHTML = null visibleLineNumberIds = new Set @@ -99,6 +112,15 @@ GutterComponent = React.createClass node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) + if top? + style = "position: absolute; top: #{top}px;" + else + style = "visibility: hidden;" + + "
#{innerHTML}
" + + buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> if softWrapped lineNumber = "•" else @@ -106,10 +128,7 @@ GutterComponent = React.createClass padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) iconHTML = '
' - innerHTML = padding + lineNumber + iconHTML - translate3d = @buildTranslate3d(top) - - "
#{innerHTML}
" + padding + lineNumber + iconHTML updateLineNumberNode: (lineNumberId, top) -> unless @lineNumberNodeTopPositions[lineNumberId] is top diff --git a/static/editor.less b/static/editor.less index 4bd80a1e6..7784299bb 100644 --- a/static/editor.less +++ b/static/editor.less @@ -67,7 +67,6 @@ .gutter { .line-number { - position: absolute; white-space: nowrap; padding: 0 .5em; From 03341776968c5112597f7780fbf8eeb7639668d9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 15 May 2014 17:36:35 -0600 Subject: [PATCH 076/119] Make lineOverdrawMargin a property --- src/editor-component.coffee | 11 ++++++----- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 6 +++--- src/lines-component.coffee | 7 ++++--- src/react-editor-view.coffee | 6 ++++-- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 353ad6281..9aa964247 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -31,7 +31,7 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkResumeDelay} = @props + {editor, cursorBlinkResumeDelay, lineOverdrawMargin} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() @@ -51,14 +51,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, visibleRowRange, maxLineNumberDigits, scrollTop, scrollHeight, - lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, - width: @gutterWidth, onWidthChanged: @onGutterWidthChanged + editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, scrollTop, + scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, + @pendingChanges, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, - lineHeight: lineHeightInPixels, visibleRowRange, @pendingChanges, + lineHeight: lineHeightInPixels, visibleRowRange, lineOverdrawMargin, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred @@ -100,6 +100,7 @@ EditorComponent = React.createClass getDefaultProps: -> cursorBlinkResumeDelay: 100 + lineOverdrawMargin: 8 componentWillMount: -> @pendingChanges = [] diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 2985910b9..72cc7d34a 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, scrollHeight, scrollWidth } diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 07cd36a75..669d6e6a2 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -60,10 +60,10 @@ GutterComponent = React.createClass @removeNonVisibleLineNumberNodes(visibleLineNumberIds) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits} = @props + {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) - endRow = Math.min(editor.getLineCount(), endRow + 8) + startRow = Math.max(0, startRow - lineOverdrawMargin) + endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) newLineNumberIds = null newLineNumbersHTML = null diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 89d992db3..ac052ae78 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -49,10 +49,11 @@ LinesComponent = React.createClass @measureCharactersInNewLines() unless @props.scrollingVertically updateLines: -> - {editor, visibleRowRange, showIndentGuide, selectionChanged} = @props + {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - 8) - endRow = Math.min(editor.getLineCount(), endRow + 8) + + startRow = Math.max(0, startRow - lineOverdrawMargin) + endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeNonVisibleLineNodes(visibleLines) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index e2f3fd3df..87e5d0693 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,6 +1,7 @@ {View, $} = require 'space-pen' React = require 'react' EditorComponent = require './editor-component' +{defaults} = require 'underscore-plus' module.exports = class ReactEditorView extends View @@ -8,7 +9,7 @@ class ReactEditorView extends View focusOnAttach: false - constructor: (@editor) -> + constructor: (@editor, @props) -> super getEditor: -> @editor @@ -37,7 +38,8 @@ class ReactEditorView extends View afterAttach: (onDom) -> return unless onDom @attached = true - @component = React.renderComponent(EditorComponent({@editor, parentView: this}), @element) + props = defaults({@editor, parentView: this}, @props) + @component = React.renderComponent(EditorComponent(props), @element) node = @component.getDOMNode() From 6017b73acfc86ceeeac4214d98d6421aaf8d3d23 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 10:34:57 -0600 Subject: [PATCH 077/119] Add ability to look up line nodes by screen row --- src/editor-component.coffee | 2 ++ src/editor-scroll-view-component.coffee | 2 ++ src/lines-component.coffee | 43 +++++++++++++++++-------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 9aa964247..ba2fc1f58 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -422,3 +422,5 @@ EditorComponent = React.createClass consolidateSelections: (e) -> e.abortKeyBinding() unless @props.editor.consolidateSelections() + + lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow) diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 72cc7d34a..fdccc23cb 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -191,3 +191,5 @@ EditorScrollViewComponent = React.createClass focus: -> @refs.input.focus() + + lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index ac052ae78..1078a3b3b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -27,7 +27,8 @@ LinesComponent = React.createClass componentWillMount: -> @measuredLines = new WeakSet @lineNodesByLineId = {} - @lineNodeTopPositions = {} + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} componentDidMount: -> @measureLineHeightAndCharWidth() @@ -43,11 +44,18 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> + unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @clearScreenRowCaches() + @measureLineHeightAndCharWidth() + @updateLines() - @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically + clearScreenRowCaches: -> + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} + updateLines: -> {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange @@ -65,7 +73,9 @@ LinesComponent = React.createClass node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) delete @lineNodesByLineId[lineId] - delete @lineNodeTopPositions[lineId] + screenRow = @screenRowsByLineId[lineId] + delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId + delete @screenRowsByLineId[lineId] node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> @@ -75,16 +85,16 @@ LinesComponent = React.createClass for line, index in visibleLines screenRow = startRow + index - top = (screenRow * lineHeight) if @hasLineNode(line.id) - @updateLineNode(line, top) + @updateLineNode(line, screenRow) else newLines ?= [] newLinesHTML ?= "" newLines.push(line) - newLinesHTML += @buildLineHTML(line, top) - @lineNodeTopPositions[line.id] = top + newLinesHTML += @buildLineHTML(line, screenRow) + @screenRowsByLineId[line.id] = screenRow + @lineIdsByScreenRow[screenRow] = line.id return unless newLines? @@ -102,10 +112,10 @@ LinesComponent = React.createClass buildTranslate3d: (top) -> "translate3d(0px, #{top}px, 0px)" - buildLineHTML: (line, top, left) -> - {editor, mini, showIndentGuide} = @props + buildLineHTML: (line, screenRow) -> + {editor, mini, showIndentGuide, lineHeight} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line - translate3d = @buildTranslate3d(top, left) + top = screenRow * lineHeight lineHTML = "
" if text is "" @@ -156,11 +166,16 @@ LinesComponent = React.createClass scopeStack.push(scope) "" - updateLineNode: (line, top) -> - unless @lineNodeTopPositions[line.id] is top + updateLineNode: (line, screenRow) -> + unless @screenRowsByLineId[line.id] is screenRow + {lineHeight} = @props lineNode = @lineNodesByLineId[line.id] - lineNode.style.top = top + 'px' - @lineNodeTopPositions[line.id] = top + lineNode.style.top = screenRow * lineHeight + 'px' + @screenRowsByLineId[line.id] = screenRow + @lineIdsByScreenRow[screenRow] = line.id + + lineNodeForScreenRow: (screenRow) -> + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureLineHeightAndCharWidth: -> node = @getDOMNode() From 0ad27303531cc0bfd8e957ab62cb58fb64e303f2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 10:44:49 -0600 Subject: [PATCH 078/119] Update specs for new line node rendering approach Lines are no longer translated on the GPU, and they aren't inserted into the DOM in an order that reflects their order in the buffer. --- spec/editor-component-spec.coffee | 73 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index a81a1dc68..9516317d6 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -4,9 +4,11 @@ nbsp = String.fromCharCode(160) describe "EditorComponent", -> [contentNode, editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame] = [] + [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, lineOverdrawMargin] = [] beforeEach -> + lineOverdrawMargin = 2 + waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -29,7 +31,7 @@ describe "EditorComponent", -> contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' - wrapperView = new ReactEditorView(editor) + wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() {component} = wrapperView component.setLineHeight(1.3) @@ -49,52 +51,54 @@ describe "EditorComponent", -> contentNode.style.width = '' describe "line rendering", -> - it "renders only the currently-visible lines, translated relative to the scroll position", -> + it "renders the currently-visible lines plus the overdraw margin", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text - expect(lines[5].textContent).toBe editor.lineForScreenRow(5).text + linesNode = node.querySelector('.lines') + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(node.querySelectorAll('.line').length).toBe 6 + 2 # no margin above + expect(component.lineNodeForScreenRow(0).textContent).toBe editor.lineForScreenRow(0).text + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNodeForScreenRow(5).textContent).toBe editor.lineForScreenRow(5).text + expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - lineNodes = node.querySelectorAll('.line') - expect(lineNodes.length).toBe 6 - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" - expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text - expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text - expect(lineNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)" + expect(node.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).textContent).toBe editor.lineForScreenRow(2).text + expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels + expect(component.lineNodeForScreenRow(9).textContent).toBe editor.lineForScreenRow(9).text - it "updates the translation of subsequent lines when lines are inserted or removed", -> + it "updates the top position of subsequent lines when lines are inserted or removed", -> editor.getBuffer().deleteRows(0, 1) lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels describe "when indent guides are enabled", -> beforeEach -> component.setShowIndentGuide(true) it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> - lines = node.querySelectorAll('.line') - line1LeafNodes = getLeafNodes(lines[1]) + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe ' ' expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line2LeafNodes[1].textContent).toBe ' ' @@ -502,14 +506,12 @@ describe "EditorComponent", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - lineNodes = node.querySelectorAll('.line') - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + linesNode = node.querySelector('.lines') + expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) - expect(lineNodes[0].style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" - expect(lineNodes[4].style['-webkit-transform']).toBe "translate3d(-100px, #{4 * lineHeightInPixels}px, 0px)" + expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> @@ -527,7 +529,7 @@ describe "EditorComponent", -> node.style.width = 10 * charWidth + 'px' component.measureHeightAndWidth() editor.setScrollBottom(editor.getScrollHeight()) - lastLineNode = last(node.querySelectorAll('.line')) + lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top expect(bottomOfLastLine).toBe topOfHorizontalScrollbar @@ -535,7 +537,6 @@ describe "EditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears node.style.width = 100 * charWidth + 'px' component.measureHeightAndWidth() - lastLineNode = last(node.querySelectorAll('.line')) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = node.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe bottomOfEditor @@ -547,11 +548,9 @@ describe "EditorComponent", -> editor.setScrollLeft(Infinity) - lineNodes = node.querySelectorAll('.line') - rightOfLongestLine = lineNodes[6].getBoundingClientRect().right + rightOfLongestLine = component.lineNodeForScreenRow(6).getBoundingClientRect().right leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - - expect(rightOfLongestLine).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line + expect(Math.round(rightOfLongestLine)).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line it "only displays dummy scrollbars when scrollable in that direction", -> expect(verticalScrollbarNode.style.display).toBe 'none' From 64c82f1c876d8b04094e7e3ba9cc6bf7d8fd649b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 11:00:05 -0600 Subject: [PATCH 079/119] Update cursor positioning text for simplified token markup --- 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 9516317d6..f7d88a07f 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -262,7 +262,7 @@ describe "EditorComponent", -> cursor = node.querySelector('.cursor') cursorRect = cursor.getBoundingClientRect() - cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild + cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild range = document.createRange() range.setStart(cursorLocationTextNode, 0) range.setEnd(cursorLocationTextNode, 1) From b000e8e4a28401c5586c7cb2a6189e16b3e25420 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 11:10:39 -0600 Subject: [PATCH 080/119] Get selection specs passing again --- spec/editor-component-spec.coffee | 91 +++++++++++++++++-------------- static/editor.less | 4 ++ 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index f7d88a07f..2f0786701 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -320,55 +320,64 @@ describe "EditorComponent", -> scrollViewNode = node.querySelector('.scroll-view') scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left - describe "for single line selections", -> - it "renders 1 region on the line and no background region", -> - # 1-line selection - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - lineNodes = node.querySelectorAll('.line') - line1Region = lineNodes[1].querySelector('.selection .region') - regionRect = line1Region.getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(regionRect.width).toBe 4 * charWidth + it "renders 1 region for 1-line selections", -> + # 1-line selection + editor.setSelectedScreenRange([[1, 6], [1, 10]]) + regions = node.querySelectorAll('.selection .region') - expect(node.querySelectorAll('.underlayer .selection .region').length).toBe 0 + expect(regions.length).toBe 1 + regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe 1 * lineHeightInPixels + expect(regionRect.height).toBe 1 * lineHeightInPixels + expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(regionRect.width).toBe 4 * charWidth - describe "for multi-line selections", -> - it "renders a region on each line and a full-width background region from the first line to the penultimate line", -> - editor.setSelectedScreenRange([[1, 6], [3, 10]]) + it "renders 2 regions for 2-line selections", -> + editor.setSelectedScreenRange([[1, 6], [2, 10]]) + regions = node.querySelectorAll('.selection .region') + expect(regions.length).toBe 2 - lineNodes = node.querySelectorAll('.line') - region1Rect = lineNodes[1].querySelector('.selection .region').getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(region1Rect.right).toBe scrollViewClientLeft + lineNodes[1].offsetWidth + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right - region2Rect = lineNodes[2].querySelector('.selection .region').getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft - expect(region2Rect.width).toBe lineNodes[2].offsetWidth + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + 0 + expect(region2Rect.width).toBe 10 * charWidth - region3Rect = lineNodes[3].querySelector('.selection .region').getBoundingClientRect() - expect(region3Rect.top).toBe 3 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.width).toBe 10 * charWidth + it "renders 3 regions for selections with more than 2 lines", -> + editor.setSelectedScreenRange([[1, 6], [5, 10]]) + regions = node.querySelectorAll('.selection .region') + expect(regions.length).toBe 3 - backgroundNodes = node.querySelectorAll('.underlayer .selection .region') - expect(backgroundNodes.length).toBe 1 - backgroundRegionRect = backgroundNodes[0].getBoundingClientRect() + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth + expect(region1Rect.right).toBe scrollViewNode.getBoundingClientRect().right - expect(backgroundRegionRect.top).toBe 1 * lineHeightInPixels - expect(backgroundRegionRect.left).toBe scrollViewClientLeft - expect(backgroundRegionRect.width).toBe scrollViewNode.offsetWidth - expect(backgroundRegionRect.height).toBe 2 * lineHeightInPixels + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 3 * lineHeightInPixels + expect(region2Rect.left).toBe scrollViewClientLeft + 0 + expect(region2Rect.right).toBe scrollViewNode.getBoundingClientRect().right - it "does not render empty selections", -> - expect(editor.getSelection().isEmpty()).toBe true - expect(node.querySelectorAll('.selection').length).toBe 0 + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe 5 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBe scrollViewClientLeft + 0 + expect(region3Rect.width).toBe 10 * charWidth + + it "does not render empty selections unless they are the first selection (to prevent a Chromium rendering artifact caused by removing it)", -> + editor.addSelectionForBufferRange([[2, 2], [2, 2]]) + expect(editor.getSelection(0).isEmpty()).toBe true + expect(editor.getSelection(1).isEmpty()).toBe true + + expect(node.querySelectorAll('.selection').length).toBe 1 describe "mouse interactions", -> linesNode = null diff --git a/static/editor.less b/static/editor.less index 7784299bb..fcd7a2575 100644 --- a/static/editor.less +++ b/static/editor.less @@ -12,6 +12,10 @@ z-index: -2; } + .lines { + min-width: 100%; + } + .cursor { z-index: 1; } From fe82e3e30f2befc9975598488d0fb248313baae7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 14:52:12 -0600 Subject: [PATCH 081/119] Only clear screen row caches on lines component if lineHeight changes --- src/lines-component.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1078a3b3b..384c7257e 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -44,10 +44,8 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> - unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') - @clearScreenRowCaches() - @measureLineHeightAndCharWidth() - + @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight @updateLines() @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically From e74dfe343804722954ea8939928047ee35d3aa54 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 14:52:24 -0600 Subject: [PATCH 082/119] Fix gutter specs and update lines when digit counts change --- spec/editor-component-spec.coffee | 74 ++++++++++++++----------------- src/editor-component.coffee | 6 ++- src/gutter-component.coffee | 65 ++++++++++++++++----------- 3 files changed, 78 insertions(+), 67 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2f0786701..facdabd32 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -151,41 +151,39 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes.length).toBe 6 - expect(lineNumberNodes[0].textContent).toBe "#{nbsp}1" - expect(lineNumberNodes[5].textContent).toBe "#{nbsp}6" + expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes.length).toBe 6 + expect(node.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number - expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3" - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{-.5 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8" - expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{4.5 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3" + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + return + expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}8" + expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 7 * lineHeightInPixels it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') - lineNumberNodes = node.querySelectorAll('.line-number') - expect(lineNumberNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(lineNumberNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[3].style['-webkit-transform']).toBe "translate3d(0px, #{3 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[4].style['-webkit-transform']).toBe "translate3d(0px, #{4 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[5].style['-webkit-transform']).toBe "translate3d(0px, #{5 * lineHeightInPixels}px, 0px)" - expect(lineNumberNodes[6].style['-webkit-transform']).toBe "translate3d(0px, #{6 * lineHeightInPixels}px, 0px)" + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 6 * lineHeightInPixels it "renders • characters for soft-wrapped lines", -> editor.setSoftWrap(true) @@ -193,28 +191,24 @@ describe "EditorComponent", -> node.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() - lines = node.querySelectorAll('.line-number') - expect(lines.length).toBe 6 - expect(lines[0].textContent).toBe "#{nbsp}1" - expect(lines[1].textContent).toBe "#{nbsp}•" - expect(lines[2].textContent).toBe "#{nbsp}2" - expect(lines[3].textContent).toBe "#{nbsp}•" - expect(lines[4].textContent).toBe "#{nbsp}3" - expect(lines[5].textContent).toBe "#{nbsp}•" + expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" + expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" + expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3" + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}•" - it "pads line numbers to be right justified based on the maximum number of line number digits", -> + it "pads line numbers to be right-justified based on the maximum number of line number digits", -> editor.getBuffer().setText([1..10].join('\n')) - lineNumberNodes = toArray(node.querySelectorAll('.line-number')) - - for node, i in lineNumberNodes[0..8] - expect(node.textContent).toBe "#{nbsp}#{i + 1}" - expect(lineNumberNodes[9].textContent).toBe '10' + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) - lineNumberNodes = toArray(node.querySelectorAll('.line-number')) - for node, i in lineNumberNodes - expect(node.textContent).toBe "#{i + 1}" + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ba2fc1f58..13e3afc44 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -51,8 +51,8 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, scrollTop, - scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, + ref: 'gutter', editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, + scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, onWidthChanged: @onGutterWidthChanged } @@ -424,3 +424,5 @@ EditorComponent = React.createClass e.abortKeyBinding() unless @props.editor.consolidateSelections() lineNodeForScreenRow: (screenRow) -> @refs.scrollView.lineNodeForScreenRow(screenRow) + + lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 669d6e6a2..593d8a411 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -22,7 +22,8 @@ GutterComponent = React.createClass componentWillMount: -> @lineNumberNodesById = {} - @lineNumberNodeTopPositions = {} + @lineNumberIdsByScreenRow = {} + @screenRowsByLineNumberId = {} componentDidMount: -> @appendDummyLineNumber() @@ -40,10 +41,18 @@ GutterComponent = React.createClass false componentDidUpdate: (oldProps) -> - @updateDummyLineNumber() if oldProps.maxLineNumberDigits isnt @props.maxLineNumberDigits + unless oldProps.maxLineNumberDigits is @props.maxLineNumberDigits + @updateDummyLineNumber() + @removeLineNumberNodes() + @measureWidth() unless @lastMeasuredWidth? and isEqualForProperties(oldProps, @props, 'maxLineNumberDigits', 'fontSize', 'fontFamily') + @clearScreenRowCaches() unless oldProps.lineHeight is @props.lineHeight @updateLineNumbers() + clearScreenRowCaches: -> + @lineNumberIdsByScreenRow = {} + @screenRowsByLineNumberId = {} + # This dummy line number element holds the gutter to the appropriate width, # since the real line numbers are absolutely positioned for performance reasons. appendDummyLineNumber: -> @@ -56,11 +65,11 @@ GutterComponent = React.createClass WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> - visibleLineNumberIds = @appendOrUpdateVisibleLineNumberNodes() - @removeNonVisibleLineNumberNodes(visibleLineNumberIds) + lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes() + @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, lineHeight, maxLineNumberDigits, lineOverdrawMargin} = @props + {editor, visibleRowRange, scrollTop, maxLineNumberDigits, lineOverdrawMargin} = @props [startRow, endRow] = visibleRowRange startRow = Math.max(0, startRow - lineOverdrawMargin) endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) @@ -71,6 +80,8 @@ GutterComponent = React.createClass wrapCount = 0 for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1) + screenRow = startRow + index + if bufferRow is lastBufferRow id = "#{bufferRow}-#{wrapCount++}" else @@ -80,17 +91,16 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - screenRow = startRow + index - top = screenRow * lineHeight if @hasLineNumberNode(id) - @updateLineNumberNode(id, top) + @updateLineNumberNode(id, screenRow) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" newLineNumberIds.push(id) - newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, top) - @lineNumberNodeTopPositions[id] = top + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow) + @screenRowsByLineNumberId[id] = screenRow + @lineNumberIdsByScreenRow[screenRow] = id if newLineNumberIds? WrapperDiv.innerHTML = newLineNumbersHTML @@ -104,23 +114,26 @@ GutterComponent = React.createClass visibleLineNumberIds - removeNonVisibleLineNumberNodes: (visibleLineNumberIds) -> + removeLineNumberNodes: (lineNumberIdsToPreserve) -> node = @refs.lineNumbers.getDOMNode() - for id, lineNumberNode of @lineNumberNodesById when not visibleLineNumberIds.has(id) - delete @lineNumberNodesById[id] - delete @lineNumberNodeTopPositions[id] + for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId) + delete @lineNumberNodesById[lineNumberId] + screenRow = @screenRowsByLineNumberId[lineNumberId] + delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId + delete @screenRowsByLineNumberId[lineNumberId] node.removeChild(lineNumberNode) - buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> - innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - if top? - style = "position: absolute; top: #{top}px;" + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> + if screenRow? + {lineHeight} = @props + style = "position: absolute; top: #{screenRow * lineHeight}px;" else style = "visibility: hidden;" + innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) "
#{innerHTML}
" - buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits, top) -> + buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped lineNumber = "•" else @@ -130,16 +143,18 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, top) -> - unless @lineNumberNodeTopPositions[lineNumberId] is top - @lineNumberNodesById[lineNumberId].style.top = top + 'px' - @lineNumberNodeTopPositions[lineNumberId] = top + updateLineNumberNode: (lineNumberId, screenRow) -> + unless @screenRowsByLineNumberId[lineNumberId] is screenRow + {lineHeight} = @props + @lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeight + 'px' + @screenRowsByLineNumberId[lineNumberId] = screenRow + @lineNumberIdsByScreenRow[screenRow] = lineNumberId hasLineNumberNode: (lineNumberId) -> @lineNumberNodesById.hasOwnProperty(lineNumberId) - buildTranslate3d: (top) -> - "translate3d(0px, #{top}px, 0px)" + lineNumberNodeForScreenRow: (screenRow) -> + @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] measureWidth: -> lineNumberNode = @refs.lineNumbers.getDOMNode().firstChild From 9b7547cbe0342ae4c0e5fd591f7f7389017df0d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:21:12 -0600 Subject: [PATCH 083/119] Get indent guide specs passing again --- spec/editor-component-spec.coffee | 13 +++++-------- src/lines-component.coffee | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index facdabd32..bcf1402f5 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -98,7 +98,7 @@ describe "EditorComponent", -> expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line2LeafNodes[1].textContent).toBe ' ' @@ -108,8 +108,7 @@ describe "EditorComponent", -> it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> editor.getBuffer().insert([1, Infinity], '\n') - lines = node.querySelectorAll('.line') - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe 3 expect(line2LeafNodes[0].textContent).toBe ' ' @@ -121,8 +120,7 @@ describe "EditorComponent", -> it "renders indent guides correctly on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - lines = node.querySelectorAll('.line') - line2LeafNodes = getLeafNodes(lines[2]) + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe 3 expect(line2LeafNodes[0].textContent).toBe ' ' expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true @@ -132,9 +130,8 @@ describe "EditorComponent", -> expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> - editor.getBuffer().setText (" hi ") - lines = node.querySelectorAll('.line') - line0LeafNodes = getLeafNodes(lines[0]) + editor.getBuffer().setText " hi " + line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe ' ' expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true expect(line0LeafNodes[1].textContent).toBe ' ' diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 384c7257e..e9c6bb465 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -46,6 +46,7 @@ LinesComponent = React.createClass componentDidUpdate: (prevProps) -> @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScreenRowCaches() unless prevProps.lineHeight is @props.lineHeight + @removeLineNodes() unless prevProps.showIndentGuide is @props.showIndentGuide @updateLines() @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.scrollingVertically @@ -62,10 +63,10 @@ LinesComponent = React.createClass endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) visibleLines = editor.linesForScreenRows(startRow, endRow - 1) - @removeNonVisibleLineNodes(visibleLines) + @removeLineNodes(visibleLines) @appendOrUpdateVisibleLineNodes(visibleLines, startRow) - removeNonVisibleLineNodes: (visibleLines) -> + removeLineNodes: (visibleLines=[]) -> visibleLineIds = new Set visibleLineIds.add(line.id.toString()) for line in visibleLines node = @getDOMNode() @@ -117,13 +118,23 @@ LinesComponent = React.createClass lineHTML = "
" if text is "" - lineHTML += " " + lineHTML += @buildEmptyLineInnerHTML(line) else lineHTML += @buildLineInnerHTML(line) lineHTML += "
" lineHTML + buildEmptyLineInnerHTML: (line) -> + {showIndentGuide} = @props + {indentLevel, tabLength} = line + + if showIndentGuide and indentLevel > 0 + indentSpan = "#{multiplyString(' ', tabLength)}" + multiplyString(indentSpan, indentLevel + 1) + else + " " + buildLineInnerHTML: (line) -> {invisibles, mini, showIndentGuide} = @props {tokens, text} = line From 57e6419d1d45038ff5bdb2d03b5773e6de1384f5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:30:50 -0600 Subject: [PATCH 084/119] Restore conditional loading of react editor renderer --- src/editor.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index bc8b72759..3dc51555d 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,10 +214,10 @@ class Editor extends Model @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... getViewClass: -> - # if atom.config.get('core.useReactEditor') - require './react-editor-view' - # else - # require './editor-view' + if atom.config.get('core.useReactEditor') + require './react-editor-view' + else + require './editor-view' destroyed: -> @unsubscribe() From a83a6e5127df9da058493bf426b86a392bc52861 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 14:45:44 -0700 Subject: [PATCH 085/119] Explicitly set permissions on temp folder Refs #2129 --- script/mkdeb | 1 + 1 file changed, 1 insertion(+) diff --git a/script/mkdeb b/script/mkdeb index aadaa7ab5..f5c14203d 100755 --- a/script/mkdeb +++ b/script/mkdeb @@ -14,6 +14,7 @@ ICON_FILE="$4" DEB_PATH="$5" TARGET_ROOT="`mktemp -d`" +chmod 755 "$TARGET_ROOT" TARGET="$TARGET_ROOT/atom-$VERSION-amd64" mkdir -p "$TARGET/usr" From c05848342237cb6d355bde7eef205e75c75d81c1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 15:56:18 -0600 Subject: [PATCH 086/119] Update the gutter width when the number of digits changes --- spec/editor-component-spec.coffee | 11 +++++++++++ src/gutter-component.coffee | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index bcf1402f5..d5f04e65f 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -202,10 +202,21 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + gutterNode = node.querySelector('.gutter') + initialGutterWidth = gutterNode.offsetWidth + # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) for screenRow in [0..8] expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" + expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth + + # Increases padding when the max number of digits goes up + editor.getBuffer().insert([0, 0], '\n\n') + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + expect(gutterNode.offsetWidth).toBe initialGutterWidth 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 593d8a411..a758a4607 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -11,6 +11,7 @@ GutterComponent = React.createClass mixins: [SubscriberMixin] lastMeasuredWidth: null + dummyLineNumberNode: null render: -> {scrollHeight, scrollTop} = @props @@ -62,7 +63,7 @@ GutterComponent = React.createClass @refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode) updateDummyLineNumber: -> - WrapperDiv.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) + @dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits) updateLineNumbers: -> lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes() From 187cf2a71092e7093d7941d048d09977277dc2e0 Mon Sep 17 00:00:00 2001 From: Haralan Dobrev Date: Sat, 17 May 2014 01:01:21 +0300 Subject: [PATCH 087/119] Change security emoji to :lock: --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 305327066..153f780fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ in the proper package's repository. * :fire: `:fire:` when removing code or files * :green_heart: `:green_heart:` when fixing the CI build * :white_check_mark: `:white_check_mark:` when adding tests - * :guardsman: `:guardsman:` when dealing with security + * :lock: `:lock:` when dealing with security ## CoffeeScript Styleguide From 532f119b9d28f3a2dace3f45397fd0c70fcbf049 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 16 May 2014 15:05:11 -0700 Subject: [PATCH 088/119] Add application:install-update to workspaceView --- src/workspace-view.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 129222585..5c45db226 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -113,6 +113,7 @@ class WorkspaceView extends View @command 'application:quit', -> ipc.send('command', 'application:quit') @command 'application:hide', -> ipc.send('command', 'application:hide') @command 'application:hide-other-applications', -> ipc.send('command', 'application:hide-other-applications') + @command 'application:install-update', -> ipc.send('command', 'application:install-update') @command 'application:unhide-all-applications', -> ipc.send('command', 'application:unhide-all-applications') @command 'application:new-window', -> ipc.send('command', 'application:new-window') @command 'application:new-file', -> ipc.send('command', 'application:new-file') From 626964f15b8f0902f52b5492d2e5d6ebd8b743d2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 16:07:35 -0600 Subject: [PATCH 089/119] Upgrade go-to-line to fix double toggle on react editor --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c06b5083f..6319ee27f 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "find-and-replace": "0.105.0", "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", - "go-to-line": "0.20.0", + "go-to-line": "0.21.0", "grammar-selector": "0.26.0", "image-view": "0.33.0", "keybinding-resolver": "0.17.0", From bd7de18c7a48a6e0400206b032aac577074af034 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 16 May 2014 15:13:05 -0700 Subject: [PATCH 090/119] Upgrade to settings-view@0.115.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c06b5083f..3a31d3f14 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.29.0", - "settings-view": "0.114.0", + "settings-view": "0.115.0", "snippets": "0.43.0", "spell-check": "0.35.0", "status-bar": "0.40.0", From b4977ff617a80b09dc93841cb3041bd17a46f0e3 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 16 May 2014 15:52:59 -0700 Subject: [PATCH 091/119] Upgrade to release-notes@0.31.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a31d3f14..7dc2035e3 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "metrics": "0.32.0", "open-on-github": "0.28.0", "package-generator": "0.30.0", - "release-notes": "0.29.0", + "release-notes": "0.31.0", "settings-view": "0.115.0", "snippets": "0.43.0", "spell-check": "0.35.0", From 8e65d30a845cc7c49ed4855d40ff9e4446a392ac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 16 May 2014 20:58:40 -0600 Subject: [PATCH 092/119] Compute rendered row range once in EditorComponent and pass it down --- src/editor-component.coffee | 15 +++++++++++---- src/editor-scroll-view-component.coffee | 4 ++-- src/gutter-component.coffee | 13 +++++-------- src/lines-component.coffee | 15 ++++++--------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 13e3afc44..463b47c5e 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -31,11 +31,11 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state - {editor, cursorBlinkResumeDelay, lineOverdrawMargin} = @props + {editor, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length if @isMounted() - visibleRowRange = editor.getVisibleRowRange() + renderedRowRange = @getRenderedRowRange() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -51,14 +51,14 @@ EditorComponent = React.createClass div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { - ref: 'gutter', editor, visibleRowRange, lineOverdrawMargin, maxLineNumberDigits, + ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, @pendingChanges, onWidthChanged: @onGutterWidthChanged } EditorScrollViewComponent { ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide, - lineHeight: lineHeightInPixels, visibleRowRange, lineOverdrawMargin, @pendingChanges, + lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred @@ -96,6 +96,13 @@ EditorComponent = React.createClass height: horizontalScrollbarHeight width: verticalScrollbarWidth + getRenderedRowRange: -> + {editor, lineOverdrawMargin} = @props + [visibleStartRow, visibleEndRow] = editor.getVisibleRowRange() + renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin) + renderedEndRow = Math.min(editor.getLineCount(), visibleEndRow + lineOverdrawMargin) + [renderedStartRow, renderedEndRow] + getInitialState: -> {} getDefaultProps: -> diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index fdccc23cb..b03200a1c 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -36,7 +36,7 @@ EditorScrollViewComponent = React.createClass CursorsComponent({editor, scrollTop, scrollLeft, cursorsMoved, selectionAdded, cursorBlinkResumeDelay}) LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, - visibleRowRange, lineOverdrawMargin, pendingChanges, scrollTop, scrollLeft, scrollingVertically, + renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, selectionChanged, scrollHeight, scrollWidth } diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a758a4607..cc20c5c43 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -33,11 +33,11 @@ GutterComponent = React.createClass # non-zero-delta change to the screen lines has occurred within the current # visible row range. shouldComponentUpdate: (newProps) -> - return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'scrollTop', 'lineHeight', 'fontSize') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight', 'fontSize') - {visibleRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges} = newProps for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 - return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start + return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start false @@ -70,10 +70,8 @@ GutterComponent = React.createClass @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, visibleRowRange, scrollTop, maxLineNumberDigits, lineOverdrawMargin} = @props - [startRow, endRow] = visibleRowRange - startRow = Math.max(0, startRow - lineOverdrawMargin) - endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) + {editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props + [startRow, endRow] = renderedRowRange newLineNumberIds = null newLineNumbersHTML = null @@ -92,7 +90,6 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) - if @hasLineNumberNode(id) @updateLineNumberNode(id, screenRow) else diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e9c6bb465..2e2bb52d0 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -35,11 +35,11 @@ LinesComponent = React.createClass shouldComponentUpdate: (newProps) -> return true if newProps.selectionChanged - return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') + return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically') - {visibleRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges} = newProps for change in pendingChanges - return true unless change.end <= visibleRowRange.start or visibleRowRange.end <= change.start + return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start false @@ -56,11 +56,8 @@ LinesComponent = React.createClass @lineIdsByScreenRow = {} updateLines: -> - {editor, visibleRowRange, showIndentGuide, selectionChanged, lineOverdrawMargin} = @props - [startRow, endRow] = visibleRowRange - - startRow = Math.max(0, startRow - lineOverdrawMargin) - endRow = Math.min(editor.getLineCount(), endRow + lineOverdrawMargin) + {editor, renderedRowRange, showIndentGuide, selectionChanged} = @props + [startRow, endRow] = renderedRowRange visibleLines = editor.linesForScreenRows(startRow, endRow - 1) @removeLineNodes(visibleLines) @@ -198,7 +195,7 @@ LinesComponent = React.createClass editor.setDefaultCharWidth(charWidth) measureCharactersInNewLines: -> - [visibleStartRow, visibleEndRow] = @props.visibleRowRange + [visibleStartRow, visibleEndRow] = @props.renderedRowRange node = @getDOMNode() for tokenizedLine in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) From 3d98f663309cb353202fd61ed884ec2513c42e44 Mon Sep 17 00:00:00 2001 From: Geoffrey Frogeye Date: Sat, 17 May 2014 17:53:17 +0200 Subject: [PATCH 093/119] Fixed a typo where cmd would appear in linux.cson Fixes #2251 again --- keymaps/linux.cson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keymaps/linux.cson b/keymaps/linux.cson index d6ceef427..dd4fd5275 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -69,8 +69,8 @@ 'ctrl-delete': 'editor:delete-to-end-of-word' 'ctrl-home': 'core:move-to-top' 'ctrl-end': 'core:move-to-bottom' - 'cmd-shift-home': 'core:select-to-top' - 'cmd-shift-end': 'core:select-to-bottom' + 'ctrl-shift-home': 'core:select-to-top' + 'ctrl-shift-end': 'core:select-to-bottom' # Sublime Parity 'ctrl-a': 'core:select-all' From 21fd2b8f1d5586e0747e237a98b86b1584c3e89e Mon Sep 17 00:00:00 2001 From: Esa Varemo Date: Sun, 18 May 2014 17:37:36 +0300 Subject: [PATCH 094/119] Make instructions for manual VS path more accurate Move the line number for the variable close to the actual current location. Add a note to not to include unescaped backward slashes as they would appear in normal Windows paths. --- docs/build-instructions/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index fc4320c9a..c589f152b 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -35,7 +35,7 @@ and These two error messages can usually be ignored. The solution to these errors is to re-run `script\build`, possibly several times. -If your Visual Studio is in a non-standard location, and you get the error `You must have Visual Studio 2010 or 2012 installed`, you need to modify `apm\node_modules\atom-package-manager\lib\config.js` around line 90 and replace the variable with your Visual Studio directory plus Common7/IDE. +If your Visual Studio is in a non-standard location, and you get the error `You must have Visual Studio 2010 or 2012 installed`, you need to modify `apm\node_modules\atom-package-manager\lib\config.js` around line 110 and replace the variable with your Visual Studio directory plus Common7/IDE. Make sure your path does not contain unescaped backward slashes that appear in normal Windows paths. Example: From 0d8a05bdb1f14ba351a9dfcefef54ab9fe190b78 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Mon, 19 May 2014 22:52:32 +0800 Subject: [PATCH 095/119] Upgrade to atom-shell@0.12.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7dc2035e3..59b5e51cd 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.12.4", + "atomShellVersion": "0.12.5", "dependencies": { "async": "0.2.6", "atom-keymap": "^0.19.0", From e643fe7e7ddddb55e9bf88edcceaae117b498b22 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 08:39:45 -0700 Subject: [PATCH 096/119] Upgrade to language-go@0.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59b5e51cd..67de377e4 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "language-css": "0.16.0", "language-gfm": "0.37.0", "language-git": "0.9.0", - "language-go": "0.11.0", + "language-go": "0.12.0", "language-html": "0.22.0", "language-hyperlink": "0.9.0", "language-java": "0.10.0", From 314833bbac33bdb7c45594550d8d10559dd2de9c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 08:52:06 -0700 Subject: [PATCH 097/119] Add missing parens on indent guide check Previously the indent guide was always showing on the whitespace only lines Closes #2274 --- spec/editor-view-spec.coffee | 7 +++++++ src/editor-view.coffee | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/editor-view-spec.coffee b/spec/editor-view-spec.coffee index d01ed45e5..fac5bc28a 100644 --- a/spec/editor-view-spec.coffee +++ b/spec/editor-view-spec.coffee @@ -1803,6 +1803,13 @@ describe "EditorView", -> expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol + describe "when editor.showIndentGuide is set to false", -> + it "does not render the indent guide on whitespace only lines (regression)", -> + editorView.attachToDom() + editor.setText(' ') + atom.config.set('editor.showIndentGuide', false) + expect(editorView.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 + describe "when soft-wrap is enabled", -> beforeEach -> jasmine.unspy(window, 'setTimeout') diff --git a/src/editor-view.coffee b/src/editor-view.coffee index 6aa18dd2a..2b69becd1 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -1490,7 +1490,7 @@ class EditorView extends View position = 0 for token in tokens @updateScopeStack(line, scopeStack, token.scopes) - hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly) + hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly)) line.push(token.getValueAsHtml({invisibles, hasIndentGuide})) position += token.value.length From 50a6d251d6e79290444358653ab6290595cef071 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 09:18:36 -0700 Subject: [PATCH 098/119] Prepare 0.97.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67de377e4..cf6e0cc8b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.96.0", + "version": "0.97.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { From f5c3bdb8453c51db694450d39fcce3de9c076c18 Mon Sep 17 00:00:00 2001 From: Pritam Baral Date: Fri, 16 May 2014 23:40:01 +0530 Subject: [PATCH 099/119] Use os.tmpdir() on Linux /tmp isn't always available, is on precious RAM-backed fs or simply not what the user has set his $TMPDIR to. According to the specification, we should use $TMPDIR, which node lets us find through os.tmpdir(). Also, contributing.md isn't in favour of using platform-dependent code. This commit focusses only on Linux, and leaves OS X as is with /tmp for discussion. --- build/Gruntfile.coffee | 4 ++-- script/clean | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 7ef1157fd..282a5ab6b 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -58,13 +58,13 @@ module.exports = (grunt) -> installDir = path.join('/Applications', appName) else appName = 'Atom' - tmpDir = '/tmp' + tmpDir = os.tmpdir() buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') shellAppDir = path.join(buildDir, appName) contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') - atomShellDownloadDir = '/tmp/atom-cached-atom-shells' + atomShellDownloadDir = path.join(tmpDir, 'atom-cached-atom-shells') installDir = process.env.INSTALL_PREFIX ? '/usr/local' coffeeConfig = diff --git a/script/clean b/script/clean index e55668215..833f64b62 100755 --- a/script/clean +++ b/script/clean @@ -8,7 +8,7 @@ var productName = require('../package.json').productName; process.chdir(path.dirname(__dirname)); var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -var tmpdir = process.platform === 'win32' ? os.tmpdir() : '/tmp'; +var tmpdir = process.platform !== 'darwin' ? os.tmpdir() : '/tmp'; // Windows: Use START as a way to ignore error if Atom.exe isnt running var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true'; From 2d96444e2199e935f5cb743b0da3a53f3342e0ad Mon Sep 17 00:00:00 2001 From: Pritam Baral Date: Sat, 17 May 2014 00:40:35 +0530 Subject: [PATCH 100/119] Use os.tmpdir() on OS X --- build/Gruntfile.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 282a5ab6b..4a58cea4b 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -48,13 +48,13 @@ module.exports = (grunt) -> installDir = path.join(process.env.ProgramFiles, appName) else if process.platform is 'darwin' appName = 'Atom.app' - tmpDir = '/tmp' + tmpDir = os.tmpdir() buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') shellAppDir = path.join(buildDir, appName) contentsDir = path.join(shellAppDir, 'Contents') appDir = path.join(contentsDir, 'Resources', 'app') - atomShellDownloadDir = '/tmp/atom-cached-atom-shells' + atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells') installDir = path.join('/Applications', appName) else appName = 'Atom' From 25d22064713b5c6044de17fc27d8690bc0ae9cc6 Mon Sep 17 00:00:00 2001 From: Pritam Baral Date: Sat, 17 May 2014 00:40:55 +0530 Subject: [PATCH 101/119] Consolidate redundant code --- build/Gruntfile.coffee | 20 +++++--------------- script/clean | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 4a58cea4b..9778467c7 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -36,35 +36,25 @@ module.exports = (grunt) -> grunt.log.write = (args...) -> grunt.log [major, minor, patch] = packageJson.version.split('.') + tmpDir = os.tmpdir() + buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') + atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells') + symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') + shellAppDir = path.join(buildDir, appName) if process.platform is 'win32' appName = 'Atom' - tmpDir = os.tmpdir() - buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') - symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') - shellAppDir = path.join(buildDir, appName) contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') - atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells') installDir = path.join(process.env.ProgramFiles, appName) else if process.platform is 'darwin' appName = 'Atom.app' - tmpDir = os.tmpdir() - buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') - symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') - shellAppDir = path.join(buildDir, appName) contentsDir = path.join(shellAppDir, 'Contents') appDir = path.join(contentsDir, 'Resources', 'app') - atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells') installDir = path.join('/Applications', appName) else appName = 'Atom' - tmpDir = os.tmpdir() - buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') - symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') - shellAppDir = path.join(buildDir, appName) contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') - atomShellDownloadDir = path.join(tmpDir, 'atom-cached-atom-shells') installDir = process.env.INSTALL_PREFIX ? '/usr/local' coffeeConfig = diff --git a/script/clean b/script/clean index 833f64b62..2ee2390e7 100755 --- a/script/clean +++ b/script/clean @@ -8,7 +8,7 @@ var productName = require('../package.json').productName; process.chdir(path.dirname(__dirname)); var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME']; -var tmpdir = process.platform !== 'darwin' ? os.tmpdir() : '/tmp'; +var tmpdir = os.tmpdir(); // Windows: Use START as a way to ignore error if Atom.exe isnt running var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true'; From 7627e0b0f09e6cbdc040aef2346110912e1d6588 Mon Sep 17 00:00:00 2001 From: Pritam Baral Date: Mon, 19 May 2014 23:56:47 +0530 Subject: [PATCH 102/119] Minor fix + Remove last references to /tmp --- CONTRIBUTING.md | 3 +-- atom.sh | 8 +++++--- build/Gruntfile.coffee | 4 +--- build/tasks/clean-task.coffee | 2 +- docs/build-instructions/freebsd.md | 2 +- docs/build-instructions/linux.md | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 153f780fd..d9b837174 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,8 +45,7 @@ in the proper package's repository. * Beware of platform differences * Use `require('atom').fs.getHomeDirectory()` to get the home directory. * Use `path.join()` to concatenate filenames. - * Temporary directory is not `/tmp` on Windows, use `os.tmpdir()` when - possible + * Use `os.tmpdir()` instead of `/tmp ## Git Commit Messages * Use the present tense diff --git a/atom.sh b/atom.sh index 840c0364a..ddeba08d9 100755 --- a/atom.sh +++ b/atom.sh @@ -70,16 +70,18 @@ elif [ $OS == 'Linux' ]; then USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..) ATOM_PATH="$USR_DIRECTORY/share/atom/atom" - [ -x "$ATOM_PATH" ] || ATOM_PATH='/tmp/atom-build/Atom/atom' + : ${TMPDIR:=/tmp} + + [ -x "$ATOM_PATH" ] || ATOM_PATH="$TMPDIR/atom-build/Atom/atom" if [ $EXPECT_OUTPUT ]; then "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" exit $? else ( - nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > /tmp/atom-nohup.out 2>&1 + nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$TMPDIR/atom-nohup.out" 2>&1 if [ $? -ne 0 ]; then - cat /tmp/atom-nohup.out + cat "$TMPDIR/atom-nohup.out" exit $? fi ) & diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 9778467c7..337c4833b 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -37,22 +37,20 @@ module.exports = (grunt) -> [major, minor, patch] = packageJson.version.split('.') tmpDir = os.tmpdir() + appName = if process.platform is 'darwin' then 'Atom.app' else 'Atom' buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build') atomShellDownloadDir = path.join(os.tmpdir(), 'atom-cached-atom-shells') symbolsDir = path.join(buildDir, 'Atom.breakpad.syms') shellAppDir = path.join(buildDir, appName) if process.platform is 'win32' - appName = 'Atom' contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') installDir = path.join(process.env.ProgramFiles, appName) else if process.platform is 'darwin' - appName = 'Atom.app' contentsDir = path.join(shellAppDir, 'Contents') appDir = path.join(contentsDir, 'Resources', 'app') installDir = path.join('/Applications', appName) else - appName = 'Atom' contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') installDir = process.env.INSTALL_PREFIX ? '/usr/local' diff --git a/build/tasks/clean-task.coffee b/build/tasks/clean-task.coffee index 1d0befa3f..97f0a9105 100644 --- a/build/tasks/clean-task.coffee +++ b/build/tasks/clean-task.coffee @@ -5,7 +5,7 @@ module.exports = (grunt) -> {rm} = require('./task-helpers')(grunt) grunt.registerTask 'partial-clean', 'Delete some of the build files', -> - tmpdir = if process.platform is 'win32' then os.tmpdir() else '/tmp' + tmpdir = os.tmpdir() rm grunt.config.get('atom.buildDir') rm require('../src/coffee-cache').cacheDir diff --git a/docs/build-instructions/freebsd.md b/docs/build-instructions/freebsd.md index 2299d9cab..147c9da23 100644 --- a/docs/build-instructions/freebsd.md +++ b/docs/build-instructions/freebsd.md @@ -15,7 +15,7 @@ FreeBSD -RELEASE 64-bit is the recommended platform. ```sh git clone https://github.com/atom/atom cd atom - script/build # Creates application at /tmp/atom-build/Atom + script/build # Creates application at $TMPDIR/atom-build/Atom sudo script/grunt install # Installs command to /usr/local/bin/atom ``` diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 359c780b9..259ef71d5 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -15,9 +15,9 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ```sh git clone https://github.com/atom/atom cd atom - script/build # Creates application at /tmp/atom-build/Atom + script/build # Creates application at $TMPDIR/atom-build/Atom sudo script/grunt install # Installs command to /usr/local/bin/atom - script/grunt mkdeb # Generates a .deb package at /tmp/atom-build + script/grunt mkdeb # Generates a .deb package at $TMPDIR/atom-build ``` ## Troubleshooting From 37bdfb716bdd0c550faff2c8119408248c5de7c3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:03:34 -0600 Subject: [PATCH 103/119] Preserve the target when scrolling w/ mousewheel in gutter Removing the target of a mouseweel event messes up velocity scrolling with the track pad, so it needs to be preserved until scrolling ceases. --- src/editor-component.coffee | 14 ++++++++++++-- src/gutter-component.coffee | 12 +++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 463b47c5e..de37124c9 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -28,6 +28,7 @@ EditorComponent = React.createClass measuringScrollbars: true pendingVerticalScrollDelta: 0 pendingHorizontalScrollDelta: 0 + mouseWheelScreenRow: null render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state @@ -53,7 +54,7 @@ EditorComponent = React.createClass GutterComponent { ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, scrollHeight, lineHeight: lineHeightInPixels, fontSize, fontFamily, - @pendingChanges, onWidthChanged: @onGutterWidthChanged + @pendingChanges, onWidthChanged: @onGutterWidthChanged, @mouseWheelScreenRow } EditorScrollViewComponent { @@ -325,7 +326,8 @@ EditorComponent = React.createClass onMouseWheel: (event) -> event.preventDefault() - + screenRow = @screenRowForNode(event.target) + @mouseWheelScreenRow = screenRow if screenRow? animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 # Only scroll in one direction at a time @@ -343,6 +345,13 @@ EditorComponent = React.createClass @pendingVerticalScrollDelta = 0 @pendingHorizontalScrollDelta = 0 + screenRowForNode: (node) -> + while node isnt document + if screenRow = node.dataset.screenRow + return parseInt(screenRow) + node = node.parentNode + null + onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) @@ -408,6 +417,7 @@ EditorComponent = React.createClass onStoppedScrolling: -> @scrollingVertically = false + @mouseWheelScreenRow = null @requestUpdate() stopScrollingAfterDelay: null # created lazily diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index cc20c5c43..a78c1b540 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -113,13 +113,15 @@ GutterComponent = React.createClass visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> + {mouseWheelScreenRow} = @props node = @refs.lineNumbers.getDOMNode() for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId) - delete @lineNumberNodesById[lineNumberId] screenRow = @screenRowsByLineNumberId[lineNumberId] - delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId - delete @screenRowsByLineNumberId[lineNumberId] - node.removeChild(lineNumberNode) + unless screenRow is mouseWheelScreenRow + delete @lineNumberNodesById[lineNumberId] + delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId + delete @screenRowsByLineNumberId[lineNumberId] + node.removeChild(lineNumberNode) buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> if screenRow? @@ -129,7 +131,7 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - "
#{innerHTML}
" + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped From 795399e184f1738fefde358afebd95b0a1aa36e8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:17:09 -0600 Subject: [PATCH 104/119] Preserve the target when scrolling w/ mousewheel on editor lines --- src/editor-component.coffee | 2 +- src/editor-scroll-view-component.coffee | 4 ++-- src/lines-component.coffee | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index de37124c9..113de150d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -62,7 +62,7 @@ EditorComponent = React.createClass lineHeight: lineHeightInPixels, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, @scrollingVertically, @cursorsMoved, @selectionChanged, @selectionAdded, cursorBlinkResumeDelay, - @onInputFocused, @onInputBlurred + @onInputFocused, @onInputBlurred, @mouseWheelScreenRow } ScrollbarComponent diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index b03200a1c..1363c5e67 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -17,7 +17,7 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props - {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically} = @props + {renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, scrollingVertically, mouseWheelScreenRow} = @props {selectionChanged, selectionAdded, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props if @isMounted() @@ -37,7 +37,7 @@ EditorScrollViewComponent = React.createClass LinesComponent { ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide, renderedRowRange, pendingChanges, scrollTop, scrollLeft, scrollingVertically, - selectionChanged, scrollHeight, scrollWidth + selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow } componentDidMount: -> diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 2e2bb52d0..1cae8b8fb 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -64,15 +64,17 @@ LinesComponent = React.createClass @appendOrUpdateVisibleLineNodes(visibleLines, startRow) removeLineNodes: (visibleLines=[]) -> + {mouseWheelScreenRow} = @props visibleLineIds = new Set visibleLineIds.add(line.id.toString()) for line in visibleLines node = @getDOMNode() for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId) - delete @lineNodesByLineId[lineId] screenRow = @screenRowsByLineId[lineId] - delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId - delete @screenRowsByLineId[lineId] - node.removeChild(lineNode) + unless screenRow is mouseWheelScreenRow + delete @lineNodesByLineId[lineId] + delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId + delete @screenRowsByLineId[lineId] + node.removeChild(lineNode) appendOrUpdateVisibleLineNodes: (visibleLines, startRow) -> {lineHeight} = @props @@ -112,7 +114,7 @@ LinesComponent = React.createClass {editor, mini, showIndentGuide, lineHeight} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line top = screenRow * lineHeight - lineHTML = "
" + lineHTML = "
" if text is "" lineHTML += @buildEmptyLineInnerHTML(line) From bfc382c398cc55b47ef83e52e61efd81f86fffdc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 14:33:17 -0600 Subject: [PATCH 105/119] Add specs for line/line-number preservation --- spec/editor-component-spec.coffee | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d5f04e65f..28f740531 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -653,6 +653,32 @@ describe "EditorComponent", -> expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 15 + describe "when the mousewheel event's target is a line", -> + it "keeps the line on the DOM if it is scrolled off-screen", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() + + lineNode = node.querySelector('.line') + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNode) + node.dispatchEvent(wheelEvent) + + expect(node.contains(lineNode)).toBe true + + describe "when the mousewheel event's target is a line number", -> + it "keeps the line number on the DOM if it is scrolled off-screen", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() + + lineNumberNode = node.querySelectorAll('.line-number')[1] + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) + node.dispatchEvent(wheelEvent) + + expect(node.contains(lineNumberNode)).toBe true + describe "input events", -> inputNode = null From c4e44297448bab8be7d89f37568af0984e1e2202 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 13:25:07 -0700 Subject: [PATCH 106/119] Upgrade to language-python@0.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf6e0cc8b..d5790d633 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "language-perl": "0.8.0", "language-php": "0.14.0", "language-property-list": "0.7.0", - "language-python": "0.16.0", + "language-python": "0.17.0", "language-ruby": "0.24.0", "language-ruby-on-rails": "0.14.0", "language-sass": "0.11.0", From bf7d2defd69d875f8d8546f713a5d57a5d975517 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 13:35:44 -0700 Subject: [PATCH 107/119] Upgrade to grunt-download-atom-shell@0.7.2 --- build/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package.json b/build/package.json index 90a2f88c1..2b6f7f2ce 100644 --- a/build/package.json +++ b/build/package.json @@ -18,7 +18,7 @@ "grunt-contrib-coffee": "~0.9.0", "grunt-contrib-less": "~0.8.0", "grunt-cson": "0.8.0", - "grunt-download-atom-shell": "~0.7.0", + "grunt-download-atom-shell": "~0.7.2", "grunt-lesslint": "0.13.0", "grunt-markdown": "~0.4.0", "grunt-peg": "~1.1.0", From 9a18ff5954ec1f0e142c5887554e73a40de4f603 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 14:03:48 -0700 Subject: [PATCH 108/119] Update atom.ico in resources directory --- resources/win/atom.ico | Bin 370070 -> 144546 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/win/atom.ico b/resources/win/atom.ico index 446140277d892351a246bfc098625f02865d493f..57d2c7aca6d47ac6f7f741b06b59be4bad6ca0c4 100644 GIT binary patch literal 144546 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

!c#S6Olpb*Fm80Yr2crWA}Z5Kcr7 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 literal 370070 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 lF1$jAxGf!t(Hm2;k>>Wi8Y%aJ>3 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 From 65cafdda038a7a174f7f1849b2265840cc849091 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 15:39:14 -0700 Subject: [PATCH 109/119] Upgrade to language-gfm@0.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c22a133e..cd420c3a0 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "language-c": "0.15.0", "language-coffee-script": "0.22.0", "language-css": "0.16.0", - "language-gfm": "0.37.0", + "language-gfm": "0.38.0", "language-git": "0.9.0", "language-go": "0.12.0", "language-html": "0.22.0", From aa557a7bdf7ad9cb5c75dfbf57c9d61bec63743b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 15:47:11 -0700 Subject: [PATCH 110/119] Upgrade to bracket-matcher@0.35.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd420c3a0..873debb59 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "autosave": "0.13.0", "background-tips": "0.13.0", "bookmarks": "0.22.0", - "bracket-matcher": "0.34.0", + "bracket-matcher": "0.35.0", "command-palette": "0.21.0", "deprecation-cop": "0.5.0", "dev-live-reload": "0.30.0", From 2c5af98cca505e042d46bd7c62ddf85e55cd327b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 16:45:04 -0700 Subject: [PATCH 111/119] Upgrade to bracket-matcher@0.36.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 873debb59..e6f57b68a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "autosave": "0.13.0", "background-tips": "0.13.0", "bookmarks": "0.22.0", - "bracket-matcher": "0.35.0", + "bracket-matcher": "0.36.0", "command-palette": "0.21.0", "deprecation-cop": "0.5.0", "dev-live-reload": "0.30.0", From ce30299122e73e5d88c2a16eb9dc3571790c6c86 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 19 May 2014 17:39:34 -0700 Subject: [PATCH 112/119] Upgrade to find-and-replace@0.106.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6f57b68a..da366ea61 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "dev-live-reload": "0.30.0", "exception-reporting": "0.17.0", "feedback": "0.33.0", - "find-and-replace": "0.105.0", + "find-and-replace": "0.106.0", "fuzzy-finder": "0.51.0", "git-diff": "0.28.0", "go-to-line": "0.21.0", From 5716c7c574e303cbe9808a8a1391df7e720790b7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 21:31:10 -0600 Subject: [PATCH 113/119] Upgrade underscore-plus for multiplyString optimization --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da366ea61..54b58e6da 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "temp": "0.5.0", "text-buffer": "^2.2.2", "theorist": "^1", - "underscore-plus": "^1.2.1", + "underscore-plus": "^1.2.2", "vm-compatibility-layer": "0.1.0" }, "packageDependencies": { From cd5f0c0047a556c91e4903b1f9f02cef0c42e4f8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 15:20:42 -0600 Subject: [PATCH 114/119] Update the screenRow on the line element's dataset in ::updateLineNode --- src/lines-component.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1cae8b8fb..bc067e95b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -179,6 +179,7 @@ LinesComponent = React.createClass {lineHeight} = @props lineNode = @lineNodesByLineId[line.id] lineNode.style.top = screenRow * lineHeight + 'px' + lineNode.dataset.screenRow = screenRow @screenRowsByLineId[line.id] = screenRow @lineIdsByScreenRow[screenRow] = line.id From 6edb0b7a3d05d104edd640f69e2bf3c53d298bc5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 16:07:56 -0600 Subject: [PATCH 115/119] Delete dead method --- src/lines-component.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index bc067e95b..7a51605ad 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -107,9 +107,6 @@ LinesComponent = React.createClass hasLineNode: (lineId) -> @lineNodesByLineId.hasOwnProperty(lineId) - buildTranslate3d: (top) -> - "translate3d(0px, #{top}px, 0px)" - buildLineHTML: (line, screenRow) -> {editor, mini, showIndentGuide, lineHeight} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line From 353eb27d2e0a7d31f4e18eec1ce6919757c4b84f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 21:34:06 -0600 Subject: [PATCH 116/119] Update dataset screenRow of gutter nodes when updating them --- src/gutter-component.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a78c1b540..1454494e1 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -147,6 +147,7 @@ GutterComponent = React.createClass unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeight} = @props @lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeight + 'px' + @lineNumberNodesById[lineNumberId].dataset.screenRow = screenRow @screenRowsByLineNumberId[lineNumberId] = screenRow @lineNumberIdsByScreenRow[screenRow] = lineNumberId From 0ad26c337a35cccf7f645ae05e71aab5841f9fbc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 19 May 2014 22:20:57 -0600 Subject: [PATCH 117/119] Don't use _.pluck when building TokenizedLines --- src/tokenized-line.coffee | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 1bbec7972..c59faea7e 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -7,11 +7,22 @@ class TokenizedLine constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel}) -> @tokens = @breakOutAtomicTokens(tokens) @startBufferColumn ?= 0 - @text = _.pluck(@tokens, 'value').join('') - @bufferDelta = _.sum(_.pluck(@tokens, 'bufferDelta')) + @text = @buildText() + @bufferDelta = @buildBufferDelta() + @id = idCounter++ @markLeadingAndTrailingWhitespaceTokens() + buildText: -> + text = "" + text += token.value for token in @tokens + text + + buildBufferDelta: -> + delta = 0 + delta += token.bufferDelta for token in @tokens + delta + copy: -> new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold}) From 6fa68632441b3be39908820de7556548dd977f7b Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Tue, 20 May 2014 22:34:37 +0800 Subject: [PATCH 118/119] Don't popup context menu with no items, fixes #2032. On OS X popuping an empty context menu would have no effect but on Linux an empty menu container would still be showed. --- src/context-menu-manager.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index c310c9c48..908e9fc8a 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -98,5 +98,6 @@ class ContextMenuManager showForEvent: (event) -> @activeElement = event.target menuTemplate = @combinedMenuTemplateForElement(event.target) + return unless menuTemplate?.length > 0 @executeBuildHandlers(event, menuTemplate) remote.getCurrentWindow().emit('context-menu', menuTemplate) From dda465d08ad84c5f277f4e44e4acaedef3d1ede6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 20 May 2014 09:47:31 -0700 Subject: [PATCH 119/119] Upgrade to language-ruby@0.25.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54b58e6da..bcee11726 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "language-php": "0.14.0", "language-property-list": "0.7.0", "language-python": "0.17.0", - "language-ruby": "0.24.0", + "language-ruby": "0.25.0", "language-ruby-on-rails": "0.14.0", "language-sass": "0.11.0", "language-shellscript": "0.8.0",