diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index dfe3c51b2..68f0374de 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -5,8 +5,14 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ## Requirements * OS with 64-bit or 32-bit architecture + * C++ toolchain + * on Ubuntu/Debian: `sudo apt-get install build-essential` * [node.js](http://nodejs.org/download/) v0.10.x - * [npm](http://www.npmjs.org/) v1.4.x + * [Ubuntu/Debian/Mint instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint-elementary-os) + * [Fedora instructions](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#fedora) + * [npm](http://www.npmjs.org/) v1.4.x + * `npm` comes with node.js so no explicit installation is needed here. + * You can check `npm` 1.4 or above is installed by running `npm -v`. * libgnome-keyring-dev * on Ubuntu/Debian: `sudo apt-get install libgnome-keyring-dev` * on Fedora: `sudo yum --assumeyes install libgnome-keyring-devel` @@ -15,7 +21,6 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * This command may require `sudo` depending on how you have [configured npm](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint-elementary-os). - ## Instructions ```sh @@ -53,4 +58,5 @@ and restart Atom. If Atom now works fine, you can make this setting permanent: See also https://github.com/atom/atom/issues/2082. ### Linux build error reports in atom/atom -* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) to get a list of reports about build errors on Linux. +* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) + to get a list of reports about build errors on Linux. diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 374742256..dbb879efc 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -29,6 +29,8 @@ 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' + 'ctrl-insert': 'core:copy' + 'shift-insert': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 2e94af6ba..9a46e7933 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -31,6 +31,8 @@ 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' + 'ctrl-insert': 'core:copy' + 'shift-insert': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' diff --git a/package.json b/package.json index 027b426c7..057590fc6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.102.0", + "version": "0.104.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -27,7 +27,7 @@ "coffeestack": "0.7.0", "delegato": "^1", "emissary": "^1.2.1", - "first-mate": "^1.6.1", + "first-mate": "^1.7", "fs-plus": "^2.2.3", "fstream": "0.1.24", "fuzzaldrin": "^1.1", @@ -54,7 +54,7 @@ "season": "^1.0.2", "semver": "1.1.4", "serializable": "^1", - "space-pen": "3.3.0", + "space-pen": "3.2.0", "temp": "0.5.0", "text-buffer": "^2.2.2", "theorist": "^1", @@ -69,32 +69,32 @@ "base16-tomorrow-dark-theme": "0.16.0", "solarized-dark-syntax": "0.17.0", "solarized-light-syntax": "0.8.0", - "archive-view": "0.31.0", + "archive-view": "0.32.0", "autocomplete": "0.28.0", "autoflow": "0.17.0", "autosave": "0.13.0", "background-tips": "0.14.0", - "bookmarks": "0.22.0", - "bracket-matcher": "0.43.0", - "command-palette": "0.21.0", + "bookmarks": "0.23.0", + "bracket-matcher": "0.44.0", + "command-palette": "0.22.0", "deprecation-cop": "0.6.0", "dev-live-reload": "0.31.0", "exception-reporting": "0.18.0", "feedback": "0.33.0", - "find-and-replace": "0.113.0", + "find-and-replace": "0.115.0", "fuzzy-finder": "0.54.0", - "git-diff": "0.28.0", + "git-diff": "0.29.0", "go-to-line": "0.22.0", "grammar-selector": "0.27.0", - "image-view": "0.34.0", + "image-view": "0.35.0", "keybinding-resolver": "0.18.0", "link": "0.22.0", - "markdown-preview": "0.74.0", + "markdown-preview": "0.76.0", "metrics": "0.32.0", "open-on-github": "0.28.0", "package-generator": "0.30.0", "release-notes": "0.32.0", - "settings-view": "0.119.0", + "settings-view": "0.120.0", "snippets": "0.45.0", "spell-check": "0.37.0", "status-bar": "0.40.0", @@ -102,7 +102,7 @@ "symbols-view": "0.55.0", "tabs": "0.41.0", "timecop": "0.19.0", - "tree-view": "0.97.0", + "tree-view": "0.99.0", "update-package-dependencies": "0.6.0", "welcome": "0.16.0", "whitespace": "0.22.0", diff --git a/script/install-cli b/script/install-cli deleted file mode 100755 index c24835852..000000000 --- a/script/install-cli +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env coffee - -path = require 'path' -CommandInstaller = require '../src/command-installer' - -callback = (error) -> - console.warn error.message if error? - -CommandInstaller.installAtomCommand(path.resolve(__dirname, '..'), callback) -CommandInstaller.installApmCommand(path.resolve(__dirname, '..'), callback) diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index 4a638be41..fa65a339e 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -11,8 +11,15 @@ module.exports = function(cb) { return; } - verifyPython27(function(error, pythonSuccessMessage) { - cb(error, (nodeSuccessMessage + "\n" + pythonSuccessMessage).trim()); + verifyNpm(function(error, npmSuccessMessage) { + if (error) { + cb(error); + return; + } + + verifyPython27(function(error, pythonSuccessMessage) { + cb(error, (nodeSuccessMessage + "\n" + npmSuccessMessage + "\n" + pythonSuccessMessage).trim()); + }); }); }); @@ -32,6 +39,31 @@ function verifyNode(cb) { } } +function verifyNpm(cb) { + var localNpmPath = path.resolve(__dirname, '..', '..', 'build', 'node_modules', '.bin', 'npm'); + if (process.platform === 'win32') + localNpmPath += ".cmd"; + + var npmCommand = fs.existsSync(localNpmPath) ? localNpmPath : 'npm'; + if (npmCommand === 'npm' && process.platform === 'win32') + npmCommand += ".cmd"; + + childProcess.execFile(npmCommand, ['-v'], { env: process.env }, function(err, stdout) { + if (err) + return cb("npm 1.4 is required to build Atom. An error (" + err + ") occured when checking the version."); + + var npmVersion = stdout ? stdout.trim() : ''; + var versionArray = npmVersion.split('.'); + var npmMajorVersion = +versionArray[0] || 0; + var npmMinorVersion = +versionArray[1] || 0; + if (npmMajorVersion === 1 && npmMinorVersion < 4) + cb("npm v1.4+ is required to build Atom."); + else + cb(null, "npm: v" + npmVersion); + }); +} + + function verifyPython27(cb) { if (process.platform == 'win32') { if (!pythonExecutable) { diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 05756b00a..16c02b421 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -165,7 +165,7 @@ describe "EditorComponent", -> beforeEach -> editor.setText "a line that wraps " editor.setSoftWrap(true) - node.style.width = 15 * charWidth + 'px' + node.style.width = 16 * charWidth + 'px' component.measureScrollView() it "doesn't show end of line invisibles at the end of wrapped lines", -> @@ -228,6 +228,13 @@ describe "EditorComponent", -> [node] describe "gutter rendering", -> + [lineNumberHasClass, gutter] = [] + + beforeEach -> + {gutter} = component.refs + lineNumberHasClass = (screenRow, klass) -> + component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() @@ -302,6 +309,267 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" expect(gutterNode.offsetWidth).toBe initialGutterWidth + describe "fold decorations", -> + describe "rendering fold decorations", -> + it "adds the foldable class to line numbers when the line is foldable", -> + expect(lineNumberHasClass(0, 'foldable')).toBe true + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe false + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe true + expect(lineNumberHasClass(5, 'foldable')).toBe false + + it "updates the foldable class on the correct line numbers when the foldable positions change", -> + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(0, 'foldable')).toBe false + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe true + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe false + expect(lineNumberHasClass(5, 'foldable')).toBe true + expect(lineNumberHasClass(6, 'foldable')).toBe false + + it "updates the foldable class on a line number that becomes foldable", -> + expect(lineNumberHasClass(11, 'foldable')).toBe false + + editor.getBuffer().insert([11, 44], '\n fold me') + expect(lineNumberHasClass(11, 'foldable')).toBe true + + editor.undo() + expect(lineNumberHasClass(11, 'foldable')).toBe false + + it "adds, updates and removes the folded class on the correct line number nodes", -> + editor.foldBufferRow(4) + expect(lineNumberHasClass(4, 'folded')).toBe true + + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberHasClass(4, 'folded')).toBe false + expect(lineNumberHasClass(5, 'folded')).toBe true + + editor.unfoldBufferRow(5) + expect(lineNumberHasClass(5, 'folded')).toBe false + + describe "mouse interactions with fold indicators", -> + [gutterNode] = [] + + buildClickEvent = (target) -> + buildMouseEvent('click', {target}) + + beforeEach -> + gutterNode = node.querySelector('.gutter') + + it "folds and unfolds the block represented by the fold indicator when clicked", -> + expect(lineNumberHasClass(1, 'folded')).toBe false + + lineNumber = component.lineNumberNodeForScreenRow(1) + target = lineNumber.querySelector('.icon-right') + + target.dispatchEvent(buildClickEvent(target)) + expect(lineNumberHasClass(1, 'folded')).toBe true + + lineNumber = component.lineNumberNodeForScreenRow(1) + target = lineNumber.querySelector('.icon-right') + + target.dispatchEvent(buildClickEvent(target)) + expect(lineNumberHasClass(1, 'folded')).toBe false + + it "does not fold when the line number node is clicked", -> + lineNumber = component.lineNumberNodeForScreenRow(1) + lineNumber.dispatchEvent(buildClickEvent(lineNumber)) + expect(lineNumberHasClass(1, 'folded')).toBe false + + describe "cursor-line decorations", -> + cursor = null + beforeEach -> + cursor = editor.getCursor() + + it "modifies the cursor-line decoration when the cursor moves", -> + cursor.setScreenPosition([0, 0]) + expect(lineNumberHasClass(0, 'cursor-line')).toBe true + + cursor.setScreenPosition([1, 0]) + expect(lineNumberHasClass(0, 'cursor-line')).toBe false + expect(lineNumberHasClass(1, 'cursor-line')).toBe true + + it "updates cursor-line decorations for multiple cursors", -> + cursor.setScreenPosition([2, 0]) + cursor2 = editor.addCursorAtScreenPosition([8, 0]) + cursor3 = editor.addCursorAtScreenPosition([10, 0]) + + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe true + expect(lineNumberHasClass(10, 'cursor-line')).toBe true + + cursor2.destroy() + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe false + expect(lineNumberHasClass(10, 'cursor-line')).toBe true + + cursor3.destroy() + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(8, 'cursor-line')).toBe false + expect(lineNumberHasClass(10, 'cursor-line')).toBe false + + it "adds cursor-line decorations to multiple lines when a selection is performed", -> + cursor.setScreenPosition([1, 0]) + editor.selectDown(2) + expect(lineNumberHasClass(0, 'cursor-line')).toBe false + expect(lineNumberHasClass(1, 'cursor-line')).toBe true + expect(lineNumberHasClass(2, 'cursor-line')).toBe true + expect(lineNumberHasClass(3, 'cursor-line')).toBe true + expect(lineNumberHasClass(4, 'cursor-line')).toBe false + + describe "when decorations are used", -> + describe "when decorations are applied to buffer rows", -> + it "renders line number classes based on the decorations on their buffer row", -> + node.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureScrollView() + + expect(component.lineNumberNodeForScreenRow(9)).not.toBeDefined() + + editor.addDecorationToBufferRow(9, type: 'gutter', class: 'fancy-class') + editor.addDecorationToBufferRow(9, type: 'someother-type', class: 'nope-class') + + verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + expect(lineNumberHasClass(9, 'fancy-class')).toBe true + expect(lineNumberHasClass(9, 'nope-class')).toBe false + + it "renders updates to gutter decorations", -> + editor.addDecorationToBufferRow(2, type: 'gutter', class: 'fancy-class') + editor.addDecorationToBufferRow(2, type: 'someother-type', class: 'nope-class') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'fancy-class')).toBe true + expect(lineNumberHasClass(2, 'nope-class')).toBe false + + editor.removeDecorationFromBufferRow(2, type: 'gutter', class: 'fancy-class') + editor.removeDecorationFromBufferRow(2, type: 'someother-type', class: 'nope-class') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'fancy-class')).toBe false + expect(lineNumberHasClass(2, 'nope-class')).toBe false + + it "renders decorations on soft-wrapped line numbers when softWrap is true", -> + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) + + editor.setSoftWrap(true) + node.style.height = 4.5 * lineHeightInPixels + 'px' + node.style.width = 30 * charWidth + 'px' + component.measureScrollView() + + expect(lineNumberHasClass(2, 'no-wrap')).toBe true + expect(lineNumberHasClass(2, 'wrap-me')).toBe true + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe true + + # should remove the wrapped decorations + editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.removeDecorationFromBufferRow(1, type: 'gutter', class: 'wrap-me') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'no-wrap')).toBe false + expect(lineNumberHasClass(2, 'wrap-me')).toBe false + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe false + + # should add them back when the nodes are not recreated + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'no-wrap') + editor.addDecorationToBufferRow(1, type: 'gutter', class: 'wrap-me', softWrap: true) + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'no-wrap')).toBe true + expect(lineNumberHasClass(2, 'wrap-me')).toBe true + expect(lineNumberHasClass(3, 'no-wrap')).toBe false + expect(lineNumberHasClass(3, 'wrap-me')).toBe true + + describe "when decorations are applied to markers", -> + {marker, decoration} = {} + beforeEach -> + marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') + decoration = {type: 'gutter', class: 'someclass'} + editor.addDecorationForMarker(marker, decoration) + waitsFor -> not component.decorationChangedImmediate? + + it "updates line number classes when the marker moves", -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false + + editor.getBuffer().insert([0, 0], '\n') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe true + expect(lineNumberHasClass(5, 'someclass')).toBe false + + editor.getBuffer().deleteRows(0, 1) + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(0, 'someclass')).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe true + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe false + + it "removes line number classes when a decoration's marker is invalidated", -> + editor.getBuffer().insert([3, 2], 'n') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + + expect(marker.isValid()).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false + + editor.getBuffer().undo() + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(marker.isValid()).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false + + it "removes the classes and unsubscribes from the marker when decoration is removed", -> + editor.removeDecorationForMarker(marker, decoration) + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false + + editor.getBuffer().insert([0, 0], '\n') + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + + it "removes the line number classes when the decoration's marker is destroyed", -> + marker.destroy() + + waitsFor -> not component.decorationChangedImmediate? + runs -> + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false + describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> cursor1 = editor.getCursor() @@ -630,12 +898,6 @@ describe "EditorComponent", -> clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() {clientX, clientY} - buildMouseEvent = (type, properties...) -> - properties = extend({bubbles: true, cancelable: true}, properties...) - event = new MouseEvent(type, properties) - Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? - event - describe "focus handling", -> inputNode = null @@ -654,6 +916,22 @@ describe "EditorComponent", -> inputNode.blur() expect(node.classList.contains('is-focused')).toBe false + describe "selection handling", -> + cursor = null + + beforeEach -> + cursor = editor.getCursor() + cursor.setScreenPosition([0, 0]) + + it "adds the 'has-selection' class to the editor when there is a selection", -> + expect(node.classList.contains('has-selection')).toBe false + + editor.selectDown() + expect(node.classList.contains('has-selection')).toBe true + + cursor.moveDown() + expect(node.classList.contains('has-selection')).toBe false + describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' @@ -938,3 +1216,12 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) wrapperView.show() expect(node.querySelector('.cursor').style['-webkit-transform']).toBe "translate3d(#{9 * charWidth}px, 0px, 0px)" + + buildMouseEvent = (type, properties...) -> + properties = extend({bubbles: true, cancelable: true}, properties...) + event = new MouseEvent(type, properties) + Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? + if properties.target? + Object.defineProperty(event, 'target', get: -> properties.target) + Object.defineProperty(event, 'srcObject', get: -> properties.target) + event diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 704d062fb..ec0bdd76e 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1622,7 +1622,7 @@ describe "Editor", -> editor.setCursorBufferPosition([9,2]) editor.insertNewline() expect(editor.lineForBufferRow(10)).toBe ' };' - + describe ".backspace()", -> describe "when there is a single cursor", -> changeScreenRangeHandler = null @@ -3205,3 +3205,114 @@ describe "Editor", -> editor.pageUp() expect(editor.getScrollTop()).toBe 0 + + describe "decorations", -> + decoration = null + beforeEach -> + decoration = {type: 'gutter', class: 'one'} + + it "can add decorations to buffer rows and remove them", -> + editor.addDecorationToBufferRow(2, decoration) + editor.addDecorationToBufferRow(2, decoration) + + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toHaveLength 1 + expect(decorations).toContain decoration + + editor.removeDecorationFromBufferRow(2, decoration) + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toHaveLength 0 + + it "can add decorations to buffer row ranges and remove them", -> + editor.addDecorationToBufferRowRange(2, 4, decoration) + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + + editor.removeDecorationFromBufferRowRange(3, 5, decoration) + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + + it "can add decorations associated with markers and remove them", -> + marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], class: 'my-marker', invalidate: 'inside') + + editor.addDecorationForMarker(marker, decoration) + expect(editor.decorationsForBufferRow 1).not.toContain decoration + expect(editor.decorationsForBufferRow 2).toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + + editor.getBuffer().insert([0, 0], '\n') + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.getBuffer().insert([4, 2], 'n') + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.getBuffer().undo() + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).toContain decoration + expect(editor.decorationsForBufferRow 4).toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + editor.removeDecorationForMarker(marker, decoration) + expect(editor.decorationsForBufferRow 2).not.toContain decoration + expect(editor.decorationsForBufferRow 3).not.toContain decoration + expect(editor.decorationsForBufferRow 4).not.toContain decoration + expect(editor.decorationsForBufferRow 5).not.toContain decoration + + describe "decorationsForBufferRow", -> + one = {type: 'one', class: 'one'} + two = {type: 'two', class: 'two'} + typeless = {class: 'typeless'} + + beforeEach -> + editor.addDecorationToBufferRow(2, one) + editor.addDecorationToBufferRow(2, two) + editor.addDecorationToBufferRow(2, typeless) + + it "returns all decorations with no decorationType specified", -> + decorations = editor.decorationsForBufferRow(2) + expect(decorations).toContain one + expect(decorations).toContain two + expect(decorations).toContain typeless + + it "returns typeless decorations with all decorationTypes", -> + decorations = editor.decorationsForBufferRow(2, 'one') + expect(decorations).toContain one + expect(decorations).not.toContain two + expect(decorations).toContain typeless + + describe "decorationsForBufferRowRange", -> + one = {type: 'one', class: 'one'} + two = {type: 'two', class: 'two'} + typeless = {class: 'typeless'} + + it "returns an object of decorations based on the decorationType", -> + editor.addDecorationToBufferRow(2, one) + editor.addDecorationToBufferRow(3, one) + editor.addDecorationToBufferRow(5, one) + + editor.addDecorationToBufferRow(3, two) + editor.addDecorationToBufferRow(4, two) + + editor.addDecorationToBufferRow(3, typeless) + editor.addDecorationToBufferRow(5, typeless) + + decorations = editor.decorationsForBufferRowRange(2, 5, 'one') + expect(decorations[2]).toContain one + + expect(decorations[3]).toContain one + expect(decorations[3]).not.toContain two + expect(decorations[3]).toContain typeless + + expect(decorations[4]).toHaveLength 0 + + expect(decorations[5]).toContain one + expect(decorations[5]).toContain typeless diff --git a/src/display-buffer-marker.coffee b/src/display-buffer-marker.coffee index c3a902b99..e2324f6b7 100644 --- a/src/display-buffer-marker.coffee +++ b/src/display-buffer-marker.coffee @@ -111,6 +111,34 @@ class DisplayBufferMarker setTailBufferPosition: (bufferPosition) -> @bufferMarker.setTailPosition(bufferPosition) + # Retrieves the screen position of the marker's start. This will always be + # less than or equal to the result of {DisplayBufferMarker::getEndScreenPosition}. + # + # Returns a {Point}. + getStartScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true) + + # Retrieves the buffer position of the marker's start. This will always be + # less than or equal to the result of {DisplayBufferMarker::getEndBufferPosition}. + # + # Returns a {Point}. + getStartBufferPosition: -> + @bufferMarker.getStartPosition() + + # Retrieves the screen position of the marker's end. This will always be + # greater than or equal to the result of {DisplayBufferMarker::getStartScreenPosition}. + # + # Returns a {Point}. + getEndScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true) + + # Retrieves the buffer position of the marker's end. This will always be + # greater than or equal to the result of {DisplayBufferMarker::getStartBufferPosition}. + # + # Returns a {Point}. + getEndBufferPosition: -> + @bufferMarker.getEndPosition() + # Sets the marker's tail to the same position as the marker's head. # # This only works if there isn't already a tail position. diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 36c64863b..36979f934 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -43,6 +43,8 @@ class DisplayBuffer extends Model @charWidthsByScope = {} @markers = {} @foldsByMarkerId = {} + @decorations = {} + @decorationMarkerSubscriptions = {} @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) @subscribe @tokenizedBuffer, 'grammar-changed', (grammar) => @emit 'grammar-changed', grammar @@ -716,6 +718,97 @@ class DisplayBuffer extends Model rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) + decorationsForBufferRow: (bufferRow, decorationType) -> + decorations = @decorations[bufferRow] ? [] + decorations = (dec for dec in decorations when not dec.type? or dec.type is decorationType) if decorationType? + decorations + + decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> + decorations = {} + for bufferRow in [startBufferRow..endBufferRow] + decorations[bufferRow] = @decorationsForBufferRow(bufferRow, decorationType) + decorations + + addDecorationToBufferRow: (bufferRow, decoration) -> + @decorations[bufferRow] ?= [] + for current in @decorations[bufferRow] + return if _.isEqual(current, decoration) + @decorations[bufferRow].push(decoration) + @emit 'decoration-changed', {bufferRow, decoration, action: 'add'} + + removeDecorationFromBufferRow: (bufferRow, decorationPattern) -> + return unless decorations = @decorations[bufferRow] + + removed = [] + i = decorations.length - 1 + while i >= 0 + if @decorationMatchesPattern(decorations[i], decorationPattern) + removed.push decorations[i] + decorations.splice(i, 1) + i-- + + delete @decorations[bufferRow] unless @decorations[bufferRow]? + + for decoration in removed + @emit 'decoration-changed', {bufferRow, decoration, action: 'remove'} + + removed + + addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + for bufferRow in [startBufferRow..endBufferRow] + @addDecorationToBufferRow(bufferRow, decoration) + return + + removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + for bufferRow in [startBufferRow..endBufferRow] + @removeDecorationFromBufferRow(bufferRow, decoration) + return + + decorationMatchesPattern: (decoration, decorationPattern) -> + return false unless decoration? and decorationPattern? + for key, value of decorationPattern + return false if decoration[key] != value + true + + addDecorationForMarker: (marker, decoration) -> + startRow = marker.getStartBufferPosition().row + endRow = marker.getEndBufferPosition().row + @addDecorationToBufferRowRange(startRow, endRow, decoration) + + changedSubscription = @subscribe marker, 'changed', (e) => + oldStartRow = e.oldHeadBufferPosition.row + oldEndRow = e.oldTailBufferPosition.row + newStartRow = e.newHeadBufferPosition.row + newEndRow = e.newTailBufferPosition.row + + # swap so head is always <= than tail + [oldEndRow, oldStartRow] = [oldStartRow, oldEndRow] if oldStartRow > oldEndRow + [newEndRow, newStartRow] = [newStartRow, newEndRow] if newStartRow > newEndRow + + @removeDecorationFromBufferRowRange(oldStartRow, oldEndRow, decoration) + @addDecorationToBufferRowRange(newStartRow, newEndRow, decoration) if e.isValid + + destroyedSubscription = @subscribe marker, 'destroyed', (e) => + @removeDecorationForMarker(marker, decoration) + + @decorationMarkerSubscriptions[marker.id] ?= [] + @decorationMarkerSubscriptions[marker.id].push {decoration, changedSubscription, destroyedSubscription} + + removeDecorationForMarker: (marker, decorationPattern) -> + return unless @decorationMarkerSubscriptions[marker.id]? + + startRow = marker.getStartBufferPosition().row + endRow = marker.getEndBufferPosition().row + @removeDecorationFromBufferRowRange(startRow, endRow, decorationPattern) + + for subscription in _.clone(@decorationMarkerSubscriptions[marker.id]) + if @decorationMatchesPattern(subscription.decoration, decorationPattern) + subscription.changedSubscription.off() + subscription.destroyedSubscription.off() + @decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], subscription) + + return + # Retrieves a {DisplayBufferMarker} based on its id. # # id - A {Number} representing a marker id @@ -959,6 +1052,8 @@ class DisplayBuffer extends Model @emit 'marker-created', @getMarker(marker.id) createFoldForMarker: (marker) -> + bufferMarker = new DisplayBufferMarker({bufferMarker: marker, displayBuffer: this}) + @addDecorationForMarker(bufferMarker, type: 'gutter', class: 'folded') new Fold(this, marker) foldForMarker: (marker) -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index b30a8a853..21459ad96 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -43,12 +43,14 @@ EditorComponent = React.createClass {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getScreenLineCount().toString().length invisibles = if showInvisibles then @state.invisibles else {} + hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() if @isMounted() renderedRowRange = @getRenderedRowRange() [renderedStartRow, renderedEndRow] = renderedRowRange cursorScreenRanges = @getCursorScreenRanges(renderedRowRange) selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange) + decorations = @getGutterDecorations(renderedRowRange) scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() scrollTop = editor.getScrollTop() @@ -67,11 +69,13 @@ EditorComponent = React.createClass className = 'editor-contents editor-colors' className += ' is-focused' if focused + className += ' has-selection' if hasSelection div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, GutterComponent { ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop, - scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow + scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow, + decorations } div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown, @@ -227,6 +231,18 @@ EditorComponent = React.createClass selectionScreenRanges + getGutterDecorations: (renderedRowRange) -> + {editor} = @props + [renderedStartRow, renderedEndRow] = renderedRowRange + + bufferRows = editor.bufferRowsForScreenRows(renderedStartRow, renderedEndRow - 1) + + decorations = {} + for bufferRow in bufferRows + decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter') + decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow) + decorations + observeEditor: -> {editor} = @props @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted @@ -235,6 +251,7 @@ EditorComponent = React.createClass @subscribe editor, 'cursors-moved', @onCursorsMoved @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged @subscribe editor, 'selection-added', @onSelectionAdded + @subscribe editor, 'decoration-changed', @onDecorationChanged @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$height.changes, @requestUpdate @@ -503,6 +520,8 @@ EditorComponent = React.createClass @onStoppedScrollingAfterDelay() onStoppedScrolling: -> + return unless @isMounted() + @scrollingVertically = false @mouseWheelScreenRow = null @requestUpdate() @@ -513,6 +532,11 @@ EditorComponent = React.createClass @cursorsMoved = true @requestUpdate() + onDecorationChanged: -> + @decorationChangedImmediate ?= setImmediate => + @requestUpdate() + @decorationChangedImmediate = null + selectToMousePositionUntilMouseUp: (event) -> {editor} = @props dragging = false diff --git a/src/editor.coffee b/src/editor.coffee index b676130e4..e2e314a4e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -214,6 +214,7 @@ class Editor extends Model @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() @subscribe @displayBuffer, 'tokenized', => @handleTokenization() @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... + @subscribe @displayBuffer, "decoration-changed", (e) => @emit 'decoration-changed', e getViewClass: -> if atom.config.get('core.useReactEditor') @@ -1057,6 +1058,106 @@ class Editor extends Model selection.insertText(fn(text)) selection.setBufferRange(range) + # Public: Get all the decorations for a buffer row. + # + # bufferRow - the {int} buffer row + # decorationType - the {String} decoration type to filter by eg. 'gutter' + # + # Returns an {Array} of decorations in the form `[{type: 'gutter', class: 'someclass'}, ...]` + # Returns an empty array when no decorations are found + decorationsForBufferRow: (bufferRow, decorationType) -> + @displayBuffer.decorationsForBufferRow(bufferRow, decorationType) + + # Public: Get all the decorations for a range of buffer rows (inclusive) + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decorationType - the {String} decoration type to filter by eg. 'gutter' + # + # Returns an {Object} of decorations in the form `{23: [{type: 'gutter', class: 'someclass'}, ...], 24: [...]}` + # Returns an {Object} with keyed with all buffer rows in the range containing empty {Array}s when no decorations are found + decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) -> + @displayBuffer.decorationsForBufferRowRange(startBufferRow, endBufferRow, decorationType) + + # Public: Adds a decoration to a buffer row. For example, use to mark a gutter + # line number with a class by using the form `{type: 'gutter', class: 'linter-error'}` + # + # bufferRow - the {int} buffer row + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing + addDecorationToBufferRow: (bufferRow, decoration) -> + @displayBuffer.addDecorationToBufferRow(bufferRow, decoration) + + # Public: Removes a decoration from a buffer row. + # + # ```coffee + # editor.removeDecorationFromBufferRow(2, {type: 'gutter', class: 'linter-error'}) + # ``` + # + # All decorations matching a pattern will be removed. For example, you might + # have decorations with a namespace like this attached to a row: + # + # ```coffee + # [ + # {type: 'gutter', namespace: 'myns', class: 'something'}, + # {type: 'gutter', namespace: 'myns', class: 'something-else'} + # ] + # ``` + # + # You can remove both with: + # + # ```coffee + # editor.removeDecorationFromBufferRow(2, {namespace: 'myns'}) + # ``` + # + # bufferRow - the {int} buffer row + # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns an {Array} of the removed decorations + removeDecorationFromBufferRow: (bufferRow, decorationPattern) -> + @displayBuffer.removeDecorationFromBufferRow(bufferRow, decorationPattern) + + # Public: Adds a decoration to line numbers in a buffer row range + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing + addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + @displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration) + + # Public: Removes a decoration from line numbers in a buffer row range + # + # startBufferRow - the {int} start of the buffer row range + # endBufferRow - the {int} end of the buffer row range (inclusive) + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing + removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) -> + @displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration) + + # Public: Adds a decoration that tracks a {Marker}. When the marker moves, + # is invalidated, or is destroyed, the decoration will be updated to reflect the marker's state. + # + # marker - the {Marker} you want this decoration to follow + # decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing + addDecorationForMarker: (marker, decoration) -> + @displayBuffer.addDecorationForMarker(marker, decoration) + + # Public: Removes all decorations associated with a {Marker} that match a + # `decorationPattern` and stop tracking the {Marker}. + # + # marker - the {Marker} to detach from + # decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}` + # + # Returns nothing + removeDecorationForMarker: (marker, decorationPattern) -> + @displayBuffer.removeDecorationForMarker(marker, decorationPattern) + # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> @displayBuffer.getMarker(id) @@ -1162,6 +1263,7 @@ class Editor extends Model addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker) @cursors.push(cursor) + @addDecorationForMarker(marker, {class: 'cursor-line'}) @emit 'cursor-added', cursor cursor diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index a4d823934..6dc07a757 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,3 +1,4 @@ +_ = require 'underscore-plus' React = require 'react-atom-fork' {div} = require 'reactionary-atom-fork' {isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' @@ -15,7 +16,7 @@ GutterComponent = React.createClass render: -> {scrollHeight, scrollTop} = @props - div className: 'gutter', + div className: 'gutter', onClick: @onClick, div className: 'line-numbers', ref: 'lineNumbers', style: height: scrollHeight WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)" @@ -24,6 +25,7 @@ GutterComponent = React.createClass @lineNumberNodesById = {} @lineNumberIdsByScreenRow = {} @screenRowsByLineNumberId = {} + @previousDecorations = {} componentDidMount: -> @appendDummyLineNumber() @@ -36,10 +38,12 @@ GutterComponent = React.createClass 'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow' ) - {renderedRowRange, pendingChanges} = newProps + {renderedRowRange, pendingChanges, decorations} = newProps for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start + return true unless _.isEqual(@previousDecorations, decorations) + false componentDidUpdate: (oldProps) -> @@ -70,7 +74,7 @@ GutterComponent = React.createClass @removeLineNumberNodes(lineNumberIdsToPreserve) appendOrUpdateVisibleLineNumberNodes: -> - {editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props + {editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props [startRow, endRow] = renderedRowRange newLineNumberIds = null @@ -91,12 +95,12 @@ GutterComponent = React.createClass visibleLineNumberIds.add(id) if @hasLineNumberNode(id) - @updateLineNumberNode(id, screenRow) + @updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow]) else newLineNumberIds ?= [] newLineNumbersHTML ?= "" newLineNumberIds.push(id) - newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow) + newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow]) @screenRowsByLineNumberId[id] = screenRow @lineNumberIdsByScreenRow[screenRow] = id @@ -110,6 +114,7 @@ GutterComponent = React.createClass @lineNumberNodesById[lineNumberId] = lineNumberNode node.appendChild(lineNumberNode) + @previousDecorations = decorations visibleLineNumberIds removeLineNumberNodes: (lineNumberIdsToPreserve) -> @@ -123,7 +128,7 @@ GutterComponent = React.createClass delete @screenRowsByLineNumberId[lineNumberId] node.removeChild(lineNumberNode) - buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) -> + buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) -> if screenRow? {lineHeightInPixels} = @props style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;" @@ -131,7 +136,13 @@ GutterComponent = React.createClass style = "visibility: hidden;" innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits) - "
#{innerHTML}
" + classes = '' + if decorations? + for decoration in decorations + classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap + classes += "line-number line-number-#{bufferRow}" + + "
#{innerHTML}
" buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) -> if softWrapped @@ -143,11 +154,23 @@ GutterComponent = React.createClass iconHTML = '
' padding + lineNumber + iconHTML - updateLineNumberNode: (lineNumberId, screenRow) -> + updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) -> + node = @lineNumberNodesById[lineNumberId] + previousDecorations = @previousDecorations[bufferRow] + + if previousDecorations? + for decoration in previousDecorations + node.classList.remove(decoration.class) if not contains(decorations, decoration) + + if decorations? + for decoration in decorations + if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap) + node.classList.add(decoration.class) + unless @screenRowsByLineNumberId[lineNumberId] is screenRow {lineHeightInPixels} = @props - @lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeightInPixels + 'px' - @lineNumberNodesById[lineNumberId].dataset.screenRow = screenRow + node.style.top = screenRow * lineHeightInPixels + 'px' + node.dataset.screenRow = screenRow @screenRowsByLineNumberId[lineNumberId] = screenRow @lineNumberIdsByScreenRow[screenRow] = lineNumberId @@ -156,3 +179,22 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] + + onClick: (event) -> + {editor} = @props + {target} = event + lineNumber = target.parentNode + + if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') + bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) + if lineNumber.classList.contains('folded') + editor.unfoldBufferRow(bufferRow) + else + editor.foldBufferRow(bufferRow) + +# Created because underscore uses === not _.isEqual, which we need +contains = (array, target) -> + return false unless array? + for object in array + return true if _.isEqual(object, target) + false diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index ed94c85f1..5aac311d7 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,4 +1,5 @@ {View, $} = require 'space-pen' +Grim = require 'Grim' React = require 'react-atom-fork' EditorComponent = require './editor-component' {defaults} = require 'underscore-plus' @@ -16,6 +17,8 @@ class ReactEditorView extends View getEditor: -> @editor + getModel: -> @editor + Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels() Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth() Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] @@ -40,12 +43,14 @@ class ReactEditorView extends View @gutter = $(node).find('.gutter') @gutter.removeClassFromAllLines = (klass) => + Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::removeDecorationFromBufferRow()` and related functions' @gutter.find('.line-number').removeClass(klass) @gutter.getLineNumberElement = (bufferRow) => @gutter.find("[data-buffer-row='#{bufferRow}']") @gutter.addClassToLine = (bufferRow, klass) => + Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::addDecorationToBufferRow()` and related functions' lines = @gutter.find("[data-buffer-row='#{bufferRow}']") lines.addClass(klass) lines.length > 0 diff --git a/static/editor.less b/static/editor.less index 4898967cc..8773962ee 100644 --- a/static/editor.less +++ b/static/editor.less @@ -69,11 +69,13 @@ .gutter { .line-number { white-space: nowrap; - padding: 0 .5em; + padding-left: .5em; .icon-right { - padding: 0; - padding-left: .1em; + padding: 0 .4em; + &:before { + text-align: center; + } } } } @@ -121,7 +123,7 @@ visibility: hidden; padding-left: .1em; padding-right: .5em; - opacity: .7; + opacity: .6; } .editor .gutter:hover .line-number.foldable .icon-right {