diff --git a/README.md b/README.md index 74caf6502..25c61f643 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,17 @@ You can also download a `.zip` file from the [releases page](https://github.com/ The Windows version does not currently automatically update so you will need to manually upgrade to future releases by re-downloading the `.zip` file. +### Debian Linux (Ubuntu) + +Currently only a 64-bit version is available. + +1. Download `atom-amd64.deb` from the [Atom releases page](https://github.com/atom/atom/releases/latest). +2. Run `sudo dpkg --install atom-amd64.deb` on the downloaded package. +3. Launch Atom using the installed `atom` command. + +The Linux version does not currently automatically update so you will need to +repeat these steps to upgrade to future releases. + ## Building * [Linux](docs/build-instructions/linux.md) diff --git a/apm/package.json b/apm/package.json index 9a1db364f..5c1b859aa 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.93.2" + "atom-package-manager": "0.96.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 188e5f51f..a189b9166 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -229,8 +229,8 @@ module.exports = (grunt) -> ciTasks = ['output-disk-space', 'download-atom-shell', 'build'] ciTasks.push('dump-symbols') if process.platform isnt 'win32' - ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('set-version', 'check-licenses', 'lint') + ciTasks.push('mkdeb') if process.platform is 'linux' ciTasks.push('test') if process.platform isnt 'linux' ciTasks.push('codesign') ciTasks.push('create-installer') if process.platform is 'win32' diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index b1d8a5802..82dedd095 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -58,11 +58,12 @@ getAssets = -> ] when 'linux' buildDir = grunt.config.get('atom.buildDir') - sourcePath = fs.listSync(buildDir, ['.deb'])[0] - if process.arch is 'ia32g' + if process.arch is 'ia32' arch = 'i386' else arch = 'amd64' + {version} = grunt.file.readJSON('package.json') + sourcePath = "#{buildDir}/atom-#{version}-#{arch}.deb" assetName = "atom-#{arch}.deb" {cp} = require('./task-helpers')(grunt) diff --git a/build/tasks/task-helpers.coffee b/build/tasks/task-helpers.coffee index a25f9ea31..a862f291f 100644 --- a/build/tasks/task-helpers.coffee +++ b/build/tasks/task-helpers.coffee @@ -53,8 +53,9 @@ module.exports = (grunt) -> proc = childProcess.spawn(options.cmd, options.args, options.opts) proc.stdout.on 'data', (data) -> stdout.push(data.toString()) proc.stderr.on 'data', (data) -> stderr.push(data.toString()) + proc.on 'error', (processError) -> error ?= processError proc.on 'close', (exitCode, signal) -> - error = new Error(signal) if exitCode != 0 + error ?= new Error(signal) if exitCode != 0 results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode} grunt.log.error results.stderr if exitCode != 0 callback(error, results, exitCode) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 34f977209..21bdeeb63 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -143,6 +143,8 @@ # Sublime Parity 'cmd-enter': 'editor:newline-below' 'cmd-shift-enter': 'editor:newline-above' + 'alt-enter': 'editor:newline' + 'shift-enter': 'editor:newline' 'cmd-]': 'editor:indent-selected-rows' 'cmd-[': 'editor:outdent-selected-rows' 'ctrl-cmd-up': 'editor:move-line-up' diff --git a/package.json b/package.json index 135144923..29a9e8677 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "delegato": "^1", "emissary": "^1.3.1", "event-kit": "0.7.2", - "first-mate": "^2.1.1", + "first-mate": "^2.1.2", "fs-plus": "^2.2.6", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -36,13 +36,13 @@ "grim": "0.12.0", "guid": "0.0.10", "jasmine-tagged": "^1.1.2", - "less-cache": "0.14.0", + "less-cache": "0.15.0", "mixto": "^1", "mkdirp": "0.3.5", "nslog": "^1.0.1", "oniguruma": "^3.0.4", "optimist": "0.4.0", - "pathwatcher": "^2.1.2", + "pathwatcher": "^2.1.3", "property-accessors": "^1", "q": "^1.0.1", "random-words": "0.0.1", @@ -57,7 +57,7 @@ "serializable": "^1", "space-pen": "3.4.7", "temp": "0.7.0", - "text-buffer": "^3.2.4", + "text-buffer": "^3.2.6", "theorist": "^1.0.2", "underscore-plus": "^1.5.1", "vm-compatibility-layer": "0.1.0" @@ -78,7 +78,7 @@ "background-tips": "0.16.0", "bookmarks": "0.28.0", "bracket-matcher": "0.55.0", - "command-palette": "0.24.0", + "command-palette": "0.25.0", "deprecation-cop": "0.10.0", "dev-live-reload": "0.34.0", "exception-reporting": "0.20.0", @@ -87,7 +87,7 @@ "fuzzy-finder": "0.58.0", "git-diff": "0.39.0", "go-to-line": "0.25.0", - "grammar-selector": "0.29.0", + "grammar-selector": "0.34.0", "image-view": "0.36.0", "incompatible-packages": "0.9.0", "keybinding-resolver": "0.20.0", @@ -97,21 +97,21 @@ "open-on-github": "0.30.0", "package-generator": "0.31.0", "release-notes": "0.36.0", - "settings-view": "0.142.0", + "settings-view": "0.145.0", "snippets": "0.52.0", "spell-check": "0.42.0", - "status-bar": "0.44.0", + "status-bar": "0.45.0", "styleguide": "0.30.0", - "symbols-view": "0.63.0", - "tabs": "0.51.0", + "symbols-view": "0.65.0", + "tabs": "0.52.0", "timecop": "0.22.0", - "tree-view": "0.125.0", + "tree-view": "0.126.0", "update-package-dependencies": "0.6.0", "welcome": "0.18.0", "whitespace": "0.25.0", "wrap-guide": "0.22.0", "language-c": "0.28.0", - "language-coffee-script": "0.30.0", + "language-coffee-script": "0.34.0", "language-css": "0.17.0", "language-gfm": "0.50.0", "language-git": "0.9.0", @@ -119,7 +119,7 @@ "language-html": "0.26.0", "language-hyperlink": "0.12.0", "language-java": "0.11.0", - "language-javascript": "0.39.0", + "language-javascript": "0.40.0", "language-json": "0.8.0", "language-less": "0.15.0", "language-make": "0.12.0", @@ -129,16 +129,16 @@ "language-php": "0.16.0", "language-property-list": "0.7.0", "language-python": "0.19.0", - "language-ruby": "0.37.0", + "language-ruby": "0.38.0", "language-ruby-on-rails": "0.18.0", "language-sass": "0.21.0", "language-shellscript": "0.8.0", "language-source": "0.8.0", - "language-sql": "0.10.0", + "language-sql": "0.11.0", "language-text": "0.6.0", "language-todo": "0.11.0", "language-toml": "0.12.0", - "language-xml": "0.20.0", + "language-xml": "0.21.0", "language-yaml": "0.17.0" }, "private": true, diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 7b1ae1618..c71f89faa 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -8,6 +8,23 @@ describe "the `atom` global", -> beforeEach -> atom.workspaceView = new WorkspaceView + describe 'window sizing methods', -> + describe '::getPosition and ::setPosition', -> + it 'sets the position of the window, and can retrieve the position just set', -> + atom.setPosition(22, 45) + expect(atom.getPosition()).toEqual x: 22, y: 45 + + describe '::getSize and ::setSize', -> + originalSize = null + beforeEach -> + originalSize = atom.getSize() + afterEach -> + atom.setSize(originalSize.width, originalSize.height) + + it 'sets the size of the window, and can retrieve the size just set', -> + atom.setSize(100, 400) + expect(atom.getSize()).toEqual width: 100, height: 400 + describe "package lifecycle methods", -> describe ".loadPackage(name)", -> it "continues if the package has an invalid package.json", -> diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee new file mode 100644 index 000000000..8985e075b --- /dev/null +++ b/spec/command-registry-spec.coffee @@ -0,0 +1,126 @@ +CommandRegistry = require '../src/command-registry' + +describe "CommandRegistry", -> + [registry, parent, child, grandchild] = [] + + beforeEach -> + parent = document.createElement("div") + child = document.createElement("div") + grandchild = document.createElement("div") + parent.classList.add('parent') + child.classList.add('child') + grandchild.classList.add('grandchild') + child.appendChild(grandchild) + parent.appendChild(child) + document.querySelector('#jasmine-content').appendChild(parent) + + registry = new CommandRegistry(parent) + + describe "command dispatch", -> + it "invokes callbacks with selectors matching the target", -> + called = false + registry.add '.grandchild', 'command', (event) -> + expect(this).toBe grandchild + expect(event.type).toBe 'command' + expect(event.eventPhase).toBe Event.BUBBLING_PHASE + expect(event.target).toBe grandchild + expect(event.currentTarget).toBe grandchild + called = true + + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(called).toBe true + + it "invokes callbacks with selectors matching ancestors of the target", -> + calls = [] + + registry.add '.child', 'command', (event) -> + expect(this).toBe child + expect(event.target).toBe grandchild + expect(event.currentTarget).toBe child + calls.push('child') + + registry.add '.parent', 'command', (event) -> + expect(this).toBe parent + expect(event.target).toBe grandchild + expect(event.currentTarget).toBe parent + calls.push('parent') + + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child', 'parent'] + + it "orders multiple matching listeners for an element by selector specificity", -> + child.classList.add('foo', 'bar') + calls = [] + + registry.add '.foo.bar', 'command', -> calls.push('.foo.bar') + registry.add '.foo', 'command', -> calls.push('.foo') + registry.add '.bar', 'command', -> calls.push('.bar') # specificity ties favor commands added later, like CSS + + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['.foo.bar', '.bar', '.foo'] + + it "stops bubbling through ancestors when .stopPropagation() is called on the event", -> + calls = [] + + registry.add '.parent', 'command', -> calls.push('parent') + registry.add '.child', 'command', -> calls.push('child-2') + registry.add '.child', 'command', (event) -> calls.push('child-1'); event.stopPropagation() + + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child-1', 'child-2'] + + it "stops invoking callbacks when .stopImmediatePropagation() is called on the event", -> + calls = [] + + registry.add '.parent', 'command', -> calls.push('parent') + registry.add '.child', 'command', -> calls.push('child-2') + registry.add '.child', 'command', (event) -> calls.push('child-1'); event.stopImmediatePropagation() + + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child-1'] + + it "allows listeners to be removed via a disposable returned by ::add", -> + calls = [] + + disposable1 = registry.add '.parent', 'command', -> calls.push('parent') + disposable2 = registry.add '.child', 'command', -> calls.push('child') + + disposable1.dispose() + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child'] + + calls = [] + disposable2.dispose() + grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual [] + + it "allows multiple commands to be registered under one selector when called with an object", -> + calls = [] + + disposable = registry.add '.child', + 'command-1': -> calls.push('command-1') + 'command-2': -> calls.push('command-2') + + grandchild.dispatchEvent(new CustomEvent('command-1', bubbles: true)) + grandchild.dispatchEvent(new CustomEvent('command-2', bubbles: true)) + + expect(calls).toEqual ['command-1', 'command-2'] + + calls = [] + disposable.dispose() + grandchild.dispatchEvent(new CustomEvent('command-1', bubbles: true)) + grandchild.dispatchEvent(new CustomEvent('command-2', bubbles: true)) + expect(calls).toEqual [] + + describe "::findCommands({target})", -> + it "returns commands that can be invoked on the target or its ancestors", -> + registry.add '.parent', 'namespace:command-1', -> + registry.add '.child', 'namespace:command-2', -> + registry.add '.grandchild', 'namespace:command-3', -> + registry.add '.grandchild.no-match', 'namespace:command-4', -> + + expect(registry.findCommands(target: grandchild)[0..2]).toEqual [ + {name: 'namespace:command-3', displayName: 'Namespace: Command 3'} + {name: 'namespace:command-2', displayName: 'Namespace: Command 2'} + {name: 'namespace:command-1', displayName: 'Namespace: Command 1'} + ] diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 9f00eff3a..9890504d3 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1011,7 +1011,7 @@ describe "DisplayBuffer", -> buffer.getMarker(marker2.id).destroy() expect(destroyedHandler).toHaveBeenCalled() - describe "DisplayBufferMarker::copy(attributes)", -> + describe "Marker::copy(attributes)", -> it "creates a copy of the marker with the given attributes merged in", -> initialMarkerCount = displayBuffer.getMarkerCount() marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]], a: 1, b: 2) @@ -1020,10 +1020,10 @@ describe "DisplayBuffer", -> marker2 = marker1.copy(b: 3) expect(marker2.getBufferRange()).toEqual marker1.getBufferRange() expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2 - expect(marker1.getAttributes()).toEqual a: 1, b: 2 - expect(marker2.getAttributes()).toEqual a: 1, b: 3 + expect(marker1.getProperties()).toEqual a: 1, b: 2 + expect(marker2.getProperties()).toEqual a: 1, b: 3 - describe "DisplayBufferMarker::getPixelRange()", -> + describe "Marker::getPixelRange()", -> it "returns the start and end positions of the marker based on the line height and character widths assigned to the DisplayBuffer", -> marker = displayBuffer.markScreenRange([[5, 10], [6, 4]]) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 86f70e9d1..b2864cafe 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -197,6 +197,50 @@ describe "EditorComponent", -> nextAnimationFrame() expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' + it "applies .leading-whitespace for lines with leading spaces and/or tabs", -> + editor.setText(' a') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false + + editor.setText('\ta') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false + + it "applies .trailing-whitespace for lines with trailing spaces and/or tabs", -> + editor.setText(' ') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + + editor.setText('\t') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + + editor.setText('a ') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + + editor.setText('a\t') + nextAnimationFrame() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + describe "when showInvisibles is enabled", -> invisibles = null @@ -2195,7 +2239,7 @@ describe "EditorComponent", -> editor.setSoftWrapped(true) callingOrder = [] - editor.onDidChangeScreenLines -> callingOrder.push 'screen-lines-changed' + editor.onDidChange -> callingOrder.push 'screen-lines-changed' wrapperView.on 'editor:display-updated', -> callingOrder.push 'editor:display-updated' editor.insertText("HELLO! HELLO!\n HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! ") nextAnimationFrame() diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 6ff0aa304..f196bec78 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -2330,6 +2330,16 @@ describe "Editor", -> expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ expect(editor.getCursorBufferPosition()).toEqual [5, 3] + describe "when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1", -> + it "inserts one tab", -> + editor.setSoftTabs(false) + buffer.setText(" \ntest") + editor.setCursorBufferPosition [1, 0] + + editor.indent(autoIndent: true) + expect(buffer.lineForRow(1)).toBe '\ttest' + expect(editor.getCursorBufferPosition()).toEqual [1, 1] + describe "when the line's indent level is greater than the suggested level of indentation", -> describe "when 'softTabs' is true (the default)", -> it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> @@ -3229,6 +3239,13 @@ describe "Editor", -> editor.destroy() expect(buffer.getMarkerCount()).toBe 0 + it "notifies ::onDidDestroy observers when the editor is destroyed", -> + destroyObserverCalled = false + editor.onDidDestroy -> destroyObserverCalled = true + + editor.destroy() + expect(destroyObserverCalled).toBe true + describe ".joinLines()", -> describe "when no text is selected", -> describe "when the line below isn't empty", -> @@ -3343,7 +3360,7 @@ describe "Editor", -> editor2.destroy() expect(editor.shouldPromptToSave()).toBeTruthy() - describe "when the edit session contains surrogate pair characters", -> + describe "when the editor contains surrogate pair characters", -> it "correctly backspaces over them", -> editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') editor.moveToBottom() @@ -3384,6 +3401,47 @@ describe "Editor", -> editor.moveLeft() expect(editor.getCursorBufferPosition()).toEqual [0, 0] + describe "when the editor contains variation sequence character pairs", -> + it "correctly backspaces over them", -> + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' + editor.backspace() + expect(editor.getText()).toBe '\u2714\uFE0E' + editor.backspace() + expect(editor.getText()).toBe '' + + it "correctly deletes over them", -> + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' + editor.delete() + expect(editor.getText()).toBe '\u2714\uFE0E' + editor.delete() + expect(editor.getText()).toBe '' + + it "correctly moves over them", -> + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 4] + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual [0, 6] + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 6] + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 4] + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 2] + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + describe ".setIndentationForBufferRow", -> describe "when the editor uses soft tabs but the row has hard tabs", -> it "only replaces whitespace characters", -> diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee index 3de6d065f..d0089316b 100644 --- a/spec/jasmine-helper.coffee +++ b/spec/jasmine-helper.coffee @@ -7,6 +7,8 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> {TerminalReporter} = require 'jasmine-tagged' + disableFocusMethods() if process.env.JANKY_SHA1 + TimeReporter = require './time-reporter' timeReporter = new TimeReporter() @@ -38,3 +40,10 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> $('body').append $$ -> @div id: 'jasmine-content' jasmineEnv.execute() + +disableFocusMethods = -> + ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach (methodName) -> + focusMethod = window[methodName] + window[methodName] = (description) -> + error = new Error('Focused spec is running on CI') + focusMethod description, -> throw error diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index fd3a4a478..ea92df2fe 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -70,6 +70,30 @@ describe "LanguageMode", -> expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7] + describe ".rowRangeForCommentAtBufferRow(bufferRow)", -> + it "returns the start/end rows of the foldable comment starting at the given row", -> + buffer.setText("//this is a multi line comment\n//another line") + expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1] + expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1] + + buffer.setText("//this is a multi line comment\n//another line\n//and one more") + expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2] + expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2] + + buffer.setText("//this is a multi line comment\n\n//with an empty line") + expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() + expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() + expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined() + + buffer.setText("//this is a single line comment\n") + console.log buffer.getLastRow() + expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() + expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() + + buffer.setText("//this is a single line comment") + console.log languageMode.isLineCommentedAtBufferRow(0) + expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() + describe "suggestedIndentForBufferRow", -> it "returns the suggested indentation based on auto-indent/outdent rules", -> expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 530367518..48fad5c8c 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -64,6 +64,7 @@ beforeEach -> atom.project = new Project(path: projectPath) atom.workspace = new Workspace() atom.keymaps.keyBindings = _.clone(keyBindingsToRestore) + atom.commands.setRootNode(document.body) window.resetTimeouts() atom.packages.packageStates = {} @@ -120,6 +121,8 @@ beforeEach -> addCustomMatchers(this) afterEach -> + atom.commands.clear() + atom.packages.deactivatePackages() atom.menu.template = [] diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee index 36ac0b356..89cf34aca 100644 --- a/spec/text-utils-spec.coffee +++ b/spec/text-utils-spec.coffee @@ -1,30 +1,32 @@ textUtils = require '../src/text-utils' describe 'text utilities', -> - describe '.getCharacterCount(string)', -> - it 'returns the number of full characters in the string', -> - expect(textUtils.getCharacterCount('abc')).toBe 3 - expect(textUtils.getCharacterCount('a\uD835\uDF97b\uD835\uDF97c')).toBe 5 - expect(textUtils.getCharacterCount('\uD835\uDF97')).toBe 1 - expect(textUtils.getCharacterCount('\uD835')).toBe 1 - expect(textUtils.getCharacterCount('\uDF97')).toBe 1 + describe '.hasPairedCharacter(string)', -> + it 'returns true when the string contains a surrogate pair or variation sequence', -> + expect(textUtils.hasPairedCharacter('abc')).toBe false + expect(textUtils.hasPairedCharacter('a\uD835\uDF97b\uD835\uDF97c')).toBe true + expect(textUtils.hasPairedCharacter('\uD835\uDF97')).toBe true + expect(textUtils.hasPairedCharacter('\u2714\uFE0E')).toBe true + expect(textUtils.hasPairedCharacter('\uD835')).toBe false + expect(textUtils.hasPairedCharacter('\uDF97')).toBe false + expect(textUtils.hasPairedCharacter('\uFE0E')).toBe false + expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe false - describe '.hasSurrogatePair(string)', -> - it 'returns true when the string contains a surrogate pair', -> - expect(textUtils.hasSurrogatePair('abc')).toBe false - expect(textUtils.hasSurrogatePair('a\uD835\uDF97b\uD835\uDF97c')).toBe true - expect(textUtils.hasSurrogatePair('\uD835\uDF97')).toBe true - expect(textUtils.hasSurrogatePair('\uD835')).toBe false - expect(textUtils.hasSurrogatePair('\uDF97')).toBe false - - describe '.isSurrogatePair(string, index)', -> - it 'returns true when the index is the start of a high/low surrogate pair', -> - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe false - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe true - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe false - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe false - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe true - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe false - expect(textUtils.isSurrogatePair('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe false - expect(textUtils.isSurrogatePair('\uD835')).toBe false - expect(textUtils.isSurrogatePair('\uDF97')).toBe false + describe '.isPairedCharacter(string, index)', -> + it 'returns true when the index is the start of a high/low surrogate pair or variation sequence', -> + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe false + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe true + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe false + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe false + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe true + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe false + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe false + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 0)).toBe false + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 1)).toBe true + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 2)).toBe false + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 3)).toBe false + expect(textUtils.isPairedCharacter('\uD835')).toBe false + expect(textUtils.isPairedCharacter('\uDF97')).toBe false + expect(textUtils.isPairedCharacter('\uFE0E')).toBe false + expect(textUtils.isPairedCharacter('\uFE0E')).toBe false + expect(textUtils.isPairedCharacter('\uFE0E\uFE0E')).toBe false diff --git a/src/atom.coffee b/src/atom.coffee index 9d0faab5d..40b52b9d6 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -7,21 +7,22 @@ screen = require 'screen' shell = require 'shell' _ = require 'underscore-plus' -{deprecated} = require 'grim' +{deprecate} = require 'grim' +{Emitter} = require 'event-kit' {Model} = require 'theorist' fs = require 'fs-plus' {$} = require './space-pen-extensions' WindowEventHandler = require './window-event-handler' -# Public: Atom global for dealing with packages, themes, menus, and the window. +# Essential: Atom global for dealing with packages, themes, menus, and the window. # # An instance of this class is always available as the `atom` global. module.exports = class Atom extends Model @version: 1 # Increment this when the serialization format changes - # Public: Load or create the Atom environment in the given mode. + # Load or create the Atom environment in the given mode. # # * `mode` A {String} mode that is either 'editor' or 'spec' depending on the # kind of environment you want to build. @@ -98,6 +99,10 @@ class Atom extends Model workspaceViewParentSelector: 'body' lastUncaughtError: null + ### + Section: Properties + ### + # Public: A {Clipboard} instance clipboard: null @@ -113,6 +118,9 @@ class Atom extends Model # Public: A {KeymapManager} instance keymaps: null + # Public: A {CommandRegistry} instance + commands: null + # Public: A {MenuManager} instance menu: null @@ -134,13 +142,18 @@ class Atom extends Model # Public: A {WorkspaceView} instance workspaceView: null + ### + Section: Construction and Destruction + ### + # Call .loadOrCreate instead constructor: (@state) -> + @emitter = new Emitter {@mode} = @state DeserializerManager = require './deserializer-manager' @deserializers = new DeserializerManager() - # Public: Sets up the basic services that should be available in all modes + # Sets up the basic services that should be available in all modes # (both spec and application). # # Call after this instance has been assigned to the `atom` global. @@ -158,6 +171,7 @@ class Atom extends Model Config = require './config' KeymapManager = require './keymap-extensions' + CommandRegistry = require './command-registry' PackageManager = require './package-manager' Clipboard = require './clipboard' Syntax = require './syntax' @@ -179,6 +193,7 @@ class Atom extends Model @config = new Config({configDirPath, resourcePath}) @keymaps = new KeymapManager({configDirPath, resourcePath}) @keymap = @keymaps # Deprecated + @commands = new CommandRegistry @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode}) @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode}) @contextMenu = new ContextMenuManager({resourcePath, devMode}) @@ -198,22 +213,172 @@ class Atom extends Model @windowEventHandler = new WindowEventHandler - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClass: -> - deprecated("Callers should be converted to use atom.deserializers") + ### + Section: Event Subscription + ### - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClasses: -> - deprecated("Callers should be converted to use atom.deserializers") + # Extended: Invoke the given callback whenever {::beep} is called. + # + # * `callback` {Function} to be called whenever {::beep} is called. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidBeep: (callback) -> + @emitter.on 'did-beep', callback - setBodyPlatformClass: -> - document.body.classList.add("platform-#{process.platform}") + ### + Section: Atom Details + ### - # Public: Get the current window + # Public: Is the current window in development mode? + inDevMode: -> + @getLoadSettings().devMode + + # Public: Is the current window running specs? + inSpecMode: -> + @getLoadSettings().isSpec + + # Public: Get the version of the Atom application. + # + # Returns the version text {String}. + getVersion: -> + @appVersion ?= @getLoadSettings().appVersion + + # Public: Determine whether the current version is an official release. + isReleasedVersion: -> + not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix + + # Public: Get the directory path to Atom's configuration area. + # + # Returns the absolute path to `~/.atom`. + getConfigDirPath: -> + @constructor.getConfigDirPath() + + # Public: Get the time taken to completely load the current window. + # + # This time include things like loading and activating packages, creating + # DOM elements for the editor, and reading the config. + # + # Returns the {Number} of milliseconds taken to load the window or null + # if the window hasn't finished loading yet. + getWindowLoadTime: -> + @loadTime + + # Public: Get the load settings for the current window. + # + # Returns an {Object} containing all the load setting key/value pairs. + getLoadSettings: -> + @constructor.getLoadSettings() + + ### + Section: Managing The Atom Window + ### + + # Essential: Open a new Atom window using the given options. + # + # Calling this method without an options parameter will open a prompt to pick + # a file/folder to open in the new window. + # + # * `options` An {Object} with the following keys: + # * `pathsToOpen` An {Array} of {String} paths to open. + # * `newWindow` A {Boolean}, true to always open a new window instead of + # reusing existing windows depending on the paths to open. + # * `devMode` A {Boolean}, true to open the window in development mode. + # Development mode loads the Atom source from the locally cloned + # repository and also loads all the packages in ~/.atom/dev/packages + # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe + # mode prevents all packages installed to ~/.atom/packages from loading. + open: (options) -> + ipc.send('open', options) + + # Essential: Close the current window. + close: -> + @getCurrentWindow().close() + + # Essential: Get the size of current window. + # + # Returns an {Object} in the format `{width: 1000, height: 700}` + getSize: -> + [width, height] = @getCurrentWindow().getSize() + {width, height} + + # Essential: Set the size of current window. + # + # * `width` The {Number} of pixels. + # * `height` The {Number} of pixels. + setSize: (width, height) -> + @getCurrentWindow().setSize(width, height) + + # Essential: Get the position of current window. + # + # Returns an {Object} in the format `{x: 10, y: 20}` + getPosition: -> + [x, y] = @getCurrentWindow().getPosition() + {x, y} + + # Essential: Set the position of current window. + # + # * `x` The {Number} of pixels. + # * `y` The {Number} of pixels. + setPosition: (x, y) -> + ipc.send('call-window-method', 'setPosition', x, y) + + # Extended: Get the current window getCurrentWindow: -> @constructor.getCurrentWindow() - # Public: Get the dimensions of this window. + # Extended: Move current window to the center of the screen. + center: -> + ipc.send('call-window-method', 'center') + + # Extended: Focus the current window. + focus: -> + ipc.send('call-window-method', 'focus') + $(window).focus() + + # Extended: Show the current window. + show: -> + ipc.send('call-window-method', 'show') + + # Extended: Hide the current window. + hide: -> + ipc.send('call-window-method', 'hide') + + # Extended: Reload the current window. + reload: -> + ipc.send('call-window-method', 'restart') + + # Extended: Returns a {Boolean} true when the current window is maximized. + isMaximixed: -> + @getCurrentWindow().isMaximized() + + maximize: -> + ipc.send('call-window-method', 'maximize') + + # Extended: Is the current window in full screen mode? + isFullScreen: -> + @getCurrentWindow().isFullScreen() + + # Extended: Set the full screen state of the current window. + setFullScreen: (fullScreen=false) -> + ipc.send('call-window-method', 'setFullScreen', fullScreen) + if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen") + + # Extended: Toggle the full screen state of the current window. + toggleFullScreen: -> + @setFullScreen(!@isFullScreen()) + + # Schedule the window to be shown and focused on the next tick. + # + # This is done in a next tick to prevent a white flicker from occurring + # if called synchronously. + displayWindow: ({maximize}={}) -> + setImmediate => + @show() + @focus() + @setFullScreen(true) if @workspace.fullScreen + @maximize() if maximize + + # Get the dimensions of this window. # # Returns an {Object} with the following keys: # * `x` The window's x-position {Number}. @@ -227,7 +392,7 @@ class Atom extends Model maximized = browserWindow.isMaximized() {x, y, width, height, maximized} - # Public: Set the dimensions of the window. + # Set the dimensions of the window. # # The window will be centered if either the x or y coordinate is not set # in the dimensions parameter. If x or y are omitted the window will be @@ -284,41 +449,6 @@ class Atom extends Model dimensions = @getWindowDimensions() @state.windowDimensions = dimensions if @isValidDimensions(dimensions) - # Public: Get the load settings for the current window. - # - # Returns an {Object} containing all the load setting key/value pairs. - getLoadSettings: -> - @constructor.getLoadSettings() - - deserializeProject: -> - Project = require './project' - - startTime = Date.now() - @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath) - @deserializeTimings.project = Date.now() - startTime - - deserializeWorkspaceView: -> - Workspace = require './workspace' - WorkspaceView = require './workspace-view' - - startTime = Date.now() - @workspace = Workspace.deserialize(@state.workspace) ? new Workspace - @workspaceView = new WorkspaceView(@workspace) - @deserializeTimings.workspace = Date.now() - startTime - - @keymaps.defaultTarget = @workspaceView[0] - $(@workspaceViewParentSelector).append(@workspaceView) - - deserializePackageStates: -> - @packages.packageStates = @state.packageStates ? {} - delete @state.packageStates - - deserializeEditorWindow: -> - @deserializeTimings = {} - @deserializePackageStates() - @deserializeProject() - @deserializeWorkspaceView() - # Call this method when establishing a real application window. startEditorWindow: -> {resourcePath, safeMode} = @getLoadSettings() @@ -369,41 +499,17 @@ class Atom extends Model @windowEventHandler?.unsubscribe() - loadThemes: -> - @themes.load() + ### + Section: Messaging the User + ### - watchThemes: -> - @themes.onDidReloadAll => - # Only reload stylesheets from non-theme packages - for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' - pack.reloadStylesheets?() - null + # Essential: Visually and audibly trigger a beep. + beep: -> + shell.beep() if @config.get('core.audioBeep') + @workspaceView.trigger 'beep' + @emitter.emit 'did-beep' - # Notify the browser project of the window's current project path - watchProjectPath: -> - onProjectPathChanged = => - ipc.send('window-command', 'project-path-changed', @project.getPath()) - @subscribe @project, 'path-changed', onProjectPathChanged - onProjectPathChanged() - - # Public: Open a new Atom window using the given options. - # - # Calling this method without an options parameter will open a prompt to pick - # a file/folder to open in the new window. - # - # * `options` An {Object} with the following keys: - # * `pathsToOpen` An {Array} of {String} paths to open. - # * `newWindow` A {Boolean}, true to always open a new window instead of - # reusing existing windows depending on the paths to open. - # * `devMode` A {Boolean}, true to open the window in development mode. - # Development mode loads the Atom source from the locally cloned - # repository and also loads all the packages in ~/.atom/dev/packages - # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe - # mode prevents all packages installed to ~/.atom/packages from loading. - open: (options) -> - ipc.send('open', options) - - # Public: Open a confirm dialog. + # Essential: A flexible way to open a dialog akin to an alert dialog. # # ## Examples # @@ -413,13 +519,13 @@ class Atom extends Model # detailedMessage: 'Be honest.' # buttons: # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') + # Bad: -> window.alert('bummer') # ``` # # * `options` An {Object} with the following keys: # * `message` The {String} message to display. - # * `detailedMessage` The {String} detailed message to display. - # * `buttons` Either an array of strings or an object where keys are + # * `detailedMessage` (optional) The {String} detailed message to display. + # * `buttons` (optional) Either an array of strings or an object where keys are # button names and the values are callbacks to invoke when clicked. # # Returns the chosen button index {Number} if the buttons option was an array. @@ -443,77 +549,71 @@ class Atom extends Model callback = buttons[buttonLabels[chosen]] callback?() - showSaveDialog: (callback) -> - callback(showSaveDialogSync()) + ### + Section: Managing the Dev Tools + ### - showSaveDialogSync: (defaultPath) -> - defaultPath ?= @project?.getPath() - currentWindow = @getCurrentWindow() - dialog = remote.require('dialog') - dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} - - # Public: Open the dev tools for the current window. + # Extended: Open the dev tools for the current window. openDevTools: -> ipc.send('call-window-method', 'openDevTools') - # Public: Toggle the visibility of the dev tools for the current window. + # Extended: Toggle the visibility of the dev tools for the current window. toggleDevTools: -> ipc.send('call-window-method', 'toggleDevTools') - # Public: Execute code in dev tools. + # Extended: Execute code in dev tools. executeJavaScriptInDevTools: (code) -> ipc.send('call-window-method', 'executeJavaScriptInDevTools', code) - # Public: Reload the current window. - reload: -> - ipc.send('call-window-method', 'restart') + ### + Section: Private + ### - # Public: Focus the current window. - focus: -> - ipc.send('call-window-method', 'focus') - $(window).focus() + deserializeProject: -> + Project = require './project' - # Public: Show the current window. - show: -> - ipc.send('call-window-method', 'show') + startTime = Date.now() + @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath) + @deserializeTimings.project = Date.now() - startTime - # Public: Hide the current window. - hide: -> - ipc.send('call-window-method', 'hide') + deserializeWorkspaceView: -> + Workspace = require './workspace' + WorkspaceView = require './workspace-view' - # Public: Set the size of current window. - # - # * `width` The {Number} of pixels. - # * `height` The {Number} of pixels. - setSize: (width, height) -> - @getCurrentWindow().setSize(width, height) + startTime = Date.now() + @workspace = Workspace.deserialize(@state.workspace) ? new Workspace + @workspaceView = new WorkspaceView(@workspace) + @deserializeTimings.workspace = Date.now() - startTime - # Public: Set the position of current window. - # - # * `x` The {Number} of pixels. - # * `y` The {Number} of pixels. - setPosition: (x, y) -> - ipc.send('call-window-method', 'setPosition', x, y) + @keymaps.defaultTarget = @workspaceView[0] + $(@workspaceViewParentSelector).append(@workspaceView) - # Public: Move current window to the center of the screen. - center: -> - ipc.send('call-window-method', 'center') + deserializePackageStates: -> + @packages.packageStates = @state.packageStates ? {} + delete @state.packageStates + deserializeEditorWindow: -> + @deserializeTimings = {} + @deserializePackageStates() + @deserializeProject() + @deserializeWorkspaceView() - # Schedule the window to be shown and focused on the next tick. - # - # This is done in a next tick to prevent a white flicker from occurring - # if called synchronously. - displayWindow: ({maximize}={}) -> - setImmediate => - @show() - @focus() - @setFullScreen(true) if @workspace.fullScreen - @maximize() if maximize + loadThemes: -> + @themes.load() - # Public: Close the current window. - close: -> - @getCurrentWindow().close() + watchThemes: -> + @themes.onDidReloadAll => + # Only reload stylesheets from non-theme packages + for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' + pack.reloadStylesheets?() + null + + # Notify the browser project of the window's current project path + watchProjectPath: -> + onProjectPathChanged = => + ipc.send('window-command', 'project-path-changed', @project.getPath()) + @subscribe @project, 'path-changed', onProjectPathChanged + onProjectPathChanged() exit: (status) -> app = remote.require('app') @@ -526,45 +626,14 @@ class Atom extends Model setRepresentedFilename: (filename) -> ipc.send('call-window-method', 'setRepresentedFilename', filename) - # Public: Is the current window in development mode? - inDevMode: -> - @getLoadSettings().devMode + showSaveDialog: (callback) -> + callback(showSaveDialogSync()) - # Public: Is the current window running specs? - inSpecMode: -> - @getLoadSettings().isSpec - - # Public: Toggle the full screen state of the current window. - toggleFullScreen: -> - @setFullScreen(!@isFullScreen()) - - # Public: Set the full screen state of the current window. - setFullScreen: (fullScreen=false) -> - ipc.send('call-window-method', 'setFullScreen', fullScreen) - if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen") - - # Public: Is the current window in full screen mode? - isFullScreen: -> - @getCurrentWindow().isFullScreen() - - maximize: -> - ipc.send('call-window-method', 'maximize') - - # Public: Get the version of the Atom application. - # - # Returns the version text {String}. - getVersion: -> - @appVersion ?= @getLoadSettings().appVersion - - # Public: Determine whether the current version is an official release. - isReleasedVersion: -> - not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix - - # Public: Get the directory path to Atom's configuration area. - # - # Returns the absolute path to `~/.atom`. - getConfigDirPath: -> - @constructor.getConfigDirPath() + showSaveDialogSync: (defaultPath) -> + defaultPath ?= @project?.getPath() + currentWindow = @getCurrentWindow() + dialog = remote.require('dialog') + dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} saveSync: -> stateString = JSON.stringify(@state) @@ -573,27 +642,12 @@ class Atom extends Model else @getCurrentWindow().loadSettings.windowState = stateString - # Public: Get the time taken to completely load the current window. - # - # This time include things like loading and activating packages, creating - # DOM elements for the editor, and reading the config. - # - # Returns the {Number} of milliseconds taken to load the window or null - # if the window hasn't finished loading yet. - getWindowLoadTime: -> - @loadTime - crashMainProcess: -> remote.process.crash() crashRenderProcess: -> process.crash() - # Public: Visually and audibly trigger a beep. - beep: -> - shell.beep() if @config.get('core.audioBeep') - @workspaceView.trigger 'beep' - getUserInitScriptPath: -> initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') @@ -605,7 +659,7 @@ class Atom extends Model catch error console.error "Failed to load `#{userInitScriptPath}`", error.stack, error - # Public: Require the module with the given globals. + # Require the module with the given globals. # # The globals will be set on the `window` object and removed after the # require completes. @@ -625,3 +679,14 @@ class Atom extends Model delete window[key] else window[key] = value + + # Deprecated: Callers should be converted to use atom.deserializers + registerRepresentationClass: -> + deprecate("Callers should be converted to use atom.deserializers") + + # Deprecated: Callers should be converted to use atom.deserializers + registerRepresentationClasses: -> + deprecate("Callers should be converted to use atom.deserializers") + + setBodyPlatformClass: -> + document.body.classList.add("platform-#{process.platform}") diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index 808bc27a4..e9a3fbee4 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -1,7 +1,7 @@ BufferedProcess = require './buffered-process' path = require 'path' -# Public: Like {BufferedProcess}, but accepts a Node script as the command +# Extended: Like {BufferedProcess}, but accepts a Node script as the command # to run. # # This is necessary on Windows since it doesn't support shebang `#!` lines. diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index ed6474f3b..211efdaf7 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -1,7 +1,7 @@ _ = require 'underscore-plus' ChildProcess = require 'child_process' -# Public: A wrapper which provides standard error/output line buffering for +# Extended: A wrapper which provides standard error/output line buffering for # Node's ChildProcess. # # ## Examples @@ -44,13 +44,14 @@ class BufferedProcess if process.platform is "win32" # Quote all arguments and escapes inner quotes if args? - cmdArgs = args.map (arg) -> + cmdArgs = args.filter (arg) -> arg? + cmdArgs = cmdArgs.map (arg) -> if command in ['explorer.exe', 'explorer'] and /^\/[a-zA-Z]+,.*$/.test(arg) # Don't wrap /root,C:\folder style arguments to explorer calls in # quotes since they will not be interpreted correctly if they are arg else - "\"#{arg.replace(/"/g, '\\"')}\"" + "\"#{arg.toString().replace(/"/g, '\\"')}\"" else cmdArgs = [] if /\s/.test(command) diff --git a/src/clipboard.coffee b/src/clipboard.coffee index 48bfb8dd5..3fba6f4e8 100644 --- a/src/clipboard.coffee +++ b/src/clipboard.coffee @@ -1,7 +1,7 @@ clipboard = require 'clipboard' crypto = require 'crypto' -# Public: Represents the clipboard used for copying and pasting in Atom. +# Extended: Represents the clipboard used for copying and pasting in Atom. # # An instance of this class is always available as the `atom.clipboard` global. # diff --git a/src/command-registry.coffee b/src/command-registry.coffee new file mode 100644 index 000000000..23a54fea2 --- /dev/null +++ b/src/command-registry.coffee @@ -0,0 +1,176 @@ +{Disposable, CompositeDisposable} = require 'event-kit' +{specificity} = require 'clear-cut' +_ = require 'underscore-plus' +{$} = require './space-pen-extensions' + +SequenceCount = 0 +SpecificityCache = {} + +module.exports = + +# Experimental: Associates listener functions with commands in a +# context-sensitive way using CSS selectors. You can access a global instance of +# this class via `atom.commands`, and commands registered there will be +# presented in the command palette. +# +# The global command registry facilitates a style of event handling known as +# *event delegation* that was popularized by jQuery. Atom commands are expressed +# as custom DOM events that can be invoked on the currently focused element via +# a key binding or manually via the command palette. Rather than binding +# listeners for command events directly to DOM nodes, you instead register +# command event listeners globally on `atom.commands` and constrain them to +# specific kinds of elements with CSS selectors. +# +# As the event bubbles upward through the DOM, all registered event listeners +# with matching selectors are invoked in order of specificity. In the event of a +# specificity tie, the most recently registered listener is invoked first. This +# mirrors the "cascade" semantics of CSS. Event listeners are invoked in the +# context of the current DOM node, meaning `this` always points at +# `event.currentTarget`. As is normally the case with DOM events, +# `stopPropagation` and `stopImmediatePropagation` can be used to terminate the +# bubbling process and prevent invocation of additional listeners. +# +# ## Example +# +# Here is a command that inserts the current date in an editor: +# +# ```coffee +# atom.commands.add '.editor', +# 'user:insert-date': (event) -> +# editor = $(this).view().getModel() +# # soon the above above line will be: +# # editor = @getModel() +# editor.insertText(new Date().toLocaleString()) +# ``` +class CommandRegistry + constructor: (@rootNode) -> + @listenersByCommandName = {} + + setRootNode: (newRootNode) -> + oldRootNode = @rootNode + @rootNode = newRootNode + + for commandName of @listenersByCommandName + oldRootNode?.removeEventListener(commandName, @dispatchCommand, true) + newRootNode?.addEventListener(commandName, @dispatchCommand, true) + + # Public: Add one or more command listeners associated with a selector. + # + # ## Arguments: Registering One Command + # + # * `selector` A {String} containing a CSS selector matching elements on which + # you want to handle the commands. The `,` combinator is not currently + # supported. + # * `commandName` A {String} containing the name of a command you want to + # handle such as `user:insert-date`. + # * `callback` A {Function} to call when the given command is invoked on an + # element matching the selector. It will be called with `this` referencing + # the matching DOM node. + # * `event` A standard DOM event instance. Call `stopPropagation` or + # `stopImmediatePropagation` to terminate bubbling early. + # + # ## Arguments: Registering Multiple Commands + # + # * `selector` A {String} containing a CSS selector matching elements on which + # you want to handle the commands. The `,` combinator is not currently + # supported. + # * `commands` An {Object} mapping command names like `user:insert-date` to + # listener {Function}s. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # added command handler(s). + add: (selector, commandName, callback) -> + if typeof commandName is 'object' + commands = commandName + disposable = new CompositeDisposable + for commandName, callback of commands + disposable.add @add(selector, commandName, callback) + return disposable + + unless @listenersByCommandName[commandName]? + @rootNode?.addEventListener(commandName, @dispatchCommand, true) + @listenersByCommandName[commandName] = [] + + listener = new CommandListener(selector, callback) + listenersForCommand = @listenersByCommandName[commandName] + listenersForCommand.push(listener) + + new Disposable => + listenersForCommand.splice(listenersForCommand.indexOf(listener), 1) + if listenersForCommand.length is 0 + delete @listenersByCommandName[commandName] + @rootNode.removeEventListener(commandName, @dispatchCommand, true) + + dispatchCommand: (event) => + propagationStopped = false + immediatePropagationStopped = false + currentTarget = event.target + + syntheticEvent = Object.create event, + eventPhase: value: Event.BUBBLING_PHASE + currentTarget: get: -> currentTarget + stopPropagation: value: -> + propagationStopped = true + stopImmediatePropagation: value: -> + propagationStopped = true + immediatePropagationStopped = true + + loop + matchingListeners = + @listenersByCommandName[event.type] + .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) + .sort (a, b) -> a.compare(b) + + for listener in matchingListeners + break if immediatePropagationStopped + listener.callback.call(currentTarget, syntheticEvent) + + break if propagationStopped + break if currentTarget is @rootNode + currentTarget = currentTarget.parentNode + + # Public: Find all registered commands matching a query. + # + # * `params` An {Object} containing one or more of the following keys: + # * `target` A DOM node that is the hypothetical target of a given command. + # + # Returns an {Array} of {Object}s containing the following keys: + # * `name` The name of the command. For example, `user:insert-date`. + # * `displayName` The display name of the command. For example, + # `User: Insert Date`. + # * `jQuery` Present if the command was registered with the legacy + # `$::command` method. + findCommands: ({target}) -> + commands = [] + target = @rootNode unless @rootNode.contains(target) + currentTarget = target + loop + for commandName, listeners of @listenersByCommandName + for listener in listeners + if currentTarget.webkitMatchesSelector(listener.selector) + commands.push + name: commandName + displayName: _.humanizeEventName(commandName) + + break if currentTarget is @rootNode + currentTarget = currentTarget.parentNode + + for name, displayName of $(target).events() when displayName + commands.push({name, displayName, jQuery: true}) + + for name, displayName of $(window).events() when displayName + commands.push({name, displayName, jQuery: true}) + + commands + + clear: -> + @listenersByCommandName = {} + +class CommandListener + constructor: (@selector, @callback) -> + @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) + @sequenceNumber = SequenceCount++ + + compare: (other) -> + other.specificity - @specificity or + other.sequenceNumber - @sequenceNumber diff --git a/src/config.coffee b/src/config.coffee index f3606d7bb..92b11f891 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -6,7 +6,7 @@ path = require 'path' async = require 'async' pathWatcher = require 'pathwatcher' -# Public: Used to access all of Atom's configuration details. +# Essential: Used to access all of Atom's configuration details. # # An instance of this class is always available as the `atom.config` global. # diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 5aa8392a6..a55daabf0 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -5,7 +5,7 @@ path = require 'path' CSON = require 'season' fs = require 'fs-plus' -# Public: Provides a registry for commands that you'd like to appear in the +# Extended: Provides a registry for commands that you'd like to appear in the # context menu. # # An instance of this class is always available as the `atom.contextMenu` diff --git a/src/cursor.coffee b/src/cursor.coffee index b736dd424..112065ff1 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -54,11 +54,14 @@ class Cursor extends Model @emitter.dispose() @needsAutoscroll = true + destroy: -> + @marker.destroy() + ### Section: Event Subscription ### - # Essential: Calls your `callback` when the cursor has been moved. + # Public: Calls your `callback` when the cursor has been moved. # # * `callback` {Function} # * `event` {Object} @@ -72,7 +75,7 @@ class Cursor extends Model onDidChangePosition: (callback) -> @emitter.on 'did-change-position', callback - # Extended: Calls your `callback` when the cursor is destroyed + # Public: Calls your `callback` when the cursor is destroyed # # * `callback` {Function} # @@ -80,7 +83,7 @@ class Cursor extends Model onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback - # Extended: Calls your `callback` when the cursor's visibility has changed + # Public: Calls your `callback` when the cursor's visibility has changed # # * `callback` {Function} # * `visibility` {Boolean} @@ -102,23 +105,9 @@ class Cursor extends Model super ### - Section: Methods + Section: Managing Cursor Position ### - destroy: -> - @marker.destroy() - - changePosition: (options, fn) -> - @clearSelection() - @needsAutoscroll = options.autoscroll ? @isLastCursor() - fn() - if @needsAutoscroll - @emit 'autoscrolled' # Support legacy editor - @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view - - getPixelRect: -> - @editor.pixelRectForScreenRange(@getScreenRange()) - # Public: Moves a cursor to a given screen position. # # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. @@ -133,10 +122,6 @@ class Cursor extends Model getScreenPosition: -> @marker.getHeadScreenPosition() - getScreenRange: -> - {row, column} = @getScreenPosition() - new Range(new Point(row, column), new Point(row, column + 1)) - # Public: Moves a cursor to a given buffer position. # # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. @@ -151,47 +136,38 @@ class Cursor extends Model getBufferPosition: -> @marker.getHeadBufferPosition() - autoscroll: (options) -> - @editor.scrollToScreenRange(@getScreenRange(), options) + # Public: Returns the cursor's current screen row. + getScreenRow: -> + @getScreenPosition().row - # Public: If the marker range is empty, the cursor is marked as being visible. - updateVisibility: -> - @setVisible(@marker.getBufferRange().isEmpty()) + # Public: Returns the cursor's current screen column. + getScreenColumn: -> + @getScreenPosition().column - # Public: Sets whether the cursor is visible. - setVisible: (visible) -> - if @visible != visible - @visible = visible - @needsAutoscroll ?= true if @visible and @isLastCursor() - @emit 'visibility-changed', @visible - @emitter.emit 'did-change-visibility', @visible + # Public: Retrieves the cursor's current buffer row. + getBufferRow: -> + @getBufferPosition().row - # Public: Returns the visibility of the cursor. - isVisible: -> @visible + # Public: Returns the cursor's current buffer column. + getBufferColumn: -> + @getBufferPosition().column - # Public: Get the RegExp used by the cursor to determine what a "word" is. - # - # * `options` (optional) {Object} with the following keys: - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the regex. (default: true) - # - # Returns a {RegExp}. - wordRegExp: ({includeNonWordCharacters}={}) -> - includeNonWordCharacters ?= true - nonWordCharacters = atom.config.get('editor.nonWordCharacters') - segments = ["^[\t ]*$"] - segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") - if includeNonWordCharacters - segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") - new RegExp(segments.join("|"), "g") + # Public: Returns the cursor's current buffer row of text excluding its line + # ending. + getCurrentBufferLine: -> + @editor.lineTextForBufferRow(@getBufferRow()) - # Public: Identifies if this cursor is the last in the {Editor}. - # - # "Last" is defined as the most recently added cursor. - # - # Returns a {Boolean}. - isLastCursor: -> - this == @editor.getLastCursor() + # Public: Returns whether the cursor is at the start of a line. + isAtBeginningOfLine: -> + @getBufferPosition().column == 0 + + # Public: Returns whether the cursor is on the line return character. + isAtEndOfLine: -> + @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) + + ### + Section: Cursor Position Details + ### # Public: Identifies if the cursor is surrounded by whitespace. # @@ -229,34 +205,42 @@ class Cursor extends Model range = [[row, column], [row, Infinity]] @editor.getTextInBufferRange(range).search(@wordRegExp()) == 0 - # Public: Prevents this cursor from causing scrolling. - clearAutoscroll: -> - @needsAutoscroll = null + # Public: Returns the indentation level of the current line. + getIndentLevel: -> + if @editor.getSoftTabs() + @getBufferColumn() / @editor.getTabLength() + else + @getBufferColumn() - # Public: Deselects the current selection. - clearSelection: -> - @selection?.clear() + # Public: Retrieves the grammar's token scopes for the line. + # + # Returns an {Array} of {String}s. + getScopes: -> + @editor.scopesForBufferPosition(@getBufferPosition()) - # Public: Returns the cursor's current screen row. - getScreenRow: -> - @getScreenPosition().row + # Public: Returns true if this cursor has no non-whitespace characters before + # its current position. + hasPrecedingCharactersOnLine: -> + bufferPosition = @getBufferPosition() + line = @editor.lineTextForBufferRow(bufferPosition.row) + firstCharacterColumn = line.search(/\S/) - # Public: Returns the cursor's current screen column. - getScreenColumn: -> - @getScreenPosition().column + if firstCharacterColumn is -1 + false + else + bufferPosition.column > firstCharacterColumn - # Public: Retrieves the cursor's current buffer row. - getBufferRow: -> - @getBufferPosition().row + # Public: Identifies if this cursor is the last in the {Editor}. + # + # "Last" is defined as the most recently added cursor. + # + # Returns a {Boolean}. + isLastCursor: -> + this == @editor.getLastCursor() - # Public: Returns the cursor's current buffer column. - getBufferColumn: -> - @getBufferPosition().column - - # Public: Returns the cursor's current buffer row of text excluding its line - # ending. - getCurrentBufferLine: -> - @editor.lineTextForBufferRow(@getBufferRow()) + ### + Section: Moving the Cursor + ### # Public: Moves the cursor up one screen row. # @@ -417,6 +401,20 @@ class Cursor extends Model @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) + # Public: Moves the cursor to the beginning of the next paragraph + moveToBeginningOfNextParagraph: -> + if position = @getBeginningOfNextParagraphBufferPosition() + @setBufferPosition(position) + + # Public: Moves the cursor to the beginning of the previous paragraph + moveToBeginningOfPreviousParagraph: -> + if position = @getBeginningOfPreviousParagraphBufferPosition() + @setBufferPosition(position) + + ### + Section: Local Positions and Ranges + ### + # Public: Retrieves the buffer position of where the current word starts. # # * `options` (optional) An {Object} with the following keys: @@ -553,15 +551,98 @@ class Cursor extends Model getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) - # Public: Moves the cursor to the beginning of the next paragraph - moveToBeginningOfNextParagraph: -> - if position = @getBeginningOfNextParagraphBufferPosition() - @setBufferPosition(position) + # Public: Retrieves the range for the current paragraph. + # + # A paragraph is defined as a block of text surrounded by empty lines. + # + # Returns a {Range}. + getCurrentParagraphBufferRange: -> + @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) - # Public: Moves the cursor to the beginning of the previous paragraph - moveToBeginningOfPreviousParagraph: -> - if position = @getBeginningOfPreviousParagraphBufferPosition() - @setBufferPosition(position) + # Public: Returns the characters preceding the cursor in the current word. + getCurrentWordPrefix: -> + @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) + + ### + Section: Visibility + ### + + # Public: If the marker range is empty, the cursor is marked as being visible. + updateVisibility: -> + @setVisible(@marker.getBufferRange().isEmpty()) + + # Public: Sets whether the cursor is visible. + setVisible: (visible) -> + if @visible != visible + @visible = visible + @needsAutoscroll ?= true if @visible and @isLastCursor() + @emit 'visibility-changed', @visible + @emitter.emit 'did-change-visibility', @visible + + # Public: Returns the visibility of the cursor. + isVisible: -> @visible + + ### + Section: Comparing to another cursor + ### + + # Public: Compare this cursor's buffer position to another cursor's buffer position. + # + # See {Point::compare} for more details. + # + # * `otherCursor`{Cursor} to compare against + compare: (otherCursor) -> + @getBufferPosition().compare(otherCursor.getBufferPosition()) + + ### + Section: Utilities + ### + + # Public: Prevents this cursor from causing scrolling. + clearAutoscroll: -> + @needsAutoscroll = null + + # Public: Deselects the current selection. + clearSelection: -> + @selection?.clear() + + # Public: Get the RegExp used by the cursor to determine what a "word" is. + # + # * `options` (optional) {Object} with the following keys: + # * `includeNonWordCharacters` A {Boolean} indicating whether to include + # non-word characters in the regex. (default: true) + # + # Returns a {RegExp}. + wordRegExp: ({includeNonWordCharacters}={}) -> + includeNonWordCharacters ?= true + nonWordCharacters = atom.config.get('editor.nonWordCharacters') + segments = ["^[\t ]*$"] + segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") + if includeNonWordCharacters + segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") + new RegExp(segments.join("|"), "g") + + ### + Section: Private + ### + + changePosition: (options, fn) -> + @clearSelection() + @needsAutoscroll = options.autoscroll ? @isLastCursor() + fn() + if @needsAutoscroll + @emit 'autoscrolled' # Support legacy editor + @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view + + getPixelRect: -> + @editor.pixelRectForScreenRange(@getScreenRange()) + + getScreenRange: -> + {row, column} = @getScreenPosition() + new Range(new Point(row, column), new Point(row, column + 1)) + + autoscroll: (options) -> + @editor.scrollToScreenRange(@getScreenRange(), options) getBeginningOfNextParagraphBufferPosition: (editor) -> start = @getBufferPosition() @@ -589,56 +670,3 @@ class Cursor extends Model position = range.start stop() @editor.screenPositionForBufferPosition(position) - - # Public: Retrieves the range for the current paragraph. - # - # A paragraph is defined as a block of text surrounded by empty lines. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) - - # Public: Returns the characters preceding the cursor in the current word. - getCurrentWordPrefix: -> - @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) - - # Public: Returns whether the cursor is at the start of a line. - isAtBeginningOfLine: -> - @getBufferPosition().column == 0 - - # Public: Returns the indentation level of the current line. - getIndentLevel: -> - if @editor.getSoftTabs() - @getBufferColumn() / @editor.getTabLength() - else - @getBufferColumn() - - # Public: Returns whether the cursor is on the line return character. - isAtEndOfLine: -> - @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) - - # Public: Retrieves the grammar's token scopes for the line. - # - # Returns an {Array} of {String}s. - getScopes: -> - @editor.scopesForBufferPosition(@getBufferPosition()) - - # Public: Returns true if this cursor has no non-whitespace characters before - # its current position. - hasPrecedingCharactersOnLine: -> - bufferPosition = @getBufferPosition() - line = @editor.lineTextForBufferRow(bufferPosition.row) - firstCharacterColumn = line.search(/\S/) - - if firstCharacterColumn is -1 - false - else - bufferPosition.column > firstCharacterColumn - - # Public: Compare this cursor's buffer position to another cursor's buffer position. - # - # See {Point::compare} for more details. - # - # * `otherCursor`{Cursor} to compare against - compare: (otherCursor) -> - @getBufferPosition().compare(otherCursor.getBufferPosition()) diff --git a/src/decoration.coffee b/src/decoration.coffee index 075641058..45e265044 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -32,7 +32,7 @@ module.exports = class Decoration EmitterMixin.includeInto(this) - # Extended: Check if the `decorationProperties.type` matches `type` + # Private: Check if the `decorationProperties.type` matches `type` # # * `decorationProperties` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also @@ -46,6 +46,10 @@ class Decoration else type is decorationProperties.type + ### + Section: Construction and Destruction + ### + constructor: (@marker, @displayBuffer, @properties) -> @emitter = new Emitter @id = nextId() @@ -55,6 +59,19 @@ class Decoration @markerDestroyDisposable = @marker.onDidDestroy => @destroy() + # Essential: Destroy this marker. + # + # If you own the marker, you should use {Marker::destroy} which will destroy + # this decoration. + destroy: -> + return if @destroyed + @markerDestroyDisposable.dispose() + @markerDestroyDisposable = null + @destroyed = true + @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() + ### Section: Event Subscription ### @@ -79,7 +96,7 @@ class Decoration @emitter.on 'did-destroy', callback ### - Section: Methods + Section: Decoration Details ### # Essential: An id unique across all {Decoration} objects @@ -98,6 +115,10 @@ class Decoration isType: (type) -> Decoration.isType(@properties, type) + ### + Section: Properties + ### + # Essential: Returns the {Decoration}'s properties. getProperties: -> @properties @@ -125,18 +146,9 @@ class Decoration Grim.deprecate 'Use Decoration::setProperties instead' @setProperties(newProperties) - # Essential: Destroy this marker. - # - # If you own the marker, you should use {Marker::destroy} which will destroy - # this decoration. - destroy: -> - return if @destroyed - @markerDestroyDisposable.dispose() - @markerDestroyDisposable = null - @destroyed = true - @emit 'destroyed' - @emitter.emit 'did-destroy' - @emitter.dispose() + ### + Section: Private methods + ### matchesPattern: (decorationPattern) -> return false unless decorationPattern? diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 7a4390f6a..9abefc4c6 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,4 +1,4 @@ -# Public: Manages the deserializers used for serialized state +# Extended: Manages the deserializers used for serialized state # # An instance of this class is always available as the `atom.deserializers` # global. diff --git a/src/display-buffer-marker.coffee b/src/display-buffer-marker.coffee deleted file mode 100644 index 8300cac23..000000000 --- a/src/display-buffer-marker.coffee +++ /dev/null @@ -1,270 +0,0 @@ -{Range} = require 'text-buffer' -_ = require 'underscore-plus' -{Subscriber} = require 'emissary' -EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' -Grim = require 'grim' - -module.exports = -class DisplayBufferMarker - EmitterMixin.includeInto(this) - Subscriber.includeInto(this) - - bufferMarkerSubscription: null - oldHeadBufferPosition: null - oldHeadScreenPosition: null - oldTailBufferPosition: null - oldTailScreenPosition: null - wasValid: true - deferredChangeEvents: null - - constructor: ({@bufferMarker, @displayBuffer}) -> - @emitter = new Emitter - @id = @bufferMarker.id - @oldHeadBufferPosition = @getHeadBufferPosition() - @oldHeadScreenPosition = @getHeadScreenPosition() - @oldTailBufferPosition = @getTailBufferPosition() - @oldTailScreenPosition = @getTailScreenPosition() - @wasValid = @isValid() - - @subscribe @bufferMarker.onDidDestroy => @destroyed() - @subscribe @bufferMarker.onDidChange (event) => @notifyObservers(event) - - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback - - on: (eventName) -> - switch eventName - when 'changed' - Grim.deprecate("Use DisplayBufferMarker::onDidChange instead") - when 'destroyed' - Grim.deprecate("Use DisplayBufferMarker::onDidDestroy instead") - - EmitterMixin::on.apply(this, arguments) - - copy: (attributes) -> - @displayBuffer.getMarker(@bufferMarker.copy(attributes).id) - - # Gets the screen range of the display marker. - # - # Returns a {Range}. - getScreenRange: -> - @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true) - - # Modifies the screen range of the display marker. - # - # screenRange - The new {Range} to use - # options - A hash of options matching those found in {Marker::setRange} - setScreenRange: (screenRange, options) -> - @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options) - - # Gets the buffer range of the display marker. - # - # Returns a {Range}. - getBufferRange: -> - @bufferMarker.getRange() - - # Modifies the buffer range of the display marker. - # - # screenRange - The new {Range} to use - # options - A hash of options matching those found in {Marker::setRange} - setBufferRange: (bufferRange, options) -> - @bufferMarker.setRange(bufferRange, options) - - getPixelRange: -> - @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false) - - # Retrieves the screen position of the marker's head. - # - # Returns a {Point}. - getHeadScreenPosition: -> - @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true) - - # Sets the screen position of the marker's head. - # - # screenRange - The new {Point} to use - # options - A hash of options matching those found in {DisplayBuffer::bufferPositionForScreenPosition} - setHeadScreenPosition: (screenPosition, options) -> - screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) - @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) - - # Retrieves the buffer position of the marker's head. - # - # Returns a {Point}. - getHeadBufferPosition: -> - @bufferMarker.getHeadPosition() - - # Sets the buffer position of the marker's head. - # - # screenRange - The new {Point} to use - # options - A hash of options matching those found in {DisplayBuffer::bufferPositionForScreenPosition} - setHeadBufferPosition: (bufferPosition) -> - @bufferMarker.setHeadPosition(bufferPosition) - - # Retrieves the screen position of the marker's tail. - # - # Returns a {Point}. - getTailScreenPosition: -> - @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true) - - # Sets the screen position of the marker's tail. - # - # screenRange - The new {Point} to use - # options - A hash of options matching those found in {DisplayBuffer::bufferPositionForScreenPosition} - setTailScreenPosition: (screenPosition, options) -> - screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) - @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) - - # Retrieves the buffer position of the marker's tail. - # - # Returns a {Point}. - getTailBufferPosition: -> - @bufferMarker.getTailPosition() - - # Sets the buffer position of the marker's tail. - # - # screenRange - The new {Point} to use - # options - A hash of options matching those found in {DisplayBuffer::bufferPositionForScreenPosition} - 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. - # - # Returns a {Point} representing the new tail position. - plantTail: -> - @bufferMarker.plantTail() - - # Removes the tail from the marker. - clearTail: -> - @bufferMarker.clearTail() - - hasTail: -> - @bufferMarker.hasTail() - - # Returns whether the head precedes the tail in the buffer - isReversed: -> - @bufferMarker.isReversed() - - # Returns a {Boolean} indicating whether the marker is valid. Markers can be - # invalidated when a region surrounding them in the buffer is changed. - isValid: -> - @bufferMarker.isValid() - - # Returns a {Boolean} indicating whether the marker has been destroyed. A marker - # can be invalid without being destroyed, in which case undoing the invalidating - # operation would restore the marker. Once a marker is destroyed by calling - # {Marker::destroy}, no undo/redo operation can ever bring it back. - isDestroyed: -> - @bufferMarker.isDestroyed() - - getAttributes: -> - @bufferMarker.getProperties() - - setAttributes: (attributes) -> - @bufferMarker.setProperties(attributes) - - matchesAttributes: (attributes) -> - attributes = @displayBuffer.translateToBufferMarkerParams(attributes) - @bufferMarker.matchesAttributes(attributes) - - # Destroys the marker - destroy: -> - @bufferMarker.destroy() - @unsubscribe() - - isEqual: (other) -> - return false unless other instanceof @constructor - @bufferMarker.isEqual(other.bufferMarker) - - compare: (other) -> - @bufferMarker.compare(other.bufferMarker) - - # Returns a {String} representation of the marker - inspect: -> - "DisplayBufferMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" - - destroyed: -> - delete @displayBuffer.markers[@id] - @emit 'destroyed' - @emitter.emit 'did-destroy' - @emitter.dispose() - - notifyObservers: ({textChanged}) -> - textChanged ?= false - - newHeadBufferPosition = @getHeadBufferPosition() - newHeadScreenPosition = @getHeadScreenPosition() - newTailBufferPosition = @getTailBufferPosition() - newTailScreenPosition = @getTailScreenPosition() - isValid = @isValid() - - return if _.isEqual(isValid, @wasValid) and - _.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and - _.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and - _.isEqual(newTailBufferPosition, @oldTailBufferPosition) and - _.isEqual(newTailScreenPosition, @oldTailScreenPosition) - - changeEvent = { - @oldHeadScreenPosition, newHeadScreenPosition, - @oldTailScreenPosition, newTailScreenPosition, - @oldHeadBufferPosition, newHeadBufferPosition, - @oldTailBufferPosition, newTailBufferPosition, - textChanged, - isValid - } - - if @deferredChangeEvents? - @deferredChangeEvents.push(changeEvent) - else - @emit 'changed', changeEvent - @emitter.emit 'did-change', changeEvent - - @oldHeadBufferPosition = newHeadBufferPosition - @oldHeadScreenPosition = newHeadScreenPosition - @oldTailBufferPosition = newTailBufferPosition - @oldTailScreenPosition = newTailScreenPosition - @wasValid = isValid - - pauseChangeEvents: -> - @deferredChangeEvents = [] - - resumeChangeEvents: -> - if deferredChangeEvents = @deferredChangeEvents - @deferredChangeEvents = null - - for event in deferredChangeEvents - @emit 'changed', event - @emitter.emit 'did-change', event diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index c86324b35..4a686c88b 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -10,7 +10,7 @@ RowMap = require './row-map' Fold = require './fold' Token = require './token' Decoration = require './decoration' -DisplayBufferMarker = require './display-buffer-marker' +Marker = require './marker' Grim = require 'grim' class BufferToScreenConversionError extends Error @@ -865,21 +865,21 @@ class DisplayBuffer extends Model @emitter.emit 'did-remove-decoration', decoration delete @decorationsByMarkerId[marker.id] if decorations.length is 0 - # Retrieves a {DisplayBufferMarker} based on its id. + # Retrieves a {Marker} based on its id. # # id - A {Number} representing a marker id # - # Returns the {DisplayBufferMarker} (if it exists). + # Returns the {Marker} (if it exists). getMarker: (id) -> unless marker = @markers[id] if bufferMarker = @buffer.getMarker(id) - marker = new DisplayBufferMarker({bufferMarker, displayBuffer: this}) + marker = new Marker({bufferMarker, displayBuffer: this}) @markers[id] = marker marker # Retrieves the active markers in the buffer. # - # Returns an {Array} of existing {DisplayBufferMarker}s. + # Returns an {Array} of existing {Marker}s. getMarkers: -> @buffer.getMarkers().map ({id}) => @getMarker(id) @@ -934,7 +934,7 @@ class DisplayBuffer extends Model # # Refer to {DisplayBuffer::findMarkers} for details. # - # Returns a {DisplayBufferMarker} or null + # Returns a {Marker} or null findMarker: (params) -> @findMarkers(params)[0] @@ -955,7 +955,7 @@ class DisplayBuffer extends Model # :containedInBufferRange - A {Range} or range-compatible {Array}. Only # returns markers contained within this range. # - # Returns an {Array} of {DisplayBufferMarker}s + # Returns an {Array} of {Marker}s findMarkers: (params) -> params = @translateToBufferMarkerParams(params) @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) @@ -1138,13 +1138,13 @@ class DisplayBuffer extends Model @pendingChangeEvent = null @emitDidChange(event, false) - handleBufferMarkerCreated: (marker) => - @createFoldForMarker(marker) if marker.matchesAttributes(@getFoldMarkerAttributes()) - if displayBufferMarker = @getMarker(marker.id) + handleBufferMarkerCreated: (textBufferMarker) => + @createFoldForMarker(textBufferMarker) if textBufferMarker.matchesParams(@getFoldMarkerAttributes()) + if marker = @getMarker(textBufferMarker.id) # The marker might have been removed in some other handler called before # this one. Only emit when the marker still exists. - @emit 'marker-created', displayBufferMarker - @emitter.emit 'did-create-marker', displayBufferMarker + @emit 'marker-created', marker + @emitter.emit 'did-create-marker', marker createFoldForMarker: (marker) -> @decorateMarker(marker, type: 'gutter', class: 'folded') diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 646ff690c..90de158d5 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -128,6 +128,7 @@ EditorComponent = React.createClass scrollableInOppositeDirection: verticallyScrollable verticalScrollbarWidth: verticalScrollbarWidth horizontalScrollbarHeight: horizontalScrollbarHeight + useHardwareAcceleration: @useHardwareAcceleration ScrollbarComponent ref: 'verticalScrollbar' @@ -140,6 +141,7 @@ EditorComponent = React.createClass scrollableInOppositeDirection: horizontallyScrollable verticalScrollbarWidth: verticalScrollbarWidth horizontalScrollbarHeight: horizontalScrollbarHeight + useHardwareAcceleration: @useHardwareAcceleration # Also used to measure the height/width of scrollbars after the initial render ScrollbarCornerComponent @@ -350,7 +352,7 @@ EditorComponent = React.createClass observeEditor: -> {editor} = @props - @subscribe editor.onDidChangeScreenLines(@onScreenLinesChanged) + @subscribe editor.onDidChange(@onScreenLinesChanged) @subscribe editor.observeCursors(@onCursorAdded) @subscribe editor.observeSelections(@onSelectionAdded) @subscribe editor.observeDecorations(@onDecorationAdded) @@ -954,7 +956,7 @@ EditorComponent = React.createClass {verticalScrollbar, horizontalScrollbar, scrollbarCorner} = @refs verticalNode = verticalScrollbar.getDOMNode() - horizontalNode = verticalScrollbar.getDOMNode() + horizontalNode = horizontalScrollbar.getDOMNode() cornerNode = scrollbarCorner.getDOMNode() originalVerticalDisplayValue = verticalNode.style.display diff --git a/src/editor-view.coffee b/src/editor-view.coffee index e91d69caa..fb8c458b4 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -9,6 +9,8 @@ EditorComponent = require './editor-component' # Public: Represents the entire visual pane in Atom. # # The EditorView manages the {Editor}, which manages the file buffers. +# `EditorView` is intentionally sparse. Most of the things you'll want +# to do are on {Editor}. # # ## Examples # diff --git a/src/editor.coffee b/src/editor.coffee index e66110d8e..a4226afaa 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -93,7 +93,7 @@ class Editor extends Model @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true for marker in @findMarkers(@getSelectionMarkerAttributes()) - marker.setAttributes(preserveFolds: true) + marker.setProperties(preserveFolds: true) @addSelection(marker) @subscribeToBuffer() @@ -155,7 +155,7 @@ class Editor extends Model @subscribe @displayBuffer.onDidTokenize => @handleTokenization() @subscribe @displayBuffer.onDidChange (e) => @emit 'screen-lines-changed', e - @emitter.emit 'did-change-screen-lines', e + @emitter.emit 'did-change', e # TODO: remove these when we remove the deprecations. Though, no one is likely using them @subscribe @displayBuffer.onDidChangeSoftWrapped (softWrapped) => @emit 'soft-wrap-changed', softWrapped @@ -171,6 +171,7 @@ class Editor extends Model @buffer.release() @displayBuffer.destroy() @languageMode.destroy() + @emitter.emit 'did-destroy' ### Section: Event Subscription @@ -192,6 +193,53 @@ class Editor extends Model onDidChangePath: (callback) -> @emitter.on 'did-change-path', callback + # Essential: Invoke the given callback synchronously when the content of the + # buffer changes. + # + # Because observers are invoked synchronously, it's important not to perform + # any expensive operations via this method. Consider {::onDidStopChanging} to + # delay expensive operations until after changes stop occurring. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange: (callback) -> + @emitter.on 'did-change', callback + + # Essential: Invoke `callback` when the buffer's contents change. It is + # emit asynchronously 300ms after the last buffer change. This is a good place + # to handle changes to the buffer without compromising typing performance. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChanging: (callback) -> + @getBuffer().onDidStopChanging(callback) + + # Essential: Calls your `callback` when a {Cursor} is moved. If there are + # multiple cursors, your callback will be called for each cursor. + # + # * `callback` {Function} + # * `event` {Object} + # * `oldBufferPosition` {Point} + # * `oldScreenPosition` {Point} + # * `newBufferPosition` {Point} + # * `newScreenPosition` {Point} + # * `textChanged` {Boolean} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeCursorPosition: (callback) -> + @emitter.on 'did-change-cursor-position', callback + + # Essential: Calls your `callback` when a selection's screen range changes. + # + # * `callback` {Function} + # * `selection` {Selection} that moved + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSelectionRange: (callback) -> + @emitter.on 'did-change-selection-range', callback + # Extended: Calls your `callback` when soft wrap was enabled or disabled. # # * `callback` {Function} @@ -209,16 +257,6 @@ class Editor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback - # Essential: Calls your `callback` when the buffer's contents change. It is - # emit asynchronously 300ms after the last buffer change. This is a good place - # to handle changes to the buffer without compromising typing performance. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChanging: (callback) -> - @getBuffer().onDidStopChanging(callback) - # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} @@ -267,6 +305,14 @@ class Editor extends Model onDidSave: (callback) -> @getBuffer().onDidSave(callback) + # Public: Invoke the given callback when the editor is destroyed. + # + # * `callback` {Function} to be called when the editor is destroyed. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + # Extended: Calls your `callback` when a {Cursor} is added to the editor. # Immediately calls your callback for each existing cursor. # @@ -296,21 +342,6 @@ class Editor extends Model onDidRemoveCursor: (callback) -> @emitter.on 'did-remove-cursor', callback - # Essential: Calls your `callback` when a {Cursor} is moved. If there are - # multiple cursors, your callback will be called for each cursor. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferPosition` {Point} - # * `oldScreenPosition` {Point} - # * `newBufferPosition` {Point} - # * `newScreenPosition` {Point} - # * `textChanged` {Boolean} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeCursorPosition: (callback) -> - @emitter.on 'did-change-cursor-position', callback - # Extended: Calls your `callback` when a {Selection} is added to the editor. # Immediately calls your callback for each existing selection. # @@ -340,15 +371,6 @@ class Editor extends Model onDidRemoveSelection: (callback) -> @emitter.on 'did-remove-selection', callback - # Essential: Calls your `callback` when a selection's screen range changes. - # - # * `callback` {Function} - # * `selection` {Selection} that moved - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSelectionRange: (callback) -> - @emitter.on 'did-change-selection-range', callback - # Extended: Calls your `callback` with each {Decoration} added to the editor. # Calls your `callback` immediately for any existing decorations. # @@ -380,9 +402,6 @@ class Editor extends Model onDidChangeCharacterWidths: (callback) -> @displayBuffer.onDidChangeCharacterWidths(callback) - onDidChangeScreenLines: (callback) -> - @emitter.on 'did-change-screen-lines', callback - onDidChangeScrollTop: (callback) -> @emitter.on 'did-change-scroll-top', callback @@ -437,7 +456,7 @@ class Editor extends Model deprecate("Use Marker::onDidChange instead. eg. `editor::decorateMarker(...).getMarker().onDidChange()`") when 'screen-lines-changed' - deprecate("Use Editor::onDidChangeScreenLines instead") + deprecate("Use Editor::onDidChange instead") when 'scroll-top-changed' deprecate("Use Editor::onDidChangeScrollTop instead") @@ -482,7 +501,7 @@ class Editor extends Model Section: File Details ### - # Public: Get the title the editor's title for display in other parts of the + # Essential: Get the editor's title for display in other parts of the # UI such as the tabs. # # If the editor's buffer is saved, its title is the file name. If it is @@ -495,7 +514,7 @@ class Editor extends Model else 'untitled' - # Public: Get the editor's long title for display in other parts of the UI + # Essential: Get the editor's long title for display in other parts of the UI # such as the window title. # # If the editor's buffer is saved, its long title is formatted as @@ -511,10 +530,25 @@ class Editor extends Model else 'untitled' - # Public: Returns the {String} path of this editor's text buffer. + # Essential: Returns the {String} path of this editor's text buffer. getPath: -> @buffer.getPath() - # Public: Saves the editor's text buffer. + # Essential: Returns {Boolean} `true` if this editor has been modified. + isModified: -> @buffer.isModified() + + # Essential: Returns {Boolean} `true` if this editor has no content. + isEmpty: -> @buffer.isEmpty() + + # Copies the current file path to the native clipboard. + copyPathToClipboard: -> + if filePath = @getPath() + atom.clipboard.write(filePath) + + ### + Section: File Operations + ### + + # Essential: Saves the editor's text buffer. # # See {TextBuffer::save} for more details. save: -> @buffer.save() @@ -526,28 +560,18 @@ class Editor extends Model # * `filePath` A {String} path. saveAs: (filePath) -> @buffer.saveAs(filePath) - # Public: Determine whether the user should be prompted to save before closing + # Determine whether the user should be prompted to save before closing # this editor. shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() - # Public: Returns {Boolean} `true` if this editor has been modified. - isModified: -> @buffer.isModified() - - isEmpty: -> @buffer.isEmpty() - - # Copies the current file path to the native clipboard. - copyPathToClipboard: -> - if filePath = @getPath() - atom.clipboard.write(filePath) - ### Section: Reading Text ### - # Public: Returns a {String} representing the entire contents of the editor. + # Essential: Returns a {String} representing the entire contents of the editor. getText: -> @buffer.getText() - # Public: Get the text in the given {Range} in buffer coordinates. + # Essential: Get the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # @@ -555,20 +579,22 @@ class Editor extends Model getTextInBufferRange: (range) -> @buffer.getTextInRange(range) - # Public: Returns a {Number} representing the number of lines in the editor. + # Essential: Returns a {Number} representing the number of lines in the buffer. getLineCount: -> @buffer.getLineCount() - # {Delegates to: DisplayBuffer.getLineCount} + # Essential: Returns a {Number} representing the number of screen lines in the + # editor. This accounts for folds. getScreenLineCount: -> @displayBuffer.getLineCount() - # Public: Returns a {Number} representing the last zero-indexed buffer row + # Essential: Returns a {Number} representing the last zero-indexed buffer row # number of the editor. getLastBufferRow: -> @buffer.getLastRow() - # {Delegates to: DisplayBuffer.getLastRow} + # Essential: Returns a {Number} representing the last zero-indexed screen row + # number of the editor. getLastScreenRow: -> @displayBuffer.getLastRow() - # Public: Returns a {String} representing the contents of the line at the + # Essential: Returns a {String} representing the contents of the line at the # given buffer row. # # * `bufferRow` A {Number} representing a zero-indexed buffer row. @@ -577,7 +603,7 @@ class Editor extends Model deprecate 'Use Editor::lineTextForBufferRow(bufferRow) instead' @lineTextForBufferRow(bufferRow) - # Public: Returns a {String} representing the contents of the line at the + # Essential: Returns a {String} representing the contents of the line at the # given screen row. # # * `screenRow` A {Number} representing a zero-indexed screen row. @@ -649,10 +675,10 @@ class Editor extends Model Section: Mutating Text ### - # Public: Replaces the entire contents of the buffer with the given {String}. + # Essential: Replaces the entire contents of the buffer with the given {String}. setText: (text) -> @buffer.setText(text) - # Public: Set the text in the given {Range} in buffer coordinates. + # Essential: Set the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # * `text` A {String} @@ -660,7 +686,47 @@ class Editor extends Model # Returns the {Range} of the newly-inserted text. setTextInBufferRange: (range, text, normalizeLineEndings) -> @getBuffer().setTextInRange(range, text, normalizeLineEndings) - # Public: Mutate the text of all the selections in a single transaction. + # Essential: For each selection, replace the selected text with the given text. + # + # * `text` A {String} representing the text to insert. + # * `options` (optional) See {Selection::insertText}. + # + # Returns a {Range} when the text has been inserted + # Returns a {Bool} false when the text has not been inserted + insertText: (text, options={}) -> + willInsert = true + cancel = -> willInsert = false + willInsertEvent = {cancel, text} + @emit('will-insert-text', willInsertEvent) + @emitter.emit 'will-insert-text', willInsertEvent + + if willInsert + options.autoIndentNewline ?= @shouldAutoIndent() + options.autoDecreaseIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) => + range = selection.insertText(text, options) + didInsertEvent = {text, range} + @emit('did-insert-text', didInsertEvent) + @emitter.emit 'did-insert-text', didInsertEvent + range + else + false + + # Essential: For each selection, replace the selected text with a newline. + insertNewline: -> + @insertText('\n') + + # Essential: For each selection, if the selection is empty, delete the character + # preceding the cursor. Otherwise delete the selected text. + delete: -> + @mutateSelectedText (selection) -> selection.delete() + + # Essential: For each selection, if the selection is empty, delete the character + # preceding the cursor. Otherwise delete the selected text. + backspace: -> + @mutateSelectedText (selection) -> selection.backspace() + + # Extended: Mutate the text of all the selections in a single transaction. # # All the changes made inside the given {Function} can be reverted with a # single call to {::undo}. @@ -840,7 +906,7 @@ class Editor extends Model @addSelectionForBufferRange([[row, 0], [row, Infinity]]) @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 - # Public: For each selection, transpose the selected text. + # Extended: For each selection, transpose the selected text. # # If the selection is empty, the characters preceding and following the cursor # are swapped. Otherwise, the selected characters are reversed. @@ -855,20 +921,28 @@ class Editor extends Model else selection.insertText selection.getText().split('').reverse().join('') - # Public: Convert the selected text to upper case. + # Extended: Convert the selected text to upper case. # # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. upperCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toUpperCase() - # Public: Convert the selected text to lower case. + # Extended: Convert the selected text to lower case. # # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. lowerCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toLowerCase() + # Extended: Toggle line comments for rows intersecting selections. + # + # If the current grammar doesn't support comments, does nothing. + # + # Returns an {Array} of the commented {Range}s. + toggleLineCommentsInSelection: -> + @mutateSelectedText (selection) -> selection.toggleLineComments() + # Convert multiple lines to a single line. # # Operates on all selections. If the selection is empty, joins the current @@ -880,47 +954,13 @@ class Editor extends Model joinLines: -> @mutateSelectedText (selection) -> selection.joinLines() - ### - Section: Adding Text - ### - - # Public: For each selection, replace the selected text with the given text. - # - # * `text` A {String} representing the text to insert. - # * `options` (optional) See {Selection::insertText}. - # - # Returns a {Range} when the text has been inserted - # Returns a {Bool} false when the text has not been inserted - insertText: (text, options={}) -> - willInsert = true - cancel = -> willInsert = false - willInsertEvent = {cancel, text} - @emit('will-insert-text', willInsertEvent) - @emitter.emit 'will-insert-text', willInsertEvent - - if willInsert - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) => - range = selection.insertText(text, options) - didInsertEvent = {text, range} - @emit('did-insert-text', didInsertEvent) - @emitter.emit 'did-insert-text', didInsertEvent - range - else - false - - # Public: For each selection, replace the selected text with a newline. - insertNewline: -> - @insertText('\n') - - # Public: For each cursor, insert a newline at beginning the following line. + # Extended: For each cursor, insert a newline at beginning the following line. insertNewlineBelow: -> @transact => @moveToEndOfLine() @insertNewline() - # Public: For each cursor, insert a newline at the end of the preceding line. + # Extended: For each cursor, insert a newline at the end of the preceding line. insertNewlineAbove: -> @transact => bufferRow = @getCursorBufferPosition().row @@ -938,14 +978,34 @@ class Editor extends Model @moveUp() @moveToEndOfLine() - ### - Section: Removing Text - ### + # Extended: For each selection, if the selection is empty, delete all characters + # of the containing word that precede the cursor. Otherwise delete the + # selected text. + deleteToBeginningOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() - # Public: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() + # Extended: For each selection, if the selection is empty, delete all characters + # of the containing line that precede the cursor. Otherwise delete the + # selected text. + deleteToBeginningOfLine: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() + + # Extended: For each selection, if the selection is not empty, deletes the + # selection; otherwise, deletes all characters of the containing line + # following the cursor. If the cursor is already at the end of the line, + # deletes the following newline. + deleteToEndOfLine: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfLine() + + # Extended: For each selection, if the selection is empty, delete all characters + # of the containing word following the cursor. Otherwise delete the selected + # text. + deleteToEndOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfWord() + + # Extended: Delete all lines intersecting selections. + deleteLine: -> + @mutateSelectedText (selection) -> selection.deleteLine() # Deprecated: Use {::deleteToBeginningOfWord} instead. backspaceToBeginningOfWord: -> @@ -957,238 +1017,21 @@ class Editor extends Model 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. - 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. - 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. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Public: For each selection, if the selection is not empty, deletes the - # selection; otherwise, deletes all characters of the containing line - # following the cursor. If the cursor is already at the end of the line, - # deletes the following newline. - deleteToEndOfLine: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfLine() - - # Public: For each selection, if the selection is empty, delete all characters - # of the containing word following the cursor. Otherwise delete the selected - # text. - deleteToEndOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfWord() - - # Public: Delete all lines intersecting selections. - deleteLine: -> - @mutateSelectedText (selection) -> selection.deleteLine() - ### - Section: Searching Text + Section: History ### - # {Delegates to: TextBuffer.scan} - scan: (args...) -> @buffer.scan(args...) - - # {Delegates to: TextBuffer.scanInRange} - scanInBufferRange: (args...) -> @buffer.scanInRange(args...) - - # {Delegates to: TextBuffer.backwardsScanInRange} - backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) - - - ### - Section: Tab Behavior - ### - - # Public: Determine if the buffer uses hard or soft tabs. - # - # Returns `true` if the first non-comment line with leading whitespace starts - # with a space character. Returns `false` if it starts with a hard tab (`\t`). - # - # Returns a {Boolean} or undefined if no non-comment lines had leading - # whitespace. - usesSoftTabs: -> - for bufferRow in [0..@buffer.getLastRow()] - continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() - - line = @buffer.lineForRow(bufferRow) - return true if line[0] is ' ' - return false if line[0] is '\t' - - undefined - - # Public: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Public: Enable or disable soft tabs for this editor. - # - # * `softTabs` A {Boolean} - setSoftTabs: (@softTabs) -> @softTabs - - # Public: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Public: Get the text representing a single level of indent. - # - # If soft tabs are enabled, the text is composed of N spaces, where N is the - # tab length. Otherwise the text is a tab character (`\t`). - # - # Returns a {String}. - getTabText: -> @buildIndentString(1) - - # Public: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @displayBuffer.getTabLength() - - # Public: Set the on-screen length of tab characters. - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) - - # If soft tabs are enabled, convert all hard tabs to soft tabs in the given - # {Range}. - normalizeTabsInBufferRange: (bufferRange) -> - return unless @getSoftTabs() - @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) - - ### - Section: Soft Wrap Behavior - ### - - # Public: Sets the column at which column will soft wrap - getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - - # Public: Determine whether lines in this editor are soft-wrapped. - # - # Returns a {Boolean}. - isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() - getSoftWrapped: -> - deprecate("Use Editor::isSoftWrapped instead") - @displayBuffer.isSoftWrapped() - - # Public: Enable or disable soft wrapping for this editor. - # - # * `softWrapped` A {Boolean} - # - # Returns a {Boolean}. - setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped) - setSoftWrap: (softWrapped) -> - deprecate("Use Editor::setSoftWrapped instead") - @setSoftWrapped(softWrapped) - - # Public: Toggle soft wrapping for this editor - # - # Returns a {Boolean}. - toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) - toggleSoftWrap: -> - deprecate("Use Editor::toggleSoftWrapped instead") - @toggleSoftWrapped() - - ### - Section: Indentation - ### - - # Public: Get the indentation level of the given a buffer row. - # - # Returns how deeply the given row is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # - # Returns a {Number}. - indentationForBufferRow: (bufferRow) -> - @indentLevelForLine(@lineTextForBufferRow(bufferRow)) - - # Public: Set the indentation level for the given buffer row. - # - # Inserts or removes hard tabs or spaces based on the soft tabs and tab length - # settings of this editor in order to bring it to the given indentation level. - # Note that if soft tabs are enabled and the tab length is 2, a row with 4 - # leading spaces would have an indentation level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # * `newLevel` A {Number} indicating the new indentation level. - # * `options` (optional) An {Object} with the following keys: - # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at - # the beginning of the line (default: false). - setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> - if preserveLeadingWhitespace - endColumn = 0 - else - endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length - newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - - # Public: Get the indentation level of the given line of text. - # - # Returns how deeply the given line is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `line` A {String} representing a line of text. - # - # Returns a {Number}. - indentLevelForLine: (line) -> - @displayBuffer.indentLevelForLine(line) - - # Indent all lines intersecting selections. See {Selection::indent} for more - # information. - indent: (options={}) -> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Public: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Public: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Public: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # Constructs the string used for tabs. - buildIndentString: (number, column=0) -> - if @getSoftTabs() - tabStopViolation = column % @getTabLength() - _.multiplyString(" ", Math.floor(number * @getTabLength()) - tabStopViolation) - else - _.multiplyString("\t", Math.floor(number)) - - ### - Section: Undo Operations - ### - - # Public: Undo the last change. + # Essential: Undo the last change. undo: -> @getLastCursor().needsAutoscroll = true @buffer.undo(this) - # Public: Redo the last change. + # Essential: Redo the last change. redo: -> @getLastCursor().needsAutoscroll = true @buffer.redo(this) - ### - Section: Text Mutation Transactions - ### - - # Public: Batch multiple operations as a single undo/redo step. + # Extended: Batch multiple operations as a single undo/redo step. # # Any group of operations that are logically grouped from the perspective of # undoing and redoing should be performed in a transaction. If you want to @@ -1198,7 +1041,7 @@ class Editor extends Model # * `fn` A {Function} to call inside the transaction. transact: (fn) -> @buffer.transact(fn) - # Public: Start an open-ended transaction. + # Extended: Start an open-ended transaction. # # Call {::commitTransaction} or {::abortTransaction} to terminate the # transaction. If you nest calls to transactions, only the outermost @@ -1206,13 +1049,13 @@ class Editor extends Model # commit, but a single call to abort will cancel all nested transactions. beginTransaction: -> @buffer.beginTransaction() - # Public: Commit an open-ended transaction started with {::beginTransaction} + # Extended: Commit an open-ended transaction started with {::beginTransaction} # and push it to the undo stack. # # If transactions are nested, only the outermost commit takes effect. commitTransaction: -> @buffer.commitTransaction() - # Public: Abort an open transaction, undoing any operations performed so far + # Extended: Abort an open transaction, undoing any operations performed so far # within the transaction. abortTransaction: -> @buffer.abortTransaction() @@ -1220,7 +1063,7 @@ class Editor extends Model Section: Editor Coordinates ### - # Public: Convert a position in buffer-coordinates to screen-coordinates. + # Essential: Convert a position in buffer-coordinates to screen-coordinates. # # The position is clipped via {::clipBufferPosition} prior to the conversion. # The position is also clipped via {::clipScreenPosition} following the @@ -1232,7 +1075,7 @@ class Editor extends Model # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) - # Public: Convert a position in screen-coordinates to buffer-coordinates. + # Essential: Convert a position in screen-coordinates to buffer-coordinates. # # The position is clipped via {::clipScreenPosition} prior to the conversion. # @@ -1242,21 +1085,21 @@ class Editor extends Model # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) - # Public: Convert a range in buffer-coordinates to screen-coordinates. + # Essential: Convert a range in buffer-coordinates to screen-coordinates. # # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. # # Returns a {Range}. screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) - # Public: Convert a range in screen-coordinates to buffer-coordinates. + # Essential: Convert a range in screen-coordinates to buffer-coordinates. # # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. # # Returns a {Range}. bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) - # Public: Clip the given {Point} to a valid position in the buffer. + # Extended: Clip the given {Point} to a valid position in the buffer. # # If the given {Point} describes a position that is actually reachable by the # cursor based on the current contents of the buffer, it is returned @@ -1277,7 +1120,7 @@ class Editor extends Model # Returns a {Point}. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - # Public: Clip the start and end of the given range to valid positions in the + # Extended: Clip the start and end of the given range to valid positions in the # buffer. See {::clipBufferPosition} for more information. # # * `range` The {Range} to clip. @@ -1285,7 +1128,7 @@ class Editor extends Model # Returns a {Range}. clipBufferRange: (range) -> @buffer.clipRange(range) - # Public: Clip the given {Point} to a valid position on screen. + # Extended: Clip the given {Point} to a valid position on screen. # # If the given {Point} describes a position that is actually reachable by the # cursor based on the current contents of the screen, it is returned @@ -1310,285 +1153,11 @@ class Editor extends Model # Returns a {Point}. clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) - - - - ### - Section: Grammars - ### - - # Public: Get the current {Grammar} of this editor. - getGrammar: -> - @displayBuffer.getGrammar() - - # Public: Set the current {Grammar} of this editor. - # - # Assigning a grammar will cause the editor to re-tokenize based on the new - # grammar. - setGrammar: (grammar) -> - @displayBuffer.setGrammar(grammar) - - # Reload the grammar based on the file name. - reloadGrammar: -> - @displayBuffer.reloadGrammar() - - ### - Section: Syntatic Queries - ### - - # Public: Get the syntactic scopes for the given position in buffer - # coordinates. - # - # For example, if called with a position inside the parameter list of an - # anonymous CoffeeScript function, the method returns the following array: - # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # - # Returns an {Array} of {String}s. - scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) - - # Public: Get the range in buffer coordinates of all tokens surrounding the - # cursor that match the given scope selector. - # - # For example, if you wanted to find the string surrounding the cursor, you - # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. - # - # Returns a {Range}. - bufferRangeForScopeAtCursor: (selector) -> - @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) - - # {Delegates to: DisplayBuffer.tokenForBufferPosition} - tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) - - # Public: Get the syntactic scopes for the most recently added cursor's - # position. See {::scopesForBufferPosition} for more information. - # - # Returns an {Array} of {String}s. - getCursorScopes: -> @getLastCursor().getScopes() - - logCursorScope: -> - console.log @getCursorScopes() - - - # Public: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineTextForBufferRow(bufferRow).match(/\S/) - scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes - @commentScopeSelector ?= new TextMateScopeSelector('comment.*') - @commentScopeSelector.matches(scopes) - - # Public: Toggle line comments for rows intersecting selections. - # - # If the current grammar doesn't support comments, does nothing. - # - # Returns an {Array} of the commented {Range}s. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - - - - - - - ### - Section: Clipboard Operations - ### - - # Public: For each selection, copy the selected text. - copySelectedText: -> - maintainClipboard = false - for selection in @getSelections() - selection.copy(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, replace the selected text with the contents of - # the clipboard. - # - # If the clipboard contains the same number of selections as the current - # editor, each selection will be replaced with the content of the - # corresponding clipboard selection text. - # - # * `options` (optional) See {Selection::insertText}. - pasteText: (options={}) -> - {text, metadata} = atom.clipboard.readWithMetadata() - - containsNewlines = text.indexOf('\n') isnt -1 - - if metadata?.selections? and metadata.selections.length is @getSelections().length - @mutateSelectedText (selection, index) -> - text = metadata.selections[index] - selection.insertText(text, options) - - return - - else if atom.config.get("editor.normalizeIndentOnPaste") and metadata?.indentBasis? - if !@getLastCursor().hasPrecedingCharactersOnLine() or containsNewlines - options.indentBasis ?= metadata.indentBasis - - @insertText(text, options) - - # Public: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cut(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, if the selection is empty, cut all characters - # of the containing line following the cursor. Otherwise cut the selected - # text. - cutToEndOfLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainClipboard) - maintainClipboard = true - - - ### - Section: Folds - ### - - # Public: Fold the most recent cursor's row based on its indentation level. - # - # The fold will extend from the nearest preceding line with a lower - # indentation level up to the nearest following row with a lower indentation - # level. - foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) - - # Public: Unfold the most recent cursor's row by one level. - unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) - - # Public: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - - # Public: Fold all foldable lines. - foldAll: -> - @languageMode.foldAll() - - # Public: Unfold all existing folds. - unfoldAll: -> - @languageMode.unfoldAll() - - # Public: Fold all foldable lines at the given indent level. - # - # * `level` A {Number}. - foldAllAtIndentLevel: (level) -> - @languageMode.foldAllAtIndentLevel(level) - - # Public: Fold the given row in buffer coordinates based on its indentation - # level. - # - # If the given row is foldable, the fold will begin there. Otherwise, it will - # begin at the first foldable row preceding the given row. - # - # * `bufferRow` A {Number}. - foldBufferRow: (bufferRow) -> - @languageMode.foldBufferRow(bufferRow) - - # Public: Unfold all folds containing the given row in buffer coordinates. - # - # * `bufferRow` A {Number} - unfoldBufferRow: (bufferRow) -> - @displayBuffer.unfoldBufferRow(bufferRow) - - # Public: Determine whether the given row in buffer coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtBufferRow: (bufferRow) -> - @languageMode.isFoldableAtBufferRow(bufferRow) - - isFoldableAtScreenRow: (screenRow) -> - bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) - @isFoldableAtBufferRow(bufferRow) - - # TODO: Rename to foldRowRange? - createFold: (startRow, endRow) -> - @displayBuffer.createFold(startRow, endRow) - - # {Delegates to: DisplayBuffer.destroyFoldWithId} - destroyFoldWithId: (id) -> - @displayBuffer.destroyFoldWithId(id) - - # Remove any {Fold}s found that intersect the given buffer row. - destroyFoldsIntersectingBufferRange: (bufferRange) -> - for row in [bufferRange.start.row..bufferRange.end.row] - @unfoldBufferRow(row) - - # Public: Fold the given buffer row if it isn't currently folded, and unfold - # it otherwise. - toggleFoldAtBufferRow: (bufferRow) -> - if @isFoldedAtBufferRow(bufferRow) - @unfoldBufferRow(bufferRow) - else - @foldBufferRow(bufferRow) - - # Public: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtScreenRow(@getCursorScreenPosition().row) - - # Public: Determine whether the given row in buffer coordinates is folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldedAtBufferRow(bufferRow) - - # Public: Determine whether the given row in screen coordinates is folded. - # - # * `screenRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @displayBuffer.isFoldedAtScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} - largestFoldContainingBufferRow: (bufferRow) -> - @displayBuffer.largestFoldContainingBufferRow(bufferRow) - - # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} - largestFoldStartingAtScreenRow: (screenRow) -> - @displayBuffer.largestFoldStartingAtScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} - outermostFoldsInBufferRowRange: (startRow, endRow) -> - @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) - - - - - ### Section: Decorations ### - # Public: Get all the decorations within a screen row range. - # - # * `startScreenRow` the {Number} beginning screen row - # * `endScreenRow` the {Number} end screen row (inclusive) - # - # Returns an {Object} of decorations in the form - # `{1: [{id: 10, type: 'gutter', class: 'someclass'}], 2: ...}` - # where the keys are {Marker} IDs, and the values are an array of decoration - # params objects attached to the marker. - # Returns an empty object when no decorations are found - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) - - # Public: Adds a decoration that tracks a {Marker}. When the marker moves, + # Essential: 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. # @@ -1629,6 +1198,19 @@ class Editor extends Model decorateMarker: (marker, decorationParams) -> @displayBuffer.decorateMarker(marker, decorationParams) + # Public: Get all the decorations within a screen row range. + # + # * `startScreenRow` the {Number} beginning screen row + # * `endScreenRow` the {Number} end screen row (inclusive) + # + # Returns an {Object} of decorations in the form + # `{1: [{id: 10, type: 'gutter', class: 'someclass'}], 2: ...}` + # where the keys are {Marker} IDs, and the values are an array of decoration + # params objects attached to the marker. + # Returns an empty object when no decorations are found + decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> + @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) + decorationForId: (id) -> @displayBuffer.decorationForId(id) @@ -1636,15 +1218,83 @@ class Editor extends Model Section: Markers ### - # Public: Get the {DisplayBufferMarker} for the given marker id. - getMarker: (id) -> - @displayBuffer.getMarker(id) + # Essential: Create a marker with the given range in buffer coordinates. This + # marker will maintain its logical location as the buffer is changed, so if + # you mark a particular word, the marker will remain over that word even if + # the word's location in the buffer changes. + # + # * `range` A {Range} or range-compatible {Array} + # * `properties` A hash of key-value pairs to associate with the marker. There + # are also reserved property names that have marker-specific meaning. + # * `reversed` (optional) Creates the marker in a reversed orientation. (default: false) + # * `persistent` (optional) Whether to include this marker when serializing the buffer. (default: true) + # * `invalidate` (optional) Determines the rules by which changes to the + # buffer *invalidate* the marker. (default: 'overlap') It can be any of + # the following strategies, in order of fragility + # * __never__: The marker is never marked as invalid. This is a good choice for + # markers representing selections in an editor. + # * __surround__: The marker is invalidated by changes that completely surround it. + # * __overlap__: The marker is invalidated by changes that surround the + # start or end of the marker. This is the default. + # * __inside__: The marker is invalidated by changes that extend into the + # inside of the marker. Changes that end at the marker's start or + # start at the marker's end do not invalidate the marker. + # * __touch__: The marker is invalidated by a change that touches the marked + # region in any way, including changes that end at the marker's + # start or start at the marker's end. This is the most fragile strategy. + # + # Returns a {Marker}. + markBufferRange: (args...) -> + @displayBuffer.markBufferRange(args...) - # Public: Get all {DisplayBufferMarker}s. - getMarkers: -> - @displayBuffer.getMarkers() + # Essential: Create a marker with the given range in screen coordinates. This + # marker will maintain its logical location as the buffer is changed, so if + # you mark a particular word, the marker will remain over that word even if + # the word's location in the buffer changes. + # + # * `range` A {Range} or range-compatible {Array} + # * `properties` A hash of key-value pairs to associate with the marker. There + # are also reserved property names that have marker-specific meaning. + # * `reversed` (optional) Creates the marker in a reversed orientation. (default: false) + # * `persistent` (optional) Whether to include this marker when serializing the buffer. (default: true) + # * `invalidate` (optional) Determines the rules by which changes to the + # buffer *invalidate* the marker. (default: 'overlap') It can be any of + # the following strategies, in order of fragility + # * __never__: The marker is never marked as invalid. This is a good choice for + # markers representing selections in an editor. + # * __surround__: The marker is invalidated by changes that completely surround it. + # * __overlap__: The marker is invalidated by changes that surround the + # start or end of the marker. This is the default. + # * __inside__: The marker is invalidated by changes that extend into the + # inside of the marker. Changes that end at the marker's start or + # start at the marker's end do not invalidate the marker. + # * __touch__: The marker is invalidated by a change that touches the marked + # region in any way, including changes that end at the marker's + # start or start at the marker's end. This is the most fragile strategy. + # + # Returns a {Marker}. + markScreenRange: (args...) -> + @displayBuffer.markScreenRange(args...) - # Public: Find all {DisplayBufferMarker}s that match the given properties. + # Essential: Mark the given position in buffer coordinates. + # + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {Marker}. + markBufferPosition: (args...) -> + @displayBuffer.markBufferPosition(args...) + + # Essential: Mark the given position in screen coordinates. + # + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {Marker}. + markScreenPosition: (args...) -> + @displayBuffer.markScreenPosition(args...) + + # Essential: Find all {Marker}s that match the given properties. # # This method finds markers based on the given properties. Markers can be # associated with custom properties that will be compared with basic equality. @@ -1666,52 +1316,25 @@ class Editor extends Model findMarkers: (properties) -> @displayBuffer.findMarkers(properties) - # Public: Mark the given range in screen coordinates. + # Extended: Get the {Marker} for the given marker id. # - # * `range` A {Range} or range-compatible {Array}. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markScreenRange: (args...) -> - @displayBuffer.markScreenRange(args...) + # * `id` {Number} id of the marker + getMarker: (id) -> + @displayBuffer.getMarker(id) - # Public: Mark the given range in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markBufferRange: (args...) -> - @displayBuffer.markBufferRange(args...) + # Extended: Get all {Marker}s. Consider using {::findMarkers} + getMarkers: -> + @displayBuffer.getMarkers() - # Public: Mark the given position in screen coordinates. - # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markScreenPosition: (args...) -> - @displayBuffer.markScreenPosition(args...) - - # Public: Mark the given position in buffer coordinates. - # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markBufferPosition: (args...) -> - @displayBuffer.markBufferPosition(args...) - - # {Delegates to: DisplayBuffer.destroyMarker} - destroyMarker: (args...) -> - @displayBuffer.destroyMarker(args...) - - # Public: Get the number of markers in this editor's buffer. + # Extended: Get the number of markers in this editor's buffer. # # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() + # {Delegates to: DisplayBuffer.destroyMarker} + destroyMarker: (args...) -> + @displayBuffer.destroyMarker(args...) ### Section: Cursors @@ -1960,7 +1583,7 @@ class Editor extends Model getCursorsOrderedByBufferPosition: -> @getCursors().sort (a, b) -> a.compare(b) - # Add a cursor based on the given {DisplayBufferMarker}. + # Add a cursor based on the given {Marker}. addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker) @cursors.push(cursor) @@ -2303,7 +1926,7 @@ class Editor extends Model # Extended: Select the range of the given marker if it is valid. # - # * `marker` A {DisplayBufferMarker} + # * `marker` A {Marker} # # Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker: (marker) -> @@ -2419,20 +2042,20 @@ class Editor extends Model _.reduce(@getSelections(), reducer, []) - # Add a {Selection} based on the given {DisplayBufferMarker}. + # Add a {Selection} based on the given {Marker}. # - # * `marker` The {DisplayBufferMarker} to highlight + # * `marker` The {Marker} to highlight # * `options` (optional) An {Object} that pertains to the {Selection} constructor. # # Returns the new {Selection}. addSelection: (marker, options={}) -> - unless marker.getAttributes().preserveFolds + unless marker.getProperties().preserveFolds @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editor: this, marker, cursor}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections(preserveFolds: marker.getAttributes().preserveFolds) + @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds) if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) @@ -2468,6 +2091,472 @@ class Editor extends Model @emit 'selection-screen-range-changed', selection @emitter.emit 'did-change-selection-range', selection + ### + Section: Searching and Replacing + ### + + # Essential: Scan regular expression matches in the entire buffer, calling the + # given iterator function on each match. + # + # `::scan` functions as the replace method as well via the `replace` + # + # If you're programmatically modifying the results, you may want to try + # {::backwardsScanInBufferRange} to avoid tripping over your own changes. + # + # * `regex` A {RegExp} to search for. + # * `iterator` A {Function} that's called on each match + # * `object` {Object} + # * `match` The current regular expression match. + # * `matchText` A {String} with the text of the match. + # * `range` The {Range} of the match. + # * `stop` Call this {Function} to terminate the scan. + # * `replace` Call this {Function} with a {String} to replace the match. + scan: (regex, iterator) -> @buffer.scan(regex, iterator) + + # Public: Scan regular expression matches in a given range, calling the given + # iterator function on each match. + # + # * `regex` A {RegExp} to search for. + # * `range` A {Range} in which to search. + # * `iterator` A {Function} that's called on each match with an {Object} + # containing the following keys: + # * `match` The current regular expression match. + # * `matchText` A {String} with the text of the match. + # * `range` The {Range} of the match. + # * `stop` Call this {Function} to terminate the scan. + # * `replace` Call this {Function} with a {String} to replace the match. + scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator) + + # Public: Scan regular expression matches in a given range in reverse order, + # calling the given iterator function on each match. + # + # * `regex` A {RegExp} to search for. + # * `range` A {Range} in which to search. + # * `iterator` A {Function} that's called on each match with an {Object} + # containing the following keys: + # * `match` The current regular expression match. + # * `matchText` A {String} with the text of the match. + # * `range` The {Range} of the match. + # * `stop` Call this {Function} to terminate the scan. + # * `replace` Call this {Function} with a {String} to replace the match. + backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator) + + ### + Section: Tab Behavior + ### + + # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this + # editor. + getSoftTabs: -> @softTabs + + # Essential: Enable or disable soft tabs for this editor. + # + # * `softTabs` A {Boolean} + setSoftTabs: (@softTabs) -> @softTabs + + # Essential: Toggle soft tabs for this editor + toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) + + # Essential: Get the on-screen length of tab characters. + # + # Returns a {Number}. + getTabLength: -> @displayBuffer.getTabLength() + + # Essential: Set the on-screen length of tab characters. + # + # * `tabLength` {Number} length of a single tab + setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + + # Extended: Determine if the buffer uses hard or soft tabs. + # + # Returns `true` if the first non-comment line with leading whitespace starts + # with a space character. Returns `false` if it starts with a hard tab (`\t`). + # + # Returns a {Boolean} or undefined if no non-comment lines had leading + # whitespace. + usesSoftTabs: -> + for bufferRow in [0..@buffer.getLastRow()] + continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() + + line = @buffer.lineForRow(bufferRow) + return true if line[0] is ' ' + return false if line[0] is '\t' + + undefined + + # Extended: Get the text representing a single level of indent. + # + # If soft tabs are enabled, the text is composed of N spaces, where N is the + # tab length. Otherwise the text is a tab character (`\t`). + # + # Returns a {String}. + getTabText: -> @buildIndentString(1) + + # If soft tabs are enabled, convert all hard tabs to soft tabs in the given + # {Range}. + normalizeTabsInBufferRange: (bufferRange) -> + return unless @getSoftTabs() + @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) + + ### + Section: Soft Wrap Behavior + ### + + # Essential: Determine whether lines in this editor are soft-wrapped. + # + # Returns a {Boolean}. + isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() + getSoftWrapped: -> + deprecate("Use Editor::isSoftWrapped instead") + @displayBuffer.isSoftWrapped() + + # Essential: Enable or disable soft wrapping for this editor. + # + # * `softWrapped` A {Boolean} + # + # Returns a {Boolean}. + setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped) + setSoftWrap: (softWrapped) -> + deprecate("Use Editor::setSoftWrapped instead") + @setSoftWrapped(softWrapped) + + # Essential: Toggle soft wrapping for this editor + # + # Returns a {Boolean}. + toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) + toggleSoftWrap: -> + deprecate("Use Editor::toggleSoftWrapped instead") + @toggleSoftWrapped() + + # Public: Gets the column at which column will soft wrap + getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() + + ### + Section: Indentation + ### + + # Essential: Get the indentation level of the given a buffer row. + # + # Returns how deeply the given row is indented based on the soft tabs and + # tab length settings of this editor. Note that if soft tabs are enabled and + # the tab length is 2, a row with 4 leading spaces would have an indentation + # level of 2. + # + # * `bufferRow` A {Number} indicating the buffer row. + # + # Returns a {Number}. + indentationForBufferRow: (bufferRow) -> + @indentLevelForLine(@lineTextForBufferRow(bufferRow)) + + # Essential: Set the indentation level for the given buffer row. + # + # Inserts or removes hard tabs or spaces based on the soft tabs and tab length + # settings of this editor in order to bring it to the given indentation level. + # Note that if soft tabs are enabled and the tab length is 2, a row with 4 + # leading spaces would have an indentation level of 2. + # + # * `bufferRow` A {Number} indicating the buffer row. + # * `newLevel` A {Number} indicating the new indentation level. + # * `options` (optional) An {Object} with the following keys: + # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at + # the beginning of the line (default: false). + setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> + if preserveLeadingWhitespace + endColumn = 0 + else + endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length + newIndentString = @buildIndentString(newLevel) + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + + # Extended: Indent rows intersecting selections by one level. + indentSelectedRows: -> + @mutateSelectedText (selection) -> selection.indentSelectedRows() + + # Extended: Outdent rows intersecting selections by one level. + outdentSelectedRows: -> + @mutateSelectedText (selection) -> selection.outdentSelectedRows() + + # Extended: Get the indentation level of the given line of text. + # + # Returns how deeply the given line is indented based on the soft tabs and + # tab length settings of this editor. Note that if soft tabs are enabled and + # the tab length is 2, a row with 4 leading spaces would have an indentation + # level of 2. + # + # * `line` A {String} representing a line of text. + # + # Returns a {Number}. + indentLevelForLine: (line) -> + @displayBuffer.indentLevelForLine(line) + + # Extended: Indent rows intersecting selections based on the grammar's suggested + # indent level. + autoIndentSelectedRows: -> + @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() + + # Indent all lines intersecting selections. See {Selection::indent} for more + # information. + indent: (options={}) -> + options.autoIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) -> selection.indent(options) + + # Constructs the string used for tabs. + buildIndentString: (number, column=0) -> + if @getSoftTabs() + tabStopViolation = column % @getTabLength() + _.multiplyString(" ", Math.floor(number * @getTabLength()) - tabStopViolation) + else + _.multiplyString("\t", Math.floor(number)) + + ### + Section: Grammars + ### + + # Essential: Get the current {Grammar} of this editor. + getGrammar: -> + @displayBuffer.getGrammar() + + # Essential: Set the current {Grammar} of this editor. + # + # Assigning a grammar will cause the editor to re-tokenize based on the new + # grammar. + # + # * `grammar` {Grammar} + setGrammar: (grammar) -> + @displayBuffer.setGrammar(grammar) + + # Reload the grammar based on the file name. + reloadGrammar: -> + @displayBuffer.reloadGrammar() + + ### + Section: Managing Syntax Scopes + ### + + # Public: Get the syntactic scopes for the most recently added cursor's + # position. See {::scopesForBufferPosition} for more information. + # + # Returns an {Array} of {String}s. + scopesAtCursor: -> @getLastCursor().getScopes() + getCursorScopes: -> + deprecate 'Use Editor::scopesAtCursor() instead' + @scopesAtCursor() + + # Essential: Get the syntactic scopes for the given position in buffer + # coordinates. + # + # For example, if called with a position inside the parameter list of an + # anonymous CoffeeScript function, the method returns the following array: + # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` + # + # * `bufferPosition` A {Point} or {Array} of [row, column]. + # + # Returns an {Array} of {String}s. + scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) + + # Extended: Get the range in buffer coordinates of all tokens surrounding the + # cursor that match the given scope selector. + # + # For example, if you wanted to find the string surrounding the cursor, you + # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. + # + # Returns a {Range}. + bufferRangeForScopeAtCursor: (selector) -> + @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + + logCursorScope: -> + console.log @scopesAtCursor() + + # {Delegates to: DisplayBuffer.tokenForBufferPosition} + tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) + + # Extended: Determine if the given row is entirely a comment + isBufferRowCommented: (bufferRow) -> + if match = @lineTextForBufferRow(bufferRow).match(/\S/) + scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes + @commentScopeSelector ?= new TextMateScopeSelector('comment.*') + @commentScopeSelector.matches(scopes) + + ### + Section: Clipboard Operations + ### + + # Essential: For each selection, copy the selected text. + copySelectedText: -> + maintainClipboard = false + for selection in @getSelections() + selection.copy(maintainClipboard) + maintainClipboard = true + + # Essential: For each selection, cut the selected text. + cutSelectedText: -> + maintainClipboard = false + @mutateSelectedText (selection) -> + selection.cut(maintainClipboard) + maintainClipboard = true + + # Essential: For each selection, replace the selected text with the contents of + # the clipboard. + # + # If the clipboard contains the same number of selections as the current + # editor, each selection will be replaced with the content of the + # corresponding clipboard selection text. + # + # * `options` (optional) See {Selection::insertText}. + pasteText: (options={}) -> + {text, metadata} = atom.clipboard.readWithMetadata() + + containsNewlines = text.indexOf('\n') isnt -1 + + if metadata?.selections? and metadata.selections.length is @getSelections().length + @mutateSelectedText (selection, index) -> + text = metadata.selections[index] + selection.insertText(text, options) + + return + + else if atom.config.get("editor.normalizeIndentOnPaste") and metadata?.indentBasis? + if !@getLastCursor().hasPrecedingCharactersOnLine() or containsNewlines + options.indentBasis ?= metadata.indentBasis + + @insertText(text, options) + + # Public: For each selection, if the selection is empty, cut all characters + # of the containing line following the cursor. Otherwise cut the selected + # text. + cutToEndOfLine: -> + maintainClipboard = false + @mutateSelectedText (selection) -> + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true + + ### + Section: Folds + ### + + # Essential: Fold the most recent cursor's row based on its indentation level. + # + # The fold will extend from the nearest preceding line with a lower + # indentation level up to the nearest following row with a lower indentation + # level. + foldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @foldBufferRow(bufferRow) + + # Essential: Unfold the most recent cursor's row by one level. + unfoldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @unfoldBufferRow(bufferRow) + + # Essential: Fold the given row in buffer coordinates based on its indentation + # level. + # + # If the given row is foldable, the fold will begin there. Otherwise, it will + # begin at the first foldable row preceding the given row. + # + # * `bufferRow` A {Number}. + foldBufferRow: (bufferRow) -> + @languageMode.foldBufferRow(bufferRow) + + # Essential: Unfold all folds containing the given row in buffer coordinates. + # + # * `bufferRow` A {Number} + unfoldBufferRow: (bufferRow) -> + @displayBuffer.unfoldBufferRow(bufferRow) + + # Extended: For each selection, fold the rows it intersects. + foldSelectedLines: -> + selection.fold() for selection in @getSelections() + + # Extended: Fold all foldable lines. + foldAll: -> + @languageMode.foldAll() + + # Extended: Unfold all existing folds. + unfoldAll: -> + @languageMode.unfoldAll() + + # Extended: Fold all foldable lines at the given indent level. + # + # * `level` A {Number}. + foldAllAtIndentLevel: (level) -> + @languageMode.foldAllAtIndentLevel(level) + + # Extended: Determine whether the given row in buffer coordinates is foldable. + # + # A *foldable* row is a row that *starts* a row range that can be folded. + # + # * `bufferRow` A {Number} + # + # Returns a {Boolean}. + isFoldableAtBufferRow: (bufferRow) -> + @languageMode.isFoldableAtBufferRow(bufferRow) + + # Extended: Determine whether the given row in screen coordinates is foldable. + # + # A *foldable* row is a row that *starts* a row range that can be folded. + # + # * `bufferRow` A {Number} + # + # Returns a {Boolean}. + isFoldableAtScreenRow: (screenRow) -> + bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) + @isFoldableAtBufferRow(bufferRow) + + # Extended: Fold the given buffer row if it isn't currently folded, and unfold + # it otherwise. + toggleFoldAtBufferRow: (bufferRow) -> + if @isFoldedAtBufferRow(bufferRow) + @unfoldBufferRow(bufferRow) + else + @foldBufferRow(bufferRow) + + # Extended: Determine whether the most recently added cursor's row is folded. + # + # Returns a {Boolean}. + isFoldedAtCursorRow: -> + @isFoldedAtScreenRow(@getCursorScreenPosition().row) + + # Extended: Determine whether the given row in buffer coordinates is folded. + # + # * `bufferRow` A {Number} + # + # Returns a {Boolean}. + isFoldedAtBufferRow: (bufferRow) -> + @displayBuffer.isFoldedAtBufferRow(bufferRow) + + # Extended: Determine whether the given row in screen coordinates is folded. + # + # * `screenRow` A {Number} + # + # Returns a {Boolean}. + isFoldedAtScreenRow: (screenRow) -> + @displayBuffer.isFoldedAtScreenRow(screenRow) + + # TODO: Rename to foldRowRange? + createFold: (startRow, endRow) -> + @displayBuffer.createFold(startRow, endRow) + + # {Delegates to: DisplayBuffer.destroyFoldWithId} + destroyFoldWithId: (id) -> + @displayBuffer.destroyFoldWithId(id) + + # Remove any {Fold}s found that intersect the given buffer row. + destroyFoldsIntersectingBufferRange: (bufferRange) -> + for row in [bufferRange.start.row..bufferRange.end.row] + @unfoldBufferRow(row) + + # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} + largestFoldContainingBufferRow: (bufferRow) -> + @displayBuffer.largestFoldContainingBufferRow(bufferRow) + + # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} + largestFoldStartingAtScreenRow: (screenRow) -> + @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + + # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} + outermostFoldsInBufferRowRange: (startRow, endRow) -> + @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) ### Section: Scrolling the Editor @@ -2539,7 +2628,6 @@ class Editor extends Model getRowsPerPage: -> Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels())) - ### Section: Config ### @@ -2556,7 +2644,6 @@ class Editor extends Model else @displayBuffer.setInvisibles(null) - ### Section: Event Handlers ### @@ -2570,7 +2657,7 @@ class Editor extends Model @emitter.emit 'did-change-grammar' handleMarkerCreated: (marker) => - if marker.matchesAttributes(@getSelectionMarkerAttributes()) + if marker.matchesProperties(@getSelectionMarkerAttributes()) @addSelection(marker) ### diff --git a/src/git.coffee b/src/git.coffee index 78916d343..339debf3b 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -10,7 +10,7 @@ GitUtils = require 'git-utils' Task = require './task' -# Public: Represents the underlying git operations performed by Atom. +# Extended: Represents the underlying git operations performed by Atom. # # This class shouldn't be instantiated directly but instead by accessing the # `atom.project` global and calling `getRepo()`. Note that this will only be @@ -47,8 +47,15 @@ class Git EmitterMixin.includeInto(this) Subscriber.includeInto(this) + @exists: (path) -> + if git = @open(path) + git.destroy() + true + else + false + ### - Section: Class Methods + Section: Construction and Destruction ### # Public: Creates a new Git instance. @@ -66,17 +73,6 @@ class Git catch null - @exists: (path) -> - if git = @open(path) - git.destroy() - true - else - false - - ### - Section: Construction - ### - constructor: (path, options={}) -> @emitter = new Emitter @repo = GitUtils.open(path) @@ -100,11 +96,26 @@ class Git if @project? @subscribe @project.eachBuffer (buffer) => @subscribeToBuffer(buffer) + # Public: Destroy this {Git} object. + # + # This destroys any tasks and subscriptions and releases the underlying + # libgit2 repository handle. + destroy: -> + if @statusTask? + @statusTask.terminate() + @statusTask = null + + if @repo? + @repo.release() + @repo = null + + @unsubscribe() + ### Section: Event Subscription ### - # Essential: Invoke the given callback when a specific file's status has + # Public: Invoke the given callback when a specific file's status has # changed. When a file is updated, reloaded, etc, and the status changes, this # will be fired. # @@ -118,7 +129,7 @@ class Git onDidChangeStatus: (callback) -> @emitter.on 'did-change-status', callback - # Essential: Invoke the given callback when a multiple files' statuses have + # Public: Invoke the given callback when a multiple files' statuses have # changed. For example, on window focus, the status of all the paths in the # repo is checked. If any of them have changed, this will be fired. Call # {::getPathStatus(path)} to get the status for your path of choice. @@ -140,7 +151,250 @@ class Git EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Repository Details + ### + + # Public: Returns the {String} path of the repository. + getPath: -> + @path ?= fs.absolute(@getRepo().getPath()) + + # Public: Returns the {String} working directory path of the repository. + getWorkingDirectory: -> @getRepo().getWorkingDirectory() + + # Public: Returns true if at the root, false if in a subfolder of the + # repository. + isProjectAtRoot: -> + @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' + + # Public: Makes a path relative to the repository's working directory. + relativize: (path) -> @getRepo().relativize(path) + + # Public: Returns true if the given branch exists. + hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? + + # Public: Retrieves a shortened version of the HEAD reference value. + # + # This removes the leading segments of `refs/heads`, `refs/tags`, or + # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + # characters. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository contains submodules. + # + # Returns a {String}. + getShortHead: (path) -> @getRepo(path).getShortHead() + + # Public: Is the given path a submodule in the repository? + # + # * `path` The {String} path to check. + # + # Returns a {Boolean}. + isSubmodule: (path) -> + return false unless path + + repo = @getRepo(path) + if repo.isSubmodule(repo.relativize(path)) + true + else + # Check if the path is a working directory in a repo that isn't the root. + repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' + + # Public: Returns the number of commits behind the current branch is from the + # its upstream remote branch. + # + # * `reference` The {String} branch reference name. + # * `path` The {String} path in the repository to get this information for, + # only needed if the repository contains submodules. + getAheadBehindCount: (reference, path) -> + @getRepo(path).getAheadBehindCount(reference) + + # Public: Get the cached ahead/behind commit counts for the current branch's + # upstream branch. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + # + # Returns an {Object} with the following keys: + # * `ahead` The {Number} of commits ahead. + # * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount: (path) -> + @getRepo(path).upstream ? @upstream + + # Public: Returns the git configuration value specified by the key. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) + + # Public: Returns the origin url of the repository. + # + # * `path` (optional) {String} path in the repository to get this information + # for, only needed if the repository has submodules. + getOriginUrl: (path) -> @getConfigValue('remote.origin.url', path) + + # Public: Returns the upstream branch for the current HEAD, or null if there + # is no upstream branch for the current HEAD. + # + # * `path` An optional {String} path in the repo to get this information for, + # only needed if the repository contains submodules. + # + # Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() + + # Public: Gets all the local and remote references. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + # + # Returns an {Object} with the following keys: + # * `heads` An {Array} of head reference names. + # * `remotes` An {Array} of remote reference names. + # * `tags` An {Array} of tag reference names. + getReferences: (path) -> @getRepo(path).getReferences() + + # Public: Returns the current {String} SHA for the given reference. + # + # * `reference` The {String} reference to get the target of. + # * `path` An optional {String} path in the repo to get the reference target + # for. Only needed if the repository contains submodules. + getReferenceTarget: (reference, path) -> + @getRepo(path).getReferenceTarget(reference) + + ### + Section: Reading Status + ### + + # Public: Returns true if the given path is modified. + isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) + + # Public: Returns true if the given path is new. + isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) + + # Public: Is the given path ignored? + # + # Returns a {Boolean}. + isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) + + # Public: Get the status of a directory in the repository's working directory. + # + # * `path` The {String} path to check. + # + # Returns a {Number} representing the status. This value can be passed to + # {::isStatusModified} or {::isStatusNew} to get more information. + getDirectoryStatus: (directoryPath) -> + directoryPath = "#{@relativize(directoryPath)}/" + directoryStatus = 0 + for path, status of @statuses + directoryStatus |= status if path.indexOf(directoryPath) is 0 + directoryStatus + + # Public: Get the status of a single path in the repository. + # + # `path` A {String} repository-relative path. + # + # Returns a {Number} representing the status. This value can be passed to + # {::isStatusModified} or {::isStatusNew} to get more information. + getPathStatus: (path) -> + repo = @getRepo(path) + relativePath = @relativize(path) + currentPathStatus = @statuses[relativePath] ? 0 + pathStatus = repo.getStatus(repo.relativize(path)) ? 0 + pathStatus = 0 if repo.isStatusIgnored(pathStatus) + if pathStatus > 0 + @statuses[relativePath] = pathStatus + else + delete @statuses[relativePath] + if currentPathStatus isnt pathStatus + @emit 'status-changed', path, pathStatus + @emitter.emit 'did-change-status', {path, pathStatus} + + pathStatus + + # Public: Get the cached status for the given path. + # + # * `path` A {String} path in the repository, relative or absolute. + # + # Returns a status {Number} or null if the path is not in the cache. + getCachedPathStatus: (path) -> + @statuses[@relativize(path)] + + # Public: Returns true if the given status indicates modification. + isStatusModified: (status) -> @getRepo().isStatusModified(status) + + # Public: Returns true if the given status indicates a new path. + isStatusNew: (status) -> @getRepo().isStatusNew(status) + + ### + Section: Retrieving Diffs + ### + + # Public: Retrieves the number of lines added and removed to a path. + # + # This compares the working directory contents of the path to the `HEAD` + # version. + # + # * `path` The {String} path to check. + # + # Returns an {Object} with the following keys: + # * `added` The {Number} of added lines. + # * `deleted` The {Number} of deleted lines. + getDiffStats: (path) -> + repo = @getRepo(path) + repo.getDiffStats(repo.relativize(path)) + + # Public: Retrieves the line diffs comparing the `HEAD` version of the given + # path and the given text. + # + # * `path` The {String} path relative to the repository. + # * `text` The {String} to compare against the `HEAD` contents + # + # Returns an {Array} of hunk {Object}s with the following keys: + # * `oldStart` The line {Number} of the old hunk. + # * `newStart` The line {Number} of the new hunk. + # * `oldLines` The {Number} of lines in the old hunk. + # * `newLines` The {Number} of lines in the new hunk + getLineDiffs: (path, text) -> + # Ignore eol of line differences on windows so that files checked in as + # LF don't report every line modified when the text contains CRLF endings. + options = ignoreEolWhitespace: process.platform is 'win32' + repo = @getRepo(path) + repo.getLineDiffs(repo.relativize(path), text, options) + + ### + Section: Checking Out + ### + + # Public: Restore the contents of a path in the working directory and index + # to the version at `HEAD`. + # + # This is essentially the same as running: + # + # ```sh + # git reset HEAD -- + # git checkout HEAD -- + # ``` + # + # * `path` The {String} path to checkout. + # + # Returns a {Boolean} that's true if the method was successful. + checkoutHead: (path) -> + repo = @getRepo(path) + headCheckedOut = repo.checkoutHead(repo.relativize(path)) + @getPathStatus(path) if headCheckedOut + headCheckedOut + + # Public: Checks out a branch in your repository. + # + # * `reference` The {String} reference to checkout. + # * `create` A {Boolean} value which, if true creates the new reference if + # it doesn't exist. + # + # Returns a Boolean that's true if the method was successful. + checkoutReference: (reference, create) -> + @getRepo().checkoutReference(reference, create) + + ### + Section: Private ### # Subscribes to buffer events. @@ -175,21 +429,6 @@ class Git else checkoutHead() - # Public: Destroy this {Git} object. - # - # This destroys any tasks and subscriptions and releases the underlying - # libgit2 repository handle. - destroy: -> - if @statusTask? - @statusTask.terminate() - @statusTask = null - - if @repo? - @repo.release() - @repo = null - - @unsubscribe() - # Returns the corresponding {Repository} getRepo: (path) -> if @repo? @@ -201,233 +440,6 @@ class Git # last time the index was read. refreshIndex: -> @getRepo().refreshIndex() - # Public: Returns the {String} path of the repository. - getPath: -> - @path ?= fs.absolute(@getRepo().getPath()) - - # Public: Returns the {String} working directory path of the repository. - getWorkingDirectory: -> @getRepo().getWorkingDirectory() - - # Public: Get the status of a single path in the repository. - # - # `path` A {String} repository-relative path. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getPathStatus: (path) -> - repo = @getRepo(path) - relativePath = @relativize(path) - currentPathStatus = @statuses[relativePath] ? 0 - pathStatus = repo.getStatus(repo.relativize(path)) ? 0 - pathStatus = 0 if repo.isStatusIgnored(pathStatus) - if pathStatus > 0 - @statuses[relativePath] = pathStatus - else - delete @statuses[relativePath] - if currentPathStatus isnt pathStatus - @emit 'status-changed', path, pathStatus - @emitter.emit 'did-change-status', {path, pathStatus} - - pathStatus - - # Public: Is the given path ignored? - # - # Returns a {Boolean}. - isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) - - # Public: Returns true if the given status indicates modification. - isStatusModified: (status) -> @getRepo().isStatusModified(status) - - # Public: Returns true if the given path is modified. - isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - - # Public: Returns true if the given status indicates a new path. - isStatusNew: (status) -> @getRepo().isStatusNew(status) - - # Public: Returns true if the given path is new. - isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) - - # Public: Returns true if at the root, false if in a subfolder of the - # repository. - isProjectAtRoot: -> - @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' - - # Public: Makes a path relative to the repository's working directory. - relativize: (path) -> @getRepo().relativize(path) - - # Public: Retrieves a shortened version of the HEAD reference value. - # - # This removes the leading segments of `refs/heads`, `refs/tags`, or - # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 - # characters. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository contains submodules. - # - # Returns a {String}. - getShortHead: (path) -> @getRepo(path).getShortHead() - - # Public: Restore the contents of a path in the working directory and index - # to the version at `HEAD`. - # - # This is essentially the same as running: - # - # ```sh - # git reset HEAD -- - # git checkout HEAD -- - # ``` - # - # * `path` The {String} path to checkout. - # - # Returns a {Boolean} that's true if the method was successful. - checkoutHead: (path) -> - repo = @getRepo(path) - headCheckedOut = repo.checkoutHead(repo.relativize(path)) - @getPathStatus(path) if headCheckedOut - headCheckedOut - - # Public: Checks out a branch in your repository. - # - # * `reference` The {String} reference to checkout. - # * `create` A {Boolean} value which, if true creates the new reference if - # it doesn't exist. - # - # Returns a Boolean that's true if the method was successful. - checkoutReference: (reference, create) -> - @getRepo().checkoutReference(reference, create) - - # Public: Retrieves the number of lines added and removed to a path. - # - # This compares the working directory contents of the path to the `HEAD` - # version. - # - # * `path` The {String} path to check. - # - # Returns an {Object} with the following keys: - # * `added` The {Number} of added lines. - # * `deleted` The {Number} of deleted lines. - getDiffStats: (path) -> - repo = @getRepo(path) - repo.getDiffStats(repo.relativize(path)) - - # Public: Is the given path a submodule in the repository? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean}. - isSubmodule: (path) -> - return false unless path - - repo = @getRepo(path) - if repo.isSubmodule(repo.relativize(path)) - true - else - # Check if the path is a working directory in a repo that isn't the root. - repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' - - # Public: Get the status of a directory in the repository's working directory. - # - # * `path` The {String} path to check. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus: (directoryPath) -> - directoryPath = "#{@relativize(directoryPath)}/" - directoryStatus = 0 - for path, status of @statuses - directoryStatus |= status if path.indexOf(directoryPath) is 0 - directoryStatus - - # Public: Retrieves the line diffs comparing the `HEAD` version of the given - # path and the given text. - # - # * `path` The {String} path relative to the repository. - # * `text` The {String} to compare against the `HEAD` contents - # - # Returns an {Array} of hunk {Object}s with the following keys: - # * `oldStart` The line {Number} of the old hunk. - # * `newStart` The line {Number} of the new hunk. - # * `oldLines` The {Number} of lines in the old hunk. - # * `newLines` The {Number} of lines in the new hunk - getLineDiffs: (path, text) -> - # Ignore eol of line differences on windows so that files checked in as - # LF don't report every line modified when the text contains CRLF endings. - options = ignoreEolWhitespace: process.platform is 'win32' - repo = @getRepo(path) - repo.getLineDiffs(repo.relativize(path), text, options) - - # Public: Returns the git configuration value specified by the key. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) - - # Public: Returns the origin url of the repository. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getOriginUrl: (path) -> @getConfigValue('remote.origin.url', path) - - # Public: Returns the upstream branch for the current HEAD, or null if there - # is no upstream branch for the current HEAD. - # - # * `path` An optional {String} path in the repo to get this information for, - # only needed if the repository contains submodules. - # - # Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() - - # Public: Returns the current {String} SHA for the given reference. - # - # * `reference` The {String} reference to get the target of. - # * `path` An optional {String} path in the repo to get the reference target - # for. Only needed if the repository contains submodules. - getReferenceTarget: (reference, path) -> - @getRepo(path).getReferenceTarget(reference) - - # Public: Gets all the local and remote references. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `heads` An {Array} of head reference names. - # * `remotes` An {Array} of remote reference names. - # * `tags` An {Array} of tag reference names. - getReferences: (path) -> @getRepo(path).getReferences() - - # Public: Returns the number of commits behind the current branch is from the - # its upstream remote branch. - # - # * `reference` The {String} branch reference name. - # * `path` The {String} path in the repository to get this information for, - # only needed if the repository contains submodules. - getAheadBehindCount: (reference, path) -> - @getRepo(path).getAheadBehindCount(reference) - - # Public: Get the cached ahead/behind commit counts for the current branch's - # upstream branch. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `ahead` The {Number} of commits ahead. - # * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount: (path) -> - @getRepo(path).upstream ? @upstream - - # Public: Get the cached status for the given path. - # - # * `path` A {String} path in the repository, relative or absolute. - # - # Returns a status {Number} or null if the path is not in the cache. - getCachedPathStatus: (path) -> - @statuses[@relativize(path)] - - # Public: Returns true if the given branch exists. - hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? - # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. refreshStatus: -> diff --git a/src/language-mode.coffee b/src/language-mode.coffee index de1a5b928..6ce4b7b3a 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -148,15 +148,20 @@ class LanguageMode return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() startRow = bufferRow - for currentRow in [bufferRow-1..0] - break if @buffer.isRowBlank(currentRow) - break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() - startRow = currentRow endRow = bufferRow - for currentRow in [bufferRow+1..@buffer.getLastRow()] - break if @buffer.isRowBlank(currentRow) - break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() - endRow = currentRow + + if bufferRow > 0 + for currentRow in [bufferRow-1..0] + break if @buffer.isRowBlank(currentRow) + break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() + startRow = currentRow + + if bufferRow < @buffer.getLastRow() + for currentRow in [bufferRow+1..@buffer.getLastRow()] + break if @buffer.isRowBlank(currentRow) + break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment() + endRow = currentRow + return [startRow, endRow] if startRow isnt endRow rowRangeForCodeFoldAtBufferRow: (bufferRow) -> diff --git a/src/marker.coffee b/src/marker.coffee new file mode 100644 index 000000000..e54fc5248 --- /dev/null +++ b/src/marker.coffee @@ -0,0 +1,399 @@ +{Range} = require 'text-buffer' +_ = require 'underscore-plus' +{Subscriber} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' +Grim = require 'grim' + +# Essential: Represents a buffer annotation that remains logically stationary +# even as the buffer changes. This is used to represent cursors, folds, snippet +# targets, misspelled words, and anything else that needs to track a logical +# location in the buffer over time. +# +# ### Marker Creation +# +# Use {Editor::markBufferRange} rather than creating Markers directly. +# +# ### Head and Tail +# +# Markers always have a *head* and sometimes have a *tail*. If you think of a +# marker as an editor selection, the tail is the part that's stationary and the +# head is the part that moves when the mouse is moved. A marker without a tail +# always reports an empty range at the head position. A marker with a head position +# greater than the tail is in a "normal" orientation. If the head precedes the +# tail the marker is in a "reversed" orientation. +# +# ### Validity +# +# Markers are considered *valid* when they are first created. Depending on the +# invalidation strategy you choose, certain changes to the buffer can cause a +# marker to become invalid, for example if the text surrounding the marker is +# deleted. The strategies, in order of descending fragility: +# +# * __never__: The marker is never marked as invalid. This is a good choice for +# markers representing selections in an editor. +# * __surround__: The marker is invalidated by changes that completely surround it. +# * __overlap__: The marker is invalidated by changes that surround the +# start or end of the marker. This is the default. +# * __inside__: The marker is invalidated by changes that extend into the +# inside of the marker. Changes that end at the marker's start or +# start at the marker's end do not invalidate the marker. +# * __touch__: The marker is invalidated by a change that touches the marked +# region in any way, including changes that end at the marker's +# start or start at the marker's end. This is the most fragile strategy. +# +# See {Editor::markBufferRange} for usage. +module.exports = +class Marker + EmitterMixin.includeInto(this) + Subscriber.includeInto(this) + + bufferMarkerSubscription: null + oldHeadBufferPosition: null + oldHeadScreenPosition: null + oldTailBufferPosition: null + oldTailScreenPosition: null + wasValid: true + deferredChangeEvents: null + + ### + Section: Construction and Destruction + ### + + constructor: ({@bufferMarker, @displayBuffer}) -> + @emitter = new Emitter + @id = @bufferMarker.id + @oldHeadBufferPosition = @getHeadBufferPosition() + @oldHeadScreenPosition = @getHeadScreenPosition() + @oldTailBufferPosition = @getTailBufferPosition() + @oldTailScreenPosition = @getTailScreenPosition() + @wasValid = @isValid() + + @subscribe @bufferMarker.onDidDestroy => @destroyed() + @subscribe @bufferMarker.onDidChange (event) => @notifyObservers(event) + + # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once + # destroyed, a marker cannot be restored by undo/redo operations. + destroy: -> + @bufferMarker.destroy() + @unsubscribe() + + # Essential: Creates and returns a new {Marker} with the same properties as this + # marker. + # + # * `properties` {Object} + copy: (properties) -> + @displayBuffer.getMarker(@bufferMarker.copy(properties).id) + + ### + Section: Event Subscription + ### + + # Essential: Invoke the given callback when the state of the marker changes. + # + # * `callback` {Function} to be called when the marker changes. + # * `event` {Object} with the following keys: + # * `oldHeadPosition` {Point} representing the former head position + # * `newHeadPosition` {Point} representing the new head position + # * `oldTailPosition` {Point} representing the former tail position + # * `newTailPosition` {Point} representing the new tail position + # * `wasValid` {Boolean} indicating whether the marker was valid before the change + # * `isValid` {Boolean} indicating whether the marker is now valid + # * `hadTail` {Boolean} indicating whether the marker had a tail before the change + # * `hasTail` {Boolean} indicating whether the marker now has a tail + # * `oldProperties` {Object} containing the marker's custom properties before the change. + # * `newProperties` {Object} containing the marker's custom properties after the change. + # * `textChanged` {Boolean} indicating whether this change was caused by a textual change + # to the buffer or whether the marker was manipulated directly via its public API. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange: (callback) -> + @emitter.on 'did-change', callback + + # Essential: Invoke the given callback when the marker is destroyed. + # + # * `callback` {Function} to be called when the marker is destroyed. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + on: (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use Marker::onDidChange instead") + when 'destroyed' + Grim.deprecate("Use Marker::onDidDestroy instead") + + EmitterMixin::on.apply(this, arguments) + + ### + Section: Marker Details + ### + + # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be + # invalidated when a region surrounding them in the buffer is changed. + isValid: -> + @bufferMarker.isValid() + + # Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker + # can be invalid without being destroyed, in which case undoing the invalidating + # operation would restore the marker. Once a marker is destroyed by calling + # {Marker::destroy}, no undo/redo operation can ever bring it back. + isDestroyed: -> + @bufferMarker.isDestroyed() + + # Essential: Returns a {Boolean} indicating whether the head precedes the tail. + isReversed: -> + @bufferMarker.isReversed() + + # Essential: Get the invalidation strategy for this marker. + # + # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`. + # + # Returns a {String}. + getInvalidationStrategy: -> + @bufferMarker.getInvalidationStrategy() + + # Essential: Returns an {Object} containing any custom properties associated with + # the marker. + getProperties: -> + @bufferMarker.getProperties() + getAttributes: -> + deprecate 'Use Marker::getProperties instead' + @getProperties() + + # Essential: Merges an {Object} containing new properties into the marker's + # existing properties. + # + # * `properties` {Object} + setProperties: (properties) -> + @bufferMarker.setProperties(properties) + setAttributes: (properties) -> + deprecate 'Use Marker::getProperties instead' + @setProperties(properties) + + matchesProperties: (attributes) -> + attributes = @displayBuffer.translateToBufferMarkerParams(attributes) + @bufferMarker.matchesParams(attributes) + matchesAttributes: (attributes) -> + deprecate 'Use Marker::matchesProperties instead' + @matchesProperties(attributes) + + ### + Section: Comparing to other markers + ### + + # Essential: Returns a {Boolean} indicating whether this marker is equivalent to + # another marker, meaning they have the same range and options. + # + # * `other` {Marker} other marker + isEqual: (other) -> + return false unless other instanceof @constructor + @bufferMarker.isEqual(other.bufferMarker) + + # Essential: Compares this marker to another based on their ranges. + # + # * `other` {Marker} + # + # Returns a {Number} + compare: (other) -> + @bufferMarker.compare(other.bufferMarker) + + ### + Section: Managing the marker's range + ### + + # Essential: Gets the buffer range of the display marker. + # + # Returns a {Range}. + getBufferRange: -> + @bufferMarker.getRange() + + # Essential: Modifies the buffer range of the display marker. + # + # * `bufferRange` The new {Range} to use + # * `properties` (optional) {Object} properties to associate with the marker. + # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. + setBufferRange: (bufferRange, properties) -> + @bufferMarker.setRange(bufferRange, properties) + + # Essential: Gets the screen range of the display marker. + # + # Returns a {Range}. + getScreenRange: -> + @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true) + + # Essential: Modifies the screen range of the display marker. + # + # * `screenRange` The new {Range} to use + # * `properties` (optional) {Object} properties to associate with the marker. + # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation. + setScreenRange: (screenRange, options) -> + @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options) + + # Essential: Retrieves the buffer position of the marker's start. This will always be + # less than or equal to the result of {Marker::getEndBufferPosition}. + # + # Returns a {Point}. + getStartBufferPosition: -> + @bufferMarker.getStartPosition() + + # Essential: Retrieves the screen position of the marker's start. This will always be + # less than or equal to the result of {Marker::getEndScreenPosition}. + # + # Returns a {Point}. + getStartScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true) + + # Essential: Retrieves the buffer position of the marker's end. This will always be + # greater than or equal to the result of {Marker::getStartBufferPosition}. + # + # Returns a {Point}. + getEndBufferPosition: -> + @bufferMarker.getEndPosition() + + # Essential: Retrieves the screen position of the marker's end. This will always be + # greater than or equal to the result of {Marker::getStartScreenPosition}. + # + # Returns a {Point}. + getEndScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true) + + # Extended: Retrieves the buffer position of the marker's head. + # + # Returns a {Point}. + getHeadBufferPosition: -> + @bufferMarker.getHeadPosition() + + # Extended: Sets the buffer position of the marker's head. + # + # * `screenRange` The new {Point} to use + # * `properties` (optional) {Object} properties to associate with the marker. + setHeadBufferPosition: (bufferPosition, properties) -> + @bufferMarker.setHeadPosition(bufferPosition, properties) + + # Extended: Retrieves the screen position of the marker's head. + # + # Returns a {Point}. + getHeadScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true) + + # Extended: Sets the screen position of the marker's head. + # + # * `screenRange` The new {Point} to use + # * `properties` (optional) {Object} properties to associate with the marker. + setHeadScreenPosition: (screenPosition, properties) -> + screenPosition = @displayBuffer.clipScreenPosition(screenPosition, properties) + @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties)) + + # Extended: Retrieves the buffer position of the marker's tail. + # + # Returns a {Point}. + getTailBufferPosition: -> + @bufferMarker.getTailPosition() + + # Extended: Sets the buffer position of the marker's tail. + # + # * `screenRange` The new {Point} to use + # * `properties` (optional) {Object} properties to associate with the marker. + setTailBufferPosition: (bufferPosition) -> + @bufferMarker.setTailPosition(bufferPosition) + + # Extended: Retrieves the screen position of the marker's tail. + # + # Returns a {Point}. + getTailScreenPosition: -> + @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true) + + # Extended: Sets the screen position of the marker's tail. + # + # * `screenRange` The new {Point} to use + # * `properties` (optional) {Object} properties to associate with the marker. + setTailScreenPosition: (screenPosition, options) -> + screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) + @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) + + # Extended: Returns a {Boolean} indicating whether the marker has a tail. + hasTail: -> + @bufferMarker.hasTail() + + # Extended: Plants the marker's tail at the current head position. After calling + # the marker's tail position will be its head position at the time of the + # call, regardless of where the marker's head is moved. + # + # * `properties` (optional) {Object} properties to associate with the marker. + plantTail: -> + @bufferMarker.plantTail() + + # Extended: Removes the marker's tail. After calling the marker's head position + # will be reported as its current tail position until the tail is planted + # again. + # + # * `properties` (optional) {Object} properties to associate with the marker. + clearTail: (properties) -> + @bufferMarker.clearTail(properties) + + ### + Section: Private utility methods + ### + + # Returns a {String} representation of the marker + inspect: -> + "Marker(id: #{@id}, bufferRange: #{@getBufferRange()})" + + destroyed: -> + delete @displayBuffer.markers[@id] + @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() + + notifyObservers: ({textChanged}) -> + textChanged ?= false + + newHeadBufferPosition = @getHeadBufferPosition() + newHeadScreenPosition = @getHeadScreenPosition() + newTailBufferPosition = @getTailBufferPosition() + newTailScreenPosition = @getTailScreenPosition() + isValid = @isValid() + + return if _.isEqual(isValid, @wasValid) and + _.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and + _.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and + _.isEqual(newTailBufferPosition, @oldTailBufferPosition) and + _.isEqual(newTailScreenPosition, @oldTailScreenPosition) + + changeEvent = { + @oldHeadScreenPosition, newHeadScreenPosition, + @oldTailScreenPosition, newTailScreenPosition, + @oldHeadBufferPosition, newHeadBufferPosition, + @oldTailBufferPosition, newTailBufferPosition, + textChanged, + isValid + } + + if @deferredChangeEvents? + @deferredChangeEvents.push(changeEvent) + else + @emit 'changed', changeEvent + @emitter.emit 'did-change', changeEvent + + @oldHeadBufferPosition = newHeadBufferPosition + @oldHeadScreenPosition = newHeadScreenPosition + @oldTailBufferPosition = newTailBufferPosition + @oldTailScreenPosition = newTailScreenPosition + @wasValid = isValid + + pauseChangeEvents: -> + @deferredChangeEvents = [] + + resumeChangeEvents: -> + if deferredChangeEvents = @deferredChangeEvents + @deferredChangeEvents = null + + for event in deferredChangeEvents + @emit 'changed', event + @emitter.emit 'did-change', event + + getPixelRange: -> + @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index da5ae6507..89462ec7a 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -5,7 +5,7 @@ ipc = require 'ipc' CSON = require 'season' fs = require 'fs-plus' -# Public: Provides a registry for menu items that you'd like to appear in the +# Extended: Provides a registry for menu items that you'd like to appear in the # application menu. # # An instance of this class is always available as the `atom.menu` global. diff --git a/src/package-manager.coffee b/src/package-manager.coffee index f95073e10..838eda68e 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -48,7 +48,7 @@ class PackageManager Section: Event Subscription ### - # Essential: Invoke the given callback when all packages have been activated. + # Public: Invoke the given callback when all packages have been activated. # # * `callback` {Function} # @@ -56,7 +56,7 @@ class PackageManager onDidLoadAll: (callback) -> @emitter.on 'did-load-all', callback - # Essential: Invoke the given callback when all packages have been activated. + # Public: Invoke the given callback when all packages have been activated. # # * `callback` {Function} # @@ -75,10 +75,10 @@ class PackageManager EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Package system data ### - # Extended: Get the path to the apm command. + # Public: Get the path to the apm command. # # Return a {String} file path to apm. getApmPath: -> @@ -86,19 +86,43 @@ class PackageManager commandName += '.cmd' if process.platform is 'win32' @apmPath ?= path.resolve(__dirname, '..', 'apm', 'node_modules', 'atom-package-manager', 'bin', commandName) - # Extended: Get the paths being used to look for packages. + # Public: Get the paths being used to look for packages. # # Returns an {Array} of {String} directory paths. getPackageDirPaths: -> _.clone(@packageDirPaths) - getPackageState: (name) -> - @packageStates[name] + ### + Section: General package data + ### - setPackageState: (name, state) -> - @packageStates[name] = state + # Public: Resolve the given package name to a path on disk. + # + # * `name` - The {String} package name. + # + # Return a {String} folder path or undefined if it could not be resolved. + resolvePackagePath: (name) -> + return name if fs.isDirectorySync(name) - # Extended: Enable the package with the given name. + packagePath = fs.resolve(@packageDirPaths..., name) + return packagePath if fs.isDirectorySync(packagePath) + + packagePath = path.join(@resourcePath, 'node_modules', name) + return packagePath if @hasAtomEngine(packagePath) + + # Public: Is the package with the given name bundled with Atom? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isBundledPackage: (name) -> + @getPackageDependencies().hasOwnProperty(name) + + ### + Section: Enabling and disabling packages + ### + + # Public: Enable the package with the given name. # # Returns the {Package} that was enabled or null if it isn't loaded. enablePackage: (name) -> @@ -106,7 +130,7 @@ class PackageManager pack?.enable() pack - # Extended: Disable the package with the given name. + # Public: Disable the package with the given name. # # Returns the {Package} that was disabled or null if it isn't loaded. disablePackage: (name) -> @@ -114,51 +138,23 @@ class PackageManager pack?.disable() pack - # Activate all the packages that should be activated. - activate: -> - for [activator, types] in @packageActivators - packages = @getLoadedPackagesForTypes(types) - activator.activatePackages(packages) - @emit 'activated' - @emitter.emit 'did-activate-all' + # Public: Is the package with the given name disabled? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isPackageDisabled: (name) -> + _.include(atom.config.get('core.disabledPackages') ? [], name) - # another type of package manager can handle other package types. - # See ThemeManager - registerPackageActivator: (activator, types) -> - @packageActivators.push([activator, types]) + ### + Section: Accessing active packages + ### - activatePackages: (packages) -> - @activatePackage(pack.name) for pack in packages - @observeDisabledPackages() - - # Activate a single package by name - activatePackage: (name) -> - if pack = @getActivePackage(name) - Q(pack) - else - pack = @loadPackage(name) - pack.activate().then => - @activePackages[pack.name] = pack - pack - - # Deactivate all packages - deactivatePackages: -> - @deactivatePackage(pack.name) for pack in @getLoadedPackages() - @unobserveDisabledPackages() - - # Deactivate the package with the given name - deactivatePackage: (name) -> - pack = @getLoadedPackage(name) - if @isPackageActive(name) - @setPackageState(pack.name, state) if state = pack.serialize?() - pack.deactivate() - delete @activePackages[pack.name] - - # Essential: Get an {Array} of all the active {Package}s. + # Public: Get an {Array} of all the active {Package}s. getActivePackages: -> _.values(@activePackages) - # Essential: Get the active {Package} with the given name. + # Public: Get the active {Package} with the given name. # # * `name` - The {String} package name. # @@ -174,6 +170,91 @@ class PackageManager isPackageActive: (name) -> @getActivePackage(name)? + ### + Section: Accessing loaded packages + ### + + # Public: Get an {Array} of all the loaded {Package}s + getLoadedPackages: -> + _.values(@loadedPackages) + + # Get packages for a certain package type + # + # * `types` an {Array} of {String}s like ['atom', 'textmate']. + getLoadedPackagesForTypes: (types) -> + pack for pack in @getLoadedPackages() when pack.getType() in types + + # Public: Get the loaded {Package} with the given name. + # + # * `name` - The {String} package name. + # + # Returns a {Package} or undefined. + getLoadedPackage: (name) -> + @loadedPackages[name] + + # Public: Is the package with the given name loaded? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isPackageLoaded: (name) -> + @getLoadedPackage(name)? + + ### + Section: Accessing available packages + ### + + # Public: Get an {Array} of {String}s of all the available package paths. + getAvailablePackagePaths: -> + packagePaths = [] + + for packageDirPath in @packageDirPaths + for packagePath in fs.listSync(packageDirPath) + packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) + + packagesPath = path.join(@resourcePath, 'node_modules') + for packageName, packageVersion of @getPackageDependencies() + packagePath = path.join(packagesPath, packageName) + packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) + + _.uniq(packagePaths) + + # Public: Get an {Array} of {String}s of all the available package names. + getAvailablePackageNames: -> + _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath) + + # Public: Get an {Array} of {String}s of all the available package metadata. + getAvailablePackageMetadata: -> + packages = [] + for packagePath in @getAvailablePackagePaths() + name = path.basename(packagePath) + metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) + packages.push(metadata) + packages + + ### + Section: Private + ### + + getPackageState: (name) -> + @packageStates[name] + + setPackageState: (name, state) -> + @packageStates[name] = state + + getPackageDependencies: -> + unless @packageDependencies? + try + metadataPath = path.join(@resourcePath, 'package.json') + {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {} + @packageDependencies ?= {} + + @packageDependencies + + hasAtomEngine: (packagePath) -> + metadata = Package.loadMetadata(packagePath, true) + metadata?.engines?.atom? + unobserveDisabledPackages: -> @disabledPackagesSubscription?.off() @disabledPackagesSubscription = null @@ -234,99 +315,42 @@ class PackageManager else throw new Error("No loaded package for name '#{name}'") - # Essential: Get the loaded {Package} with the given name. - # - # * `name` - The {String} package name. - # - # Returns a {Package} or undefined. - getLoadedPackage: (name) -> - @loadedPackages[name] + # Activate all the packages that should be activated. + activate: -> + for [activator, types] in @packageActivators + packages = @getLoadedPackagesForTypes(types) + activator.activatePackages(packages) + @emit 'activated' + @emitter.emit 'did-activate-all' - # Essential: Is the package with the given name loaded? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageLoaded: (name) -> - @getLoadedPackage(name)? + # another type of package manager can handle other package types. + # See ThemeManager + registerPackageActivator: (activator, types) -> + @packageActivators.push([activator, types]) - # Essential: Get an {Array} of all the loaded {Package}s - getLoadedPackages: -> - _.values(@loadedPackages) + activatePackages: (packages) -> + @activatePackage(pack.name) for pack in packages + @observeDisabledPackages() - # Get packages for a certain package type - # - # * `types` an {Array} of {String}s like ['atom', 'textmate']. - getLoadedPackagesForTypes: (types) -> - pack for pack in @getLoadedPackages() when pack.getType() in types + # Activate a single package by name + activatePackage: (name) -> + if pack = @getActivePackage(name) + Q(pack) + else + pack = @loadPackage(name) + pack.activate().then => + @activePackages[pack.name] = pack + pack - # Extended: Resolve the given package name to a path on disk. - # - # * `name` - The {String} package name. - # - # Return a {String} folder path or undefined if it could not be resolved. - resolvePackagePath: (name) -> - return name if fs.isDirectorySync(name) + # Deactivate all packages + deactivatePackages: -> + @deactivatePackage(pack.name) for pack in @getLoadedPackages() + @unobserveDisabledPackages() - packagePath = fs.resolve(@packageDirPaths..., name) - return packagePath if fs.isDirectorySync(packagePath) - - packagePath = path.join(@resourcePath, 'node_modules', name) - return packagePath if @hasAtomEngine(packagePath) - - # Essential: Is the package with the given name disabled? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageDisabled: (name) -> - _.include(atom.config.get('core.disabledPackages') ? [], name) - - hasAtomEngine: (packagePath) -> - metadata = Package.loadMetadata(packagePath, true) - metadata?.engines?.atom? - - # Extended: Is the package with the given name bundled with Atom? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isBundledPackage: (name) -> - @getPackageDependencies().hasOwnProperty(name) - - getPackageDependencies: -> - unless @packageDependencies? - try - metadataPath = path.join(@resourcePath, 'package.json') - {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {} - @packageDependencies ?= {} - - @packageDependencies - - # Extended: Get an {Array} of {String}s of all the available package paths. - getAvailablePackagePaths: -> - packagePaths = [] - - for packageDirPath in @packageDirPaths - for packagePath in fs.listSync(packageDirPath) - packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) - - packagesPath = path.join(@resourcePath, 'node_modules') - for packageName, packageVersion of @getPackageDependencies() - packagePath = path.join(packagesPath, packageName) - packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) - - _.uniq(packagePaths) - - # Extended: Get an {Array} of {String}s of all the available package names. - getAvailablePackageNames: -> - _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath) - - # Extended: Get an {Array} of {String}s of all the available package metadata. - getAvailablePackageMetadata: -> - packages = [] - for packagePath in @getAvailablePackagePaths() - name = path.basename(packagePath) - metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) - packages.push(metadata) - packages + # Deactivate the package with the given name + deactivatePackage: (name) -> + pack = @getLoadedPackage(name) + if @isPackageActive(name) + @setPackageState(pack.name, state) if state = pack.serialize?() + pack.deactivate() + delete @activePackages[pack.name] diff --git a/src/pane-view.coffee b/src/pane-view.coffee index 21f4c8cfa..0dca0bf6c 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -6,7 +6,7 @@ PropertyAccessors = require 'property-accessors' Pane = require './pane' -# Extended: A container which can contains multiple items to be switched between. +# A container which can contains multiple items to be switched between. # # Items can be almost anything however most commonly they're {EditorView}s. # @@ -162,7 +162,7 @@ class PaneView extends View deprecate 'Please return a Disposable object from your ::onDidChangeTitle method!' unless disposable?.dispose? @activeItemDisposables.add(disposable) if disposable?.dispose? else if item.on? - deprecate '::on methods for items are no longer supported. If you would like your item to title change behavior, please implement a ::onDidChangeTitle() method.' + deprecate '::on methods for items are no longer supported. If you would like your item to support title change behavior, please implement a ::onDidChangeTitle() method.' disposable = item.on('title-changed', @activeItemTitleChanged) @activeItemDisposables.add(disposable) if disposable?.dispose? diff --git a/src/pane.coffee b/src/pane.coffee index c6351f656..658cee6dd 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -518,6 +518,7 @@ class Pane extends Model destroyed: -> @container.activateNextPane() if @isActive() @emitter.emit 'did-destroy' + @emitter.dispose() item.destroy?() for item in @items.slice() ### diff --git a/src/project.coffee b/src/project.coffee index 371a7af3e..361d9c950 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -23,14 +23,16 @@ class Project extends Model atom.deserializers.add(this) Serializable.includeInto(this) - # Public: Find the local path for the given repository URL. - # - # * `repoUrl` {String} url to a git repository @pathForRepositoryUrl: (repoUrl) -> + deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.' [repoName] = url.parse(repoUrl).path.split('/')[-1..] repoName = repoName.replace(/\.git$/, '') path.join(atom.config.get('core.projectHome'), repoName) + ### + Section: Construction and Destruction + ### + constructor: ({path, @buffers}={}) -> @buffers ?= [] @@ -40,14 +42,6 @@ class Project extends Model @setPath(path) - serializeParams: -> - path: @path - buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained()) - - deserializeParams: (params) -> - params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) - params - destroyed: -> buffer.destroy() for buffer in @getBuffers() @destroyRepo() @@ -60,9 +54,29 @@ class Project extends Model destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() + ### + Section: Serialization + ### + + serializeParams: -> + path: @path + buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained()) + + deserializeParams: (params) -> + params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) + params + + ### + Section: Accessing the git repository + ### + # Public: Returns the {Git} repository if available. getRepo: -> @repo + ### + Section: Managing Paths + ### + # Public: Returns the project's {String} fullpath. getPath: -> @rootDirectory?.path @@ -124,6 +138,99 @@ class Project extends Model contains: (pathToCheck) -> @rootDirectory?.contains(pathToCheck) ? false + ### + Section: Searching and Replacing + ### + + # Public: Performs a search across all the files in the project. + # + # * `regex` {RegExp} to search with. + # * `options` (optional) {Object} (default: {}) + # * `paths` An {Array} of glob patterns to search within + # * `iterator` {Function} callback on each file found + scan: (regex, options={}, iterator) -> + if _.isFunction(options) + iterator = options + options = {} + + deferred = Q.defer() + + searchOptions = + ignoreCase: regex.ignoreCase + inclusions: options.paths + includeHidden: true + excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') + exclusions: atom.config.get('core.ignoredNames') + + task = Task.once require.resolve('./scan-handler'), @getPath(), regex.source, searchOptions, -> + deferred.resolve() + + task.on 'scan:result-found', (result) => + iterator(result) unless @isPathModified(result.filePath) + + task.on 'scan:file-error', (error) -> + iterator(null, error) + + if _.isFunction(options.onPathsSearched) + task.on 'scan:paths-searched', (numberOfPathsSearched) -> + options.onPathsSearched(numberOfPathsSearched) + + for buffer in @getBuffers() when buffer.isModified() + filePath = buffer.getPath() + continue unless @contains(filePath) + matches = [] + buffer.scan regex, (match) -> matches.push match + iterator {filePath, matches} if matches.length > 0 + + promise = deferred.promise + promise.cancel = -> + task.terminate() + deferred.resolve('cancelled') + promise + + # Public: Performs a replace across all the specified files in the project. + # + # * `regex` A {RegExp} to search with. + # * `replacementText` Text to replace all matches of regex with + # * `filePaths` List of file path strings to run the replace on. + # * `iterator` A {Function} callback on each file with replacements: + # * `options` {Object} with keys `filePath` and `replacements` + replace: (regex, replacementText, filePaths, iterator) -> + deferred = Q.defer() + + openPaths = (buffer.getPath() for buffer in @getBuffers()) + outOfProcessPaths = _.difference(filePaths, openPaths) + + inProcessFinished = !openPaths.length + outOfProcessFinished = !outOfProcessPaths.length + checkFinished = -> + deferred.resolve() if outOfProcessFinished and inProcessFinished + + unless outOfProcessFinished.length + flags = 'g' + flags += 'i' if regex.ignoreCase + + task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> + outOfProcessFinished = true + checkFinished() + + task.on 'replace:path-replaced', iterator + task.on 'replace:file-error', (error) -> iterator(null, error) + + for buffer in @getBuffers() + continue unless buffer.getPath() in filePaths + replacements = buffer.replace(regex, replacementText, iterator) + iterator({filePath: buffer.getPath(), replacements}) if replacements + + inProcessFinished = true + checkFinished() + + deferred.promise + + ### + Section: Private + ### + # Given a path to a file, this constructs and associates a new # {Editor}, showing the file. # @@ -222,91 +329,6 @@ class Project extends Model [buffer] = @buffers.splice(index, 1) buffer?.destroy() - # Public: Performs a search across all the files in the project. - # - # * `regex` {RegExp} to search with. - # * `options` (optional) {Object} (default: {}) - # * `paths` An {Array} of glob patterns to search within - # * `iterator` {Function} callback on each file found - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - deferred = Q.defer() - - searchOptions = - ignoreCase: regex.ignoreCase - inclusions: options.paths - includeHidden: true - excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') - exclusions: atom.config.get('core.ignoredNames') - - task = Task.once require.resolve('./scan-handler'), @getPath(), regex.source, searchOptions, -> - deferred.resolve() - - task.on 'scan:result-found', (result) => - iterator(result) unless @isPathModified(result.filePath) - - task.on 'scan:file-error', (error) -> - iterator(null, error) - - if _.isFunction(options.onPathsSearched) - task.on 'scan:paths-searched', (numberOfPathsSearched) -> - options.onPathsSearched(numberOfPathsSearched) - - for buffer in @getBuffers() when buffer.isModified() - filePath = buffer.getPath() - continue unless @contains(filePath) - matches = [] - buffer.scan regex, (match) -> matches.push match - iterator {filePath, matches} if matches.length > 0 - - promise = deferred.promise - promise.cancel = -> - task.terminate() - deferred.resolve('cancelled') - promise - - # Public: Performs a replace across all the specified files in the project. - # - # * `regex` A {RegExp} to search with. - # * `replacementText` Text to replace all matches of regex with - # * `filePaths` List of file path strings to run the replace on. - # * `iterator` A {Function} callback on each file with replacements: - # * `options` {Object} with keys `filePath` and `replacements` - replace: (regex, replacementText, filePaths, iterator) -> - deferred = Q.defer() - - openPaths = (buffer.getPath() for buffer in @getBuffers()) - outOfProcessPaths = _.difference(filePaths, openPaths) - - inProcessFinished = !openPaths.length - outOfProcessFinished = !outOfProcessPaths.length - checkFinished = -> - deferred.resolve() if outOfProcessFinished and inProcessFinished - - unless outOfProcessFinished.length - flags = 'g' - flags += 'i' if regex.ignoreCase - - task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> - outOfProcessFinished = true - checkFinished() - - task.on 'replace:path-replaced', iterator - task.on 'replace:file-error', (error) -> iterator(null, error) - - for buffer in @getBuffers() - continue unless buffer.getPath() in filePaths - replacements = buffer.replace(regex, replacementText, iterator) - iterator({filePath: buffer.getPath(), replacements}) if replacements - - inProcessFinished = true - checkFinished() - - deferred.promise - buildEditorForBuffer: (buffer, editorOptions) -> editor = new Editor(_.extend({buffer, registerEditor: true}, editorOptions)) editor diff --git a/src/scroll-view.coffee b/src/scroll-view.coffee index 10e760e1b..c6d470579 100644 --- a/src/scroll-view.coffee +++ b/src/scroll-view.coffee @@ -1,6 +1,6 @@ {View} = require './space-pen-extensions' -# Public: Represents a view that scrolls. +# Extended: Represents a view that scrolls. # # Handles several core events to update scroll position: # diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee index 4a494da5e..1e23d686c 100644 --- a/src/scrollbar-component.coffee +++ b/src/scrollbar-component.coffee @@ -9,9 +9,11 @@ ScrollbarComponent = React.createClass render: -> {orientation, className, scrollHeight, scrollWidth, visible} = @props {scrollableInOppositeDirection, horizontalScrollbarHeight, verticalScrollbarWidth} = @props + {useHardwareAcceleration} = @props style = {} style.display = 'none' unless visible + style.transform = 'translateZ(0)' if useHardwareAcceleration # See atom/atom#3559 switch orientation when 'vertical' style.width = verticalScrollbarWidth diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index ae5ffdc93..b3cc4a1bc 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -46,7 +46,11 @@ class SelectListView extends View inputThrottle: 50 cancelling: false - # Public: Initialize the select list view. + ### + Section: Construction + ### + + # Essential: Initialize the select list view. # # This method can be overridden by subclasses but `super` should always # be called. @@ -85,13 +89,39 @@ class SelectListView extends View @confirmSelection() if $(e.target).closest('li').hasClass('selected') e.preventDefault() - schedulePopulateList: -> - clearTimeout(@scheduleTimeout) - populateCallback = => - @populateList() if @isOnDom() - @scheduleTimeout = setTimeout(populateCallback, @inputThrottle) + ### + Section: Methods that must be overridden + ### - # Public: Set the array of items to display in the list. + # Essential: Create a view for the given model item. + # + # This method must be overridden by subclasses. + # + # This is called when the item is about to appended to the list view. + # + # * `item` The model item being rendered. This will always be one of the items + # previously passed to {::setItems}. + # + # Returns a String of HTML, DOM element, jQuery object, or View. + viewForItem: (item) -> + throw new Error("Subclass must implement a viewForItem(item) method") + + # Essential: Callback function for when an item is selected. + # + # This method must be overridden by subclasses. + # + # * `item` The selected model item. This will always be one of the items + # previously passed to {::setItems}. + # + # Returns a DOM element, jQuery object, or {View}. + confirmed: (item) -> + throw new Error("Subclass must implement a confirmed(item) method") + + ### + Section: Managing the list of items + ### + + # Essential: Set the array of items to display in the list. # # This should be model items not actual views. {::viewForItem} will be # called to render the item when it is being appended to the list view. @@ -101,30 +131,26 @@ class SelectListView extends View @populateList() @setLoading() - # Public: Set the error message to display. + # Essential: Get the model item that is currently selected in the list view. # - # * `message` The {String} error message (default: ''). - setError: (message='') -> - if message.length is 0 - @error.text('').hide() - else - @setLoading() - @error.text(message).show() + # Returns a model item. + getSelectedItem: -> + @getSelectedItemView().data('select-list-item') - # Public: Set the loading message to display. + # Extended: Get the property name to use when filtering items. # - # * `message` The {String} loading message (default: ''). - setLoading: (message='') -> - if message.length is 0 - @loading.text("") - @loadingBadge.text("") - @loadingArea.hide() - else - @setError() - @loading.text(message) - @loadingArea.show() + # This method may be overridden by classes to allow fuzzy filtering based + # on a specific property of the item objects. + # + # For example if the objects you pass to {::setItems} are of the type + # `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method + # to fuzzy filter by that property when text is entered into this view's + # editor. + # + # Returns the property name to fuzzy filter by. + getFilterKey: -> - # Public: Get the filter query to use when fuzzy filtering the visible + # Extended: Get the filter query to use when fuzzy filtering the visible # elements. # # By default this method returns the text in the mini editor but it can be @@ -134,7 +160,12 @@ class SelectListView extends View getFilterQuery: -> @filterEditorView.getEditor().getText() - # Public: Populate the list view with the model items previously set by + # Extended: Set the maximum numbers of items to display in the list. + # + # * `maxItems` The maximum {Number} of items to display. + setMaxItems: (@maxItems) -> + + # Extended: Populate the list view with the model items previously set by # calling {::setItems}. # # Subclasses may override this method but should always call `super`. @@ -161,7 +192,34 @@ class SelectListView extends View else @setError(@getEmptyMessage(@items.length, filteredItems.length)) - # Public: Get the message to display when there are no items. + ### + Section: Messages to the user + ### + + # Essential: Set the error message to display. + # + # * `message` The {String} error message (default: ''). + setError: (message='') -> + if message.length is 0 + @error.text('').hide() + else + @setLoading() + @error.text(message).show() + + # Essential: Set the loading message to display. + # + # * `message` The {String} loading message (default: ''). + setLoading: (message='') -> + if message.length is 0 + @loading.text("") + @loadingBadge.text("") + @loadingArea.hide() + else + @setError() + @loading.text(message) + @loadingArea.show() + + # Extended: Get the message to display when there are no items. # # Subclasses may override this method to customize the message. # @@ -171,10 +229,36 @@ class SelectListView extends View # Returns a {String} message (default: 'No matches found'). getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found' - # Public: Set the maximum numbers of items to display in the list. + ### + Section: View Actions + ### + + # Essential: Cancel and close this select list view. # - # * `maxItems` The maximum {Number} of items to display. - setMaxItems: (@maxItems) -> + # This restores focus to the previously focused element if + # {::storeFocusedElement} was called prior to this view being attached. + cancel: -> + @list.empty() + @cancelling = true + filterEditorViewFocused = @filterEditorView.isFocused + @cancelled() + @detach() + @restoreFocus() if filterEditorViewFocused + @cancelling = false + clearTimeout(@scheduleTimeout) + + # Extended: Focus the fuzzy filter editor view. + focusFilterEditor: -> + @filterEditorView.focus() + + # Extended: Store the currently focused element. This element will be given + # back focus when {::cancel} is called. + storeFocusedElement: -> + @previouslyFocusedElement = $(':focus') + + ### + Section: Private + ### selectPreviousItemView: -> view = @getSelectedItemView().prev() @@ -202,68 +286,6 @@ class SelectListView extends View else if desiredBottom > @list.scrollBottom() @list.scrollBottom(desiredBottom) - getSelectedItemView: -> - @list.find('li.selected') - - # Public: Get the model item that is currently selected in the list view. - # - # Returns a model item. - getSelectedItem: -> - @getSelectedItemView().data('select-list-item') - - confirmSelection: -> - item = @getSelectedItem() - if item? - @confirmed(item) - else - @cancel() - - # Public: Create a view for the given model item. - # - # This method must be overridden by subclasses. - # - # This is called when the item is about to appended to the list view. - # - # * `item` The model item being rendered. This will always be one of the items - # previously passed to {::setItems}. - # - # Returns a String of HTML, DOM element, jQuery object, or View. - viewForItem: (item) -> - throw new Error("Subclass must implement a viewForItem(item) method") - - # Public: Callback function for when an item is selected. - # - # This method must be overridden by subclasses. - # - # * `item` The selected model item. This will always be one of the items - # previously passed to {::setItems}. - # - # Returns a DOM element, jQuery object, or {View}. - confirmed: (item) -> - throw new Error("Subclass must implement a confirmed(item) method") - - # Public: Get the property name to use when filtering items. - # - # This method may be overridden by classes to allow fuzzy filtering based - # on a specific property of the item objects. - # - # For example if the objects you pass to {::setItems} are of the type - # `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method - # to fuzzy filter by that property when text is entered into this view's - # editor. - # - # Returns the property name to fuzzy filter by. - getFilterKey: -> - - # Public: Focus the fuzzy filter editor view. - focusFilterEditor: -> - @filterEditorView.focus() - - # Public: Store the currently focused element. This element will be given - # back focus when {::cancel} is called. - storeFocusedElement: -> - @previouslyFocusedElement = $(':focus') - restoreFocus: -> if @previouslyFocusedElement?.isOnDom() @previouslyFocusedElement.focus() @@ -273,16 +295,18 @@ class SelectListView extends View cancelled: -> @filterEditorView.getEditor().setText('') - # Public: Cancel and close this select list view. - # - # This restores focus to the previously focused element if - # {::storeFocusedElement} was called prior to this view being attached. - cancel: -> - @list.empty() - @cancelling = true - filterEditorViewFocused = @filterEditorView.isFocused - @cancelled() - @detach() - @restoreFocus() if filterEditorViewFocused - @cancelling = false + getSelectedItemView: -> + @list.find('li.selected') + + confirmSelection: -> + item = @getSelectedItem() + if item? + @confirmed(item) + else + @cancel() + + schedulePopulateList: -> clearTimeout(@scheduleTimeout) + populateCallback = => + @populateList() if @isOnDom() + @scheduleTimeout = setTimeout(populateCallback, @inputThrottle) diff --git a/src/selection.coffee b/src/selection.coffee index c381d4994..38c69b401 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -30,6 +30,9 @@ class Selection extends Model @emitter.emit 'did-destroy' @emitter.dispose() + destroy: -> + @marker.destroy() + ### Section: Event Subscription ### @@ -60,37 +63,11 @@ class Selection extends Model super + ### - Section: Methods + Section: Managing the selection range ### - destroy: -> - @marker.destroy() - - finalize: -> - @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) - if @isEmpty() - @wordwise = false - @linewise = false - - clearAutoscroll: -> - @needsAutoscroll = null - - # Public: Determines if the selection contains anything. - isEmpty: -> - @getBufferRange().isEmpty() - - # Public: Determines if the ending position of a marker is greater than the - # starting position. - # - # This can happen when, for example, you highlight text "up" in a {TextBuffer}. - isReversed: -> - @marker.isReversed() - - # Public: Returns whether the selection is a single line or not. - isSingleScreenLine: -> - @getScreenRange().isSingleLine() - # Public: Returns the screen {Range} for the selection. getScreenRange: -> @marker.getScreenRange() @@ -148,55 +125,61 @@ class Selection extends Model getHeadBufferPosition: -> @marker.getHeadBufferPosition() - autoscroll: -> - @editor.scrollToScreenRange(@getScreenRange()) + ### + Section: Info about the selection + ### + + # Public: Determines if the selection contains anything. + isEmpty: -> + @getBufferRange().isEmpty() + + # Public: Determines if the ending position of a marker is greater than the + # starting position. + # + # This can happen when, for example, you highlight text "up" in a {TextBuffer}. + isReversed: -> + @marker.isReversed() + + # Public: Returns whether the selection is a single line or not. + isSingleScreenLine: -> + @getScreenRange().isSingleLine() # Public: Returns the text in the selection. getText: -> @editor.buffer.getTextInRange(@getBufferRange()) + # Public: Identifies if a selection intersects with a given buffer range. + # + # * `bufferRange` A {Range} to check against. + # + # Returns a {Boolean} + 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. + # + # Returns a {Boolean} + intersectsWith: (otherSelection, exclusive) -> + @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + + ### + Section: Modifying the selected range + ### + # Public: Clears the selection, moving the marker to the head. clear: -> - @marker.setAttributes(goalBufferRange: null) + @marker.setProperties(goalBufferRange: null) @marker.clearTail() unless @retainSelection @finalize() - # Public: Modifies the selection to encompass the current word. - # - # Returns a {Range}. - selectWord: -> - options = {} - options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() - if @cursor.isBetweenWordAndNonWord() - options.includeNonWordCharacters = false - - @setBufferRange(@cursor.getCurrentWordBufferRange(options)) - @wordwise = true - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire word on which - # the cursors rests. - expandOverWord: -> - @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange())) - - # Public: Selects an entire line in the buffer. - # - # * `row` The line {Number} to select (default: the row of the cursor). - selectLine: (row=@cursor.getBufferPosition().row) -> - range = @editor.bufferRangeForBufferRow(row, includeNewline: true) - @setBufferRange(@getBufferRange().union(range)) - @linewise = true - @wordwise = false - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire line on which - # the cursor currently rests. - # - # It also includes the newline character. - expandOverLine: -> - range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) - @setBufferRange(range) - # Public: Selects the text from the current cursor position to a given screen # position. # @@ -311,46 +294,45 @@ class Selection extends Model selectToBeginningOfPreviousParagraph: -> @modifySelection => @cursor.moveToBeginningOfPreviousParagraph() - # Public: Moves the selection down one row. - addSelectionBelow: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() - nextRow = range.end.row + 1 + # Public: Modifies the selection to encompass the current word. + # + # Returns a {Range}. + selectWord: -> + options = {} + options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() + if @cursor.isBetweenWordAndNonWord() + options.includeNonWordCharacters = false - for row in [nextRow..@editor.getLastBufferRow()] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipBufferRange(range) + @setBufferRange(@cursor.getCurrentWordBufferRange(options)) + @wordwise = true + @initialScreenRange = @getScreenRange() - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() + # Public: Expands the newest selection to include the entire word on which + # the cursors rests. + expandOverWord: -> + @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange())) - @editor.addSelectionForBufferRange(range, goalBufferRange: range) - break + # Public: Selects an entire line in the buffer. + # + # * `row` The line {Number} to select (default: the row of the cursor). + selectLine: (row=@cursor.getBufferPosition().row) -> + range = @editor.bufferRangeForBufferRow(row, includeNewline: true) + @setBufferRange(@getBufferRange().union(range)) + @linewise = true + @wordwise = false + @initialScreenRange = @getScreenRange() - # FIXME: I have no idea what this does. - getGoalBufferRange: -> - if goalBufferRange = @marker.getAttributes().goalBufferRange - Range.fromObject(goalBufferRange) + # Public: Expands the newest selection to include the entire line on which + # the cursor currently rests. + # + # It also includes the newline character. + expandOverLine: -> + range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) + @setBufferRange(range) - # Public: Moves the selection up one row. - addSelectionAbove: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() - previousRow = range.end.row - 1 - - for row in [previousRow..0] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipBufferRange(range) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - @editor.addSelectionForBufferRange(range, goalBufferRange: range) - break + ### + Section: Modifying the selected text + ### # Public: Replaces text at the current selection. # @@ -391,71 +373,6 @@ class Selection extends Model newBufferRange - # Public: Indents the given text to the suggested level based on the grammar. - # - # * `text` The {String} to indent within the selection. - # * `indentBasis` The beginning indent level. - normalizeIndents: (text, indentBasis) -> - textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] - isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) - - lines = text.split('\n') - firstLineIndentLevel = @editor.indentLevelForLine(lines[0]) - if isCursorInsideExistingLine - minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow()) - else - minimumIndentLevel = @cursor.getIndentLevel() - - normalizedLines = [] - for line, i in lines - if i == 0 - indentLevel = 0 - else if line == '' # remove all indentation from empty lines - indentLevel = 0 - else - lineIndentLevel = @editor.indentLevelForLine(lines[i]) - indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis) - - normalizedLines.push(@setIndentationForLine(line, indentLevel)) - - normalizedLines.join('\n') - - # Indent the current line(s). - # - # If the selection is empty, indents the current line if the cursor precedes - # non-whitespace characters, and otherwise inserts a tab. If the selection is - # non empty, calls {::indentSelectedRows}. - # - # * `options` (optional) {Object} with the keys: - # * `autoIndent` If `true`, the line is indented to an automatically-inferred - # level. Otherwise, {Editor::getTabText} is inserted. - indent: ({ autoIndent }={}) -> - { row, column } = @cursor.getBufferPosition() - - if @isEmpty() - @cursor.skipLeadingWhitespace() - desiredIndent = @editor.suggestedIndentForBufferRow(row) - delta = desiredIndent - @cursor.getIndentLevel() - - if autoIndent and delta > 0 - @insertText(@editor.buildIndentString(delta)) - else - @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) - else - @indentSelectedRows() - - # Public: If the selection spans multiple rows, indent all of them. - indentSelectedRows: -> - [start, end] = @getBufferRowRange() - for row in [start..end] - @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 - - # Public: ? - setIndentationForLine: (line, indentLevel) -> - desiredIndentLevel = Math.max(0, indentLevel) - desiredIndentString = @editor.buildIndentString(desiredIndentLevel) - line.replace(/^[\t ]*/, desiredIndentString) - # Public: Removes the first character before the selection if the selection # is empty otherwise it deletes the selection. backspace: -> @@ -632,41 +549,110 @@ class Selection extends Model @editor.createFold(range.start.row, range.end.row) @cursor.setBufferPosition([range.end.row + 1, 0]) - modifySelection: (fn) -> - @retainSelection = true - @plantTail() - fn() - @retainSelection = false + # Public: Indents the given text to the suggested level based on the grammar. + # + # * `text` The {String} to indent within the selection. + # * `indentBasis` The beginning indent level. + normalizeIndents: (text, indentBasis) -> + textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] + isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) - # 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. - # - # Returns a {Point} representing the new tail position. - plantTail: -> - @marker.plantTail() + lines = text.split('\n') + firstLineIndentLevel = @editor.indentLevelForLine(lines[0]) + if isCursorInsideExistingLine + minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow()) + else + minimumIndentLevel = @cursor.getIndentLevel() - # Public: Identifies if a selection intersects with a given buffer range. - # - # * `bufferRange` A {Range} to check against. - # - # Returns a {Boolean} - intersectsBufferRange: (bufferRange) -> - @getBufferRange().intersectsWith(bufferRange) + normalizedLines = [] + for line, i in lines + if i == 0 + indentLevel = 0 + else if line == '' # remove all indentation from empty lines + indentLevel = 0 + else + lineIndentLevel = @editor.indentLevelForLine(lines[i]) + indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis) - intersectsScreenRowRange: (startRow, endRow) -> - @getScreenRange().intersectsRowRange(startRow, endRow) + normalizedLines.push(@setIndentationForLine(line, indentLevel)) - intersectsScreenRow: (screenRow) -> - @getScreenRange().intersectsRow(screenRow) + normalizedLines.join('\n') - # Public: Identifies if a selection intersects with another selection. + # Indent the current line(s). # - # * `otherSelection` A {Selection} to check against. + # If the selection is empty, indents the current line if the cursor precedes + # non-whitespace characters, and otherwise inserts a tab. If the selection is + # non empty, calls {::indentSelectedRows}. # - # Returns a {Boolean} - intersectsWith: (otherSelection, exclusive) -> - @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + # * `options` (optional) {Object} with the keys: + # * `autoIndent` If `true`, the line is indented to an automatically-inferred + # level. Otherwise, {Editor::getTabText} is inserted. + indent: ({ autoIndent }={}) -> + { row, column } = @cursor.getBufferPosition() + + if @isEmpty() + @cursor.skipLeadingWhitespace() + desiredIndent = @editor.suggestedIndentForBufferRow(row) + delta = desiredIndent - @cursor.getIndentLevel() + + if autoIndent and delta > 0 + delta = Math.max(delta, 1) unless @editor.getSoftTabs() + @insertText(@editor.buildIndentString(delta)) + else + @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) + else + @indentSelectedRows() + + # Public: If the selection spans multiple rows, indent all of them. + indentSelectedRows: -> + [start, end] = @getBufferRowRange() + for row in [start..end] + @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 + + setIndentationForLine: (line, indentLevel) -> + desiredIndentLevel = Math.max(0, indentLevel) + desiredIndentString = @editor.buildIndentString(desiredIndentLevel) + line.replace(/^[\t ]*/, desiredIndentString) + + ### + Section: Managing multiple selections + ### + + # Public: Moves the selection down one row. + addSelectionBelow: -> + range = (@getGoalBufferRange() ? @getBufferRange()).copy() + nextRow = range.end.row + 1 + + for row in [nextRow..@editor.getLastBufferRow()] + range.start.row = row + range.end.row = row + clippedRange = @editor.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editor.addSelectionForBufferRange(range, goalBufferRange: range) + break + + # Public: Moves the selection up one row. + addSelectionAbove: -> + range = (@getGoalBufferRange() ? @getBufferRange()).copy() + previousRow = range.end.row - 1 + + for row in [previousRow..0] + range.start.row = row + range.end.row = row + clippedRange = @editor.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editor.addSelectionForBufferRange(range, goalBufferRange: range) + break # Public: Combines the given selection into this selection and then destroys # the given selection. @@ -683,6 +669,10 @@ class Selection extends Model @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) otherSelection.destroy() + ### + Section: Comparing to other selections + ### + # Public: Compare this selection's buffer range to another selection's buffer # range. # @@ -692,7 +682,41 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) + ### + Section: Private Utilities + ### + screenRangeChanged: -> @emit 'screen-range-changed', @getScreenRange() @emitter.emit 'did-change-range' @editor.selectionRangeChanged(this) + + finalize: -> + @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) + if @isEmpty() + @wordwise = false + @linewise = false + + autoscroll: -> + @editor.scrollToScreenRange(@getScreenRange()) + + clearAutoscroll: -> + @needsAutoscroll = null + + modifySelection: (fn) -> + @retainSelection = true + @plantTail() + fn() + @retainSelection = false + + # 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. + # + # Returns a {Point} representing the new tail position. + plantTail: -> + @marker.plantTail() + + getGoalBufferRange: -> + if goalBufferRange = @marker.getProperties().goalBufferRange + Range.fromObject(goalBufferRange) diff --git a/src/syntax.coffee b/src/syntax.coffee index e4ab7de03..09af37c7c 100644 --- a/src/syntax.coffee +++ b/src/syntax.coffee @@ -9,7 +9,7 @@ PropertyAccessors = require 'property-accessors' {$, $$} = require './space-pen-extensions' Token = require './token' -# Public: Syntax class holding the grammars used for tokenizing. +# Extended: Syntax class holding the grammars used for tokenizing. # # An instance of this class is always available as the `atom.syntax` global. # diff --git a/src/task.coffee b/src/task.coffee index 8d8d0316c..64ef7bb32 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -2,14 +2,38 @@ _ = require 'underscore-plus' child_process = require 'child_process' {Emitter} = require 'emissary' -# Public: Run a node script in a separate process. +# Extended: Run a node script in a separate process. # -# Used by the fuzzy-finder. +# Used by the fuzzy-finder and [find in project](https://github.com/atom/atom/blob/master/src/scan-handler.coffee). +# +# For a real-world example, see the [scan-handler](https://github.com/atom/atom/blob/master/src/scan-handler.coffee) +# and the [instantiation of the task](https://github.com/atom/atom/blob/4a20f13162f65afc816b512ad7201e528c3443d7/src/project.coffee#L245). # # ## Examples # +# In your package code: +# # ```coffee # {Task} = require 'atom' +# +# task = Task.once '/path/to/task-file.coffee', parameter1, parameter2, -> +# console.log 'task has finished' +# +# task.on 'some-event-from-the-task', (data) => +# console.log data.someString # prints 'yep this is it' +# ``` +# +# In `'/path/to/task-file.coffee'`: +# +# ```coffee +# module.exports = (parameter1, parameter2) -> +# # Indicates that this task will be async. +# # Call the `callback` to finish the task +# callback = @async() +# +# emit('some-event-from-the-task', {someString: 'yep this is it'}) +# +# callback() # ``` # # ## Events @@ -55,7 +79,7 @@ class Task # receives a completion callback, this is overridden. callback: null - # Public: Creates a task. + # Public: Creates a task. You should probably use {.once} # # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that # exports a single {Function} to execute. diff --git a/src/text-utils.coffee b/src/text-utils.coffee index a043d7c73..90bf220ff 100644 --- a/src/text-utils.coffee +++ b/src/text-utils.coffee @@ -4,34 +4,47 @@ isHighSurrogate = (string, index) -> isLowSurrogate = (string, index) -> 0xDC00 <= string.charCodeAt(index) <= 0xDFFF +isVariationSelector = (string, index) -> + 0xFE00 <= string.charCodeAt(index) <= 0xFE0F + # Is the character at the given index the start of a high/low surrogate pair? # -# string - The {String} to check for a surrogate pair. -# index - The {Number} index to look for a surrogate pair at. +# * `string` The {String} to check for a surrogate pair. +# * `index` The {Number} index to look for a surrogate pair at. # # Return a {Boolean}. isSurrogatePair = (string, index=0) -> isHighSurrogate(string, index) and isLowSurrogate(string, index + 1) -# Get the number of characters in the string accounting for surrogate pairs. +# Is the character at the given index the start of a variation sequence? # -# This method counts high/low surrogate pairs as a single character and will -# always returns a value less than or equal to `string.length`. +# * `string` The {String} to check for a variation sequence. +# * `index` The {Number} index to look for a variation sequence at. # -# string - The {String} to count the number of full characters in. -# -# Returns a {Number}. -getCharacterCount = (string) -> - count = string.length - count-- for index in [0...string.length] when isSurrogatePair(string, index) - count +# Return a {Boolean}. +isVariationSequence = (string, index=0) -> + not isVariationSelector(string, index) and isVariationSelector(string, index + 1) -# Does the given string contain at least one surrogate pair? +# Is the character at the given index the start of high/low surrogate pair +# or a variation sequence? # -# string - The {String} to check for the presence of surrogate pairs. +# * `string` The {String} to check for a surrogate pair or variation sequence. +# * `index` The {Number} index to look for a surrogate pair at. +# +# Return a {Boolean}. +isPairedCharacter = (string, index=0) -> + isSurrogatePair(string, index) or isVariationSequence(string, index) + +# Does the given string contain at least surrogate pair or variation sequence? +# +# * `string` The {String} to check for the presence of paired characters. # # Returns a {Boolean}. -hasSurrogatePair = (string) -> - string.length isnt getCharacterCount(string) +hasPairedCharacter = (string) -> + index = 0 + while index < string.length + return true if isPairedCharacter(string, index) + index++ + false -module.exports = {getCharacterCount, isSurrogatePair, hasSurrogatePair} +module.exports = {isPairedCharacter, hasPairedCharacter} diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 23c630043..1aebe757b 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -85,17 +85,29 @@ class ThemeManager EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Accessing Available Themes ### getAvailableNames: -> # TODO: Maybe should change to list all the available themes out there? @getLoadedNames() + ### + Section: Accessing Loaded Themes + ### + # Public: Get an array of all the loaded theme names. getLoadedNames: -> theme.name for theme in @getLoadedThemes() + # Public: Get an array of all the loaded themes. + getLoadedThemes: -> + pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() + + ### + Section: Accessing Active Themes + ### + # Public: Get an array of all the active theme names. getActiveNames: -> theme.name for theme in @getActiveThemes() @@ -104,13 +116,13 @@ class ThemeManager getActiveThemes: -> pack for pack in @packageManager.getActivePackages() when pack.isTheme() - # Public: Get an array of all the loaded themes. - getLoadedThemes: -> - pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() - activatePackages: -> @activateThemes() - # Get the enabled theme names from the config. + ### + Section: Managing Enabled Themes + ### + + # Public: Get the enabled theme names from the config. # # Returns an array of theme names in the order that they should be activated. getEnabledThemeNames: -> @@ -145,6 +157,147 @@ class ThemeManager # the first/top theme to override later themes in the stack. themeNames.reverse() + # Public: Set the list of enabled themes. + # + # * `enabledThemeNames` An {Array} of {String} theme names. + setEnabledThemes: (enabledThemeNames) -> + atom.config.set('core.themes', enabledThemeNames) + + ### + Section: Managing Stylesheets + ### + + # Public: Returns the {String} path to the user's stylesheet under ~/.atom + getUserStylesheetPath: -> + stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less']) + if fs.isFileSync(stylesheetPath) + stylesheetPath + else + path.join(@configDirPath, 'styles.less') + + # Public: Resolve and apply the stylesheet specified by the path. + # + # This supports both CSS and Less stylsheets. + # + # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute + # path or a relative path that will be resolved against the load path. + # + # Returns the absolute path to the required stylesheet. + requireStylesheet: (stylesheetPath, type='bundled') -> + if fullPath = @resolveStylesheet(stylesheetPath) + content = @loadStylesheet(fullPath) + @applyStylesheet(fullPath, content, type) + else + throw new Error("Could not find a file at path '#{stylesheetPath}'") + + fullPath + + unwatchUserStylesheet: -> + @userStylesheetFile?.off() + @userStylesheetFile = null + @removeStylesheet(@userStylesheetPath) if @userStylesheetPath? + + loadUserStylesheet: -> + @unwatchUserStylesheet() + userStylesheetPath = @getUserStylesheetPath() + return unless fs.isFileSync(userStylesheetPath) + + @userStylesheetPath = userStylesheetPath + @userStylesheetFile = new File(userStylesheetPath) + @userStylesheetFile.on 'contents-changed moved removed', => + @loadUserStylesheet() + userStylesheetContents = @loadStylesheet(userStylesheetPath, true) + @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme') + + loadBaseStylesheets: -> + @requireStylesheet('bootstrap/less/bootstrap') + @reloadBaseStylesheets() + + reloadBaseStylesheets: -> + @requireStylesheet('../static/atom') + if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) + @requireStylesheet(nativeStylesheetPath) + + stylesheetElementForId: (id) -> + document.head.querySelector("""style[id="#{id}"]""") + + resolveStylesheet: (stylesheetPath) -> + if path.extname(stylesheetPath).length > 0 + fs.resolveOnLoadPath(stylesheetPath) + else + fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) + + loadStylesheet: (stylesheetPath, importFallbackVariables) -> + if path.extname(stylesheetPath) is '.less' + @loadLessStylesheet(stylesheetPath, importFallbackVariables) + else + fs.readFileSync(stylesheetPath, 'utf8') + + loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> + unless @lessCache? + LessCompileCache = require './less-compile-cache' + @lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()}) + + try + if importFallbackVariables + baseVarImports = """ + @import "variables/ui-variables"; + @import "variables/syntax-variables"; + """ + less = fs.readFileSync(lessStylesheetPath, 'utf8') + @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n')) + else + @lessCache.read(lessStylesheetPath) + catch error + console.error """ + Error compiling Less stylesheet: #{lessStylesheetPath} + Line number: #{error.line} + #{error.message} + """ + + removeStylesheet: (stylesheetPath) -> + fullPath = @resolveStylesheet(stylesheetPath) ? stylesheetPath + element = @stylesheetElementForId(@stringToId(fullPath)) + if element? + {sheet} = element + element.remove() + @emit 'stylesheet-removed', sheet + @emitter.emit 'did-remove-stylesheet', sheet + @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' + + applyStylesheet: (path, text, type='bundled') -> + styleId = @stringToId(path) + styleElement = @stylesheetElementForId(styleId) + + if styleElement? + @emit 'stylesheet-removed', styleElement.sheet + @emitter.emit 'did-remove-stylesheet', styleElement.sheet + styleElement.textContent = text + else + styleElement = document.createElement('style') + styleElement.setAttribute('class', type) + styleElement.setAttribute('id', styleId) + styleElement.textContent = text + + elementToInsertBefore = _.last(document.head.querySelectorAll("style.#{type}"))?.nextElementSibling + if elementToInsertBefore? + document.head.insertBefore(styleElement, elementToInsertBefore) + else + document.head.appendChild(styleElement) + + @emit 'stylesheet-added', styleElement.sheet + @emitter.emit 'did-add-stylesheet', styleElement.sheet + @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' + + ### + Section: Private + ### + + stringToId: (string) -> + string.replace(/\\/g, '/') + activateThemes: -> deferred = Q.defer() @@ -194,12 +347,6 @@ class ThemeManager refreshLessCache: -> @lessCache?.setImportPaths(@getImportPaths()) - # Public: Set the list of enabled themes. - # - # * `enabledThemeNames` An {Array} of {String} theme names. - setEnabledThemes: (enabledThemeNames) -> - atom.config.set('core.themes', enabledThemeNames) - getImportPaths: -> activeThemes = @getActiveThemes() if activeThemes.length > 0 @@ -212,133 +359,6 @@ class ThemeManager themePaths.filter (themePath) -> fs.isDirectorySync(themePath) - # Public: Returns the {String} path to the user's stylesheet under ~/.atom - getUserStylesheetPath: -> - stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less']) - if fs.isFileSync(stylesheetPath) - stylesheetPath - else - path.join(@configDirPath, 'styles.less') - - unwatchUserStylesheet: -> - @userStylesheetFile?.off() - @userStylesheetFile = null - @removeStylesheet(@userStylesheetPath) if @userStylesheetPath? - - loadUserStylesheet: -> - @unwatchUserStylesheet() - userStylesheetPath = @getUserStylesheetPath() - return unless fs.isFileSync(userStylesheetPath) - - @userStylesheetPath = userStylesheetPath - @userStylesheetFile = new File(userStylesheetPath) - @userStylesheetFile.on 'contents-changed moved removed', => - @loadUserStylesheet() - userStylesheetContents = @loadStylesheet(userStylesheetPath, true) - @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme') - - loadBaseStylesheets: -> - @requireStylesheet('bootstrap/less/bootstrap') - @reloadBaseStylesheets() - - reloadBaseStylesheets: -> - @requireStylesheet('../static/atom') - if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) - @requireStylesheet(nativeStylesheetPath) - - stylesheetElementForId: (id) -> - document.head.querySelector("""style[id="#{id}"]""") - - resolveStylesheet: (stylesheetPath) -> - if path.extname(stylesheetPath).length > 0 - fs.resolveOnLoadPath(stylesheetPath) - else - fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) - - # Public: Resolve and apply the stylesheet specified by the path. - # - # This supports both CSS and Less stylsheets. - # - # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute - # path or a relative path that will be resolved against the load path. - # - # Returns the absolute path to the required stylesheet. - requireStylesheet: (stylesheetPath, type='bundled') -> - if fullPath = @resolveStylesheet(stylesheetPath) - content = @loadStylesheet(fullPath) - @applyStylesheet(fullPath, content, type) - else - throw new Error("Could not find a file at path '#{stylesheetPath}'") - - fullPath - - loadStylesheet: (stylesheetPath, importFallbackVariables) -> - if path.extname(stylesheetPath) is '.less' - @loadLessStylesheet(stylesheetPath, importFallbackVariables) - else - fs.readFileSync(stylesheetPath, 'utf8') - - loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> - unless @lessCache? - LessCompileCache = require './less-compile-cache' - @lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()}) - - try - if importFallbackVariables - baseVarImports = """ - @import "variables/ui-variables"; - @import "variables/syntax-variables"; - """ - less = fs.readFileSync(lessStylesheetPath, 'utf8') - @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n')) - else - @lessCache.read(lessStylesheetPath) - catch error - console.error """ - Error compiling Less stylesheet: #{lessStylesheetPath} - Line number: #{error.line} - #{error.message} - """ - - stringToId: (string) -> - string.replace(/\\/g, '/') - - removeStylesheet: (stylesheetPath) -> - fullPath = @resolveStylesheet(stylesheetPath) ? stylesheetPath - element = @stylesheetElementForId(@stringToId(fullPath)) - if element? - {sheet} = element - element.remove() - @emit 'stylesheet-removed', sheet - @emitter.emit 'did-remove-stylesheet', sheet - @emit 'stylesheets-changed' - @emitter.emit 'did-change-stylesheets' - - applyStylesheet: (path, text, type='bundled') -> - styleId = @stringToId(path) - styleElement = @stylesheetElementForId(styleId) - - if styleElement? - @emit 'stylesheet-removed', styleElement.sheet - @emitter.emit 'did-remove-stylesheet', styleElement.sheet - styleElement.textContent = text - else - styleElement = document.createElement('style') - styleElement.setAttribute('class', type) - styleElement.setAttribute('id', styleId) - styleElement.textContent = text - - elementToInsertBefore = _.last(document.head.querySelectorAll("style.#{type}"))?.nextElementSibling - if elementToInsertBefore? - document.head.insertBefore(styleElement, elementToInsertBefore) - else - document.head.appendChild(styleElement) - - @emit 'stylesheet-added', styleElement.sheet - @emitter.emit 'did-add-stylesheet', styleElement.sheet - @emit 'stylesheets-changed' - @emitter.emit 'did-change-stylesheets' - updateGlobalEditorStyle: (property, value) -> unless styleNode = @stylesheetElementForId('global-editor-styles') @applyStylesheet('global-editor-styles', '.editor {}') diff --git a/src/token.coffee b/src/token.coffee index fabf33de8..a36117ea8 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -12,7 +12,7 @@ MaxTokenLength = 20000 module.exports = class Token value: null - hasSurrogatePair: false + hasPairedCharacter: false scopes: null isAtomic: null isHardTab: null @@ -23,7 +23,7 @@ class Token constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) -> @screenDelta = @value.length @bufferDelta ?= @screenDelta - @hasSurrogatePair = textUtils.hasSurrogatePair(@value) + @hasPairedCharacter = textUtils.hasPairedCharacter(@value) isEqual: (other) -> @value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic @@ -57,11 +57,11 @@ class Token WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g") breakOutAtomicTokens: (tabLength, breakOutLeadingSoftTabs, startColumn) -> - if @hasSurrogatePair + if @hasPairedCharacter outputTokens = [] column = startColumn - for token in @breakOutSurrogatePairs() + for token in @breakOutPairedCharacters() if token.isAtomic outputTokens.push(token) else @@ -98,27 +98,27 @@ class Token outputTokens - breakOutSurrogatePairs: -> + breakOutPairedCharacters: -> outputTokens = [] index = 0 - nonSurrogatePairStart = 0 + nonPairStart = 0 while index < @value.length - if textUtils.isSurrogatePair(@value, index) - if nonSurrogatePairStart isnt index - outputTokens.push(new Token({value: @value[nonSurrogatePairStart...index], @scopes})) - outputTokens.push(@buildSurrogatePairToken(@value, index)) + if textUtils.isPairedCharacter(@value, index) + if nonPairStart isnt index + outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes})) + outputTokens.push(@buildPairedCharacterToken(@value, index)) index += 2 - nonSurrogatePairStart = index + nonPairStart = index else index++ - if nonSurrogatePairStart isnt index - outputTokens.push(new Token({value: @value[nonSurrogatePairStart...index], @scopes})) + if nonPairStart isnt index + outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes})) outputTokens - buildSurrogatePairToken: (value, index) -> + buildPairedCharacterToken: (value, index) -> new Token( value: value[index..index + 1] scopes: @scopes @@ -153,6 +153,8 @@ class Token getValueAsHtml: ({hasIndentGuide}) -> if @isHardTab classes = 'hard-tab' + classes += ' leading-whitespace' if @hasLeadingWhitespace() + classes += ' trailing-whitespace' if @hasTrailingWhitespace() classes += ' indent-guide' if hasIndentGuide classes += ' invisible-character' if @hasInvisibleCharacters html = "#{@escapeString(@value)}" diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 458d6d311..4c5c2fe3e 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -15,7 +15,12 @@ PaneRowView = require './pane-row-view' PaneContainerView = require './pane-container-view' Editor = require './editor' -# Essential: The top-level view for the entire window. An instance of this class is +atom.commands.add '.workspace', + 'window:increase-font-size': -> @getModel().increaseFontSize() + 'window:decrease-font-size': -> @getModel().decreaseFontSize() + 'window:reset-font-size': -> @getModel().resetFontSize() + +# Extended: The top-level view for the entire window. An instance of this class is # available via the `atom.workspaceView` global. # # It is backed by a model object, an instance of {Workspace}, which is available @@ -77,8 +82,10 @@ class WorkspaceView extends View @div class: 'vertical', outlet: 'vertical', => @div class: 'panes', outlet: 'panes' - initialize: (@model) -> - @model = atom.workspace ? new Workspace unless @model? + initialize: (model) -> + @model = model ? atom.workspace ? new Workspace unless @model? + @element.getModel = -> model + atom.commands.setRootNode(@[0]) panes = new PaneContainerView(@model.paneContainer) @panes.replaceWith(panes) @@ -136,12 +143,10 @@ class WorkspaceView extends View @command 'application:open-your-stylesheet', -> ipc.send('command', 'application:open-your-stylesheet') @command 'application:open-license', => @model.openLicense() - @command 'window:install-shell-commands', => @installShellCommands() + if process.platform is 'darwin' + @command 'window:install-shell-commands', => @installShellCommands() @command 'window:run-package-specs', -> ipc.send('run-package-specs', path.join(atom.project.getPath(), 'spec')) - @command 'window:increase-font-size', => @increaseFontSize() - @command 'window:decrease-font-size', => @decreaseFontSize() - @command 'window:reset-font-size', => @model.resetFontSize() @command 'window:focus-next-pane', => @focusNextPaneView() @command 'window:focus-previous-pane', => @focusPreviousPaneView() @@ -162,12 +167,175 @@ class WorkspaceView extends View @command 'core:save', => @saveActivePaneItem() @command 'core:save-as', => @saveActivePaneItemAs() - # Public: Get the underlying model object. + @deprecatedViewEvents() + + ### + Section: Accessing the Workspace Model + ### + + # Essential: Get the underlying model object. # # Returns a {Workspace}. getModel: -> @model - # Public: Install the Atom shell commands on the user's system. + ### + Section: Accessing Views + ### + + # Essential: Register a function to be called for every current and future + # editor view in the workspace (only includes {EditorView}s that are pane + # items). + # + # * `callback` A {Function} with an {EditorView} as its only argument. + # * `editorView` {EditorView} + # + # Returns a subscription object with an `.off` method that you can call to + # unregister the callback. + eachEditorView: (callback) -> + callback(editorView) for editorView in @getEditorViews() + attachedCallback = (e, editorView) -> + callback(editorView) unless editorView.mini + @on('editor:attached', attachedCallback) + off: => @off('editor:attached', attachedCallback) + + # Essential: Register a function to be called for every current and future + # pane view in the workspace. + # + # * `callback` A {Function} with a {PaneView} as its only argument. + # * `paneView` {PaneView} + # + # Returns a subscription object with an `.off` method that you can call to + # unregister the callback. + eachPaneView: (callback) -> + @panes.eachPaneView(callback) + + # Essential: Get all existing pane views. + # + # Prefer {Workspace::getPanes} if you don't need access to the view objects. + # Also consider using {::eachPaneView} if you want to register a callback for + # all current and *future* pane views. + # + # Returns an Array of all open {PaneView}s. + getPaneViews: -> + @panes.getPaneViews() + + # Essential: Get the active pane view. + # + # Prefer {Workspace::getActivePane} if you don't actually need access to the + # view. + # + # Returns a {PaneView}. + getActivePaneView: -> + @panes.getActivePaneView() + + # Essential: Get the view associated with the active pane item. + # + # Returns a view. + getActiveView: -> + @panes.getActiveView() + + ### + Section: Adding elements to the workspace + ### + + # Essential: Prepend an element or view to the panels at the top of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToTop: (element) -> + @vertical.prepend(element) + + # Essential: Append an element or view to the panels at the top of the workspace. + # + # * `element` jQuery object or DOM element + appendToTop: (element) -> + @panes.before(element) + + # Essential: Prepend an element or view to the panels at the bottom of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToBottom: (element) -> + @panes.after(element) + + # Essential: Append an element or view to the panels at the bottom of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToBottom: (element) -> + @vertical.append(element) + + # Essential: Prepend an element or view to the panels at the left of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToLeft: (element) -> + @horizontal.prepend(element) + + # Essential: Append an element or view to the panels at the left of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToLeft: (element) -> + @vertical.before(element) + + # Essential: Prepend an element or view to the panels at the right of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToRight: (element) -> + @vertical.after(element) + + # Essential: Append an element or view to the panels at the right of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToRight: (element) -> + @horizontal.append(element) + + ### + Section: Focusing pane views + ### + + # Focus the previous pane by id. + focusPreviousPaneView: -> @model.activatePreviousPane() + + # Focus the next pane by id. + focusNextPaneView: -> @model.activateNextPane() + + # Focus the pane directly above the active pane. + focusPaneViewAbove: -> @panes.focusPaneViewAbove() + + # Focus the pane directly below the active pane. + focusPaneViewBelow: -> @panes.focusPaneViewBelow() + + # Focus the pane directly to the left of the active pane. + focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft() + + # Focus the pane directly to the right of the active pane. + focusPaneViewOnRight: -> @panes.focusPaneViewOnRight() + + ### + Section: Private + ### + + afterAttach: (onDom) -> + @focus() if onDom + + # Called by SpacePen + beforeRemove: -> + @model.destroy() + + setEditorFontSize: (fontSize) -> + atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') + + setEditorFontFamily: (fontFamily) -> + atom.themes.updateGlobalEditorStyle('font-family', fontFamily) + + setEditorLineHeight: (lineHeight) -> + atom.themes.updateGlobalEditorStyle('line-height', lineHeight) + + # Install the Atom shell commands on the user's system. installShellCommands: -> showErrorDialog = (error) -> installDirectory = CommandInstaller.getInstallDirectory() @@ -202,9 +370,6 @@ class WorkspaceView extends View $(document.body).focus() true - afterAttach: (onDom) -> - @focus() if onDom - # Prompts to save all unsaved items confirmClose: -> @panes.confirmClose() @@ -243,143 +408,104 @@ class WorkspaceView extends View for editorElement in @panes.element.querySelectorAll('.pane > .item-views > .editor') $(editorElement).view() - # Public: Prepend an element or view to the panels at the top of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToTop: (element) -> - @vertical.prepend(element) - # Public: Append an element or view to the panels at the top of the workspace. - # - # * `element` jQuery object or DOM element - appendToTop: (element) -> - @panes.before(element) + ### + Section: Deprecated + ### - # Public: Prepend an element or view to the panels at the bottom of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToBottom: (element) -> - @panes.after(element) + deprecatedViewEvents: -> + originalWorkspaceViewOn = @on - # Public: Append an element or view to the panels at the bottom of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToBottom: (element) -> - @vertical.append(element) + @on = (eventName) => + switch eventName + when 'beep' + deprecate('Use Atom::onDidBeep instead') + when 'cursor:moved' + deprecate('Use Editor::onDidChangeCursorPosition instead') + when 'editor:attached' + deprecate('Use Editor::onDidAddTextEditor instead') + when 'editor:detached' + deprecate('Use Editor::onDidDestroy instead') + when 'editor:will-be-removed' + deprecate('Use Editor::onDidDestroy instead') + when 'pane:active-item-changed' + deprecate('Use Pane::onDidChangeActiveItem instead') + when 'pane:active-item-modified-status-changed' + deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead') + when 'pane:active-item-title-changed' + deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead') + when 'pane:attached' + deprecate('Use Workspace::onDidAddPane instead') + when 'pane:became-active' + deprecate('Use Pane::onDidActivate instead') + when 'pane:became-inactive' + deprecate('Use Pane::onDidChangeActive instead') + when 'pane:item-added' + deprecate('Use Pane::onDidAddItem instead') + when 'pane:item-moved' + deprecate('Use Pane::onDidMoveItem instead') + when 'pane:item-removed' + deprecate('Use Pane::onDidRemoveItem instead') + when 'pane:removed' + deprecate('Use Pane::onDidDestroy instead') + when 'pane-container:active-pane-item-changed' + deprecate('Use Workspace::onDidChangeActivePaneItem instead') + when 'selection:changed' + deprecate('Use Editor::onDidChangeSelectionRange instead') + when 'uri-opened' + deprecate('Use Workspace::onDidOpen instead') + originalWorkspaceViewOn.apply(this, arguments) - # Public: Prepend an element or view to the panels at the left of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToLeft: (element) -> - @horizontal.prepend(element) + EditorView = require './editor-view' + originalEditorViewOn = EditorView::on + EditorView::on = (eventName) -> + switch eventName + when 'cursor:moved' + deprecate('Use Editor::onDidChangeCursorPosition instead') + when 'editor:attached' + deprecate('Use Editor::onDidAddTextEditor instead') + when 'editor:detached' + deprecate('Use Editor::onDidDestroy instead') + when 'editor:will-be-removed' + deprecate('Use Editor::onDidDestroy instead') + when 'selection:changed' + deprecate('Use Editor::onDidChangeSelectionRange instead') + originalEditorViewOn.apply(this, arguments) - # Public: Append an element or view to the panels at the left of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToLeft: (element) -> - @vertical.before(element) - - # Public: Prepend an element or view to the panels at the right of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToRight: (element) -> - @vertical.after(element) - - # Public: Append an element or view to the panels at the right of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToRight: (element) -> - @horizontal.append(element) - - # Public: Get the active pane view. - # - # Prefer {Workspace::getActivePane} if you don't actually need access to the - # view. - # - # Returns a {PaneView}. - getActivePaneView: -> - @panes.getActivePaneView() - - # Public: Get the view associated with the active pane item. - # - # Returns a view. - getActiveView: -> - @panes.getActiveView() - - # Focus the previous pane by id. - focusPreviousPaneView: -> @model.activatePreviousPane() - - # Focus the next pane by id. - focusNextPaneView: -> @model.activateNextPane() - - # Public: Focus the pane directly above the active pane. - focusPaneViewAbove: -> @panes.focusPaneViewAbove() - - # Public: Focus the pane directly below the active pane. - focusPaneViewBelow: -> @panes.focusPaneViewBelow() - - # Public: Focus the pane directly to the left of the active pane. - focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft() - - # Public: Focus the pane directly to the right of the active pane. - focusPaneViewOnRight: -> @panes.focusPaneViewOnRight() - - # Public: Register a function to be called for every current and future - # pane view in the workspace. - # - # * `callback` A {Function} with a {PaneView} as its only argument. - # * `paneView` {PaneView} - # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. - eachPaneView: (callback) -> - @panes.eachPaneView(callback) - - # Public: Get all existing pane views. - # - # Prefer {Workspace::getPanes} if you don't need access to the view objects. - # Also consider using {::eachPaneView} if you want to register a callback for - # all current and *future* pane views. - # - # Returns an Array of all open {PaneView}s. - getPaneViews: -> - @panes.getPaneViews() - - # Public: Register a function to be called for every current and future - # editor view in the workspace (only includes {EditorView}s that are pane - # items). - # - # * `callback` A {Function} with an {EditorView} as its only argument. - # * `editorView` {EditorView} - # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. - eachEditorView: (callback) -> - callback(editorView) for editorView in @getEditorViews() - attachedCallback = (e, editorView) -> - callback(editorView) unless editorView.mini - @on('editor:attached', attachedCallback) - off: => @off('editor:attached', attachedCallback) - - # Called by SpacePen - beforeRemove: -> - @model.destroy() - - setEditorFontSize: (fontSize) -> - atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') - - setEditorFontFamily: (fontFamily) -> - atom.themes.updateGlobalEditorStyle('font-family', fontFamily) - - setEditorLineHeight: (lineHeight) -> - atom.themes.updateGlobalEditorStyle('line-height', lineHeight) + originalPaneViewOn = PaneView::on + PaneView::on = (eventName) -> + switch eventName + when 'cursor:moved' + deprecate('Use Editor::onDidChangeCursorPosition instead') + when 'editor:attached' + deprecate('Use Editor::onDidAddTextEditor instead') + when 'editor:detached' + deprecate('Use Editor::onDidDestroy instead') + when 'editor:will-be-removed' + deprecate('Use Editor::onDidDestroy instead') + when 'pane:active-item-changed' + deprecate('Use Pane::onDidChangeActiveItem instead') + when 'pane:active-item-modified-status-changed' + deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead') + when 'pane:active-item-title-changed' + deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead') + when 'pane:attached' + deprecate('Use Workspace::onDidAddPane instead') + when 'pane:became-active' + deprecate('Use Pane::onDidActivate instead') + when 'pane:became-inactive' + deprecate('Use Pane::onDidChangeActive instead') + when 'pane:item-added' + deprecate('Use Pane::onDidAddItem instead') + when 'pane:item-moved' + deprecate('Use Pane::onDidMoveItem instead') + when 'pane:item-removed' + deprecate('Use Pane::onDidRemoveItem instead') + when 'pane:removed' + deprecate('Use Pane::onDidDestroy instead') + when 'selection:changed' + deprecate('Use Editor::onDidChangeSelectionRange instead') + originalPaneViewOn.apply(this, arguments) # Deprecated eachPane: (callback) -> diff --git a/src/workspace.coffee b/src/workspace.coffee index 64ea6404b..76450181b 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -10,7 +10,7 @@ Editor = require './editor' PaneContainer = require './pane-container' Pane = require './pane' -# Public: Represents the state of the user interface for the entire window. +# Essential: Represents the state of the user interface for the entire window. # An instance of this class is available via the `atom.workspace` global. # # Interact with this object to open files, be notified of current and future @@ -91,6 +91,52 @@ class Workspace extends Model Section: Event Subscription ### + # Essential: Invoke the given callback with all current and future text + # editors in the workspace. + # + # * `callback` {Function} to be called with current and future text editors. + # * `editor` An {Editor} that is present in {::getTextEditors} at the time + # of subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors: (callback) -> + callback(textEditor) for textEditor in @getTextEditors() + @onDidAddTextEditor ({textEditor}) -> callback(textEditor) + + # Essential: Invoke the given callback with all current and future panes items in + # the workspace. + # + # * `callback` {Function} to be called with current and future pane items. + # * `item` An item that is present in {::getPaneItems} at the time of + # subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems: (callback) -> @paneContainer.observePaneItems(callback) + + # Essential: Invoke the given callback when the active pane item changes. + # + # * `callback` {Function} to be called when the active pane item changes. + # * `event` {Object} with the following keys: + # * `activeItem` The active pane item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem: (callback) -> @paneContainer.onDidChangeActivePaneItem(callback) + + # Essential: Invoke the given callback whenever an item is opened. Unlike + # {::onDidAddPaneItem}, observers will be notified for items that are already + # present in the workspace when they are reopened. + # + # * `callback` {Function} to be called whenever an item is opened. + # * `event` {Object} with the following keys: + # * `uri` {String} representing the opened URI. Could be `undefined`. + # * `item` The opened item. + # * `pane` The pane in which the item was opened. + # * `index` The index of the opened item on its pane. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidOpen: (callback) -> + @emitter.on 'did-open', callback + # Extended: Invoke the given callback when a pane is added to the workspace. # # * `callback` {Function} to be called panes are added. @@ -140,25 +186,6 @@ class Workspace extends Model # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem: (callback) -> @paneContainer.onDidAddPaneItem(callback) - # Extended: Invoke the given callback when the active pane item changes. - # - # * `callback` {Function} to be called when the active pane item changes. - # * `event` {Object} with the following keys: - # * `activeItem` The active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePaneItem: (callback) -> @paneContainer.onDidChangeActivePaneItem(callback) - - # Extended: Invoke the given callback with all current and future panes items in - # the workspace. - # - # * `callback` {Function} to be called with current and future pane items. - # * `item` An item that is present in {::getPaneItems} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems: (callback) -> @paneContainer.observePaneItems(callback) - # Extended: Invoke the given callback when a text editor is added to the # workspace. # @@ -174,33 +201,6 @@ class Workspace extends Model @onDidAddPaneItem ({item, pane, index}) -> callback({textEditor: item, pane, index}) if item instanceof Editor - # Essential: Invoke the given callback with all current and future text - # editors in the workspace. - # - # * `callback` {Function} to be called with current and future text editors. - # * `editor` An {Editor} that is present in {::getTextEditors} at the time - # of subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeTextEditors: (callback) -> - callback(textEditor) for textEditor in @getTextEditors() - @onDidAddTextEditor ({textEditor}) -> callback(textEditor) - - # Essential: Invoke the given callback whenever an item is opened. Unlike - # ::onDidAddPaneItem, observers will be notified for items that are already - # present in the workspace when they are reopened. - # - # * `callback` {Function} to be called whenever an item is opened. - # * `event` {Object} with the following keys: - # * `uri` {String} representing the opened URI. Could be `undefined`. - # * `item` The opened item. - # * `pane` The pane in which the item was opened. - # * `index` The index of the opened item on its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidOpen: (callback) -> - @emitter.on 'did-open', callback - eachEditor: (callback) -> deprecate("Use Workspace::observeTextEditors instead") @@ -323,7 +323,7 @@ class Workspace extends Model .catch (error) -> console.error(error.stack ? error) - # Extended: Asynchronously reopens the last-closed item's URI if it hasn't already been + # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been # reopened. # # Returns a promise that is resolved when the item is opened @@ -339,7 +339,9 @@ class Workspace extends Model if uri = @destroyedItemUris.pop() @openSync(uri) - # Extended: Register an opener for a uri. + # TODO: make ::registerOpener() return a disposable + + # Public: Register an opener for a uri. # # An {Editor} will be used if no openers return a value. # @@ -355,7 +357,7 @@ class Workspace extends Model registerOpener: (opener) -> @openers.push(opener) - # Extended: Unregister an opener registered with {::registerOpener}. + # Unregister an opener registered with {::registerOpener}. unregisterOpener: (opener) -> _.remove(@openers, opener) @@ -396,7 +398,7 @@ class Workspace extends Model getActiveEditor: -> @activePane?.getActiveEditor() - # Extended: Save all pane items. + # Save all pane items. saveAll: -> @paneContainer.saveAll() diff --git a/static/atom.less b/static/atom.less index d7567f34e..02302c2ab 100644 --- a/static/atom.less +++ b/static/atom.less @@ -20,7 +20,6 @@ @import "overlay"; @import "lists"; @import "popover-list"; -@import "notification"; @import "messages"; @import "markdown"; @import "editor"; diff --git a/static/notification.less b/static/notification.less deleted file mode 100644 index 1f676a982..000000000 --- a/static/notification.less +++ /dev/null @@ -1,56 +0,0 @@ -.notification { - position: absolute; - top: 40px; - left: 50%; - margin-left: -5%; - z-index: 9999; - border: 2px solid rgba(0, 0, 0, 0.2); - border-radius: 5px; - box-shadow: - inset 1px 1px 0 rgba(255, 255, 255, 0.05), - 0 0 5px rgba(0, 0, 0, 0.5); - background: -webkit-linear-gradient( - rgba(20, 20, 20, 0.5), - rgba(0, 0, 0, 0.5)); - color: #eee; - width: 300px; -} - -.notification .content { - padding: 10px; - margin-left: 42px; - box-sizing: border-box; -} - -.notification .content:after { - content: "."; - display: block; - clear: both; - visibility: hidden; -} - -.notification .title { - font-size: 12px; - font-weight: bold; - margin-bottom: 3px; -} - -.notification .message { - font-size: 12px; - color: #777; -} - -.notification .icon { - display: inline-block; - font-family: 'Octicons Regular'; - font-size: 32px; - width: 32px; - height: 32px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; -} - -/* TODO: Full Octicon Support */ -.icon-gist { - content: "\f20e"; -}