diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..4e9918e86 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +v0.10.21 diff --git a/apm/package.json b/apm/package.json index a47620ea2..9a1db364f 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.92.0" + "atom-package-manager": "0.93.2" } } diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index c60811985..0a3901ec9 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -62,8 +62,8 @@ describe "editorView.", -> describe "empty-vs-set-innerHTML.", -> [firstRow, lastRow] = [] beforeEach -> - firstRow = editorView.getFirstVisibleScreenRow() - lastRow = editorView.getLastVisibleScreenRow() + firstRow = editorView.getModel().getFirstVisibleScreenRow() + lastRow = editorView.getModel().getLastVisibleScreenRow() benchmark "build-gutter-html.", 1000, -> editorView.gutter.renderLineNumbers(null, firstRow, lastRow) @@ -97,8 +97,8 @@ describe "editorView.", -> describe "multiple-lines.", -> [firstRow, lastRow] = [] beforeEach -> - firstRow = editorView.getFirstVisibleScreenRow() - lastRow = editorView.getLastVisibleScreenRow() + firstRow = editorView.getModel().getFirstVisibleScreenRow() + lastRow = editorView.getModel().getLastVisibleScreenRow() benchmark "cache-entire-visible-area", 100, -> for i in [firstRow..lastRow] diff --git a/benchmark/browser-process-startup.coffee b/benchmark/browser-process-startup.coffee new file mode 100755 index 000000000..06f2a0d48 --- /dev/null +++ b/benchmark/browser-process-startup.coffee @@ -0,0 +1,56 @@ +#!/usr/bin/env coffee + +{spawn, exec} = require 'child_process' +fs = require 'fs' +os = require 'os' +path = require 'path' +_ = require 'underscore-plus' +temp = require 'temp' + +directoryToOpen = temp.mkdirSync('browser-process-startup-') +socketPath = path.join(os.tmpdir(), 'atom.sock') +numberOfRuns = 10 + +deleteSocketFile = -> + try + fs.unlinkSync(socketPath) if fs.existsSync(socketPath) + catch error + console.error(error) + +launchAtom = (callback) -> + deleteSocketFile() + + cmd = 'atom' + args = ['--safe', '--new-window', '--foreground', directoryToOpen] + atomProcess = spawn(cmd, args) + + output = '' + startupTimes = [] + dataListener = (data) -> + output += data + if match = /App load time: (\d+)/.exec(output) + startupTime = parseInt(match[1]) + atomProcess.stderr.removeListener 'data', dataListener + atomProcess.kill() + exec 'pkill -9 Atom', (error) -> + console.error(error) if error? + callback(startupTime) + + atomProcess.stderr.on 'data', dataListener + +startupTimes = [] +collector = (startupTime) -> + startupTimes.push(startupTime) + if startupTimes.length < numberOfRuns + launchAtom(collector) + else + maxTime = _.max(startupTimes) + minTime = _.min(startupTimes) + totalTime = startupTimes.reduce (previousValue=0, currentValue) -> previousValue + currentValue + console.log "Startup Runs: #{startupTimes.length}" + console.log "First run time: #{startupTimes[0]}ms" + console.log "Max time: #{maxTime}ms" + console.log "Min time: #{minTime}ms" + console.log "Average time: #{Math.round(totalTime/startupTimes.length)}ms" + +launchAtom(collector) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index ee5f8f727..188e5f51f 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -225,9 +225,18 @@ module.exports = (grunt) -> grunt.registerTask('compile', ['coffee', 'prebuild-less', 'cson', 'peg']) grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint']) grunt.registerTask('test', ['shell:kill-atom', 'run-specs']) - grunt.registerTask('ci', ['output-disk-space', 'download-atom-shell', 'build', 'dump-symbols', 'set-version', 'check-licenses', 'lint', 'test', 'create-installer', 'codesign', 'publish-build']) grunt.registerTask('docs', ['markdown:guides', 'build-docs']) + 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('test') if process.platform isnt 'linux' + ciTasks.push('codesign') + ciTasks.push('create-installer') if process.platform is 'win32' + ciTasks.push('publish-build') + grunt.registerTask('ci', ciTasks) + defaultTasks = ['download-atom-shell', 'build', 'set-version'] defaultTasks.push 'install' unless process.platform is 'linux' grunt.registerTask('default', defaultTasks) diff --git a/build/package.json b/build/package.json index ea8514acb..422a48693 100644 --- a/build/package.json +++ b/build/package.json @@ -7,8 +7,8 @@ }, "dependencies": { "async": "~0.2.9", - "donna": "~1.0", - "tello": "~0.2", + "donna": "1.0.1", + "tello": "1.0.3", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", @@ -27,7 +27,7 @@ "harmony-collections": "~0.3.8", "json-front-matter": "~0.1.3", "legal-eagle": "~0.4.0", - "minidump": "~0.7", + "minidump": "~0.8", "normalize-package-data": "0.2.12", "npm": "~1.4.5", "rcedit": "~0.1.2", diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee index 605269933..75e21de8a 100644 --- a/build/tasks/docs-task.coffee +++ b/build/tasks/docs-task.coffee @@ -11,6 +11,7 @@ module.exports = (grunt) -> modulesPath = path.resolve(__dirname, '..', '..', 'node_modules') classes = {} fs.traverseTreeSync modulesPath, (modulePath) -> + return false if modulePath.match(/node_modules/g).length > 1 # dont need the dependencies of the dependencies return true unless path.basename(modulePath) is 'package.json' return true unless fs.isFileSync(modulePath) diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 69ba1b20b..b1d8a5802 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -28,7 +28,7 @@ module.exports = (gruntObject) -> grunt.registerTask 'prepare-docs', 'Move api.json to atom-api.json', -> docsOutputDir = grunt.config.get('docsOutputDir') buildDir = grunt.config.get('atom.buildDir') - cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json') + cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json') grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', -> done = @async() @@ -45,16 +45,32 @@ module.exports = (gruntObject) -> uploadAssets(release, buildDir, assets, done) getAssets = -> - if process.platform is 'darwin' - [ - {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'} - {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'} - {assetName: 'atom-api.json', sourcePath: 'atom-api.json'} - ] - else - [ - {assetName: 'atom-windows.zip', sourcePath: 'Atom'} - ] + switch process.platform + when 'darwin' + [ + {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'} + {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'} + {assetName: 'atom-api.json', sourcePath: 'atom-api.json'} + ] + when 'win32' + [ + {assetName: 'atom-windows.zip', sourcePath: 'Atom'} + ] + when 'linux' + buildDir = grunt.config.get('atom.buildDir') + sourcePath = fs.listSync(buildDir, ['.deb'])[0] + if process.arch is 'ia32g' + arch = 'i386' + else + arch = 'amd64' + assetName = "atom-#{arch}.deb" + + {cp} = require('./task-helpers')(grunt) + cp sourcePath, path.join(buildDir, assetName) + + [ + {assetName, sourcePath} + ] logError = (message, error, details) -> grunt.log.error(message) diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 40bf03780..6230f9b96 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -33,26 +33,41 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. If you have problems with permissions don't forget to prefix with `sudo` -From the cloned repository directory: +1. Clone the Atom repository: - 1. Build: + ```sh + git clone https://github.com/atom/atom + cd atom + ``` - ```sh - $ script/build - ``` - This will create the atom application at `$TMPDIR/atom-build/Atom`. - 2. Install the `atom` and `apm` commands to `/usr/local/bin` by executing: +2. Checkout the latest Atom release: - ```sh - $ sudo script/grunt install - ``` - 3. *Optionally*, you may generate a `.deb` package at `$TMPDIR/atom-build`: + ```sh + git fetch + git checkout $(git describe --tags `git rev-list --tags --max-count=1`) + ``` - ```sh - $ script/grunt mkdeb - ``` +3. Build Atom: -Use the newly installed atom by restarting any running atom instances. + ```sh + script/build + ``` + + This will create the atom application at `$TMPDIR/atom-build/Atom`. + +4. Install the `atom` and `apm` commands to `/usr/local/bin` by executing: + + ```sh + sudo script/grunt install + ``` + +5. *Optionally*, you may generate a `.deb` package at `$TMPDIR/atom-build`: + + ```sh + script/grunt mkdeb + ``` + +Use the newly installed Atom by fully quitting Atom and then reopening. ## Advanced Options @@ -88,7 +103,7 @@ this is the reason for this error you can issue and restart Atom. If Atom now works fine, you can make this setting permanent: ```sh - echo 32768 > /proc/sys/fs/inotify/max_user_watches + echo 32768 | sudo tee -a /proc/sys/fs/inotify/max_user_watches ``` See also https://github.com/atom/atom/issues/2082. @@ -100,6 +115,15 @@ have Node.js installed, or node isn't identified as Node.js on your machine. If it's the latter, entering `sudo ln -s /usr/bin/nodejs /usr/bin/node` into your terminal may fix the issue. +#### You can also use Alternatives + +On some variants (mostly Debian based distros) it's preferable for you to use +Alternatives so that changes to the binary paths can be fixed or altered easily: + +```sh +sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 1 --slave /usr/bin/js js /usr/bin/nodejs +``` + ### Linux build error reports in atom/atom * Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) to get a list of reports about build errors on Linux. diff --git a/docs/your-first-package.md b/docs/your-first-package.md index 02cb98379..fe9550bd9 100644 --- a/docs/your-first-package.md +++ b/docs/your-first-package.md @@ -53,7 +53,7 @@ module.exports = convert: -> # This assumes the active pane item is an editor - editor = atom.workspace.activePaneItem + editor = atom.workspace.getActivePaneItem() editor.insertText('Hello, World!') ``` @@ -131,7 +131,7 @@ inserting 'Hello, World!' convert the selected text to ASCII art. ```coffeescript convert: -> # This assumes the active pane item is an editor - editor = atom.workspace.activePaneItem + editor = atom.workspace.getActivePaneItem() selection = editor.getLastSelection() figlet = require 'figlet' diff --git a/dot-atom/init.coffee b/dot-atom/init.coffee index 4d10e775b..cf8a5a249 100644 --- a/dot-atom/init.coffee +++ b/dot-atom/init.coffee @@ -11,4 +11,4 @@ # atom.workspaceView.eachEditorView (editorView) -> # editor = editorView.getEditor() # if path.extname(editor.getPath()) is '.md' -# editor.setSoftWrap(true) +# editor.setSoftWrapped(true) diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index 43ec6951d..fd683364a 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -13,6 +13,9 @@ # 'enter': 'editor:newline' # # '.workspace': -# 'ctrl-P': 'core:move-up' +# 'ctrl-shift-p': 'core:move-up' # 'ctrl-p': 'core:move-down' # +# You can find more information about keymaps in these guides: +# * https://atom.io/docs/latest/customizing-atom#customizing-key-bindings +# * https://atom.io/docs/latest/advanced/keymaps diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 9c164c9c0..34f977209 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -119,7 +119,7 @@ 'cmd-shift-right': 'editor:select-to-end-of-line' 'alt-backspace': 'editor:delete-to-beginning-of-word' 'alt-delete': 'editor:delete-to-end-of-word' - 'ctrl-a': 'editor:move-to-beginning-of-line' + 'ctrl-a': 'editor:move-to-first-character-of-line' 'ctrl-e': 'editor:move-to-end-of-line' 'ctrl-k': 'editor:cut-to-end-of-line' diff --git a/menus/darwin.cson b/menus/darwin.cson index 900e42ce7..b892c56cc 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -18,6 +18,8 @@ { type: 'separator' } { label: 'Install Shell Commands', command: 'window:install-shell-commands' } { type: 'separator' } + { label: 'Services', submenu: [] } + { type: 'separator' } { label: 'Hide Atom', command: 'application:hide' } { label: 'Hide Others', command: 'application:hide-other-applications' } { label: 'Show All', command: 'application:unhide-all-applications' } diff --git a/menus/linux.cson b/menus/linux.cson index 2f2396b85..ba87732dd 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -85,6 +85,7 @@ { label: 'Open Your Keymap', command: 'application:open-your-keymap' } { label: 'Open Your Snippets', command: 'application:open-your-snippets' } { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { type: 'separator' } ] } diff --git a/package.json b/package.json index d24ca30de..135144923 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.125.0", + "version": "0.130.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -17,106 +17,106 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.15.9", + "atomShellVersion": "0.16.2", "dependencies": { "async": "0.2.6", - "atom-keymap": "^2.0.5", + "atom-keymap": "^2.1.1", "bootstrap": "git+https://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372", "clear-cut": "0.4.0", "coffee-script": "1.7.0", "coffeestack": "0.7.0", "delegato": "^1", - "emissary": "^1.2.2", - "first-mate": "^2.0.5", + "emissary": "^1.3.1", + "event-kit": "0.7.2", + "first-mate": "^2.1.1", "fs-plus": "^2.2.6", "fstream": "0.1.24", - "fuzzaldrin": "^1.1", + "fuzzaldrin": "^2.1", "git-utils": "^2.1.4", "grim": "0.12.0", "guid": "0.0.10", "jasmine-tagged": "^1.1.2", - "less-cache": "0.13.0", + "less-cache": "0.14.0", "mixto": "^1", "mkdirp": "0.3.5", "nslog": "^1.0.1", "oniguruma": "^3.0.4", "optimist": "0.4.0", - "pathwatcher": "^2.0.10", + "pathwatcher": "^2.1.2", "property-accessors": "^1", "q": "^1.0.1", "random-words": "0.0.1", "react-atom-fork": "^0.11.1", "reactionary-atom-fork": "^1.0.0", "runas": "1.0.1", - "scandal": "1.0.0", + "scandal": "1.0.2", "scoped-property-store": "^0.9.0", "scrollbar-style": "^1.0.2", "season": "^1.0.2", "semver": "1.1.4", "serializable": "^1", - "space-pen": "3.4.6", + "space-pen": "3.4.7", "temp": "0.7.0", - "text-buffer": "^3.1.0", + "text-buffer": "^3.2.4", "theorist": "^1.0.2", "underscore-plus": "^1.5.1", "vm-compatibility-layer": "0.1.0" }, "packageDependencies": { "atom-dark-syntax": "0.19.0", - "atom-dark-ui": "0.34.0", + "atom-dark-ui": "0.35.0", "atom-light-syntax": "0.20.0", - "atom-light-ui": "0.29.0", + "atom-light-ui": "0.30.0", "base16-tomorrow-dark-theme": "0.21.0", "base16-tomorrow-light-theme": "0.4.0", "solarized-dark-syntax": "0.22.0", "solarized-light-syntax": "0.12.0", - "archive-view": "0.36.0", - "autocomplete": "0.31.0", + "archive-view": "0.37.0", + "autocomplete": "0.32.0", "autoflow": "0.18.0", - "autosave": "0.15.0", + "autosave": "0.17.0", "background-tips": "0.16.0", "bookmarks": "0.28.0", - "bracket-matcher": "0.54.0", + "bracket-matcher": "0.55.0", "command-palette": "0.24.0", "deprecation-cop": "0.10.0", "dev-live-reload": "0.34.0", "exception-reporting": "0.20.0", "feedback": "0.33.0", - "find-and-replace": "0.132.0", - "fuzzy-finder": "0.57.0", + "find-and-replace": "0.138.0", + "fuzzy-finder": "0.58.0", "git-diff": "0.39.0", "go-to-line": "0.25.0", "grammar-selector": "0.29.0", "image-view": "0.36.0", "incompatible-packages": "0.9.0", - "keybinding-resolver": "0.19.0", + "keybinding-resolver": "0.20.0", "link": "0.25.0", - "markdown-preview": "0.101.0", - "metrics": "0.33.0", + "markdown-preview": "0.103.0", + "metrics": "0.34.0", "open-on-github": "0.30.0", "package-generator": "0.31.0", "release-notes": "0.36.0", - "settings-view": "0.141.0", - "snippets": "0.51.0", + "settings-view": "0.142.0", + "snippets": "0.52.0", "spell-check": "0.42.0", "status-bar": "0.44.0", "styleguide": "0.30.0", "symbols-view": "0.63.0", - "tabs": "0.50.0", + "tabs": "0.51.0", "timecop": "0.22.0", - "tree-view": "0.114.0", + "tree-view": "0.125.0", "update-package-dependencies": "0.6.0", "welcome": "0.18.0", "whitespace": "0.25.0", - "wrap-guide": "0.21.0", - + "wrap-guide": "0.22.0", "language-c": "0.28.0", "language-coffee-script": "0.30.0", "language-css": "0.17.0", - "language-gfm": "0.48.0", + "language-gfm": "0.50.0", "language-git": "0.9.0", "language-go": "0.17.0", - "language-html": "0.25.0", + "language-html": "0.26.0", "language-hyperlink": "0.12.0", "language-java": "0.11.0", "language-javascript": "0.39.0", @@ -126,10 +126,10 @@ "language-mustache": "0.10.0", "language-objective-c": "0.11.0", "language-perl": "0.9.0", - "language-php": "0.15.0", + "language-php": "0.16.0", "language-property-list": "0.7.0", "language-python": "0.19.0", - "language-ruby": "0.35.0", + "language-ruby": "0.37.0", "language-ruby-on-rails": "0.18.0", "language-sass": "0.21.0", "language-shellscript": "0.8.0", @@ -138,7 +138,7 @@ "language-text": "0.6.0", "language-todo": "0.11.0", "language-toml": "0.12.0", - "language-xml": "0.19.0", + "language-xml": "0.20.0", "language-yaml": "0.17.0" }, "private": true, diff --git a/resources/linux/Atom.desktop.in b/resources/linux/Atom.desktop.in index 0694c0cfb..84e8eb808 100644 --- a/resources/linux/Atom.desktop.in +++ b/resources/linux/Atom.desktop.in @@ -5,4 +5,5 @@ Exec=<%= installDir %>/share/atom/atom %U Icon=<%= iconName %> Type=Application StartupNotify=true -Categories=GNOME;GTK;Utility;TextEditor; +Categories=GNOME;GTK;Utility;TextEditor;Development; +MimeType=text/plain; diff --git a/script/cibuild b/script/cibuild index 6e76d8831..863478ec1 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,9 +5,6 @@ var path = require('path'); process.chdir(path.dirname(__dirname)); -if (process.platform == 'linux') - throw new Error('cibuild can not run on linux yet!'); - var homeDir = process.platform == 'win32' ? process.env.USERPROFILE : process.env.HOME; function loadEnvironmentVariables(filePath) { @@ -27,7 +24,7 @@ function loadEnvironmentVariables(filePath) { function readEnvironmentVariables() { if (process.platform === 'win32') loadEnvironmentVariables(path.resolve('/jenkins/config/atomcredentials')); - else { + else if (process.platform === 'darwin') { loadEnvironmentVariables('/var/lib/jenkins/config/atomcredentials'); loadEnvironmentVariables('/var/lib/jenkins/config/xcodekeychain'); } diff --git a/script/cibuild-atom-linux b/script/cibuild-atom-linux new file mode 100755 index 000000000..c4e957189 --- /dev/null +++ b/script/cibuild-atom-linux @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +export ATOM_ACCESS_TOKEN=$BUILD_ATOM_LINUX_ACCESS_TOKEN + +if [ -d /usr/local/share/nodenv ]; then + export NODENV_ROOT=/usr/local/share/nodenv + export PATH=/usr/local/share/nodenv/bin:/usr/local/share/nodenv/shims:$PATH + export NODENV_VERSION="v0.10.21" +fi + +script/cibuild diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index fa65a339e..554c27dd0 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -31,7 +31,7 @@ function verifyNode(cb) { var nodeMajorVersion = +versionArray[0]; var nodeMinorVersion = +versionArray[1]; if (nodeMajorVersion === 0 && nodeMinorVersion < 10) { - error = "node v0.10 is required to build Atom."; + error = "node v0.10 is required to build Atom, node " + nodeVersion + " is installed."; cb(error); } else { diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 4dc934587..7b1ae1618 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -241,15 +241,15 @@ describe "the `atom` global", -> two = atom.themes.stringToId(two) three = atom.themes.stringToId(three) - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() atom.packages.activatePackage("package-with-stylesheets-manifest") - expect(atom.themes.stylesheetElementForId(one)).toExist() - expect(atom.themes.stylesheetElementForId(two)).toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() expect($('#jasmine-content').css('font-size')).toBe '1px' describe "when the metadata does not contain a 'stylesheets' manifest", -> @@ -263,14 +263,14 @@ describe "the `atom` global", -> two = atom.themes.stringToId(two) three = atom.themes.stringToId(three) - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() atom.packages.activatePackage("package-with-stylesheets") - expect(atom.themes.stylesheetElementForId(one)).toExist() - expect(atom.themes.stylesheetElementForId(two)).toExist() - expect(atom.themes.stylesheetElementForId(three)).toExist() + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() expect($('#jasmine-content').css('font-size')).toBe '3px' describe "grammar loading", -> @@ -350,7 +350,7 @@ describe "the `atom` global", -> atom.packages.deactivatePackage("package-that-throws-on-activate") expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - it "absorbs exceptions that are thrown by the package module's serialize methods", -> + it "absorbs exceptions that are thrown by the package module's serialize method", -> spyOn(console, 'error') waitsForPromise -> @@ -365,6 +365,16 @@ describe "the `atom` global", -> expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 expect(console.error).toHaveBeenCalled() + it "absorbs exceptions that are thrown by the package module's deactivate method", -> + spyOn(console, 'error') + + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-deactivate") + + runs -> + expect(-> atom.packages.deactivatePackage("package-that-throws-on-deactivate")).not.toThrow() + expect(console.error).toHaveBeenCalled() + it "removes the package's grammars", -> waitsForPromise -> atom.packages.activatePackage('package-with-grammars') @@ -520,7 +530,7 @@ describe "the `atom` global", -> reloadedHandler = jasmine.createSpy('reloadedHandler') reloadedHandler.reset() - atom.themes.on('reloaded', reloadedHandler) + atom.themes.onDidReloadAll reloadedHandler pack = atom.packages.disablePackage(packageName) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 75e906376..9f00eff3a 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -9,7 +9,7 @@ describe "DisplayBuffer", -> buffer = atom.project.bufferForPathSync('sample.js') displayBuffer = new DisplayBuffer({buffer, tabLength}) changeHandler = jasmine.createSpy 'changeHandler' - displayBuffer.on 'changed', changeHandler + displayBuffer.onDidChange changeHandler waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -58,7 +58,7 @@ describe "DisplayBuffer", -> describe "soft wrapping", -> beforeEach -> - displayBuffer.setSoftWrap(true) + displayBuffer.setSoftWrapped(true) displayBuffer.setEditorWidthInChars(50) changeHandler.reset() @@ -129,7 +129,7 @@ describe "DisplayBuffer", -> expect(event).toEqual(start: 7, end: 8, screenDelta: -1, bufferDelta: 0) - describe "when the update causes a line to softwrap an additional time", -> + describe "when the update causes a line to soft wrap an additional time", -> it "rewraps the line and emits a change event", -> buffer.insert([6, 28], '1234567890') expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? ' @@ -162,7 +162,7 @@ describe "DisplayBuffer", -> describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", -> it "correctly renders the original wrapped line", -> buffer = atom.project.buildBufferSync(null, '') - displayBuffer = new DisplayBuffer({buffer, tabLength, editorWidthInChars: 30, softWrap: true}) + displayBuffer = new DisplayBuffer({buffer, tabLength, editorWidthInChars: 30, softWrapped: true}) buffer.insert([0, 0], "the quick brown fox jumps over the lazy dog.") buffer.insert([0, Infinity], '\n') @@ -220,10 +220,10 @@ describe "DisplayBuffer", -> displayBuffer.setWidth(50) displayBuffer.manageScrollPosition = true - displayBuffer.setSoftWrap(false) + displayBuffer.setSoftWrapped(false) displayBuffer.setScrollLeft(Infinity) expect(displayBuffer.getScrollLeft()).toBeGreaterThan 0 - displayBuffer.setSoftWrap(true) + displayBuffer.setSoftWrapped(true) expect(displayBuffer.getScrollLeft()).toBe 0 displayBuffer.setScrollLeft(10) expect(displayBuffer.getScrollLeft()).toBe 0 @@ -234,7 +234,7 @@ describe "DisplayBuffer", -> buffer.release() buffer = atom.project.bufferForPathSync('two-hundred.txt') displayBuffer = new DisplayBuffer({buffer, tabLength}) - displayBuffer.on 'changed', changeHandler + displayBuffer.onDidChange changeHandler describe "when folds are created and destroyed", -> describe "when a fold spans multiple lines", -> @@ -568,7 +568,7 @@ describe "DisplayBuffer", -> describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", -> beforeEach -> - displayBuffer.setSoftWrap(true) + displayBuffer.setSoftWrapped(true) displayBuffer.setEditorWidthInChars(50) it "allows valid positions", -> @@ -643,7 +643,7 @@ describe "DisplayBuffer", -> it "correctly translates positions on soft wrapped lines containing tabs", -> buffer.setText('\t\taa bb cc dd ee ff gg') - displayBuffer.setSoftWrap(true) + displayBuffer.setSoftWrapped(true) displayBuffer.setEditorWidthInChars(10) expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 0] expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 10] @@ -686,7 +686,7 @@ describe "DisplayBuffer", -> expect(marker2.getScreenRange()).toEqual [[5, 4], [5, 10]] it "emits a 'marker-created' event on the DisplayBuffer whenever a marker is created", -> - displayBuffer.on 'marker-created', markerCreatedHandler = jasmine.createSpy("markerCreatedHandler") + displayBuffer.onDidCreateMarker markerCreatedHandler = jasmine.createSpy("markerCreatedHandler") marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) expect(markerCreatedHandler).toHaveBeenCalledWith(marker1) @@ -722,7 +722,7 @@ describe "DisplayBuffer", -> beforeEach -> marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker.on 'changed', markerChangedHandler = jasmine.createSpy("markerChangedHandler") + marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler") it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", -> marker.setHeadScreenPosition([8, 20]) @@ -859,8 +859,8 @@ describe "DisplayBuffer", -> it "updates markers before emitting buffer change events, but does not notify their observers until the change event", -> marker2 = displayBuffer.markBufferRange([[8, 1], [8, 1]]) - marker2.on 'changed', marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler") - displayBuffer.on 'changed', changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange() + marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler") + displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange() # New change ---- @@ -886,7 +886,7 @@ describe "DisplayBuffer", -> marker2ChangedHandler.reset() marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]]) - marker3.on 'changed', marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler") + marker3.onDidChange marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler") onDisplayBufferChange = -> # calls change handler first @@ -932,7 +932,7 @@ describe "DisplayBuffer", -> expect(marker3ChangedHandler).toHaveBeenCalled() it "updates the position of markers before emitting change events that aren't caused by a buffer change", -> - displayBuffer.on 'changed', changeHandler = jasmine.createSpy("changeHandler").andCallFake -> + displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> # calls change handler first expect(markerChangedHandler).not.toHaveBeenCalled() # but still updates the markers @@ -998,16 +998,16 @@ describe "DisplayBuffer", -> expect(marker.isValid()).toBeFalsy() expect(displayBuffer.getMarker(marker.id)).toBeUndefined() - it "emits 'destroyed' events when markers are destroyed", -> + it "notifies ::onDidDestroy observers when markers are destroyed", -> destroyedHandler = jasmine.createSpy("destroyedHandler") marker = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker.on 'destroyed', destroyedHandler + marker.onDidDestroy destroyedHandler marker.destroy() expect(destroyedHandler).toHaveBeenCalled() destroyedHandler.reset() marker2 = displayBuffer.markScreenRange([[5, 4], [5, 10]]) - marker2.on 'destroyed', destroyedHandler + marker2.onDidDestroy destroyedHandler buffer.getMarker(marker2.id).destroy() expect(destroyedHandler).toHaveBeenCalled() @@ -1041,9 +1041,8 @@ describe "DisplayBuffer", -> describe 'when a marker is created', -> it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', -> displayBuffer2 = new DisplayBuffer({buffer, tabLength}) - displayBuffer.on 'marker-created', markerCreated1 = jasmine.createSpy().andCallFake (marker) -> - marker.destroy() - displayBuffer2.on 'marker-created', markerCreated2 = jasmine.createSpy() + displayBuffer.onDidCreateMarker markerCreated1 = jasmine.createSpy().andCallFake (marker) -> marker.destroy() + displayBuffer2.onDidCreateMarker markerCreated2 = jasmine.createSpy() displayBuffer.markBufferRange([[0, 0], [1, 5]], {}) @@ -1051,15 +1050,15 @@ describe "DisplayBuffer", -> expect(markerCreated2).not.toHaveBeenCalled() describe "decorations", -> - [marker, decoration, decorationParams] = [] + [marker, decoration, decorationProperties] = [] beforeEach -> marker = displayBuffer.markBufferRange([[2, 13], [3, 15]]) - decorationParams = {type: 'gutter', class: 'one'} - decoration = displayBuffer.decorateMarker(marker, decorationParams) + decorationProperties = {type: 'gutter', class: 'one'} + decoration = displayBuffer.decorateMarker(marker, decorationProperties) it "can add decorations associated with markers and remove them", -> expect(decoration).toBeDefined() - expect(decoration.getParams()).toBe decorationParams + expect(decoration.getProperties()).toBe decorationProperties expect(displayBuffer.decorationForId(decoration.id)).toBe decoration expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration @@ -1074,12 +1073,12 @@ describe "DisplayBuffer", -> describe "when a decoration is updated via Decoration::update()", -> it "emits an 'updated' event containing the new and old params", -> - decoration.on 'updated', updatedSpy = jasmine.createSpy() - decoration.update type: 'gutter', class: 'two' + decoration.onDidChangeProperties updatedSpy = jasmine.createSpy() + decoration.setProperties type: 'gutter', class: 'two' - {oldParams, newParams} = updatedSpy.mostRecentCall.args[0] - expect(oldParams).toEqual decorationParams - expect(newParams).toEqual type: 'gutter', class: 'two', id: decoration.id + {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] + expect(oldProperties).toEqual decorationProperties + expect(newProperties).toEqual type: 'gutter', class: 'two', id: decoration.id describe "::setScrollTop", -> beforeEach -> @@ -1172,7 +1171,7 @@ describe "DisplayBuffer", -> it "recomputes the scroll width when the scoped character widths change in a batch", -> operatorWidth = 20 - displayBuffer.on 'character-widths-changed', changedSpy = jasmine.createSpy() + displayBuffer.onDidChangeCharacterWidths changedSpy = jasmine.createSpy() displayBuffer.batchCharacterMeasurement -> displayBuffer.setScopedCharWidth(['source.js', 'keyword.operator.js'], '<', operatorWidth) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2cdb306e9..86f70e9d1 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -278,7 +278,7 @@ describe "EditorComponent", -> describe "when soft wrapping is enabled", -> beforeEach -> editor.setText "a line that wraps \n" - editor.setSoftWrap(true) + editor.setSoftWrapped(true) nextAnimationFrame() componentNode.style.width = 16 * charWidth + editor.getVerticalScrollbarWidth() + 'px' component.measureHeightAndWidth() @@ -457,7 +457,7 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 6 * lineHeightInPixels it "renders • characters for soft-wrapped lines", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' component.measureHeightAndWidth() @@ -893,7 +893,7 @@ describe "EditorComponent", -> it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> editor.setText("a line that wraps, ok") - editor.setSoftWrap(true) + editor.setSoftWrapped(true) componentNode.style.width = 16 * charWidth + 'px' component.measureHeightAndWidth() nextAnimationFrame() @@ -1134,7 +1134,7 @@ describe "EditorComponent", -> it "renders the decoration's new params", -> expect(componentNode.querySelector('.test-highlight')).toBeTruthy() - decoration.update(type: 'highlight', class: 'new-test-highlight') + decoration.setProperties(type: 'highlight', class: 'new-test-highlight') nextAnimationFrame() expect(componentNode.querySelector('.test-highlight')).toBeFalsy() @@ -1331,9 +1331,19 @@ describe "EditorComponent", -> gutterNode = componentNode.querySelector('.gutter') describe "when the gutter is clicked", -> - it "moves the cursor to the beginning of the clicked row", -> + it "selects the clicked row", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) - expect(editor.getCursorScreenPosition()).toEqual [4, 0] + expect(editor.getSelectedScreenRange()).toEqual [[4, 0], [5, 0]] + + describe "when the gutter is meta-clicked", -> + it "creates a new selection for the clicked row", -> + editor.setSelectedScreenRange([[3, 0], [3, 2]]) + + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [5, 0]]] + + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [5, 0]], [[6, 0], [7, 0]]] describe "when the gutter is shift-clicked", -> beforeEach -> @@ -1366,6 +1376,40 @@ describe "EditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + describe "when the gutter is meta-clicked and dragged", -> + beforeEach -> + editor.setSelectedScreenRange([[3, 0], [3, 2]]) + + describe "when dragging downward", -> + it "selects the rows between the start and end of the drag", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] + + it "merges overlapping selections", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] + + describe "when dragging upward", -> + it "selects the rows between the start and end of the drag", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] + + it "merges overlapping selections", -> + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) + nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] + describe "when the gutter is shift-clicked and dragged", -> describe "when the shift-click is below the existing selection's tail", -> describe "when dragging downward", -> @@ -2022,7 +2066,7 @@ describe "EditorComponent", -> describe "soft wrapping", -> beforeEach -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) nextAnimationFrame() it "updates the wrap location when the editor is resized", -> @@ -2148,10 +2192,10 @@ describe "EditorComponent", -> describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) callingOrder = [] - editor.on 'screen-lines-changed', -> callingOrder.push 'screen-lines-changed' + editor.onDidChangeScreenLines -> 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 36eb57bee..6ff0aa304 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -110,7 +110,7 @@ describe "Editor", -> runs -> expect(editor1.getTabLength()).toBe 4 - expect(editor1.getSoftWrap()).toBe true + expect(editor1.isSoftWrapped()).toBe true expect(editor1.getSoftTabs()).toBe false atom.config.set('editor.tabLength', 100) @@ -122,7 +122,7 @@ describe "Editor", -> runs -> expect(editor2.getTabLength()).toBe 100 - expect(editor2.getSoftWrap()).toBe false + expect(editor2.isSoftWrapped()).toBe false expect(editor2.getSoftTabs()).toBe true describe "title", -> @@ -138,13 +138,24 @@ describe "Editor", -> buffer.setPath(undefined) expect(editor.getLongTitle()).toBe 'untitled' - it "emits 'title-changed' events when the underlying buffer path", -> - titleChangedHandler = jasmine.createSpy("titleChangedHandler") - editor.on 'title-changed', titleChangedHandler + it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> + observed = [] + editor.onDidChangeTitle (title) -> observed.push(title) buffer.setPath('/foo/bar/baz.txt') buffer.setPath(undefined) - expect(titleChangedHandler.callCount).toBe 2 + + expect(observed).toEqual ['baz.txt', 'untitled'] + + describe "path", -> + it "notifies ::onDidChangePath observers when the underlying buffer path changes", -> + observed = [] + editor.onDidChangePath (filePath) -> observed.push(filePath) + + buffer.setPath(__filename) + buffer.setPath(undefined) + + expect(observed).toEqual [__filename, undefined] describe "cursor", -> describe ".getLastCursor()", -> @@ -163,21 +174,6 @@ describe "Editor", -> editor.moveDown() expect(editor.getCursorBufferPosition()).toEqual [1, 1] - it "emits a single 'cursors-moved' event for all moved cursors", -> - editor.on 'cursors-moved', cursorsMovedHandler = jasmine.createSpy("cursorsMovedHandler") - - editor.moveDown() - expect(cursorsMovedHandler.callCount).toBe 1 - - cursorsMovedHandler.reset() - editor.addCursorAtScreenPosition([3, 0]) - editor.moveDown() - expect(cursorsMovedHandler.callCount).toBe 1 - - cursorsMovedHandler.reset() - editor.getLastCursor().moveDown() - expect(cursorsMovedHandler.callCount).toBe 1 - describe ".setCursorScreenPosition(screenPosition)", -> it "clears a goal column established by vertical movement", -> # set a goal column by moving down @@ -203,7 +199,7 @@ describe "Editor", -> describe "when soft-wrap is enabled and code is folded", -> beforeEach -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) editor.createFold(2, 3) @@ -321,6 +317,21 @@ describe "Editor", -> editor.moveLeft() expect(editor.getCursorScreenPosition()).toEqual [1, 7] + it "moves the cursor by n columns to the left", -> + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual [1, 4] + + it "moves the cursor by two rows up when the columnCount is longer than an entire line", -> + editor.setCursorScreenPosition([2, 2]) + editor.moveLeft(34) + expect(editor.getCursorScreenPosition()).toEqual [0, 29] + + it "moves the cursor to the beginning columnCount is longer than the position in the buffer", -> + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(100) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + describe "when the cursor is in the first column", -> describe "when there is a previous line", -> it "wraps to the end of the previous line", -> @@ -328,12 +339,28 @@ describe "Editor", -> editor.moveLeft() expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) + it "moves the cursor by one row up and n columns to the left", -> + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual [0, 26] + + describe "when the next line is empty", -> + it "wraps to the beginning of the previous line", -> + editor.setCursorScreenPosition([11, 0]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual [10, 0] + describe "when the cursor is on the first line", -> it "remains in the same position (0,0)", -> editor.setCursorScreenPosition(row: 0, column: 0) editor.moveLeft() expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) + it "remains in the same position (0,0) when columnCount is specified", -> + editor.setCursorScreenPosition([0, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> it "skips tabLength worth of whitespace at a time", -> editor.setCursorBufferPosition([5, 6]) @@ -368,6 +395,21 @@ describe "Editor", -> editor.moveRight() expect(editor.getCursorScreenPosition()).toEqual [3, 4] + it "moves the cursor by n columns to the right", -> + editor.setCursorScreenPosition([3, 7]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual [3, 11] + + it "moves the cursor by two rows down when the columnCount is longer than an entire line", -> + editor.setCursorScreenPosition([0, 29]) + editor.moveRight(34) + expect(editor.getCursorScreenPosition()).toEqual [2, 2] + + it "moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position", -> + editor.setCursorScreenPosition([11, 5]) + editor.moveRight(100) + expect(editor.getCursorScreenPosition()).toEqual [12, 2] + describe "when the cursor is on the last column of a line", -> describe "when there is a subsequent line", -> it "wraps to the beginning of the next line", -> @@ -375,6 +417,17 @@ describe "Editor", -> editor.moveRight() expect(editor.getCursorScreenPosition()).toEqual [1, 0] + it "moves the cursor by one row down and n columns to the right", -> + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual [1, 3] + + describe "when the next line is empty", -> + it "wraps to the beginning of the next line", -> + editor.setCursorScreenPosition([9, 4]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual [10, 0] + describe "when the cursor is on the last line", -> it "remains in the same position", -> lastLineIndex = buffer.getLines().length - 1 @@ -427,7 +480,7 @@ describe "Editor", -> describe ".moveToBeginningOfScreenLine()", -> describe "when soft wrap is on", -> it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition([1, 2]) editor.moveToBeginningOfScreenLine() @@ -447,7 +500,7 @@ describe "Editor", -> describe ".moveToEndOfScreenLine()", -> describe "when soft wrap is on", -> it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition([1, 2]) editor.moveToEndOfScreenLine() @@ -466,7 +519,7 @@ describe "Editor", -> describe ".moveToBeginningOfLine()", -> it "moves cursor to the beginning of the buffer line", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition([1, 2]) editor.moveToBeginningOfLine() @@ -475,7 +528,7 @@ describe "Editor", -> describe ".moveToEndOfLine()", -> it "moves cursor to the end of the buffer line", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition([0, 2]) editor.moveToEndOfLine() @@ -485,7 +538,7 @@ describe "Editor", -> describe ".moveToFirstCharacterOfLine()", -> describe "when soft wrap is on", -> it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition [2,5] editor.addCursorAtScreenPosition [8,7] @@ -725,37 +778,6 @@ describe "Editor", -> editor.setCursorBufferPosition([3, 1]) expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() - describe "cursor-moved events", -> - cursorMovedHandler = null - - beforeEach -> - editor.foldBufferRow(4) - editor.setSelectedBufferRange([[8, 1], [9, 0]]) - cursorMovedHandler = jasmine.createSpy("cursorMovedHandler") - editor.on 'cursor-moved', cursorMovedHandler - - describe "when the position of the cursor changes", -> - it "emits a cursor-moved event", -> - buffer.insert([9, 0], '...') - expect(cursorMovedHandler).toHaveBeenCalledWith( - oldBufferPosition: [9, 0] - oldScreenPosition: [6, 0] - newBufferPosition: [9, 3] - newScreenPosition: [6, 3] - textChanged: true - ) - - describe "when the position of the associated selection's tail changes, but not the cursor's position", -> - it "does not emit a cursor-moved event", -> - buffer.insert([8, 0], '...') - expect(cursorMovedHandler).not.toHaveBeenCalled() - - describe "::getCursorBufferPositions()", -> - it "returns the cursor positions in the order they were added", -> - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([4, 5]) - expect(editor.getCursorBufferPositions()).toEqual [[0, 0], [8, 5], [4, 5]] - describe "::getCursorScreenPositions()", -> it "returns the cursor positions in the order they were added", -> editor.foldBufferRow(4) @@ -924,6 +946,27 @@ describe "Editor", -> expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) expect(selection1.isReversed()).toBeFalsy() + describe "when counts are passed into the selection functions", -> + it "expands each selection to its cursor's new location", -> + editor.setSelectedBufferRanges([[[0,9], [0,13]], [[3,16], [3,21]]]) + [selection1, selection2] = editor.getSelections() + + editor.selectRight(2) + expect(selection1.getBufferRange()).toEqual [[0,9], [0,15]] + expect(selection2.getBufferRange()).toEqual [[3,16], [3,23]] + + editor.selectLeft(3) + expect(selection1.getBufferRange()).toEqual [[0,9], [0,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [3,20]] + + editor.selectDown(3) + expect(selection1.getBufferRange()).toEqual [[0,9], [3,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [6,20]] + + editor.selectUp(2) + expect(selection1.getBufferRange()).toEqual [[0,9], [1,12]] + expect(selection2.getBufferRange()).toEqual [[3,16], [4,20]] + describe ".selectToBufferPosition(bufferPosition)", -> it "expands the last selection to the given position", -> editor.setSelectedBufferRange([[3, 0], [4, 5]]) @@ -1573,7 +1616,7 @@ describe "Editor", -> beforeEach -> editor.setSelectedBufferRange([[1, 0], [1, 2]]) - it "will-insert-text and did-insert-text events are emitted when inserting text", -> + it "replaces the selection with the given text", -> range = editor.insertText('xxx') expect(range).toEqual [ [[1, 0], [1, 3]] ] expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' @@ -1655,19 +1698,19 @@ describe "Editor", -> editor.insertText('holy cow') expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined() - describe "when will-insert-text and did-insert-text events are used", -> + describe "when there are ::onWillInsertText and ::onDidInsertText observers", -> beforeEach -> editor.setSelectedBufferRange([[1, 0], [1, 2]]) - it "will-insert-text and did-insert-text events are emitted when inserting text", -> + it "notifies the observers when inserting text", -> willInsertSpy = jasmine.createSpy().andCallFake -> expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' didInsertSpy = jasmine.createSpy().andCallFake -> expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - editor.on('will-insert-text', willInsertSpy) - editor.on('did-insert-text', didInsertSpy) + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) expect(editor.insertText('xxx')).toBeTruthy() expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' @@ -1682,14 +1725,14 @@ describe "Editor", -> options = didInsertSpy.mostRecentCall.args[0] expect(options.text).toBe 'xxx' - it "text insertion is prevented when cancel is called from a will-insert-text handler", -> + it "cancels text insertion when an ::onWillInsertText observer calls cancel on an event", -> willInsertSpy = jasmine.createSpy().andCallFake ({cancel}) -> cancel() didInsertSpy = jasmine.createSpy() - editor.on('will-insert-text', willInsertSpy) - editor.on('did-insert-text', didInsertSpy) + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) expect(editor.insertText('xxx')).toBe false expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' @@ -1846,7 +1889,7 @@ describe "Editor", -> beforeEach -> selection = editor.getLastSelection() changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.on 'screen-range-changed', changeScreenRangeHandler + selection.onDidChangeRange changeScreenRangeHandler describe "when the cursor is on the middle of the line", -> it "removes the character before the cursor", -> @@ -2344,7 +2387,7 @@ describe "Editor", -> describe ".cutToEndOfLine()", -> describe "when soft wrap is on", -> it "cuts up to the end of the line", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorScreenPosition([2, 2]) editor.cutToEndOfLine() @@ -2846,7 +2889,7 @@ describe "Editor", -> describe "when soft wrap is enabled", -> it "deletes the entire line that the cursor is on", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setEditorWidthInChars(10) editor.setCursorBufferPosition([6]) @@ -3344,7 +3387,7 @@ describe "Editor", -> describe ".setIndentationForBufferRow", -> describe "when the editor uses soft tabs but the row has hard tabs", -> it "only replaces whitespace characters", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setText("\t1\n\t2") editor.setCursorBufferPosition([0, 0]) editor.setIndentationForBufferRow(0, 2) @@ -3352,7 +3395,7 @@ describe "Editor", -> describe "when the indentation level is a non-integer", -> it "does not throw an exception", -> - editor.setSoftWrap(true) + editor.setSoftWrapped(true) editor.setText("\t1\n\t2") editor.setCursorBufferPosition([0, 0]) editor.setIndentationForBufferRow(0, 2.1) diff --git a/spec/fixtures/packages/package-that-throws-on-deactivate/index.coffee b/spec/fixtures/packages/package-that-throws-on-deactivate/index.coffee new file mode 100644 index 000000000..5def0ed2d --- /dev/null +++ b/spec/fixtures/packages/package-that-throws-on-deactivate/index.coffee @@ -0,0 +1,4 @@ +module.exports = + activate: -> + deactivate: -> throw new Error('Top that') + serialize: -> diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index b40f68182..2cc0a6239 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -111,10 +111,10 @@ describe "Git", -> fs.writeFileSync(filePath, 'ch ch changes') repo.getPathStatus(filePath) statusHandler = jasmine.createSpy('statusHandler') - repo.on 'status-changed', statusHandler + repo.onDidChangeStatus statusHandler repo.checkoutHead(filePath) expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0..1]).toEqual [filePath, 0] + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} repo.checkoutHead(filePath) expect(statusHandler.callCount).toBe 1 @@ -167,11 +167,11 @@ describe "Git", -> it "trigger a status-changed event when the new status differs from the last cached one", -> statusHandler = jasmine.createSpy("statusHandler") - repo.on 'status-changed', statusHandler + repo.onDidChangeStatus statusHandler fs.writeFileSync(filePath, '') status = repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0..1]).toEqual [filePath, status] + expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} fs.writeFileSync(filePath, 'abc') status = repo.getPathStatus(filePath) @@ -208,7 +208,7 @@ describe "Git", -> it "returns status information for all new and modified files", -> fs.writeFileSync(modifiedPath, 'making this path modified') statusHandler = jasmine.createSpy('statusHandler') - repo.on 'statuses-changed', statusHandler + repo.onDidChangeStatuses statusHandler repo.refreshStatus() waitsFor -> @@ -232,19 +232,19 @@ describe "Git", -> editor.insertNewline() statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().on 'status-changed', statusHandler + atom.project.getRepo().onDidChangeStatus statusHandler editor.save() expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} it "emits a status-changed event when a buffer is reloaded", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().on 'status-changed', statusHandler + atom.project.getRepo().onDidChangeStatus statusHandler editor.getBuffer().reload() expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} editor.getBuffer().reload() expect(statusHandler.callCount).toBe 1 @@ -252,11 +252,11 @@ describe "Git", -> fs.writeFileSync(editor.getPath(), 'changed') statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepo().on 'status-changed', statusHandler - editor.getBuffer().emit 'path-changed' + atom.project.getRepo().onDidChangeStatus statusHandler + editor.getBuffer().emitter.emit 'did-change-path' expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith editor.getPath(), 256 - editor.getBuffer().emit 'path-changed' + expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} + editor.getBuffer().emitter.emit 'did-change-path' expect(statusHandler.callCount).toBe 1 describe "when a project is deserialized", -> @@ -283,7 +283,7 @@ describe "Git", -> buffer.append('changes') statusHandler = jasmine.createSpy('statusHandler') - project2.getRepo().on 'status-changed', statusHandler + project2.getRepo().onDidChangeStatus statusHandler buffer.save() expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith buffer.getPath(), 256 + expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index bbb00ca81..b94f86a0f 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -102,6 +102,6 @@ describe "Package", -> theme.activate() it "deactivated event fires on .deactivate()", -> - theme.on 'deactivated', spy = jasmine.createSpy() + theme.onDidDeactivate spy = jasmine.createSpy() theme.deactivate() expect(spy).toHaveBeenCalled() diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index df9921e69..1701e3f03 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -27,42 +27,91 @@ describe "PaneContainer", -> it "preserves the active pane across serialization, independent of focus", -> pane3A.activate() - expect(containerA.activePane).toBe pane3A + expect(containerA.getActivePane()).toBe pane3A containerB = containerA.testSerialization() [pane1B, pane2B, pane3B] = containerB.getPanes() - expect(containerB.activePane).toBe pane3B + expect(containerB.getActivePane()).toBe pane3B - describe "::activePane", -> + it "does not allow the root pane to be destroyed", -> + container = new PaneContainer + container.getRoot().destroy() + expect(container.getRoot()).toBeDefined() + expect(container.getRoot().isDestroyed()).toBe false + + describe "::getActivePane()", -> [container, pane1, pane2] = [] beforeEach -> container = new PaneContainer - pane1 = container.root + pane1 = container.getRoot() - it "references the first pane if no pane has been made active", -> - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true + it "returns the first pane if no pane has been made active", -> + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true - it "references the most pane on which ::activate was most recently called", -> + it "returns the most pane on which ::activate() was most recently called", -> pane2 = pane1.splitRight() pane2.activate() - expect(container.activePane).toBe pane2 - expect(pane1.active).toBe false - expect(pane2.active).toBe true + expect(container.getActivePane()).toBe pane2 + expect(pane1.isActive()).toBe false + expect(pane2.isActive()).toBe true pane1.activate() - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true - expect(pane2.active).toBe false + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true + expect(pane2.isActive()).toBe false - it "is reassigned to the next pane if the current active pane is destroyed", -> + it "returns the next pane if the current active pane is destroyed", -> pane2 = pane1.splitRight() pane2.activate() pane2.destroy() - expect(container.activePane).toBe pane1 - expect(pane1.active).toBe true + expect(container.getActivePane()).toBe pane1 + expect(pane1.isActive()).toBe true - it "does not allow the root pane to be destroyed", -> - pane1.destroy() - expect(container.root).toBe pane1 - expect(pane1.isDestroyed()).toBe false + describe "::onDidChangeActivePaneItem()", -> + [container, pane1, pane2, observed] = [] + + beforeEach -> + container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container.getRoot().splitRight(items: [new Object, new Object]) + [pane1, pane2] = container.getPanes() + + observed = [] + container.onDidChangeActivePaneItem (item) -> observed.push(item) + + it "invokes observers when the active item of the active pane changes", -> + pane2.activateNextItem() + pane2.activateNextItem() + expect(observed).toEqual [pane2.itemAtIndex(1), pane2.itemAtIndex(0)] + + it "invokes observers when the active pane changes", -> + pane1.activate() + pane2.activate() + expect(observed).toEqual [pane1.itemAtIndex(0), pane2.itemAtIndex(0)] + + describe "::observePanes()", -> + it "invokes observers with all current and future panes", -> + container = new PaneContainer + container.getRoot().splitRight() + [pane1, pane2] = container.getPanes() + + observed = [] + container.observePanes (pane) -> observed.push(pane) + + pane3 = pane2.splitDown() + pane4 = pane2.splitRight() + + expect(observed).toEqual [pane1, pane2, pane3, pane4] + + describe "::observePaneItems()", -> + it "invokes observers with all current and future pane items", -> + container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container.getRoot().splitRight(items: [new Object]) + [pane1, pane2] = container.getPanes() + observed = [] + container.observePaneItems (pane) -> observed.push(pane) + + pane3 = pane2.splitDown(items: [new Object]) + pane3.addItems([new Object, new Object]) + + expect(observed).toEqual container.getPaneItems() diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 04b1c2474..2f2de1910 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -21,39 +21,83 @@ describe "Pane", -> describe "construction", -> it "sets the active item to the first item", -> pane = new Pane(items: [new Item("A"), new Item("B")]) - expect(pane.activeItem).toBe pane.items[0] + expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) it "compacts the items array", -> pane = new Pane(items: [undefined, new Item("A"), null, new Item("B")]) - expect(pane.items.length).toBe 2 - expect(pane.activeItem).toBe pane.items[0] + expect(pane.getItems().length).toBe 2 + expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) + + describe "::activate()", -> + [container, pane1, pane2] = [] + + beforeEach -> + container = new PaneContainer(root: new Pane) + container.getRoot().splitRight() + [pane1, pane2] = container.getPanes() + + it "changes the active pane on the container", -> + expect(container.getActivePane()).toBe pane2 + pane1.activate() + expect(container.getActivePane()).toBe pane1 + pane2.activate() + expect(container.getActivePane()).toBe pane2 + + it "invokes ::onDidChangeActivePane observers on the container", -> + observed = [] + container.onDidChangeActivePane (activePane) -> observed.push(activePane) + + pane1.activate() + pane1.activate() + pane2.activate() + pane1.activate() + expect(observed).toEqual [pane1, pane2, pane1] + + it "invokes ::onDidChangeActive observers on the relevant panes", -> + observed = [] + pane1.onDidChangeActive (active) -> observed.push(active) + pane1.activate() + pane2.activate() + expect(observed).toEqual [true, false] + + it "invokes ::onDidActivate() observers", -> + eventCount = 0 + pane1.onDidActivate -> eventCount++ + pane1.activate() + pane1.activate() + pane2.activate() + expect(eventCount).toBe 2 describe "::addItem(item, index)", -> it "adds the item at the given index", -> pane = new Pane(items: [new Item("A"), new Item("B")]) - [item1, item2] = pane.items + [item1, item2] = pane.getItems() item3 = new Item("C") pane.addItem(item3, 1) - expect(pane.items).toEqual [item1, item3, item2] + expect(pane.getItems()).toEqual [item1, item3, item2] - it "adds the item after the active item ", -> + it "adds the item after the active item if no index is provided", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItem(item2) item4 = new Item("D") pane.addItem(item4) - expect(pane.items).toEqual [item1, item2, item4, item3] + expect(pane.getItems()).toEqual [item1, item2, item4, item3] it "sets the active item after adding the first item", -> pane = new Pane item = new Item("A") - events = [] - pane.on 'item-added', -> events.push('item-added') - pane.$activeItem.changes.onValue -> events.push('active-item-changed') - pane.addItem(item) - expect(pane.activeItem).toBe item - expect(events).toEqual ['item-added', 'active-item-changed'] + expect(pane.getActiveItem()).toBe item + + it "invokes ::onDidAddItem() observers", -> + pane = new Pane(items: [new Item("A"), new Item("B")]) + events = [] + pane.onDidAddItem (event) -> events.push(event) + + item = new Item("C") + pane.addItem(item, 1) + expect(events).toEqual [{item, index: 1}] describe "::activateItem(item)", -> pane = null @@ -62,83 +106,102 @@ describe "Pane", -> pane = new Pane(items: [new Item("A"), new Item("B")]) it "changes the active item to the current item", -> - expect(pane.activeItem).toBe pane.items[0] - pane.activateItem(pane.items[1]) - expect(pane.activeItem).toBe pane.items[1] + expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) + pane.activateItem(pane.itemAtIndex(1)) + expect(pane.getActiveItem()).toBe pane.itemAtIndex(1) it "adds the given item if it isn't present in ::items", -> item = new Item("C") pane.activateItem(item) - expect(item in pane.items).toBe true - expect(pane.activeItem).toBe item + expect(item in pane.getItems()).toBe true + expect(pane.getActiveItem()).toBe item + + it "invokes ::onDidChangeActiveItem() observers", -> + observed = [] + pane.onDidChangeActiveItem (item) -> observed.push(item) + pane.activateItem(pane.itemAtIndex(1)) + expect(observed).toEqual [pane.itemAtIndex(1)] describe "::activateNextItem() and ::activatePreviousItem()", -> it "sets the active item to the next/previous item, looping around at either end", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.activatePreviousItem() - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activatePreviousItem() - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.activateNextItem() - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activateNextItem() - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 describe "::activateItemAtIndex(index)", -> it "activates the item at the given index", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItemAtIndex(2) - expect(pane.activeItem).toBe item3 + expect(pane.getActiveItem()).toBe item3 pane.activateItemAtIndex(1) - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.activateItemAtIndex(0) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 # Doesn't fail with out-of-bounds indices pane.activateItemAtIndex(100) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.activateItemAtIndex(-1) - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 describe "::destroyItem(item)", -> [pane, item1, item2, item3] = [] beforeEach -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() - it "removes the item from the items list", -> - expect(pane.activeItem).toBe item1 + it "removes the item from the items list and destroyes it", -> + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item2) - expect(item2 in pane.items).toBe false - expect(pane.activeItem).toBe item1 + expect(item2 in pane.getItems()).toBe false + expect(item2.isDestroyed()).toBe true + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item1) - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false + expect(item1.isDestroyed()).toBe true + + it "invokes ::onWillDestroyItem() observers before destroying the item", -> + events = [] + pane.onWillDestroyItem (event) -> + expect(item2.isDestroyed()).toBe false + events.push(event) + + pane.destroyItem(item2) + expect(item2.isDestroyed()).toBe true + expect(events).toEqual [{item: item2, index: 1}] + + it "invokes ::onDidRemoveItem() observers", -> + events = [] + pane.onDidRemoveItem (event) -> events.push(event) + pane.destroyItem(item2) + expect(events).toEqual [{item: item2, index: 1, destroyed: true}] describe "when the destroyed item is the active item and is the first item", -> it "activates the next item", -> - expect(pane.activeItem).toBe item1 + expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item1) - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 describe "when the destroyed item is the active item and is not the first item", -> beforeEach -> pane.activateItem(item2) it "activates the previous item", -> - expect(pane.activeItem).toBe item2 + expect(pane.getActiveItem()).toBe item2 pane.destroyItem(item2) - expect(pane.activeItem).toBe item1 - - it "emits 'item-removed' with the item, its index, and true indicating the item is being destroyed", -> - pane.on 'item-removed', itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") - pane.destroyItem(item2) - expect(itemRemovedHandler).toHaveBeenCalledWith(item2, 1, true) + expect(pane.getActiveItem()).toBe item1 describe "if the item is modified", -> itemUri = null @@ -157,7 +220,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).toHaveBeenCalled() - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "when the item has no uri", -> @@ -170,7 +233,7 @@ describe "Pane", -> expect(atom.showSaveDialogSync).toHaveBeenCalled() expect(item1.saveAs).toHaveBeenCalledWith("/selected/path") - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "if the [Don't Save] option is selected", -> @@ -179,7 +242,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.items).toBe false + expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "if the [Cancel] option is selected", -> @@ -188,7 +251,7 @@ describe "Pane", -> pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.items).toBe true + expect(item1 in pane.getItems()).toBe true expect(item1.isDestroyed()).toBe false describe "when the last item is destroyed", -> @@ -197,7 +260,7 @@ describe "Pane", -> expect(atom.config.get('core.destroyEmptyPanes')).toBe false pane.destroyItem(item) for item in pane.getItems() expect(pane.isDestroyed()).toBe false - expect(pane.activeItem).toBeUndefined() + expect(pane.getActiveItem()).toBeUndefined() expect(-> pane.saveActiveItem()).not.toThrow() expect(-> pane.saveActiveItemAs()).not.toThrow() @@ -210,10 +273,10 @@ describe "Pane", -> describe "::destroyActiveItem()", -> it "destroys the active item", -> pane = new Pane(items: [new Item("A"), new Item("B")]) - activeItem = pane.activeItem + activeItem = pane.getActiveItem() pane.destroyActiveItem() expect(activeItem.isDestroyed()).toBe true - expect(activeItem in pane.items).toBe false + expect(activeItem in pane.getItems()).toBe false it "does not throw an exception if there are no more items", -> pane = new Pane @@ -222,27 +285,40 @@ describe "Pane", -> describe "::destroyItems()", -> it "destroys all items", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.destroyItems() expect(item1.isDestroyed()).toBe true expect(item2.isDestroyed()).toBe true expect(item3.isDestroyed()).toBe true - expect(pane.items).toEqual [] + expect(pane.getItems()).toEqual [] + + describe "::observeItems()", -> + it "invokes the observer with all current and future items", -> + pane = new Pane(items: [new Item, new Item]) + [item1, item2] = pane.getItems() + + observed = [] + pane.observeItems (item) -> observed.push(item) + + item3 = new Item + pane.addItem(item3) + + expect(observed).toEqual [item1, item2, item3] describe "when an item emits a destroyed event", -> it "removes it from the list of items", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items - pane.items[1].destroy() - expect(pane.items).toEqual [item1, item3] + [item1, item2, item3] = pane.getItems() + pane.itemAtIndex(1).destroy() + expect(pane.getItems()).toEqual [item1, item3] describe "::destroyInactiveItems()", -> it "destroys all items but the active item", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() pane.activateItem(item2) pane.destroyInactiveItems() - expect(pane.items).toEqual [item2] + expect(pane.getItems()).toEqual [item2] describe "::saveActiveItem()", -> pane = null @@ -253,30 +329,30 @@ describe "Pane", -> describe "when the active item has a uri", -> beforeEach -> - pane.activeItem.uri = "test" + pane.getActiveItem().uri = "test" describe "when the active item has a save method", -> it "saves the current item", -> - pane.activeItem.save = jasmine.createSpy("save") + pane.getActiveItem().save = jasmine.createSpy("save") pane.saveActiveItem() - expect(pane.activeItem.save).toHaveBeenCalled() + expect(pane.getActiveItem().save).toHaveBeenCalled() describe "when the current item has no save method", -> it "does nothing", -> - expect(pane.activeItem.save).toBeUndefined() + expect(pane.getActiveItem().save).toBeUndefined() pane.saveActiveItem() describe "when the current item has no uri", -> describe "when the current item has a saveAs method", -> it "opens a save dialog and saves the current item as the selected path", -> - pane.activeItem.saveAs = jasmine.createSpy("saveAs") + pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItem() expect(atom.showSaveDialogSync).toHaveBeenCalled() - expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path') + expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item has no saveAs method", -> it "does nothing", -> - expect(pane.activeItem.saveAs).toBeUndefined() + expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItem() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() @@ -289,22 +365,22 @@ describe "Pane", -> describe "when the current item has a saveAs method", -> it "opens the save dialog and calls saveAs on the item with the selected path", -> - pane.activeItem.path = __filename - pane.activeItem.saveAs = jasmine.createSpy("saveAs") + pane.getActiveItem().path = __filename + pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItemAs() expect(atom.showSaveDialogSync).toHaveBeenCalledWith(__filename) - expect(pane.activeItem.saveAs).toHaveBeenCalledWith('/selected/path') + expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item does not have a saveAs method", -> it "does nothing", -> - expect(pane.activeItem.saveAs).toBeUndefined() + expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItemAs() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() describe "::itemForUri(uri)", -> it "returns the item for which a call to .getUri() returns the given uri", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) - [item1, item2, item3] = pane.items + [item1, item2, item3] = pane.getItems() item1.uri = "a" item2.uri = "b" expect(pane.itemForUri("a")).toBe item1 @@ -312,24 +388,32 @@ describe "Pane", -> expect(pane.itemForUri("bogus")).toBeUndefined() describe "::moveItem(item, index)", -> - it "moves the item to the given index and emits an 'item-moved' event with the item and its new index", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) - [item1, item2, item3, item4] = pane.items - pane.on 'item-moved', itemMovedHandler = jasmine.createSpy("itemMovedHandler") + [pane, item1, item2, item3, item4] = [] + beforeEach -> + pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) + [item1, item2, item3, item4] = pane.getItems() + + it "moves the item to the given index and invokes ::onDidMoveItem observers", -> pane.moveItem(item1, 2) expect(pane.getItems()).toEqual [item2, item3, item1, item4] - expect(itemMovedHandler).toHaveBeenCalledWith(item1, 2) - itemMovedHandler.reset() pane.moveItem(item2, 3) expect(pane.getItems()).toEqual [item3, item1, item4, item2] - expect(itemMovedHandler).toHaveBeenCalledWith(item2, 3) - itemMovedHandler.reset() pane.moveItem(item2, 1) expect(pane.getItems()).toEqual [item3, item2, item1, item4] - expect(itemMovedHandler).toHaveBeenCalledWith(item2, 1) + + it "invokes ::onDidMoveItem() observers", -> + events = [] + pane.onDidMoveItem (event) -> events.push(event) + + pane.moveItem(item1, 2) + pane.moveItem(item2, 3) + expect(events).toEqual [ + {item: item1, oldIndex: 0, newIndex: 2} + {item: item2, oldIndex: 0, newIndex: 3} + ] describe "::moveItemToPane(item, pane, index)", -> [container, pane1, pane2] = [] @@ -339,13 +423,20 @@ describe "Pane", -> pane1 = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) container = new PaneContainer(root: pane1) pane2 = pane1.splitRight(items: [new Item("D"), new Item("E")]) - [item1, item2, item3] = pane1.items - [item4, item5] = pane2.items + [item1, item2, item3] = pane1.getItems() + [item4, item5] = pane2.getItems() it "moves the item to the given pane at the given index", -> pane1.moveItemToPane(item2, pane2, 1) - expect(pane1.items).toEqual [item1, item3] - expect(pane2.items).toEqual [item4, item2, item5] + expect(pane1.getItems()).toEqual [item1, item3] + expect(pane2.getItems()).toEqual [item4, item2, item5] + + it "invokes ::onDidRemoveItem() observers", -> + events = [] + pane1.onDidRemoveItem (event) -> events.push(event) + pane1.moveItemToPane(item2, pane2, 1) + + expect(events).toEqual [{item: item2, index: 1, destroyed: false}] describe "when the moved item the last item in the source pane", -> beforeEach -> @@ -368,22 +459,27 @@ describe "Pane", -> [pane1, container] = [] beforeEach -> - pane1 = new Pane(items: ["A"]) + pane1 = new Pane(items: [new Item("A")]) container = new PaneContainer(root: pane1) describe "::splitLeft(params)", -> describe "when the parent is the container root", -> it "replaces itself with a row and inserts a new pane to the left of itself", -> - pane2 = pane1.splitLeft(items: ["B"]) - pane3 = pane1.splitLeft(items: ["C"]) + pane2 = pane1.splitLeft(items: [new Item("B")]) + pane3 = pane1.splitLeft(items: [new Item("C")]) expect(container.root.orientation).toBe 'horizontal' expect(container.root.children).toEqual [pane2, pane3, pane1] + describe "when `copyActiveItem: true` is passed in the params", -> + it "duplicates the active item", -> + pane2 = pane1.splitLeft(copyActiveItem: true) + expect(pane2.getActiveItem()).toEqual pane1.getActiveItem() + describe "when the parent is a column", -> it "replaces itself with a row and inserts a new pane to the left of itself", -> pane1.splitDown() - pane2 = pane1.splitLeft(items: ["B"]) - pane3 = pane1.splitLeft(items: ["C"]) + pane2 = pane1.splitLeft(items: [new Item("B")]) + pane3 = pane1.splitLeft(items: [new Item("C")]) row = container.root.children[0] expect(row.orientation).toBe 'horizontal' expect(row.children).toEqual [pane2, pane3, pane1] @@ -391,16 +487,21 @@ describe "Pane", -> describe "::splitRight(params)", -> describe "when the parent is the container root", -> it "replaces itself with a row and inserts a new pane to the right of itself", -> - pane2 = pane1.splitRight(items: ["B"]) - pane3 = pane1.splitRight(items: ["C"]) + pane2 = pane1.splitRight(items: [new Item("B")]) + pane3 = pane1.splitRight(items: [new Item("C")]) expect(container.root.orientation).toBe 'horizontal' expect(container.root.children).toEqual [pane1, pane3, pane2] + describe "when `copyActiveItem: true` is passed in the params", -> + it "duplicates the active item", -> + pane2 = pane1.splitRight(copyActiveItem: true) + expect(pane2.getActiveItem()).toEqual pane1.getActiveItem() + describe "when the parent is a column", -> it "replaces itself with a row and inserts a new pane to the right of itself", -> pane1.splitDown() - pane2 = pane1.splitRight(items: ["B"]) - pane3 = pane1.splitRight(items: ["C"]) + pane2 = pane1.splitRight(items: [new Item("B")]) + pane3 = pane1.splitRight(items: [new Item("C")]) row = container.root.children[0] expect(row.orientation).toBe 'horizontal' expect(row.children).toEqual [pane1, pane3, pane2] @@ -408,16 +509,21 @@ describe "Pane", -> describe "::splitUp(params)", -> describe "when the parent is the container root", -> it "replaces itself with a column and inserts a new pane above itself", -> - pane2 = pane1.splitUp(items: ["B"]) - pane3 = pane1.splitUp(items: ["C"]) + pane2 = pane1.splitUp(items: [new Item("B")]) + pane3 = pane1.splitUp(items: [new Item("C")]) expect(container.root.orientation).toBe 'vertical' expect(container.root.children).toEqual [pane2, pane3, pane1] + describe "when `copyActiveItem: true` is passed in the params", -> + it "duplicates the active item", -> + pane2 = pane1.splitUp(copyActiveItem: true) + expect(pane2.getActiveItem()).toEqual pane1.getActiveItem() + describe "when the parent is a row", -> it "replaces itself with a column and inserts a new pane above itself", -> pane1.splitRight() - pane2 = pane1.splitUp(items: ["B"]) - pane3 = pane1.splitUp(items: ["C"]) + pane2 = pane1.splitUp(items: [new Item("B")]) + pane3 = pane1.splitUp(items: [new Item("C")]) column = container.root.children[0] expect(column.orientation).toBe 'vertical' expect(column.children).toEqual [pane2, pane3, pane1] @@ -425,16 +531,21 @@ describe "Pane", -> describe "::splitDown(params)", -> describe "when the parent is the container root", -> it "replaces itself with a column and inserts a new pane below itself", -> - pane2 = pane1.splitDown(items: ["B"]) - pane3 = pane1.splitDown(items: ["C"]) + pane2 = pane1.splitDown(items: [new Item("B")]) + pane3 = pane1.splitDown(items: [new Item("C")]) expect(container.root.orientation).toBe 'vertical' expect(container.root.children).toEqual [pane1, pane3, pane2] + describe "when `copyActiveItem: true` is passed in the params", -> + it "duplicates the active item", -> + pane2 = pane1.splitDown(copyActiveItem: true) + expect(pane2.getActiveItem()).toEqual pane1.getActiveItem() + describe "when the parent is a row", -> it "replaces itself with a column and inserts a new pane below itself", -> pane1.splitRight() - pane2 = pane1.splitDown(items: ["B"]) - pane3 = pane1.splitDown(items: ["C"]) + pane2 = pane1.splitDown(items: [new Item("B")]) + pane3 = pane1.splitDown(items: [new Item("C")]) column = container.root.children[0] expect(column.orientation).toBe 'vertical' expect(column.children).toEqual [pane1, pane3, pane2] @@ -455,7 +566,7 @@ describe "Pane", -> pane2 = pane1.splitRight() it "destroys the pane's destroyable items", -> - [item1, item2] = pane1.items + [item1, item2] = pane1.getItems() pane1.destroy() expect(item1.isDestroyed()).toBe true expect(item2.isDestroyed()).toBe true @@ -493,12 +604,12 @@ describe "Pane", -> it "can serialize and deserialize the pane and all its items", -> newPane = pane.testSerialization() - expect(newPane.items).toEqual pane.items + expect(newPane.getItems()).toEqual pane.getItems() it "restores the active item on deserialization", -> pane.activateItemAtIndex(1) newPane = pane.testSerialization() - expect(newPane.activeItem).toEqual newPane.items[1] + expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1) it "does not include items that cannot be deserialized", -> spyOn(console, 'warn') @@ -506,8 +617,8 @@ describe "Pane", -> pane.activateItem(unserializable) newPane = pane.testSerialization() - expect(newPane.activeItem).toEqual pane.items[0] - expect(newPane.items.length).toBe pane.items.length - 1 + expect(newPane.getActiveItem()).toEqual pane.itemAtIndex(0) + expect(newPane.getItems().length).toBe pane.getItems().length - 1 it "includes the pane's focus state in the serialized state", -> pane.focus() diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 9f73884b5..56cb6ddc8 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -1,6 +1,7 @@ PaneContainerView = require '../src/pane-container-view' PaneView = require '../src/pane-view' fs = require 'fs-plus' +{Emitter} = require 'event-kit' {$, View} = require 'atom' path = require 'path' temp = require 'temp' @@ -12,9 +13,14 @@ describe "PaneView", -> @deserialize: ({id, text}) -> new TestView({id, text}) @content: ({id, text}) -> @div class: 'test-view', id: id, tabindex: -1, text initialize: ({@id, @text}) -> + @emitter = new Emitter serialize: -> { deserializer: 'TestView', @id, @text } getUri: -> @id isEqual: (other) -> other? and @id == other.id and @text == other.text + changeTitle: -> + @emitter.emit 'did-change-title', 'title' + onDidChangeTitle: (callback) -> + @emitter.on 'did-change-title', callback beforeEach -> atom.deserializers.add(TestView) @@ -29,7 +35,7 @@ describe "PaneView", -> runs -> pane = container.getRoot() - paneModel = pane.model + paneModel = pane.getModel() paneModel.addItems([view1, editor1, view2, editor2]) afterEach -> @@ -37,7 +43,7 @@ describe "PaneView", -> describe "when the active pane item changes", -> it "hides all item views except the active one", -> - expect(pane.activeItem).toBe view1 + expect(pane.getActiveItem()).toBe view1 expect(view1.css('display')).not.toBe 'none' pane.activateItem(view2) @@ -48,7 +54,7 @@ describe "PaneView", -> itemChangedHandler = jasmine.createSpy("itemChangedHandler") container.on 'pane:active-item-changed', itemChangedHandler - expect(pane.activeItem).toBe view1 + expect(pane.getActiveItem()).toBe view1 paneModel.activateItem(view2) paneModel.activateItem(view2) @@ -145,22 +151,47 @@ describe "PaneView", -> expect(view1.data('preservative')).toBe 1234 describe "when the title of the active item changes", -> - it "emits pane:active-item-title-changed", -> - activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler") - pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler + describe 'when there is no onDidChangeTitle method', -> + beforeEach -> + view1.onDidChangeTitle = null + view2.onDidChangeTitle = null - expect(pane.activeItem).toBe view1 + pane.activateItem(view2) + pane.activateItem(view1) - view2.trigger 'title-changed' - expect(activeItemTitleChangedHandler).not.toHaveBeenCalled() + it "emits pane:active-item-title-changed", -> + activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler") + pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler - view1.trigger 'title-changed' - expect(activeItemTitleChangedHandler).toHaveBeenCalled() - activeItemTitleChangedHandler.reset() + expect(pane.getActiveItem()).toBe view1 - pane.activateItem(view2) - view2.trigger 'title-changed' - expect(activeItemTitleChangedHandler).toHaveBeenCalled() + view2.trigger 'title-changed' + expect(activeItemTitleChangedHandler).not.toHaveBeenCalled() + + view1.trigger 'title-changed' + expect(activeItemTitleChangedHandler).toHaveBeenCalled() + activeItemTitleChangedHandler.reset() + + pane.activateItem(view2) + view2.trigger 'title-changed' + expect(activeItemTitleChangedHandler).toHaveBeenCalled() + + describe 'when there is a onDidChangeTitle method', -> + it "emits pane:active-item-title-changed", -> + activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler") + pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler + + expect(pane.getActiveItem()).toBe view1 + view2.changeTitle() + expect(activeItemTitleChangedHandler).not.toHaveBeenCalled() + + view1.changeTitle() + expect(activeItemTitleChangedHandler).toHaveBeenCalled() + activeItemTitleChangedHandler.reset() + + pane.activateItem(view2) + view2.changeTitle() + expect(activeItemTitleChangedHandler).toHaveBeenCalled() describe "when an unmodifed buffer's path is deleted", -> it "removes the pane item", -> @@ -246,7 +277,7 @@ describe "PaneView", -> it "transfers focus to the active view", -> focusHandler = jasmine.createSpy("focusHandler") - pane.activeItem.on 'focus', focusHandler + pane.getActiveItem().on 'focus', focusHandler pane.focus() expect(focusHandler).toHaveBeenCalled() @@ -259,7 +290,7 @@ describe "PaneView", -> describe "when a pane is split", -> it "builds the appropriate pane-row and pane-column views", -> pane1 = pane - pane1Model = pane.model + pane1Model = pane.getModel() pane.activateItem(editor1) pane2Model = pane1Model.splitRight(items: [pane1Model.copyActiveItem()]) diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee index 33d941115..8c3a65d0b 100644 --- a/spec/random-editor-spec.coffee +++ b/spec/random-editor-spec.coffee @@ -50,9 +50,9 @@ describe "Editor", -> randomlyMutateEditor = -> if Math.random() < .2 - softWrap = not editor.getSoftWrap() - steps.push(['setSoftWrap', softWrap]) - editor.setSoftWrap(softWrap) + softWrapped = not editor.isSoftWrapped() + steps.push(['setSoftWrapped', softWrapped]) + editor.setSoftWrapped(softWrapped) else range = getRandomRange() text = getRandomText() @@ -79,7 +79,7 @@ describe "Editor", -> text getReferenceScreenLines = -> - if editor.getSoftWrap() + if editor.isSoftWrapped() screenLines = [] bufferRows = [] for bufferRow in [0..tokenizedBuffer.getLastRow()] diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index 020dffa86..cbaf3be86 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -57,10 +57,10 @@ describe "Selection", -> expect(selection.isReversed()).toBeFalsy() describe "when only the selection's tail is moved (regression)", -> - it "emits the 'screen-range-changed' event", -> + it "notifies ::onDidChangeRange observers", -> selection.setBufferRange([[2, 0], [2, 10]], reversed: true) changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.on 'screen-range-changed', changeScreenRangeHandler + selection.onDidChangeRange changeScreenRangeHandler buffer.insert([2, 5], 'abc') expect(changeScreenRangeHandler).toHaveBeenCalled() diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 6fdbed35b..530367518 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -21,6 +21,7 @@ clipboard = require 'clipboard' atom.themes.loadBaseStylesheets() atom.themes.requireStylesheet '../static/jasmine' +atom.themes.initialLoadComplete = true fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') atom.packages.packageDirPaths.unshift(fixturePackagesPath) @@ -137,8 +138,7 @@ afterEach -> jasmine.unspy(atom, 'saveSync') ensureNoPathSubscriptions() - atom.syntax.off() - ensureNoDeprecatedFunctionsCalled() if isCoreSpec + atom.syntax.clearObservers() waits(0) # yield to ui thread to make screen update more frequently ensureNoPathSubscriptions = -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index d3a30c589..7028d7c95 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -69,7 +69,7 @@ describe "ThemeManager", -> describe "when the core.themes config value changes", -> it "add/removes stylesheets to reflect the new config value", -> - themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() + themeManager.onDidReloadAll reloadHandler = jasmine.createSpy() spyOn(themeManager, 'getUserStylesheetPath').andCallFake -> null waitsForPromise -> @@ -131,8 +131,8 @@ describe "ThemeManager", -> describe "requireStylesheet(path)", -> it "synchronously loads css at the given path and installs a style tag for it in the head", -> - themeManager.on 'stylesheets-changed', stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") - themeManager.on 'stylesheet-added', stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") + themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") + themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") cssPath = atom.project.resolve('css.css') lengthBefore = $('head style').length @@ -193,8 +193,8 @@ describe "ThemeManager", -> themeManager.requireStylesheet(cssPath) expect($(document.body).css('font-weight')).toBe("bold") - themeManager.on 'stylesheet-removed', stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") - themeManager.on 'stylesheets-changed', stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") + themeManager.onDidRemoveStylesheet stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") + themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") themeManager.removeStylesheet(cssPath) @@ -217,7 +217,7 @@ describe "ThemeManager", -> themeManager.activateThemes() it "loads the correct values from the theme's ui-variables file", -> - themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() + themeManager.onDidReloadAll reloadHandler = jasmine.createSpy() atom.config.set('core.themes', ['theme-with-ui-variables']) waitsFor -> @@ -234,7 +234,7 @@ describe "ThemeManager", -> describe "when there is a theme with incomplete variables", -> it "loads the correct values from the fallback ui-variables", -> - themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() + themeManager.onDidReloadAll reloadHandler = jasmine.createSpy() atom.config.set('core.themes', ['theme-with-incomplete-ui-variables']) waitsFor -> @@ -251,7 +251,7 @@ describe "ThemeManager", -> it 'adds theme-* classes to the workspace for each active theme', -> expect(atom.workspaceView).toHaveClass 'theme-atom-dark-ui' - themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() + themeManager.onDidReloadAll reloadHandler = jasmine.createSpy() atom.config.set('core.themes', ['theme-with-ui-variables']) waitsFor -> @@ -273,9 +273,9 @@ describe "ThemeManager", -> themeManager.activateThemes() runs -> - themeManager.on 'stylesheets-changed', stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") - themeManager.on 'stylesheet-removed', stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") - themeManager.on 'stylesheet-added', stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") + themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler") + themeManager.onDidRemoveStylesheet stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") + themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") spyOn(themeManager, 'loadUserStylesheet').andCallThrough() expect($(document.body).css('border-style')).toBe 'dotted' @@ -316,7 +316,9 @@ describe "ThemeManager", -> themeManager.activateThemes() runs -> - themeManager.once 'reloaded', -> reloaded = true + disposable = themeManager.onDidReloadAll -> + disposable.dispose() + reloaded = true spyOn(console, 'warn') expect(-> atom.config.set('core.themes', ['atom-light-ui', 'theme-really-does-not-exist'])).not.toThrow() diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index ad90c4ded..ec1dc5009 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -41,7 +41,7 @@ describe "TokenizedBuffer", -> buffer = atom.project.bufferForPathSync('sample.js') tokenizedBuffer = new TokenizedBuffer({buffer}) startTokenizing(tokenizedBuffer) - tokenizedBuffer.on "changed", changeHandler = jasmine.createSpy('changeHandler') + tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler') afterEach -> tokenizedBuffer.destroy() @@ -468,7 +468,7 @@ describe "TokenizedBuffer", -> runs -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer - tokenizedBuffer.on 'tokenized', tokenizedHandler + tokenizedBuffer.onDidTokenize tokenizedHandler fullyTokenize(tokenizedBuffer) expect(tokenizedHandler.callCount).toBe(1) @@ -483,7 +483,7 @@ describe "TokenizedBuffer", -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer fullyTokenize(tokenizedBuffer) - tokenizedBuffer.on 'tokenized', tokenizedHandler + tokenizedBuffer.onDidTokenize tokenizedHandler editor.getBuffer().insert([0, 0], "'") fullyTokenize(tokenizedBuffer) expect(tokenizedHandler).not.toHaveBeenCalled() @@ -499,7 +499,7 @@ describe "TokenizedBuffer", -> runs -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer - tokenizedBuffer.on 'tokenized', tokenizedHandler + tokenizedBuffer.onDidTokenize tokenizedHandler fullyTokenize(tokenizedBuffer) tokenizedHandler.reset() @@ -753,7 +753,7 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 3 expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 2 - tokenizedBuffer.on "changed", changeHandler = jasmine.createSpy('changeHandler') + tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler') buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') @@ -771,7 +771,7 @@ describe "TokenizedBuffer", -> buffer.insert([7, 0], '\n\n') buffer.insert([5, 0], '\n\n') - tokenizedBuffer.on "changed", changeHandler = jasmine.createSpy('changeHandler') + tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler') buffer.setTextInRange([[7, 0], [8, 65]], ' ok') diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 6a449cc5c..6c588ec2d 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -256,3 +256,35 @@ describe "Window", -> elements.trigger "core:focus-previous" expect(elements.find("[tabindex=1]:focus")).toExist() + + describe "the window:open-path event", -> + beforeEach -> + spyOn(atom.workspace, 'open') + + describe "when the project does not have a path", -> + beforeEach -> + atom.project.setPath() + + describe "when the opened path exists", -> + it "sets the project path to the opened path", -> + $(window).trigger('window:open-path', [{pathToOpen: __filename}]) + + expect(atom.project.getPath()).toBe __dirname + + describe "when the opened path does not exist but its parent directory does", -> + it "sets the project path to the opened path's parent directory", -> + $(window).trigger('window:open-path', [{pathToOpen: path.join(__dirname, 'this-path-does-not-exist.txt')}]) + + expect(atom.project.getPath()).toBe __dirname + + describe "when the opened path is a file", -> + it "opens it in the workspace", -> + $(window).trigger('window:open-path', [{pathToOpen: __filename}]) + + expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename + + describe "when the opened path is a directory", -> + it "does not open it in the workspace", -> + $(window).trigger('window:open-path', [{pathToOpen: __dirname}]) + + expect(atom.workspace.open.callCount).toBe 0 diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index a75896582..f95910d01 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -8,8 +8,12 @@ describe "Workspace", -> atom.workspace = workspace = new Workspace describe "::open(uri, options)", -> + openEvents = null + beforeEach -> - spyOn(workspace.activePane, 'activate').andCallThrough() + openEvents = [] + workspace.onDidOpen (event) -> openEvents.push(event) + spyOn(workspace.getActivePane(), 'activate').andCallThrough() describe "when the 'searchAllPanes' option is false (default)", -> describe "when called without a uri", -> @@ -21,18 +25,21 @@ describe "Workspace", -> runs -> expect(editor1.getPath()).toBeUndefined() - expect(workspace.activePane.items).toEqual [editor1] - expect(workspace.activePaneItem).toBe editor1 - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePane().items).toEqual [editor1] + expect(workspace.getActivePaneItem()).toBe editor1 + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}] + openEvents = [] waitsForPromise -> workspace.open().then (editor) -> editor2 = editor runs -> expect(editor2.getPath()).toBeUndefined() - expect(workspace.activePane.items).toEqual [editor1, editor2] - expect(workspace.activePaneItem).toBe editor2 - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePane().items).toEqual [editor1, editor2] + expect(workspace.getActivePaneItem()).toBe editor2 + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}] describe "when called with a uri", -> describe "when the active pane already has an editor for the given uri", -> @@ -51,8 +58,29 @@ describe "Workspace", -> runs -> expect(editor).toBe editor1 - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePaneItem()).toBe editor + expect(workspace.getActivePane().activate).toHaveBeenCalled() + + expect(openEvents).toEqual [ + { + uri: atom.project.resolve('a') + item: editor1 + pane: atom.workspace.getActivePane() + index: 0 + } + { + uri: atom.project.resolve('b') + item: editor2 + pane: atom.workspace.getActivePane() + index: 1 + } + { + uri: atom.project.resolve('a') + item: editor1 + pane: atom.workspace.getActivePane() + index: 0 + } + ] describe "when the active pane does not have an editor for the given uri", -> it "adds and activates a new editor for the given path on the active pane", -> @@ -62,9 +90,9 @@ describe "Workspace", -> runs -> expect(editor.getUri()).toBe atom.project.resolve('a') - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.items).toEqual [editor] - expect(workspace.activePane.activate).toHaveBeenCalled() + expect(workspace.getActivePaneItem()).toBe editor + expect(workspace.getActivePane().items).toEqual [editor] + expect(workspace.getActivePane().activate).toHaveBeenCalled() describe "when the 'searchAllPanes' option is true", -> describe "when an editor for the given uri is already open on an inactive pane", -> @@ -83,14 +111,14 @@ describe "Workspace", -> workspace.open('b').then (o) -> editor2 = o runs -> - expect(workspace.activePaneItem).toBe editor2 + expect(workspace.getActivePaneItem()).toBe editor2 waitsForPromise -> workspace.open('a', searchAllPanes: true) runs -> - expect(workspace.activePane).toBe pane1 - expect(workspace.activePaneItem).toBe editor1 + expect(workspace.getActivePane()).toBe pane1 + expect(workspace.getActivePaneItem()).toBe editor1 describe "when no editor for the given uri is open in any pane", -> it "opens an editor for the given uri in the active pane", -> @@ -99,21 +127,21 @@ describe "Workspace", -> workspace.open('a', searchAllPanes: true).then (o) -> editor = o runs -> - expect(workspace.activePaneItem).toBe editor + expect(workspace.getActivePaneItem()).toBe editor describe "when the 'split' option is set", -> describe "when the 'split' option is 'left'", -> it "opens the editor in the leftmost pane of the current pane axis", -> - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitRight() - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 editor = null waitsForPromise -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] expect(pane2.items).toEqual [] @@ -123,37 +151,37 @@ describe "Workspace", -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] expect(pane2.items).toEqual [] describe "when a pane axis is the leftmost sibling of the current pane", -> it "opens the new item in the current pane", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitLeft() pane3 = pane2.splitDown() pane1.activate() - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 waitsForPromise -> workspace.open('a', split: 'left').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 expect(pane1.items).toEqual [editor] describe "when the 'split' option is 'right'", -> it "opens the editor in the rightmost pane of the current pane axis", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = null waitsForPromise -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> pane2 = workspace.getPanes().filter((p) -> p != pane1)[0] - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 expect(pane1.items).toEqual [] expect(pane2.items).toEqual [editor] @@ -163,18 +191,18 @@ describe "Workspace", -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> - expect(workspace.activePane).toBe pane2 + expect(workspace.getActivePane()).toBe pane2 expect(pane1.items).toEqual [] expect(pane2.items).toEqual [editor] describe "when a pane axis is the rightmost sibling of the current pane", -> it "opens the new item in a new pane split to the right of the current pane", -> editor = null - pane1 = workspace.activePane + pane1 = workspace.getActivePane() pane2 = pane1.splitRight() pane3 = pane2.splitDown() pane1.activate() - expect(workspace.activePane).toBe pane1 + expect(workspace.getActivePane()).toBe pane1 pane4 = null waitsForPromise -> @@ -182,7 +210,7 @@ describe "Workspace", -> runs -> pane4 = workspace.getPanes().filter((p) -> p != pane1)[0] - expect(workspace.activePane).toBe pane4 + expect(workspace.getActivePane()).toBe pane4 expect(pane4.items).toEqual [editor] expect(workspace.paneContainer.root.children[0]).toBe pane1 expect(workspace.paneContainer.root.children[1]).toBe pane4 @@ -203,21 +231,21 @@ describe "Workspace", -> workspace.open("bar://baz").then (item) -> expect(item).toEqual { bar: "bar://baz" } - it "emits an 'editor-created' event", -> + it "notifies ::onDidAddTextEditor observers", -> absolutePath = require.resolve('./fixtures/dir/a') newEditorHandler = jasmine.createSpy('newEditorHandler') - workspace.on 'editor-created', newEditorHandler + workspace.onDidAddTextEditor newEditorHandler editor = null waitsForPromise -> workspace.open(absolutePath).then (e) -> editor = e runs -> - expect(newEditorHandler).toHaveBeenCalledWith editor + expect(newEditorHandler.argsForCall[0][0].textEditor).toBe editor describe "::reopenItem()", -> it "opens the uri associated with the last closed pane that isn't currently open", -> - pane = workspace.activePane + pane = workspace.getActivePane() waitsForPromise -> workspace.open('a').then -> workspace.open('b').then -> @@ -226,44 +254,44 @@ describe "Workspace", -> runs -> # does not reopen items with no uri - expect(workspace.activePaneItem.getUri()).toBeUndefined() + expect(workspace.getActivePaneItem().getUri()).toBeUndefined() pane.destroyActiveItem() waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).not.toBeUndefined() + expect(workspace.getActivePaneItem().getUri()).not.toBeUndefined() # destroy all items - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('file1') pane.destroyActiveItem() - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('b') pane.destroyActiveItem() - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('a') pane.destroyActiveItem() # reopens items with uris - expect(workspace.activePaneItem).toBeUndefined() + expect(workspace.getActivePaneItem()).toBeUndefined() waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('a') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('a') # does not reopen items that are already open waitsForPromise -> workspace.open('b') runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('b') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('b') waitsForPromise -> workspace.reopenItem() runs -> - expect(workspace.activePaneItem.getUri()).toBe atom.project.resolve('file1') + expect(workspace.getActivePaneItem().getUri()).toBe atom.project.resolve('file1') describe "::increase/decreaseFontSize()", -> it "increases/decreases the font size without going below 1", -> @@ -282,7 +310,22 @@ describe "Workspace", -> describe "::openLicense()", -> it "opens the license as plain-text in a buffer", -> waitsForPromise -> workspace.openLicense() - runs -> expect(workspace.activePaneItem.getText()).toMatch /Copyright/ + runs -> expect(workspace.getActivePaneItem().getText()).toMatch /Copyright/ + + describe "::observeTextEditors()", -> + it "invokes the observer with current and future text editors", -> + observed = [] + + waitsForPromise -> workspace.open() + waitsForPromise -> workspace.open() + waitsForPromise -> workspace.openLicense() + + runs -> + workspace.observeTextEditors (editor) -> observed.push(editor) + + waitsForPromise -> workspace.open() + + expect(observed).toEqual workspace.getTextEditors() describe "when an editor is destroyed", -> it "removes the editor", -> @@ -292,23 +335,9 @@ describe "Workspace", -> workspace.open("a").then (e) -> editor = e runs -> - expect(workspace.getEditors()).toHaveLength 1 + expect(workspace.getTextEditors()).toHaveLength 1 editor.destroy() - expect(workspace.getEditors()).toHaveLength 0 - - describe "when an editor is copied", -> - it "emits an 'editor-created' event", -> - editor = null - handler = jasmine.createSpy('editorCreatedHandler') - workspace.on 'editor-created', handler - - waitsForPromise -> - workspace.open("a").then (o) -> editor = o - - runs -> - expect(handler.callCount).toBe 1 - editorCopy = editor.copy() - expect(handler.callCount).toBe 2 + expect(workspace.getTextEditors()).toHaveLength 0 it "stores the active grammars used by all the open editors", -> waitsForPromise -> @@ -317,14 +346,19 @@ describe "Workspace", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.packages.activatePackage('language-todo') + waitsForPromise -> atom.workspace.open('sample.coffee') runs -> - atom.workspace.getActiveEditor().setText('i = /test/;') + atom.workspace.getActiveEditor().setText """ + i = /test/; #FIXME + """ state = atom.workspace.serialize() - expect(state.packagesWithActiveGrammars).toEqual ['language-coffee-script', 'language-javascript'] + expect(state.packagesWithActiveGrammars).toEqual ['language-coffee-script', 'language-javascript', 'language-todo'] jsPackage = atom.packages.getLoadedPackage('language-javascript') coffeePackage = atom.packages.getLoadedPackage('language-coffee-script') diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index c6aa502b3..8adbc69e5 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -42,7 +42,7 @@ describe "WorkspaceView", -> runs -> editorView1 = atom.workspaceView.getActiveView() buffer = editorView1.getEditor().getBuffer() - editorView1.splitRight() + editorView1.getPaneView().getModel().splitRight(copyActiveItem: true) expect(atom.workspaceView.getActivePaneView()).toBe atom.workspaceView.getPaneViews()[1] simulateReload() @@ -196,7 +196,8 @@ describe "WorkspaceView", -> atom.workspaceView.attachToDom() rightEditorView = atom.workspaceView.getActiveView() rightEditorView.getEditor().setText("\t \n") - leftEditorView = rightEditorView.splitLeft() + rightEditorView.getPaneView().getModel().splitLeft(copyActiveItem: true) + leftEditorView = atom.workspaceView.getActiveView() expect(rightEditorView.find(".line:first").text()).toBe " " expect(leftEditorView.find(".line:first").text()).toBe " " @@ -207,14 +208,16 @@ describe "WorkspaceView", -> expect(rightEditorView.find(".line:first").text()).toBe withInvisiblesShowing expect(leftEditorView.find(".line:first").text()).toBe withInvisiblesShowing - lowerLeftEditorView = leftEditorView.splitDown() + leftEditorView.getPaneView().getModel().splitDown(copyActiveItem: true) + lowerLeftEditorView = atom.workspaceView.getActiveView() expect(lowerLeftEditorView.find(".line:first").text()).toBe withInvisiblesShowing atom.workspaceView.trigger "window:toggle-invisibles" expect(rightEditorView.find(".line:first").text()).toBe " " expect(leftEditorView.find(".line:first").text()).toBe " " - lowerRightEditorView = rightEditorView.splitDown() + rightEditorView.getPaneView().getModel().splitDown(copyActiveItem: true) + lowerRightEditorView = atom.workspaceView.getActiveView() expect(lowerRightEditorView.find(".line:first").text()).toBe " " describe ".eachEditorView(callback)", -> @@ -241,7 +244,7 @@ describe "WorkspaceView", -> atom.workspaceView.eachEditorView(callback) count = 0 callbackEditor = null - atom.workspaceView.getActiveView().splitRight() + atom.workspaceView.getActiveView().getPaneView().getModel().splitRight(copyActiveItem: true) expect(count).toBe 1 expect(callbackEditor).toBe atom.workspaceView.getActiveView() @@ -259,10 +262,10 @@ describe "WorkspaceView", -> subscription = atom.workspaceView.eachEditorView(callback) expect(count).toBe 1 - atom.workspaceView.getActiveView().splitRight() + atom.workspaceView.getActiveView().getPaneView().getModel().splitRight(copyActiveItem: true) expect(count).toBe 2 subscription.off() - atom.workspaceView.getActiveView().splitRight() + atom.workspaceView.getActiveView().getPaneView().getModel().splitRight(copyActiveItem: true) expect(count).toBe 2 describe "core:close", -> @@ -271,7 +274,7 @@ describe "WorkspaceView", -> paneView1 = atom.workspaceView.getActivePaneView() editorView = atom.workspaceView.getActiveView() - editorView.splitRight() + editorView.getPaneView().getModel().splitRight(copyActiveItem: true) paneView2 = atom.workspaceView.getActivePaneView() expect(paneView1).not.toBe paneView2 diff --git a/src/atom.coffee b/src/atom.coffee index 4fb113302..9d0faab5d 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -187,7 +187,7 @@ class Atom extends Model @syntax = @deserializers.deserialize(@state.syntax) ? new Syntax() - @subscribe @packages, 'activated', => @watchThemes() + @subscribe @packages.onDidActivateAll => @watchThemes() Project = require './project' TextBuffer = require 'text-buffer' @@ -373,7 +373,7 @@ class Atom extends Model @themes.load() watchThemes: -> - @themes.on 'reloaded', => + @themes.onDidReloadAll => # Only reload stylesheets from non-theme packages for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' pack.reloadStylesheets?() @@ -408,20 +408,19 @@ class Atom extends Model # ## Examples # # ```coffee - # atom.confirm - # message: 'How you feeling?' - # detailedMessage: 'Be honest.' - # buttons: - # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') + # atom.confirm + # message: 'How you feeling?' + # detailedMessage: 'Be honest.' + # buttons: + # Good: -> window.alert('good to hear') + # 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 - # button names and the values are callbacks to invoke when - # clicked. + # button names and the values are callbacks to invoke when clicked. # # Returns the chosen button index {Number} if the buttons option was an array. confirm: ({message, detailedMessage, buttons}={}) -> diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index d47bba1d9..7460952a1 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -5,13 +5,11 @@ AutoUpdateManager = require './auto-update-manager' BrowserWindow = require 'browser-window' Menu = require 'menu' app = require 'app' -dialog = require 'dialog' fs = require 'fs' ipc = require 'ipc' path = require 'path' os = require 'os' net = require 'net' -shell = require 'shell' url = require 'url' {EventEmitter} = require 'events' _ = require 'underscore-plus' @@ -103,11 +101,12 @@ class AtomApplication window.once 'window:loaded', => @autoUpdateManager.emitUpdateAvailableEvent(window) - focusHandler = => @lastFocusedWindow = window - window.browserWindow.on 'focus', focusHandler - window.browserWindow.once 'closed', => - @lastFocusedWindow = null if window is @lastFocusedWindow - window.browserWindow.removeListener 'focus', focusHandler + unless window.isSpec + focusHandler = => @lastFocusedWindow = window + window.browserWindow.on 'focus', focusHandler + window.browserWindow.once 'closed', => + @lastFocusedWindow = null if window is @lastFocusedWindow + window.browserWindow.removeListener 'focus', focusHandler # Creates server to listen for additional atom application launches. # @@ -155,8 +154,8 @@ class AtomApplication atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) - @on 'application:open-documentation', -> shell.openExternal('https://atom.io/docs/latest/?app') - @on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms') + @on 'application:open-documentation', -> require('shell').openExternal('https://atom.io/docs/latest/?app') + @on 'application:open-terms-of-use', -> require('shell').openExternal('https://atom.io/terms') @on 'application:install-update', -> @autoUpdateManager.install() @on 'application:check-for-update', => @autoUpdateManager.check() @@ -484,5 +483,15 @@ class AtomApplication when 'folder' then ['openDirectory'] when 'all' then ['openFile', 'openDirectory'] else throw new Error("#{type} is an invalid type for promptForPath") - dialog.showOpenDialog title: 'Open', properties: properties.concat(['multiSelections', 'createDirectory']), (pathsToOpen) => + + # Show the open dialog as child window on Windows and Linux, and as + # independent dialog on OS X. This matches most native apps. + parentWindow = + if process.platform is 'darwin' + null + else + BrowserWindow.getFocusedWindow() + + dialog = require 'dialog' + dialog.showOpenDialog parentWindow, title: 'Open', properties: properties.concat(['multiSelections', 'createDirectory']), (pathsToOpen) => @openPaths({pathsToOpen, devMode, safeMode, window}) diff --git a/src/browser/atom-protocol-handler.coffee b/src/browser/atom-protocol-handler.coffee index 786d50243..6777a21ee 100644 --- a/src/browser/atom-protocol-handler.coffee +++ b/src/browser/atom-protocol-handler.coffee @@ -1,5 +1,5 @@ app = require 'app' -fs = require 'fs-plus' +fs = require 'fs' path = require 'path' protocol = require 'protocol' @@ -24,5 +24,5 @@ class AtomProtocolHandler relativePath = path.normalize(request.url.substr(7)) for loadPath in @loadPaths filePath = path.join(loadPath, relativePath) - break if fs.isFileSync(filePath) + break if fs.statSyncNoException(filePath).isFile?() return new protocol.RequestFileJob(filePath) diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 1022f6f76..c8e395720 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -1,7 +1,5 @@ BrowserWindow = require 'browser-window' -ContextMenu = require './context-menu' app = require 'app' -dialog = require 'dialog' path = require 'path' fs = require 'fs' url = require 'url' @@ -25,7 +23,12 @@ class AtomWindow # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath - @browserWindow = new BrowserWindow show: false, title: 'Atom', icon: @constructor.iconPath + @browserWindow = new BrowserWindow + show: false + title: 'Atom' + icon: @constructor.iconPath + 'web-preferences': + 'subpixel-font-scaling': false global.atomApplication.addWindow(this) @handleEvents() @@ -73,6 +76,13 @@ class AtomWindow getInitialPath: -> @browserWindow.loadSettings.initialPath + setupContextMenu: -> + ContextMenu = null + + @browserWindow.on 'context-menu', (menuTemplate) => + ContextMenu ?= require './context-menu' + new ContextMenu(menuTemplate, this) + containsPath: (pathToCheck) -> initialPath = @getInitialPath() if not initialPath @@ -95,6 +105,7 @@ class AtomWindow @browserWindow.on 'unresponsive', => return if @isSpec + dialog = require 'dialog' chosen = dialog.showMessageBox @browserWindow, type: 'warning' buttons: ['Close', 'Keep Waiting'] @@ -105,6 +116,7 @@ class AtomWindow @browserWindow.webContents.on 'crashed', => global.atomApplication.exit(100) if @exitWhenDone + dialog = require 'dialog' chosen = dialog.showMessageBox @browserWindow, type: 'warning' buttons: ['Close Window', 'Reload', 'Keep It Open'] @@ -114,8 +126,7 @@ class AtomWindow when 0 then @browserWindow.destroy() when 1 then @browserWindow.restart() - @browserWindow.on 'context-menu', (menuTemplate) => - new ContextMenu(menuTemplate, this) + @setupContextMenu() if @isSpec # Workaround for https://github.com/atom/atom-shell/issues/380 diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 01151f2a9..828af310d 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -1,44 +1,47 @@ -https = require 'https' -autoUpdater = require 'auto-updater' -dialog = require 'dialog' +autoUpdater = null _ = require 'underscore-plus' {EventEmitter} = require 'events' -IDLE_STATE='idle' -CHECKING_STATE='checking' -DOWNLOADING_STATE='downloading' -UPDATE_AVAILABLE_STATE='update-available' -NO_UPDATE_AVAILABLE_STATE='no-update-available' -ERROR_STATE='error' +IdleState = 'idle' +CheckingState = 'checking' +DownladingState = 'downloading' +UpdateAvailableState = 'update-available' +NoUpdateAvailableState = 'no-update-available' +ErrorState = 'error' module.exports = class AutoUpdateManager _.extend @prototype, EventEmitter.prototype constructor: (@version) -> - @state = IDLE_STATE + @state = IdleState @feedUrl = "https://atom.io/api/updates?version=#{@version}" + process.nextTick => @setupAutoUpdater() + + setupAutoUpdater: -> + autoUpdater = require 'auto-updater' + if process.platform is 'win32' autoUpdater.checkForUpdates = => @checkForUpdatesShim() autoUpdater.setFeedUrl @feedUrl autoUpdater.on 'checking-for-update', => - @setState(CHECKING_STATE) + @setState(CheckingState) autoUpdater.on 'update-not-available', => - @setState(NO_UPDATE_AVAILABLE_STATE) + @setState(NoUpdateAvailableState) autoUpdater.on 'update-available', => - @setState(DOWNLOADING_STATE) + @setState(DownladingState) autoUpdater.on 'error', (event, message) => - @setState(ERROR_STATE) + @setState(ErrorState) console.error "Error Downloading Update: #{message}" autoUpdater.on 'update-downloaded', (event, @releaseNotes, @releaseVersion) => - @setState(UPDATE_AVAILABLE_STATE) + @setState(UpdateAvailableState) @emitUpdateAvailableEvent(@getWindows()...) # Only released versions should check for updates. @@ -48,6 +51,8 @@ class AutoUpdateManager # Windows doesn't have an auto-updater, so use this method to shim the events. checkForUpdatesShim: -> autoUpdater.emit 'checking-for-update' + + https = require 'https' request = https.get @feedUrl, (response) -> if response.statusCode == 200 body = "" @@ -67,7 +72,7 @@ class AutoUpdateManager atomWindow.sendCommand('window:update-available', [@releaseVersion, @releaseNotes]) setState: (state) -> - return unless @state != state + return if @state is state @state = state @emit 'state-changed', @state @@ -86,10 +91,12 @@ class AutoUpdateManager onUpdateNotAvailable: => autoUpdater.removeListener 'error', @onUpdateError + dialog = require 'dialog' dialog.showMessageBox type: 'info', buttons: ['OK'], message: 'No update available.', detail: "Version #{@version} is the latest version." onUpdateError: (event, message) => autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable + dialog = require 'dialog' dialog.showMessageBox type: 'warning', buttons: ['OK'], message: 'There was an error checking for updates.', detail: message getWindows: -> diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 8955ab0a7..f9afb0c30 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -3,11 +3,9 @@ global.shellStartTime = Date.now() crashReporter = require 'crash-reporter' app = require 'app' fs = require 'fs' -module = require 'module' path = require 'path' optimist = require 'optimist' nslog = require 'nslog' -dialog = require 'dialog' console.log = nslog @@ -33,14 +31,14 @@ start = -> app.on 'will-finish-launching', -> setupCrashReporter() - app.on 'finish-launching', -> + app.on 'ready', -> app.removeListener 'open-file', addPathToOpen app.removeListener 'open-url', addUrlToOpen args.pathsToOpen = args.pathsToOpen.map (pathToOpen) -> path.resolve(args.executedFrom ? process.cwd(), pathToOpen.toString()) - require('coffee-script').register() + setupCoffeeScript() if args.devMode require(path.join(args.resourcePath, 'src', 'coffee-cache')).register() AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application') @@ -57,6 +55,15 @@ global.devResourcePath = path.normalize(global.devResourcePath) if global.devRes setupCrashReporter = -> crashReporter.start(productName: 'Atom', companyName: 'GitHub') +setupCoffeeScript = -> + CoffeeScript = null + + require.extensions['.coffee'] = (module, filePath) -> + CoffeeScript ?= require('coffee-script') + coffee = fs.readFileSync(filePath, 'utf8') + js = CoffeeScript.compile(coffee, filename: filePath) + module._compile(js, filePath) + parseCommandLine = -> version = app.getVersion() options = optimist(process.argv[1..]) @@ -109,9 +116,7 @@ parseCommandLine = -> else if devMode resourcePath = global.devResourcePath - try - fs.statSync resourcePath - catch + unless fs.statSyncNoException(resourcePath) resourcePath = path.dirname(path.dirname(__dirname)) {resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile} diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 91cfe04db..5aa8392a6 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -24,7 +24,7 @@ class ContextMenuManager @commandOptions = x: e.pageX, y: e.pageY ] - atom.keymaps.on 'bundled-keymaps-loaded', => @loadPlatformItems() + atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() loadPlatformItems: -> menusDirPath = path.join(@resourcePath, 'menus') diff --git a/src/cursor.coffee b/src/cursor.coffee index f86721d6d..b736dd424 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -1,37 +1,14 @@ {Point, Range} = require 'text-buffer' {Model} = require 'theorist' +{Emitter} = require 'event-kit' _ = require 'underscore-plus' +Grim = require 'grim' # Extended: The `Cursor` class represents the little blinking line identifying # where text can be inserted. # # Cursors belong to {Editor}s and have some metadata attached in the form # of a {Marker}. -# -# ## Events -# -# ### moved -# -# Extended: Emit when a cursor has been moved. If there are multiple cursors, -# it will be emit for each cursor. -# -# * `event` {Object} -# * `oldBufferPosition` {Point} -# * `oldScreenPosition` {Point} -# * `newBufferPosition` {Point} -# * `newScreenPosition` {Point} -# * `textChanged` {Boolean} -# -# ### destroyed -# -# Extended: Emit when the cursor is destroyed -# -# ### visibility-changed -# -# Extended: Emit when the Cursor is hidden or shown -# -# * `visible` {Boolean} true when cursor is visible -# module.exports = class Cursor extends Model screenPosition: null @@ -42,9 +19,11 @@ class Cursor extends Model # Instantiated by an {Editor} constructor: ({@editor, @marker, id}) -> + @emitter = new Emitter + @assignId(id) @updateVisibility() - @marker.on 'changed', (e) => + @marker.onDidChange (e) => @updateVisibility() {oldHeadScreenPosition, newHeadScreenPosition} = e {oldHeadBufferPosition, newHeadBufferPosition} = e @@ -65,13 +44,67 @@ class Cursor extends Model textChanged: textChanged @emit 'moved', movedEvent - @editor.cursorMoved(movedEvent) - @marker.on 'destroyed', => + @emitter.emit 'did-change-position' + @editor.cursorMoved(this, movedEvent) + @marker.onDidDestroy => @destroyed = true @editor.removeCursor(this) @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() @needsAutoscroll = true + ### + Section: Event Subscription + ### + + # Essential: Calls your `callback` when the cursor has been moved. + # + # * `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. + onDidChangePosition: (callback) -> + @emitter.on 'did-change-position', callback + + # Extended: Calls your `callback` when the cursor is destroyed + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + # Extended: Calls your `callback` when the cursor's visibility has changed + # + # * `callback` {Function} + # * `visibility` {Boolean} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeVisibility: (callback) -> + @emitter.on 'did-change-visibility', callback + + on: (eventName) -> + switch eventName + when 'moved' + Grim.deprecate("Use Cursor::onDidChangePosition instead") + when 'destroyed' + Grim.deprecate("Use Cursor::onDidDestroy instead") + when 'destroyed' + Grim.deprecate("Use Cursor::onDidDestroy instead") + else + Grim.deprecate("::on is no longer supported. Use the event subscription methods instead") + super + + ### + Section: Methods + ### + destroy: -> @marker.destroy() @@ -131,6 +164,7 @@ class Cursor extends Model @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 @@ -225,6 +259,11 @@ class Cursor extends Model @editor.lineTextForBufferRow(@getBufferRow()) # Public: Moves the cursor up one screen row. + # + # * `rowCount` (optional) {Number} number of rows to move (default: 1) + # * `options` (optional) {Object} with the following keys: + # * `moveToEndOfSelection` if true, move to the left of the selection if a + # selection exists. moveUp: (rowCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @@ -237,7 +276,12 @@ class Cursor extends Model @goalColumn = column # Public: Moves the cursor down one screen row. - moveDown: (rowCount = 1, {moveToEndOfSelection}={}) -> + # + # * `rowCount` (optional) {Number} number of rows to move (default: 1) + # * `options` (optional) {Object} with the following keys: + # * `moveToEndOfSelection` if true, move to the left of the selection if a + # selection exists. + moveDown: (rowCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() { row, column } = range.end @@ -250,30 +294,51 @@ class Cursor extends Model # Public: Moves the cursor left one screen column. # + # * `columnCount` (optional) {Number} number of columns to move (default: 1) # * `options` (optional) {Object} with the following keys: # * `moveToEndOfSelection` if true, move to the left of the selection if a # selection exists. - moveLeft: ({moveToEndOfSelection}={}) -> + moveLeft: (columnCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @setScreenPosition(range.start) else {row, column} = @getScreenPosition() - [row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity] + + while columnCount > column and row > 0 + columnCount -= column + column = @editor.lineTextForScreenRow(--row).length + columnCount-- # subtract 1 for the row move + + column = column - columnCount @setScreenPosition({row, column}) # Public: Moves the cursor right one screen column. # + # * `columnCount` (optional) {Number} number of columns to move (default: 1) # * `options` (optional) {Object} with the following keys: # * `moveToEndOfSelection` if true, move to the right of the selection if a # selection exists. - moveRight: ({moveToEndOfSelection}={}) -> + moveRight: (columnCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @setScreenPosition(range.end) else { row, column } = @getScreenPosition() - @setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true) + maxLines = @editor.getScreenLineCount() + rowLength = @editor.lineTextForScreenRow(row).length + columnsRemainingInLine = rowLength - column + + while columnCount > columnsRemainingInLine and row < maxLines - 1 + columnCount -= columnsRemainingInLine + columnCount-- # subtract 1 for the row move + + column = 0 + rowLength = @editor.lineTextForScreenRow(++row).length + columnsRemainingInLine = rowLength + + column = column + columnCount + @setScreenPosition({row, column}, skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true) # Public: Moves the cursor to the top of the buffer. moveToTop: -> diff --git a/src/decoration.coffee b/src/decoration.coffee index 3d888ab34..075641058 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -1,5 +1,7 @@ _ = require 'underscore-plus' -{Subscriber, Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' +Grim = require 'grim' idCounter = 0 nextId = -> idCounter++ @@ -26,44 +28,59 @@ nextId = -> idCounter++ # # You should only use {Decoration::destroy} when you still need or do not own # the marker. -# -# ## Events -# -# ### updated -# -# Extended: When the {Decoration} is updated via {Decoration::update}. -# -# * `event` {Object} -# * `oldParams` {Object} the old parameters the decoration used to have -# * `newParams` {Object} the new parameters the decoration now has -# -# ### destroyed -# -# Extended: When the {Decoration} is destroyed -# module.exports = class Decoration - Emitter.includeInto(this) + EmitterMixin.includeInto(this) - # Extended: Check if the `decorationParams.type` matches `type` + # Extended: Check if the `decorationProperties.type` matches `type` # - # * `decorationParams` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` + # * `decorationProperties` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also # be an {Array} of {String}s, where it will return true if the decoration's # type matches any in the array. # # Returns {Boolean} - @isType: (decorationParams, type) -> - if _.isArray(decorationParams.type) - type in decorationParams.type + @isType: (decorationProperties, type) -> + if _.isArray(decorationProperties.type) + type in decorationProperties.type else - type is decorationParams.type + type is decorationProperties.type - constructor: (@marker, @displayBuffer, @params) -> + constructor: (@marker, @displayBuffer, @properties) -> + @emitter = new Emitter @id = nextId() - @params.id = @id + @properties.id = @id @flashQueue = null - @isDestroyed = false + @destroyed = false + + @markerDestroyDisposable = @marker.onDidDestroy => @destroy() + + ### + Section: Event Subscription + ### + + # Essential: When the {Decoration} is updated via {Decoration::update}. + # + # * `callback` {Function} + # * `event` {Object} + # * `oldProperties` {Object} the old parameters the decoration used to have + # * `newProperties` {Object} the new parameters the decoration now has + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeProperties: (callback) -> + @emitter.on 'did-change-properties', callback + + # Essential: Invoke the given callback when the {Decoration} is destroyed + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + ### + Section: Methods + ### # Essential: An id unique across all {Decoration} objects getId: -> @id @@ -71,9 +88,6 @@ class Decoration # Essential: Returns the marker associated with this {Decoration} getMarker: -> @marker - # Essential: Returns the {Decoration}'s params. - getParams: -> @params - # Public: Check if this decoration is of type `type` # # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also @@ -82,9 +96,16 @@ class Decoration # # Returns {Boolean} isType: (type) -> - Decoration.isType(@params, type) + Decoration.isType(@properties, type) - # Essential: Update the marker with new params. Allows you to change the decoration's class. + # Essential: Returns the {Decoration}'s properties. + getProperties: -> + @properties + getParams: -> + Grim.deprecate 'Use Decoration::getProperties instead' + @getProperties() + + # Essential: Update the marker with new Properties. Allows you to change the decoration's class. # # ## Examples # @@ -92,37 +113,60 @@ class Decoration # decoration.update({type: 'gutter', class: 'my-new-class'}) # ``` # - # * `newParams` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` - update: (newParams) -> - return if @isDestroyed - oldParams = @params - @params = newParams - @params.id = @id - @displayBuffer.decorationUpdated(this) - @emit 'updated', {oldParams, newParams} + # * `newProperties` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` + setProperties: (newProperties) -> + return if @destroyed + oldProperties = @properties + @properties = newProperties + @properties.id = @id + @emit 'updated', {oldParams: oldProperties, newParams: newProperties} + @emitter.emit 'did-change-properties', {oldProperties, newProperties} + update: (newProperties) -> + 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 @isDestroyed - @isDestroyed = true - @displayBuffer.removeDecoration(this) + return if @destroyed + @markerDestroyDisposable.dispose() + @markerDestroyDisposable = null + @destroyed = true @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() matchesPattern: (decorationPattern) -> return false unless decorationPattern? for key, value of decorationPattern - return false if @params[key] != value + return false if @properties[key] != value true + onDidFlash: (callback) -> + @emitter.on 'did-flash', callback + flash: (klass, duration=500) -> flashObject = {class: klass, duration} @flashQueue ?= [] @flashQueue.push(flashObject) @emit 'flash' + @emitter.emit 'did-flash' consumeNextFlash: -> return @flashQueue.shift() if @flashQueue?.length > 0 null + + on: (eventName) -> + switch eventName + when 'updated' + Grim.deprecate 'Use Decoration::onDidChangeProperties instead' + when 'destroyed' + Grim.deprecate 'Use Decoration::onDidDestroy instead' + when 'flash' + Grim.deprecate 'Use Decoration::onDidFlash instead' + else + Grim.deprecate 'Decoration::on is deprecated. Use event subscription methods instead.' + + EmitterMixin::on.apply(this, arguments) diff --git a/src/display-buffer-marker.coffee b/src/display-buffer-marker.coffee index e2324f6b7..8300cac23 100644 --- a/src/display-buffer-marker.coffee +++ b/src/display-buffer-marker.coffee @@ -1,10 +1,13 @@ {Range} = require 'text-buffer' _ = require 'underscore-plus' -{Emitter, Subscriber} = require 'emissary' +{Subscriber} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' +Grim = require 'grim' module.exports = class DisplayBufferMarker - Emitter.includeInto(this) + EmitterMixin.includeInto(this) Subscriber.includeInto(this) bufferMarkerSubscription: null @@ -13,8 +16,10 @@ class DisplayBufferMarker oldTailBufferPosition: null oldTailScreenPosition: null wasValid: true + deferredChangeEvents: null constructor: ({@bufferMarker, @displayBuffer}) -> + @emitter = new Emitter @id = @bufferMarker.id @oldHeadBufferPosition = @getHeadBufferPosition() @oldHeadScreenPosition = @getHeadScreenPosition() @@ -22,8 +27,23 @@ class DisplayBufferMarker @oldTailScreenPosition = @getTailScreenPosition() @wasValid = @isValid() - @subscribe @bufferMarker, 'destroyed', => @destroyed() - @subscribe @bufferMarker, 'changed', (event) => @notifyObservers(event) + @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) @@ -199,6 +219,8 @@ class DisplayBufferMarker destroyed: -> delete @displayBuffer.markers[@id] @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() notifyObservers: ({textChanged}) -> textChanged ?= false @@ -215,7 +237,7 @@ class DisplayBufferMarker _.isEqual(newTailBufferPosition, @oldTailBufferPosition) and _.isEqual(newTailScreenPosition, @oldTailScreenPosition) - @emit 'changed', { + changeEvent = { @oldHeadScreenPosition, newHeadScreenPosition, @oldTailScreenPosition, newTailScreenPosition, @oldHeadBufferPosition, newHeadBufferPosition, @@ -224,8 +246,25 @@ class DisplayBufferMarker 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 5146f1816..c86324b35 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1,8 +1,9 @@ _ = require 'underscore-plus' -{Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter guid = require 'guid' Serializable = require 'serializable' {Model} = require 'theorist' +{Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' TokenizedBuffer = require './tokenized-buffer' RowMap = require './row-map' @@ -10,6 +11,7 @@ Fold = require './fold' Token = require './token' Decoration = require './decoration' DisplayBufferMarker = require './display-buffer-marker' +Grim = require 'grim' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -22,7 +24,7 @@ class DisplayBuffer extends Model @properties manageScrollPosition: false - softWrap: null + softWrapped: null editorWidthInChars: null lineHeightInPixels: null defaultCharWidth: null @@ -40,7 +42,10 @@ class DisplayBuffer extends Model constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @invisibles}={}) -> super - @softWrap ?= atom.config.get('editor.softWrap') ? false + + @emitter = new Emitter + + @softWrapped ?= atom.config.get('editor.softWrap') ? false @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles}) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @@ -48,29 +53,23 @@ class DisplayBuffer extends Model @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} - @decorationMarkerChangedSubscriptions = {} - @decorationMarkerDestroyedSubscriptions = {} @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) - @subscribe @tokenizedBuffer, 'grammar-changed', (grammar) => @emit 'grammar-changed', grammar - @subscribe @tokenizedBuffer, 'tokenized', => @emit 'tokenized' - @subscribe @tokenizedBuffer, 'changed', @handleTokenizedBufferChange - @subscribe @buffer, 'markers-updated', @handleBufferMarkersUpdated - @subscribe @buffer, 'marker-created', @handleBufferMarkerCreated - - @subscribe @$softWrap, (softWrap) => - @emit 'soft-wrap-changed', softWrap - @updateWrappedScreenLines() + @subscribe @tokenizedBuffer.onDidChange @handleTokenizedBufferChange + @subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated + @subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated @subscribe atom.config.observe 'editor.preferredLineLength', callNow: false, => - @updateWrappedScreenLines() if @softWrap and atom.config.get('editor.softWrapAtPreferredLineLength') + @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength') @subscribe atom.config.observe 'editor.softWrapAtPreferredLineLength', callNow: false, => - @updateWrappedScreenLines() if @softWrap + @updateWrappedScreenLines() if @isSoftWrapped() + + @updateAllScreenLines() serializeParams: -> id: @id - softWrap: @softWrap + softWrapped: @isSoftWrapped() editorWidthInChars: @editorWidthInChars scrollTop: @scrollTop scrollLeft: @scrollLeft @@ -96,12 +95,71 @@ class DisplayBuffer extends Model @rowMap = new RowMap @updateScreenLines(0, @buffer.getLineCount(), null, suppressChangeEvent: true) - emitChanged: (eventProperties, refreshMarkers=true) -> + onDidChangeSoftWrapped: (callback) -> + @emitter.on 'did-change-soft-wrapped', callback + + onDidChangeGrammar: (callback) -> + @tokenizedBuffer.onDidChangeGrammar(callback) + + onDidTokenize: (callback) -> + @tokenizedBuffer.onDidTokenize(callback) + + onDidChange: (callback) -> + @emitter.on 'did-change', callback + + onDidChangeCharacterWidths: (callback) -> + @emitter.on 'did-change-character-widths', callback + + observeDecorations: (callback) -> + callback(decoration) for decoration in @getDecorations() + @onDidAddDecoration(callback) + + onDidAddDecoration: (callback) -> + @emitter.on 'did-add-decoration', callback + + onDidRemoveDecoration: (callback) -> + @emitter.on 'did-remove-decoration', callback + + onDidCreateMarker: (callback) -> + @emitter.on 'did-create-marker', callback + + onDidUpdateMarkers: (callback) -> + @emitter.on 'did-update-markers', callback + + on: (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use DisplayBuffer::onDidChange instead") + when 'grammar-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeGrammar instead") + when 'soft-wrap-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeSoftWrap instead") + when 'character-widths-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeCharacterWidths instead") + when 'decoration-added' + Grim.deprecate("Use DisplayBuffer::onDidAddDecoration instead") + when 'decoration-removed' + Grim.deprecate("Use DisplayBuffer::onDidRemoveDecoration instead") + when 'decoration-changed' + Grim.deprecate("Use decoration.getMarker().onDidChange() instead") + when 'decoration-updated' + Grim.deprecate("Use Decoration::onDidChangeProperties instead") + when 'marker-created' + Grim.deprecate("Use Decoration::onDidCreateMarker instead") + when 'markers-updated' + Grim.deprecate("Use Decoration::onDidUpdateMarkers instead") + else + Grim.deprecate("DisplayBuffer::on is deprecated. Use event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) + + emitDidChange: (eventProperties, refreshMarkers=true) -> if refreshMarkers - @pauseMarkerObservers() + @pauseMarkerChangeEvents() @refreshMarkerScreenPositions() @emit 'changed', eventProperties - @resumeMarkerObservers() + @emitter.emit 'did-change', eventProperties + @resumeMarkerChangeEvents() updateWrappedScreenLines: -> start = 0 @@ -109,7 +167,7 @@ class DisplayBuffer extends Model @updateAllScreenLines() screenDelta = @getLastRow() - end bufferDelta = 0 - @emitChanged({ start, end, screenDelta, bufferDelta }) + @emitDidChange({ start, end, screenDelta, bufferDelta }) # Sets the visibility of the tokenized buffer. # @@ -153,7 +211,7 @@ class DisplayBuffer extends Model horizontallyScrollable: (reentrant) -> return false unless @width? - return false if @getSoftWrap() + return false if @isSoftWrapped() if reentrant @getScrollWidth() > @getWidth() else @@ -178,7 +236,7 @@ class DisplayBuffer extends Model setWidth: (newWidth) -> oldWidth = @width @width = newWidth - @updateWrappedScreenLines() if newWidth isnt oldWidth and @softWrap + @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped() @setScrollTop(@getScrollTop()) # Ensure scrollTop is still valid in case horizontal scrollbar disappeared @width @@ -251,6 +309,7 @@ class DisplayBuffer extends Model characterWidthsChanged: -> @computeScrollWidth() @emit 'character-widths-changed', @scopedCharacterWidthsChangeCount + @emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount clearScopedCharWidths: -> @charWidthsByScope = {} @@ -344,11 +403,15 @@ class DisplayBuffer extends Model setInvisibles: (@invisibles) -> @tokenizedBuffer.setInvisibles(@invisibles) - # Deprecated: Use the softWrap property directly - setSoftWrap: (@softWrap) -> @softWrap + setSoftWrapped: (softWrapped) -> + if softWrapped isnt @softWrapped + @softWrapped = softWrapped + @updateWrappedScreenLines() + @emit 'soft-wrap-changed', @softWrapped + @emitter.emit 'did-change-soft-wrapped', @softWrapped + @softWrapped - # Deprecated: Use the softWrap property directly - getSoftWrap: -> @softWrap + isSoftWrapped: -> @softWrapped # Set the number of characters that fit horizontally in the editor. # @@ -357,7 +420,7 @@ class DisplayBuffer extends Model if editorWidthInChars > 0 previousWidthInChars = @editorWidthInChars @editorWidthInChars = editorWidthInChars - if editorWidthInChars isnt previousWidthInChars and @softWrap + if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped() @updateWrappedScreenLines() # Returns the editor width in characters for soft wrap. @@ -627,7 +690,7 @@ class DisplayBuffer extends Model unless screenLine? throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", - softWrapEnabled: @getSoftWrap() + softWrapEnabled: @isSoftWrapped() foldCount: @findFoldMarkers().length lastBufferRow: @buffer.getLastRow() lastScreenRow: @getLastRow() @@ -743,7 +806,7 @@ class DisplayBuffer extends Model # Returns a {Number} representing the `line` position where the wrap would take place. # Returns `null` if a wrap wouldn't occur. findWrapColumn: (line, softWrapColumn=@getSoftWrapColumn()) -> - return unless @softWrap + return unless @isSoftWrapped() return unless line.length > softWrapColumn if /\s/.test(line[softWrapColumn]) @@ -766,6 +829,12 @@ class DisplayBuffer extends Model decorationForId: (id) -> @decorationsById[id] + getDecorations: -> + allDecorations = [] + for markerId, decorations of @decorationsByMarkerId + allDecorations = allDecorations.concat(decorations) if decorations? + allDecorations + decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> decorationsByMarkerId = {} for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) @@ -775,24 +844,13 @@ class DisplayBuffer extends Model decorateMarker: (marker, decorationParams) -> marker = @getMarker(marker.id) - - @decorationMarkerDestroyedSubscriptions[marker.id] ?= @subscribe marker, 'destroyed', => - @removeAllDecorationsForMarker(marker) - - @decorationMarkerChangedSubscriptions[marker.id] ?= @subscribe marker, 'changed', (event) => - decorations = @decorationsByMarkerId[marker.id] - - # Why check existence? Markers may get destroyed or decorations removed - # in the change handler. Bookmarks does this. - if decorations? - for decoration in decorations - @emit 'decoration-changed', marker, decoration, event - decoration = new Decoration(marker, this, decorationParams) + @subscribe decoration.onDidDestroy => @removeDecoration(decoration) @decorationsByMarkerId[marker.id] ?= [] @decorationsByMarkerId[marker.id].push(decoration) @decorationsById[decoration.id] = decoration - @emit 'decoration-added', marker, decoration + @emit 'decoration-added', decoration + @emitter.emit 'did-add-decoration', decoration decoration removeDecoration: (decoration) -> @@ -803,25 +861,9 @@ class DisplayBuffer extends Model if index > -1 decorations.splice(index, 1) delete @decorationsById[decoration.id] - @emit 'decoration-removed', marker, decoration - @removedAllMarkerDecorations(marker) if decorations.length is 0 - - removeAllDecorationsForMarker: (marker) -> - decorations = @decorationsByMarkerId[marker.id].slice() - for decoration in decorations - @emit 'decoration-removed', marker, decoration - @removedAllMarkerDecorations(marker) - - removedAllMarkerDecorations: (marker) -> - @decorationMarkerChangedSubscriptions[marker.id].off() - @decorationMarkerDestroyedSubscriptions[marker.id].off() - - delete @decorationsByMarkerId[marker.id] - delete @decorationMarkerChangedSubscriptions[marker.id] - delete @decorationMarkerDestroyedSubscriptions[marker.id] - - decorationUpdated: (decoration) -> - @emit 'decoration-updated', decoration + @emit 'decoration-removed', decoration + @emitter.emit 'did-remove-decoration', decoration + delete @decorationsByMarkerId[marker.id] if decorations.length is 0 # Retrieves a {DisplayBufferMarker} based on its id. # @@ -965,12 +1007,13 @@ class DisplayBuffer extends Model getFoldMarkerAttributes: (attributes={}) -> _.extend(attributes, class: 'fold', displayBufferId: @id) - pauseMarkerObservers: -> - marker.pauseEvents() for marker in @getMarkers() + pauseMarkerChangeEvents: -> + marker.pauseChangeEvents() for marker in @getMarkers() - resumeMarkerObservers: -> - marker.resumeEvents() for marker in @getMarkers() + resumeMarkerChangeEvents: -> + marker.resumeChangeEvents() for marker in @getMarkers() @emit 'markers-updated' + @emitter.emit 'did-update-markers' refreshMarkerScreenPositions: -> for marker in @getMarkers() @@ -1012,10 +1055,10 @@ class DisplayBuffer extends Model bufferDelta: bufferDelta if options.delayChangeEvent - @pauseMarkerObservers() + @pauseMarkerChangeEvents() @pendingChangeEvent = changeEvent else - @emitChanged(changeEvent, options.refreshMarkers) + @emitDidChange(changeEvent, options.refreshMarkers) buildScreenLines: (startBufferRow, endBufferRow) -> screenLines = [] @@ -1087,13 +1130,13 @@ class DisplayBuffer extends Model computeScrollWidth: -> @scrollWidth = @pixelPositionForScreenPosition([@longestScreenRow, @maxLineLength]).left - @scrollWidth += 1 unless @getSoftWrap() + @scrollWidth += 1 unless @isSoftWrapped() @setScrollLeft(Math.min(@getScrollLeft(), @getMaxScrollLeft())) handleBufferMarkersUpdated: => if event = @pendingChangeEvent @pendingChangeEvent = null - @emitChanged(event, false) + @emitDidChange(event, false) handleBufferMarkerCreated: (marker) => @createFoldForMarker(marker) if marker.matchesAttributes(@getFoldMarkerAttributes()) @@ -1101,6 +1144,7 @@ class DisplayBuffer extends Model # 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 createFoldForMarker: (marker) -> @decorateMarker(marker, type: 'gutter', class: 'folded') diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 3446bb6a2..646ff690c 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -1,3 +1,4 @@ +_ = require 'underscore-plus' React = require 'react-atom-fork' {div, span} = require 'reactionary-atom-fork' {debounce, defaults, isEqualForProperties} = require 'underscore-plus' @@ -30,9 +31,8 @@ EditorComponent = React.createClass updateRequested: false updatesPaused: false updateRequestedWhilePaused: false - cursorsMoved: false + cursorMoved: false selectionChanged: false - selectionAdded: false scrollingVertically: false mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 @@ -176,10 +176,16 @@ EditorComponent = React.createClass @listenForDOMEvents() @listenForCommands() - @subscribe atom.themes, 'stylesheet-added stylesheet-removed stylesheet-updated', @onStylesheetsChanged + @subscribe atom.themes.onDidAddStylesheet @onStylesheetsChanged + @subscribe atom.themes.onDidUpdateStylesheet @onStylesheetsChanged + @subscribe atom.themes.onDidRemoveStylesheet @onStylesheetsChanged + unless atom.themes.isInitialLoadComplete() + @subscribe atom.themes.onDidReloadAll @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars @domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval) + @updateParentViewFocusedClassIfNeeded({}) + @updateParentViewMiniClassIfNeeded({}) @checkForVisibilityChange() componentWillUnmount: -> @@ -195,16 +201,16 @@ EditorComponent = React.createClass @props.editor.setMini(newProps.mini) componentDidUpdate: (prevProps, prevState) -> - cursorsMoved = @cursorsMoved + cursorMoved = @cursorMoved selectionChanged = @selectionChanged @pendingChanges.length = 0 - @cursorsMoved = false + @cursorMoved = false @selectionChanged = false if @props.editor.isAlive() @updateParentViewFocusedClassIfNeeded(prevState) @updateParentViewMiniClassIfNeeded(prevState) - @props.parentView.trigger 'cursor:moved' if cursorsMoved + @props.parentView.trigger 'cursor:moved' if cursorMoved @props.parentView.trigger 'selection:changed' if selectionChanged @props.parentView.trigger 'editor:display-updated' @@ -305,7 +311,7 @@ EditorComponent = React.createClass if marker.isValid() for decoration in decorations if decoration.isType('gutter') or decoration.isType('line') - decorationParams = decoration.getParams() + decorationParams = decoration.getProperties() screenRange ?= marker.getScreenRange() headScreenRow ?= marker.getHeadScreenPosition().row startRow = screenRange.start.row @@ -332,7 +338,7 @@ EditorComponent = React.createClass if marker.isValid() and not screenRange.isEmpty() for decoration in decorations if decoration.isType('highlight') - decorationParams = decoration.getParams() + decorationParams = decoration.getProperties() filteredDecorations[markerId] ?= id: markerId startPixelPosition: editor.pixelPositionForScreenPosition(screenRange.start) @@ -344,15 +350,12 @@ EditorComponent = React.createClass observeEditor: -> {editor} = @props - @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged - @subscribe editor, 'cursors-moved', @onCursorsMoved - @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged - @subscribe editor, 'selection-added', @onSelectionAdded - @subscribe editor, 'decoration-added', @onDecorationChanged - @subscribe editor, 'decoration-removed', @onDecorationChanged - @subscribe editor, 'decoration-changed', @onDecorationChanged - @subscribe editor, 'decoration-updated', @onDecorationChanged - @subscribe editor, 'character-widths-changed', @onCharacterWidthsChanged + @subscribe editor.onDidChangeScreenLines(@onScreenLinesChanged) + @subscribe editor.observeCursors(@onCursorAdded) + @subscribe editor.observeSelections(@onSelectionAdded) + @subscribe editor.observeDecorations(@onDecorationAdded) + @subscribe editor.onDidRemoveDecoration(@onDecorationRemoved) + @subscribe editor.onDidChangeCharacterWidths(@onCharacterWidthsChanged) @subscribe editor.$scrollTop.changes, @onScrollTopChanged @subscribe editor.$scrollLeft.changes, @requestUpdate @subscribe editor.$verticalScrollbarWidth.changes, @requestUpdate @@ -477,7 +480,7 @@ EditorComponent = React.createClass 'editor:add-selection-above': -> editor.addSelectionAbove() 'editor:split-selections-into-lines': -> editor.splitSelectionsIntoLines() 'editor:toggle-soft-tabs': -> editor.toggleSoftTabs() - 'editor:toggle-soft-wrap': -> editor.toggleSoftWrap() + 'editor:toggle-soft-wrap': -> editor.toggleSoftWrapped() 'editor:fold-all': -> editor.foldAll() 'editor:unfold-all': -> editor.unfoldAll() 'editor:fold-current-row': -> editor.foldCurrentRow() @@ -644,8 +647,12 @@ EditorComponent = React.createClass onGutterMouseDown: (event) -> return unless event.button is 0 # only handle the left mouse button - if event.shiftKey + {shiftKey, metaKey, ctrlKey} = event + + if shiftKey @onGutterShiftClick(event) + else if metaKey or (ctrlKey and process.platform isnt 'darwin') + @onGutterMetaClick(event) else @onGutterClick(event) @@ -653,14 +660,38 @@ EditorComponent = React.createClass {editor} = @props clickedRow = @screenPositionForMouseEvent(event).row - editor.setCursorScreenPosition([clickedRow, 0]) + editor.setSelectedScreenRange([[clickedRow, 0], [clickedRow + 1, 0]], preserveFolds: true) @handleDragUntilMouseUp event, (screenPosition) -> dragRow = screenPosition.row if dragRow < clickedRow # dragging up - editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]]) + editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true) else - editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]]) + editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true) + + onGutterMetaClick: (event) -> + {editor} = @props + clickedRow = @screenPositionForMouseEvent(event).row + + bufferRange = editor.bufferRangeForScreenRange([[clickedRow, 0], [clickedRow + 1, 0]]) + rowSelection = editor.addSelectionForBufferRange(bufferRange, preserveFolds: true) + + @handleDragUntilMouseUp event, (screenPosition) -> + dragRow = screenPosition.row + + if dragRow < clickedRow # dragging up + rowSelection.setScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true) + else + rowSelection.setScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true) + + # After updating the selected screen range, merge overlapping selections + editor.mergeIntersectingSelections(preserveFolds: true) + + # The merge process will possibly destroy the current selection because + # it will be merged into another one. Therefore, we need to obtain a + # reference to the new selection that contains the originally selected row + rowSelection = _.find editor.getSelections(), (selection) -> + selection.intersectsBufferRange(bufferRange) onGutterShiftClick: (event) -> {editor} = @props @@ -675,14 +706,15 @@ EditorComponent = React.createClass @handleDragUntilMouseUp event, (screenPosition) -> dragRow = screenPosition.row if dragRow < tailPosition.row # dragging up - editor.setSelectedScreenRange([[dragRow, 0], tailPosition]) + editor.setSelectedScreenRange([[dragRow, 0], tailPosition], preserveFolds: true) else - editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]]) + editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true) onStylesheetsChanged: (stylesheet) -> return unless @performedInitialMeasurement + return unless atom.themes.isInitialLoadComplete() - @refreshScrollbars() if @containsScrollbarSelector(stylesheet) + @refreshScrollbars() if not stylesheet? or @containsScrollbarSelector(stylesheet) @sampleFontStyling() @sampleBackgroundColors() @remeasureCharacterWidths() @@ -692,17 +724,22 @@ EditorComponent = React.createClass @pendingChanges.push(change) @requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events - onSelectionChanged: (selection) -> + onSelectionAdded: (selection) -> {editor} = @props + + @subscribe selection.onDidChangeRange => @onSelectionChanged(selection) + @subscribe selection.onDidDestroy => + @onSelectionChanged(selection) + @unsubscribe(selection) + if editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true @requestUpdate() - onSelectionAdded: (selection) -> + onSelectionChanged: (selection) -> {editor} = @props if editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true - @selectionAdded = true @requestUpdate() onScrollTopChanged: -> @@ -720,13 +757,24 @@ EditorComponent = React.createClass onStoppedScrollingAfterDelay: null # created lazily - onCursorsMoved: -> - @cursorsMoved = true + onCursorAdded: (cursor) -> + @subscribe cursor.onDidChangePosition @onCursorMoved + + onCursorMoved: -> + @cursorMoved = true + @requestUpdate() + + onDecorationAdded: (decoration) -> + @subscribe decoration.onDidChangeProperties(@onDecorationChanged) + @subscribe decoration.getMarker().onDidChange(@onDecorationChanged) @requestUpdate() onDecorationChanged: -> @requestUpdate() + onDecorationRemoved: -> + @requestUpdate() + onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) -> @requestUpdate() diff --git a/src/editor-view.coffee b/src/editor-view.coffee index 2f945f458..e91d69caa 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -70,9 +70,9 @@ class EditorView extends View # The constructor for setting up an `EditorView` instance. # # * `editorOrParams` Either an {Editor}, or an object with one property, `mini`. - # If `mini` is `true`, a "miniature" `Editor` is constructed. - # Typically, this is ideal for scenarios where you need an Atom editor, - # but without all the chrome, like scrollbars, gutter, _e.t.c._. + # If `mini` is `true`, a "miniature" `Editor` is constructed. + # Typically, this is ideal for scenarios where you need an Atom editor, + # but without all the chrome, like scrollbars, gutter, _e.t.c._. # constructor: (editorOrParams, props) -> super @@ -86,7 +86,7 @@ class EditorView extends View props.placeholderText = placeholderText @editor ?= new Editor buffer: new TextBuffer - softWrap: false + softWrapped: false tabLength: 2 softTabs: true mini: mini @@ -101,7 +101,6 @@ class EditorView extends View @overlayer = $(node).find('.lines').addClass('overlayer') @hiddenInput = $(node).find('.hidden-input') - # FIXME: there should be a better way to deal with the gutter element @subscribe atom.config.observe 'editor.showLineNumbers', => @gutter = $(node).find('.gutter') @@ -136,7 +135,7 @@ class EditorView extends View Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth() Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] - Object.defineProperty @::, 'active', get: -> @is(@getPane()?.activeView) + Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView) Object.defineProperty @::, 'isFocused', get: -> @component?.state.focused Object.defineProperty @::, 'mini', get: -> @component?.props.mini @@ -144,12 +143,12 @@ class EditorView extends View return unless onDom return if @attached @attached = true - @component.pollDOM() + @component.checkForVisibilityChange() + @focus() if @focusOnAttach @addGrammarScopeAttribute() - @subscribe @editor, 'grammar-changed', => - @addGrammarScopeAttribute() + @subscribe @editor.onDidChangeGrammar => @addGrammarScopeAttribute() @trigger 'editor:attached', [this] @@ -169,45 +168,28 @@ class EditorView extends View else @editor.getScrollLeft() - # Public: Scrolls the editor to the bottom. scrollToBottom: -> + deprecate 'Use Editor::scrollToBottom instead. You can get the editor via editorView.getModel()' @editor.setScrollBottom(Infinity) - # Public: Scrolls the editor to the given screen position. - # - # * `screenPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} matching the options available to {::scrollToScreenPosition} scrollToScreenPosition: (screenPosition, options) -> + deprecate 'Use Editor::scrollToScreenPosition instead. You can get the editor via editorView.getModel()' @editor.scrollToScreenPosition(screenPosition, options) - # Public: Scrolls the editor to the given buffer position. - # - # * `bufferPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} matching the options available to {::scrollToBufferPosition} scrollToBufferPosition: (bufferPosition, options) -> + deprecate 'Use Editor::scrollToBufferPosition instead. You can get the editor via editorView.getModel()' @editor.scrollToBufferPosition(bufferPosition, options) scrollToCursorPosition: -> + deprecate 'Use Editor::scrollToCursorPosition instead. You can get the editor via editorView.getModel()' @editor.scrollToCursorPosition() - # Public: Converts a buffer position to a pixel position. - # - # * `bufferPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. pixelPositionForBufferPosition: (bufferPosition) -> + deprecate 'Use Editor::pixelPositionForBufferPosition instead. You can get the editor via editorView.getModel()' @editor.pixelPositionForBufferPosition(bufferPosition) - # Public: Converts a screen position to a pixel position. - # - # * `screenPosition` An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForScreenPosition: (screenPosition) -> + deprecate 'Use Editor::pixelPositionForScreenPosition instead. You can get the editor via editorView.getModel()' @editor.pixelPositionForScreenPosition(screenPosition) appendToLinesView: (view) -> @@ -231,31 +213,50 @@ class EditorView extends View unmountComponent: -> React.unmountComponentAtNode(@element) if @component.isMounted() - # Public: Split the editor view left. splitLeft: -> - pane = @getPane() + deprecate """ + Use Pane::splitLeft instead. + To duplicate this editor into the split use: + editorView.getPaneView().getModel().splitLeft(copyActiveItem: true) + """ + pane = @getPaneView() pane?.splitLeft(pane?.copyActiveItem()).activeView - # Public: Split the editor view right. splitRight: -> - pane = @getPane() + deprecate """ + Use Pane::splitRight instead. + To duplicate this editor into the split use: + editorView.getPaneView().getModel().splitRight(copyActiveItem: true) + """ + pane = @getPaneView() pane?.splitRight(pane?.copyActiveItem()).activeView - # Public: Split the editor view up. splitUp: -> - pane = @getPane() + deprecate """ + Use Pane::splitUp instead. + To duplicate this editor into the split use: + editorView.getPaneView().getModel().splitUp(copyActiveItem: true) + """ + pane = @getPaneView() pane?.splitUp(pane?.copyActiveItem()).activeView - # Public: Split the editor view down. splitDown: -> - pane = @getPane() + deprecate """ + Use Pane::splitDown instead. + To duplicate this editor into the split use: + editorView.getPaneView().getModel().splitDown(copyActiveItem: true) + """ + pane = @getPaneView() pane?.splitDown(pane?.copyActiveItem()).activeView - # Public: Get this view's pane. + # Public: Get this {EditorView}'s {PaneView}. # - # Returns a {Pane}. - getPane: -> + # Returns a {PaneView} + getPaneView: -> @parent('.item-views').parents('.pane').view() + getPane: -> + deprecate 'Use EditorView::getPaneView() instead' + @getPaneView() show: -> super @@ -273,72 +274,47 @@ class EditorView extends View deprecate('Use editorView.getModel().pageUp()') @editor.pageUp() - # Public: Retrieves the number of the row that is visible and currently at the - # top of the editor. - # - # Returns a {Number}. getFirstVisibleScreenRow: -> - @editor.getVisibleRowRange()[0] + deprecate 'Use Editor::getFirstVisibleScreenRow instead. You can get the editor via editorView.getModel()' + @editor.getFirstVisibleScreenRow() - # Public: Retrieves the number of the row that is visible and currently at the - # bottom of the editor. - # - # Returns a {Number}. getLastVisibleScreenRow: -> - @editor.getVisibleRowRange()[1] + deprecate 'Use Editor::getLastVisibleScreenRow instead. You can get the editor via editorView.getModel()' + @editor.getLastVisibleScreenRow() - # Public: Gets the font family for the editor. - # - # Returns a {String} identifying the CSS `font-family`. getFontFamily: -> + deprecate 'This is going away. Use atom.config.get("editor.fontFamily") instead' @component?.getFontFamily() - # Public: Sets the font family for the editor. - # - # * `fontFamily` A {String} identifying the CSS `font-family`. setFontFamily: (fontFamily) -> + deprecate 'This is going away. Use atom.config.set("editor.fontFamily", "my-font") instead' @component?.setFontFamily(fontFamily) - # Public: Retrieves the font size for the editor. - # - # Returns a {Number} indicating the font size in pixels. getFontSize: -> + deprecate 'This is going away. Use atom.config.get("editor.fontSize") instead' @component?.getFontSize() - # Public: Sets the font size for the editor. - # - # * `fontSize` A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> + deprecate 'This is going away. Use atom.config.set("editor.fontSize", 12) instead' @component?.setFontSize(fontSize) + setLineHeight: (lineHeight) -> + deprecate 'This is going away. Use atom.config.set("editor.lineHeight", 1.5) instead' + @component.setLineHeight(lineHeight) + setWidthInChars: (widthInChars) -> @component.getDOMNode().style.width = (@editor.getDefaultCharWidth() * widthInChars) + 'px' - # Public: Sets the line height of the editor. - # - # Calling this method has no effect when called on a mini editor. - # - # * `lineHeight` A {Number} without a unit suffix identifying the CSS `line-height`. - setLineHeight: (lineHeight) -> - @component.setLineHeight(lineHeight) - - # Public: Sets whether you want to show the indentation guides. - # - # * `showIndentGuide` A {Boolean} you can set to `true` if you want to see the - # indentation guides. setShowIndentGuide: (showIndentGuide) -> + deprecate 'This is going away. Use atom.config.set("editor.showIndentGuide", true|false) instead' @component.setShowIndentGuide(showIndentGuide) - # Public: Enables/disables soft wrap on the editor. - # - # * `softWrap` A {Boolean} which, if `true`, enables soft wrap - setSoftWrap: (softWrap) -> - @editor.setSoftWrap(softWrap) + setSoftWrap: (softWrapped) -> + deprecate 'Use Editor::setSoftWrapped instead. You can get the editor via editorView.getModel()' + @editor.setSoftWrapped(softWrapped) - # Public: Set whether invisible characters are shown. - # - # * `showInvisibles` A {Boolean} which, if `true`, show invisible characters. setShowInvisibles: (showInvisibles) -> + deprecate 'This is going away. Use atom.config.set("editor.showInvisibles", true|false) instead' @component.setShowInvisibles(showInvisibles) getText: -> diff --git a/src/editor.coffee b/src/editor.coffee index 337b5da9d..e66110d8e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -4,6 +4,8 @@ Serializable = require 'serializable' Delegator = require 'delegato' {deprecate} = require 'grim' {Model} = require 'theorist' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' LanguageMode = require './language-mode' DisplayBuffer = require './display-buffer' @@ -24,12 +26,12 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # ## Accessing Editor Instances # # The easiest way to get hold of `Editor` objects is by registering a callback -# with `::eachEditor` on the `atom.workspace` global. Your callback will then -# be called with all current editor instances and also when any editor is +# with `::observeTextEditors` on the `atom.workspace` global. Your callback will +# then be called with all current editor instances and also when any editor is # created in the future. # # ```coffee -# atom.workspace.eachEditor (editor) -> +# atom.workspace.observeTextEditors (editor) -> # editor.insertText('Hello World') # ``` # @@ -50,134 +52,6 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # # **When in doubt, just default to buffer coordinates**, then experiment with # soft wraps and folds to ensure your code interacts with them correctly. -# -# ## Events -# -# ### path-changed -# -# Essential: Emit when the buffer's path, and therefore title, has changed. -# -# ### title-changed -# -# Essential: Emit when the buffer's path, and therefore title, has changed. -# -# ### modified-status-changed -# -# Extended: Emit when the result of {::isModified} changes. -# -# ### soft-wrap-changed -# -# Extended: Emit when soft wrap was enabled or disabled. -# -# * `softWrap` {Boolean} indicating whether soft wrap is enabled or disabled. -# -# ### grammar-changed -# -# Extended: Emit when the grammar that interprets and colorizes the text has -# been changed. -# -# -# -# ### contents-modified -# -# Essential: Emit 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. -# -# ### contents-conflicted -# -# Extended: Emitted when the buffer's underlying file changes on disk at a -# moment when the result of {::isModified} is true. -# -# ### will-insert-text -# -# Extended: Emit before the text has been inserted. -# -# * `event` event {Object} -# * `text` {String} text to be inserted -# * `cancel` {Function} Call to prevent the text from being inserted -# -# ### did-insert-text -# -# Extended: Emit after the text has been inserted. -# -# * `event` event {Object} -# * `text` {String} text to be inserted -# -# -# -# ### cursor-moved -# -# Essential: Emit when a cursor has been moved. If there are multiple cursors, -# it will be emit for each cursor. -# -# * `event` {Object} -# * `oldBufferPosition` {Point} -# * `oldScreenPosition` {Point} -# * `newBufferPosition` {Point} -# * `newScreenPosition` {Point} -# * `textChanged` {Boolean} -# -# ### cursor-added -# -# Extended: Emit when a cursor has been added. -# -# * `cursor` {Cursor} that was added -# -# ### cursor-removed -# -# Extended: Emit when a cursor has been removed. -# -# * `cursor` {Cursor} that was removed -# -# -# -# ### selection-screen-range-changed -# -# Essential: Emit when a selection's screen range changes. -# -# * `selection`: {Selection} object that has a changed range -# -# ### selection-added -# -# Extended: Emit when a selection's was added. -# -# * `selection`: {Selection} object that was added -# -# ### selection-removed -# -# Extended: Emit when a selection's was removed. -# -# * `selection`: {Selection} object that was removed -# -# -# -# ### decoration-added -# -# Extended: Emit when a {Decoration} is added to the editor. -# -# * `decoration` {Decoration} that was added -# -# ### decoration-removed -# -# Extended: Emit when a {Decoration} is removed from the editor. -# -# * `decoration` {Decoration} that was removed -# -# ### decoration-changed -# -# Extended: Emit when a {Decoration}'s underlying marker changes. Say the user -# inserts newlines above a decoration. That action will move the marker down, -# and fire this event. -# -# * `decoration` {Decoration} that was added -# -# ### decoration-updated -# -# Extended: Emit when a {Decoration} is updated via the {Decoration::update} method. -# -# * `decoration` {Decoration} that was updated -# module.exports = class Editor extends Model Serializable.includeInto(this) @@ -203,9 +77,10 @@ class Editor extends Model '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft', 'manageScrollPosition', toProperty: 'displayBuffer' - constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini}) -> + constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini}) -> super + @emitter = new Emitter @cursors = [] @selections = [] @@ -213,7 +88,7 @@ class Editor extends Model invisibles = atom.config.get('editor.invisibles') @displayBuffer?.setInvisibles(invisibles) - @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrap, invisibles}) + @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, invisibles}) @buffer = @displayBuffer.buffer @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true @@ -231,8 +106,12 @@ class Editor extends Model @languageMode = new LanguageMode(this) - @subscribe @$scrollTop, (scrollTop) => @emit 'scroll-top-changed', scrollTop - @subscribe @$scrollLeft, (scrollLeft) => @emit 'scroll-left-changed', scrollLeft + @subscribe @$scrollTop, (scrollTop) => + @emit 'scroll-top-changed', scrollTop + @emitter.emit 'did-change-scroll-top', scrollTop + @subscribe @$scrollLeft, (scrollLeft) => + @emit 'scroll-left-changed', scrollLeft + @emitter.emit 'did-change-scroll-left', scrollLeft @subscribe atom.config.observe 'editor.showInvisibles', callNow: false, (show) => @updateInvisibles() @subscribe atom.config.observe 'editor.invisibles', callNow: false, => @updateInvisibles() @@ -253,29 +132,35 @@ class Editor extends Model subscribeToBuffer: -> @buffer.retain() - @subscribe @buffer, "path-changed", => + @subscribe @buffer.onDidChangePath => unless atom.project.getPath()? atom.project.setPath(path.dirname(@getPath())) @emit "title-changed" + @emitter.emit 'did-change-title', @getTitle() @emit "path-changed" - @subscribe @buffer, "contents-modified", => @emit "contents-modified" - @subscribe @buffer, "contents-conflicted", => @emit "contents-conflicted" - @subscribe @buffer, "modified-status-changed", => @emit "modified-status-changed" - @subscribe @buffer, "destroyed", => @destroy() + @emitter.emit 'did-change-path', @getPath() + @subscribe @buffer.onDidDestroy => @destroy() + + # TODO: remove these thwne we remove the deprecations. They are old events. + @subscribe @buffer.onDidStopChanging => @emit "contents-modified" + @subscribe @buffer.onDidConflict => @emit "contents-conflicted" + @subscribe @buffer.onDidChangeModified => @emit "modified-status-changed" + @preserveCursorPositionOnBufferReload() subscribeToDisplayBuffer: -> - @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated - @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e - @subscribe @displayBuffer, "markers-updated", => @mergeIntersectingSelections() - @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() - @subscribe @displayBuffer, 'tokenized', => @handleTokenization() - @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... - @subscribe @displayBuffer, "decoration-added", (args...) => @emit 'decoration-added', args... - @subscribe @displayBuffer, "decoration-removed", (args...) => @emit 'decoration-removed', args... - @subscribe @displayBuffer, "decoration-changed", (args...) => @emit 'decoration-changed', args... - @subscribe @displayBuffer, "decoration-updated", (args...) => @emit 'decoration-updated', args... - @subscribe @displayBuffer, "character-widths-changed", (changeCount) => @emit 'character-widths-changed', changeCount + @subscribe @displayBuffer.onDidCreateMarker @handleMarkerCreated + @subscribe @displayBuffer.onDidUpdateMarkers => @mergeIntersectingSelections() + @subscribe @displayBuffer.onDidChangeGrammar => @handleGrammarChange() + @subscribe @displayBuffer.onDidTokenize => @handleTokenization() + @subscribe @displayBuffer.onDidChange (e) => + @emit 'screen-lines-changed', e + @emitter.emit 'did-change-screen-lines', 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 + @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration + @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration getViewClass: -> require './editor-view' @@ -287,6 +172,280 @@ class Editor extends Model @displayBuffer.destroy() @languageMode.destroy() + ### + Section: Event Subscription + ### + + # Essential: Calls your `callback` when the buffer's title has changed. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeTitle: (callback) -> + @emitter.on 'did-change-title', callback + + # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePath: (callback) -> + @emitter.on 'did-change-path', callback + + # Extended: Calls your `callback` when soft wrap was enabled or disabled. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSoftWrapped: (callback) -> + @displayBuffer.onDidChangeSoftWrapped(callback) + + # Extended: Calls your `callback` when the grammar that interprets and colorizes the text has + # been changed. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + 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} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeModified: (callback) -> + @getBuffer().onDidChangeModified(callback) + + # Extended: Calls your `callback` when the buffer's underlying file changes on + # disk at a moment when the result of {::isModified} is true. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidConflict: (callback) -> + @getBuffer().onDidConflict(callback) + + # Extended: Calls your `callback` before text has been inserted. + # + # * `callback` {Function} + # * `event` event {Object} + # * `text` {String} text to be inserted + # * `cancel` {Function} Call to prevent the text from being inserted + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillInsertText: (callback) -> + @emitter.on 'will-insert-text', callback + + # Extended: Calls your `callback` adter text has been inserted. + # + # * `callback` {Function} + # * `event` event {Object} + # * `text` {String} text to be inserted + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidInsertText: (callback) -> + @emitter.on 'did-insert-text', callback + + # Public: Invoke the given callback after the buffer is saved to disk. + # + # * `callback` {Function} to be called after the buffer is saved. + # * `event` {Object} with the following keys: + # * `path` The path to which the buffer was saved. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidSave: (callback) -> + @getBuffer().onDidSave(callback) + + # Extended: Calls your `callback` when a {Cursor} is added to the editor. + # Immediately calls your callback for each existing cursor. + # + # * `callback` {Function} + # * `selection` {Selection} that was added + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeCursors: (callback) -> + callback(cursor) for cursor in @getCursors() + @onDidAddCursor(callback) + + # Extended: Calls your `callback` when a {Cursor} is added to the editor. + # + # * `callback` {Function} + # * `cursor` {Cursor} that was added + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddCursor: (callback) -> + @emitter.on 'did-add-cursor', callback + + # Extended: Calls your `callback` when a {Cursor} is removed from the editor. + # + # * `callback` {Function} + # * `cursor` {Cursor} that was removed + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + 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. + # + # * `callback` {Function} + # * `selection` {Selection} that was added + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeSelections: (callback) -> + callback(selection) for selection in @getSelections() + @onDidAddSelection(callback) + + # Extended: Calls your `callback` when a {Selection} is added to the editor. + # + # * `callback` {Function} + # * `selection` {Selection} that was added + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddSelection: (callback) -> + @emitter.on 'did-add-selection', callback + + # Extended: Calls your `callback` when a {Selection} is removed from the editor. + # + # * `callback` {Function} + # * `selection` {Selection} that was removed + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + 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. + # + # * `callback` {Function} + # * `decoration` {Decoration} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeDecorations: (callback) -> + @displayBuffer.observeDecorations(callback) + + # Extended: Calls your `callback` when a {Decoration} is added to the editor. + # + # * `callback` {Function} + # * `decoration` {Decoration} that was added + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddDecoration: (callback) -> + @displayBuffer.onDidAddDecoration(callback) + + # Extended: Calls your `callback` when a {Decoration} is removed from the editor. + # + # * `callback` {Function} + # * `decoration` {Decoration} that was removed + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveDecoration: (callback) -> + @displayBuffer.onDidRemoveDecoration(callback) + + onDidChangeCharacterWidths: (callback) -> + @displayBuffer.onDidChangeCharacterWidths(callback) + + onDidChangeScreenLines: (callback) -> + @emitter.on 'did-change-screen-lines', callback + + onDidChangeScrollTop: (callback) -> + @emitter.on 'did-change-scroll-top', callback + + onDidChangeScrollLeft: (callback) -> + @emitter.on 'did-change-scroll-left', callback + + on: (eventName) -> + switch eventName + when 'title-changed' + deprecate("Use Editor::onDidChangeTitle instead") + when 'path-changed' + deprecate("Use Editor::onDidChangePath instead") + when 'modified-status-changed' + deprecate("Use Editor::onDidChangeModified instead") + when 'soft-wrap-changed' + deprecate("Use Editor::onDidChangeSoftWrapped instead") + when 'grammar-changed' + deprecate("Use Editor::onDidChangeGrammar instead") + when 'character-widths-changed' + deprecate("Use Editor::onDidChangeCharacterWidths instead") + when 'contents-modified' + deprecate("Use Editor::onDidStopChanging instead") + when 'contents-conflicted' + deprecate("Use Editor::onDidConflict instead") + + when 'will-insert-text' + deprecate("Use Editor::onWillInsertText instead") + when 'did-insert-text' + deprecate("Use Editor::onDidInsertText instead") + + when 'cursor-added' + deprecate("Use Editor::onDidAddCursor instead") + when 'cursor-removed' + deprecate("Use Editor::onDidRemoveCursor instead") + when 'cursor-moved' + deprecate("Use Editor::onDidChangeCursorPosition instead") + + when 'selection-added' + deprecate("Use Editor::onDidAddSelection instead") + when 'selection-removed' + deprecate("Use Editor::onDidRemoveSelection instead") + when 'selection-screen-range-changed' + deprecate("Use Editor::onDidChangeSelectionRange instead") + + when 'decoration-added' + deprecate("Use Editor::onDidAddDecoration instead") + when 'decoration-removed' + deprecate("Use Editor::onDidRemoveDecoration instead") + when 'decoration-updated' + deprecate("Use Decoration::onDidChangeProperties instead. You will get the decoration back from `Editor::decorateMarker()`") + when 'decoration-changed' + deprecate("Use Marker::onDidChange instead. eg. `editor::decorateMarker(...).getMarker().onDidChange()`") + + when 'screen-lines-changed' + deprecate("Use Editor::onDidChangeScreenLines instead") + + when 'scroll-top-changed' + deprecate("Use Editor::onDidChangeScrollTop instead") + when 'scroll-left-changed' + deprecate("Use Editor::onDidChangeScrollLeft instead") + + EmitterMixin::on.apply(this, arguments) + # Retrieves the current {TextBuffer}. getBuffer: -> @buffer @@ -735,14 +894,18 @@ class Editor extends Model insertText: (text, options={}) -> willInsert = true cancel = -> willInsert = false - @emit('will-insert-text', {cancel, text}) + 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) - @emit('did-insert-text', {text, range}) + didInsertEvent = {text, range} + @emit('did-insert-text', didInsertEvent) + @emitter.emit 'did-insert-text', didInsertEvent range else false @@ -904,18 +1067,31 @@ class Editor extends Model # Public: Sets the column at which column will soft wrap getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - # Public: Get whether soft wrap is enabled for this editor. - getSoftWrap: -> @displayBuffer.getSoftWrap() - - # Public: Enable or disable soft wrap for this editor. + # Public: Determine whether lines in this editor are soft-wrapped. # - # * `softWrap` A {Boolean} - setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap) - - # Public: Toggle soft wrap for this editor - toggleSoftWrap: -> @setSoftWrap(not @getSoftWrap()) + # 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 @@ -1199,7 +1375,8 @@ class Editor extends Model isBufferRowCommented: (bufferRow) -> if match = @lineTextForBufferRow(bufferRow).match(/\S/) scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes - new TextMateScopeSelector('comment.*').matches(scopes) + @commentScopeSelector ?= new TextMateScopeSelector('comment.*') + @commentScopeSelector.matches(scopes) # Public: Toggle line comments for rows intersecting selections. # @@ -1619,7 +1796,7 @@ class Editor extends Model # Essential: Move every cursor up one row in screen coordinates. # - # * `lineCount` {Number} number of lines to move + # * `lineCount` (optional) {Number} number of lines to move moveUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) moveCursorUp: (lineCount) -> @@ -1628,7 +1805,7 @@ class Editor extends Model # Essential: Move every cursor down one row in screen coordinates. # - # * `lineCount` {Number} number of lines to move + # * `lineCount` (optional) {Number} number of lines to move moveDown: (lineCount) -> @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) moveCursorDown: (lineCount) -> @@ -1636,15 +1813,19 @@ class Editor extends Model @moveDown(lineCount) # Essential: Move every cursor left one column. - moveLeft: -> - @moveCursors (cursor) -> cursor.moveLeft(moveToEndOfSelection: true) + # + # * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveLeft: (columnCount) -> + @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) moveCursorLeft: -> deprecate("Use Editor::moveLeft() instead") @moveLeft() # Essential: Move every cursor right one column. - moveRight: -> - @moveCursors (cursor) -> cursor.moveRight(moveToEndOfSelection: true) + # + # * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveRight: (columnCount) -> + @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) moveCursorRight: -> deprecate("Use Editor::moveRight() instead") @moveRight() @@ -1787,23 +1968,22 @@ class Editor extends Model @decorateMarker(marker, type: 'gutter', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) @emit 'cursor-added', cursor + @emitter.emit 'did-add-cursor', cursor cursor # Remove the given cursor from this editor. removeCursor: (cursor) -> _.remove(@cursors, cursor) @emit 'cursor-removed', cursor + @emitter.emit 'did-remove-cursor', cursor moveCursors: (fn) -> - @movingCursors = true fn(cursor) for cursor in @getCursors() @mergeCursors() - @movingCursors = false - @emit 'cursors-moved' - cursorMoved: (event) -> + cursorMoved: (cursor, event) -> @emit 'cursor-moved', event - @emit 'cursors-moved' unless @movingCursors + @emitter.emit 'did-change-cursor-position', event # Merge cursors that have the same screen position mergeCursors: -> @@ -1817,9 +1997,9 @@ class Editor extends Model preserveCursorPositionOnBufferReload: -> cursorPosition = null - @subscribe @buffer, "will-reload", => + @subscribe @buffer.onWillReload => cursorPosition = @getCursorBufferPosition() - @subscribe @buffer, "reloaded", => + @subscribe @buffer.onDidReload => @setCursorBufferPosition(cursorPosition) if cursorPosition cursorPosition = null @@ -1978,7 +2158,7 @@ class Editor extends Model # Essential: Move the cursor of each selection one character upward while # preserving the selection's tail position. # - # * `rowCount` {Number} of rows to select up + # * `rowCount` (optional) {Number} number of rows to select (default: 1) # # This method may merge selections that end up intesecting. selectUp: (rowCount) -> @@ -1987,7 +2167,7 @@ class Editor extends Model # Essential: Move the cursor of each selection one character downward while # preserving the selection's tail position. # - # * `rowCount` {Number} of rows to select down + # * `rowCount` (optional) {Number} number of rows to select (default: 1) # # This method may merge selections that end up intesecting. selectDown: (rowCount) -> @@ -1996,16 +2176,20 @@ class Editor extends Model # Essential: Move the cursor of each selection one character leftward while # preserving the selection's tail position. # + # * `columnCount` (optional) {Number} number of columns to select (default: 1) + # # This method may merge selections that end up intesecting. - selectLeft: -> - @expandSelectionsBackward (selection) -> selection.selectLeft() + selectLeft: (columnCount) -> + @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) # Essential: Move the cursor of each selection one character rightward while # preserving the selection's tail position. # + # * `columnCount` (optional) {Number} number of columns to select (default: 1) + # # This method may merge selections that end up intesecting. - selectRight: -> - @expandSelectionsForward (selection) -> selection.selectRight() + selectRight: (columnCount) -> + @expandSelectionsForward (selection) -> selection.selectRight(columnCount) # Essential: Select from the top of the buffer to the end of the last selection # in the buffer. @@ -2248,19 +2432,21 @@ class Editor extends Model selection = new Selection(_.extend({editor: this, marker, cursor}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections() + @mergeIntersectingSelections(preserveFolds: marker.getAttributes().preserveFolds) if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) return selection else @emit 'selection-added', selection + @emitter.emit 'did-add-selection', selection selection # Remove the given selection. removeSelection: (selection) -> _.remove(@selections, selection) @emit 'selection-removed', selection + @emitter.emit 'did-remove-selection', selection # Reduce one or more selections to a single empty selection based on the most # recently added cursor. @@ -2277,22 +2463,62 @@ class Editor extends Model else false - selectionScreenRangeChanged: (selection) -> + # Called by the selection + selectionRangeChanged: (selection) -> @emit 'selection-screen-range-changed', selection + @emitter.emit 'did-change-selection-range', selection ### Section: Scrolling the Editor ### - # Public: Scroll the editor to reveal the most recently added cursor if it is + # Essential: Scroll the editor to reveal the most recently added cursor if it is # off-screen. # # * `options` (optional) {Object} - # * `center` Center the editor around the cursor if possible. Defauls to true. + # * `center` Center the editor around the cursor if possible. (default: true) scrollToCursorPosition: (options) -> @getLastCursor().autoscroll(center: options?.center ? true) + # Essential: Scrolls the editor to the given buffer position. + # + # * `bufferPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # * `options` (optional) {Object} + # * `center` Center the editor around the position if possible. (default: false) + scrollToBufferPosition: (bufferPosition, options) -> + @displayBuffer.scrollToBufferPosition(bufferPosition, options) + + # Essential: Scrolls the editor to the given screen position. + # + # * `screenPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # * `options` (optional) {Object} + # * `center` Center the editor around the position if possible. (default: false) + scrollToScreenPosition: (screenPosition, options) -> + @displayBuffer.scrollToScreenPosition(screenPosition, options) + + # Essential: Scrolls the editor to the top + scrollToTop: -> + @setScrollTop(0) + + # Essential: Scrolls the editor to the bottom + scrollToBottom: -> + @setScrollBottom(Infinity) + + scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) + + horizontallyScrollable: -> @displayBuffer.horizontallyScrollable() + + verticallyScrollable: -> @displayBuffer.verticallyScrollable() + + getHorizontalScrollbarHeight: -> @displayBuffer.getHorizontalScrollbarHeight() + setHorizontalScrollbarHeight: (height) -> @displayBuffer.setHorizontalScrollbarHeight(height) + + getVerticalScrollbarWidth: -> @displayBuffer.getVerticalScrollbarWidth() + setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width) + pageUp: -> newScrollTop = @getScrollTop() - @getHeight() @moveUp(@getRowsPerPage()) @@ -2341,6 +2567,7 @@ class Editor extends Model handleGrammarChange: -> @unfoldAll() @emit 'grammar-changed' + @emitter.emit 'did-change-grammar' handleMarkerCreated: (marker) => if marker.matchesAttributes(@getSelectionMarkerAttributes()) @@ -2350,6 +2577,36 @@ class Editor extends Model Section: Editor Rendering ### + # Extended: Retrieves the number of the row that is visible and currently at the + # top of the editor. + # + # Returns a {Number}. + getFirstVisibleScreenRow: -> + @getVisibleRowRange()[0] + + # Extended: Retrieves the number of the row that is visible and currently at the + # bottom of the editor. + # + # Returns a {Number}. + getLastVisibleScreenRow: -> + @getVisibleRowRange()[1] + + # Extended: Converts a buffer position to a pixel position. + # + # * `bufferPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. + pixelPositionForBufferPosition: (bufferPosition) -> @displayBuffer.pixelPositionForBufferPosition(bufferPosition) + + # Extended: Converts a screen position to a pixel position. + # + # * `screenPosition` An object that represents a screen position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. + pixelPositionForScreenPosition: (screenPosition) -> @displayBuffer.pixelPositionForScreenPosition(screenPosition) + getSelectionMarkerAttributes: -> type: 'selection', editorId: @id, invalidate: 'never' @@ -2403,30 +2660,10 @@ class Editor extends Model selectionIntersectsVisibleRowRange: (selection) -> @displayBuffer.selectionIntersectsVisibleRowRange(selection) - pixelPositionForScreenPosition: (screenPosition) -> @displayBuffer.pixelPositionForScreenPosition(screenPosition) - - pixelPositionForBufferPosition: (bufferPosition) -> @displayBuffer.pixelPositionForBufferPosition(bufferPosition) - screenPositionForPixelPosition: (pixelPosition) -> @displayBuffer.screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange) - scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) - - scrollToScreenPosition: (screenPosition, options) -> @displayBuffer.scrollToScreenPosition(screenPosition, options) - - scrollToBufferPosition: (bufferPosition, options) -> @displayBuffer.scrollToBufferPosition(bufferPosition, options) - - horizontallyScrollable: -> @displayBuffer.horizontallyScrollable() - - verticallyScrollable: -> @displayBuffer.verticallyScrollable() - - getHorizontalScrollbarHeight: -> @displayBuffer.getHorizontalScrollbarHeight() - setHorizontalScrollbarHeight: (height) -> @displayBuffer.setHorizontalScrollbarHeight(height) - - getVerticalScrollbarWidth: -> @displayBuffer.getVerticalScrollbarWidth() - setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width) - # Deprecated: Call {::joinLines} instead. joinLine: -> deprecate("Use Editor::joinLines() instead") diff --git a/src/fold.coffee b/src/fold.coffee index 4b61d24af..617d590ce 100644 --- a/src/fold.coffee +++ b/src/fold.coffee @@ -14,8 +14,8 @@ class Fold @id = @marker.id @displayBuffer.foldsByMarkerId[@marker.id] = this @updateDisplayBuffer() - @marker.on 'destroyed', => @destroyed() - @marker.on 'changed', ({isValid}) => @destroy() unless isValid + @marker.onDidDestroy => @destroyed() + @marker.onDidChange ({isValid}) => @destroy() unless isValid # Returns whether this fold is contained within another fold isInsideLargerFold: -> diff --git a/src/git.coffee b/src/git.coffee index 7cf4014a9..78916d343 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -1,9 +1,12 @@ {basename, join} = require 'path' _ = require 'underscore-plus' -{Emitter, Subscriber} = require 'emissary' +{Subscriber} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' fs = require 'fs-plus' GitUtils = require 'git-utils' +{deprecate} = require 'grim' Task = require './task' @@ -20,28 +23,34 @@ Task = require './task' # For a repository with submodules this would have the following outcome: # # ```coffee -# repo = atom.project.getRepo() -# repo.getShortHead() # 'master' -# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' +# repo = atom.project.getRepo() +# repo.getShortHead() # 'master' +# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' # ``` # # ## Examples # +# ### Logging the URL of the origin remote +# # ```coffee -# git = atom.project.getRepo() -# console.log git.getOriginUrl() +# git = atom.project.getRepo() +# console.log git.getOriginUrl() # ``` # -# ## Requiring in packages +# ### Requiring in packages # # ```coffee -# {Git} = require 'atom' +# {Git} = require 'atom' # ``` module.exports = class Git - Emitter.includeInto(this) + EmitterMixin.includeInto(this) Subscriber.includeInto(this) + ### + Section: Class Methods + ### + # Public: Creates a new Git instance. # # * `path` The {String} path to the Git repository to open. @@ -64,7 +73,12 @@ class Git else false + ### + Section: Construction + ### + constructor: (path, options={}) -> + @emitter = new Emitter @repo = GitUtils.open(path) unless @repo? throw new Error("No Git repository found searching path: #{path}") @@ -86,12 +100,59 @@ class Git if @project? @subscribe @project.eachBuffer (buffer) => @subscribeToBuffer(buffer) + ### + Section: Event Subscription + ### + + # Essential: 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. + # + # * `callback` {Function} + # * `event` {Object} + # * `path` {String} the old parameters the decoration used to have + # * `pathStatus` {Number} representing the status. This value can be passed to + # {::isStatusModified} or {::isStatusNew} to get more information. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatus: (callback) -> + @emitter.on 'did-change-status', callback + + # Essential: 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. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatuses: (callback) -> + @emitter.on 'did-change-statuses', callback + + on: (eventName) -> + switch eventName + when 'status-changed' + deprecate 'Use Git::onDidChangeStatus instead' + when 'statuses-changed' + deprecate 'Use Git::onDidChangeStatuses instead' + else + deprecate 'Git::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + ### + Section: Instance Methods + ### + # Subscribes to buffer events. subscribeToBuffer: (buffer) -> - @subscribe buffer, 'saved reloaded path-changed', => + getBufferPathStatus = => if path = buffer.getPath() @getPathStatus(path) - @subscribe buffer, 'destroyed', => @unsubscribe(buffer) + + @subscribe buffer.onDidSave(getBufferPathStatus) + @subscribe buffer.onDidReload(getBufferPathStatus) + @subscribe buffer.onDidChangePath(getBufferPathStatus) + @subscribe buffer.onDidDestroy => @unsubscribe(buffer) # Subscribes to editor view event. checkoutHeadForEditor: (editor) -> @@ -165,6 +226,8 @@ class Git 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? @@ -385,4 +448,6 @@ class Git for submodulePath, submoduleRepo of @getRepo().submodules submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} - @emit 'statuses-changed' unless statusesUnchanged + unless statusesUnchanged + @emit 'statuses-changed' + @emitter.emit 'did-change-statuses' diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index e9eeddaf9..c6cf62490 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -21,7 +21,7 @@ GutterComponent = React.createClass if gutterBackgroundColor isnt 'rbga(0, 0, 0, 0)' backgroundColor = gutterBackgroundColor - div className: 'gutter', onClick: @onClick, onMouseDown: onMouseDown, + div className: 'gutter', onClick: @onClick, onMouseDown: @onMouseDown, div className: 'line-numbers', ref: 'lineNumbers', style: height: Math.max(scrollHeight, scrollViewHeight) WebkitTransform: @getTransform() @@ -215,6 +215,14 @@ GutterComponent = React.createClass lineNumberNodeForScreenRow: (screenRow) -> @lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]] + onMouseDown: (event) -> + {editor} = @props + {target} = event + lineNumber = target.parentNode + + unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') + @props.onMouseDown(event) + onClick: (event) -> {editor} = @props {target} = event diff --git a/src/highlight-component.coffee b/src/highlight-component.coffee index a26cb528b..b33a41974 100644 --- a/src/highlight-component.coffee +++ b/src/highlight-component.coffee @@ -22,11 +22,12 @@ HighlightComponent = React.createClass {editor, decoration} = @props if decoration.id? @decoration = editor.decorationForId(decoration.id) - @decoration.on 'flash', @startFlashAnimation + @decorationDisposable = @decoration.onDidFlash @startFlashAnimation @startFlashAnimation() componentWillUnmount: -> - @decoration?.off 'flash', @startFlashAnimation + @decorationDisposable?.dispose() + @decorationDisposable = null startFlashAnimation: -> return unless flash = @decoration.consumeNextFlash() diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index a4153de92..754bd502a 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -4,9 +4,13 @@ KeymapManager = require 'atom-keymap' CSON = require 'season' {jQuery} = require 'space-pen' +KeymapManager::onDidLoadBundledKeymaps = (callback) -> + @emitter.on 'did-load-bundled-keymaps', callback + KeymapManager::loadBundledKeymaps = -> @loadKeymap(path.join(@resourcePath, 'keymaps')) - @emit('bundled-keymaps-loaded') + @emit 'bundled-keymaps-loaded' + @emitter.emit 'did-load-bundled-keymaps' KeymapManager::getUserKeymapPath = -> if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap')) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index b9f64a2fc..da5ae6507 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -14,8 +14,8 @@ class MenuManager constructor: ({@resourcePath}) -> @pendingUpdateOperation = null @template = [] - atom.keymaps.on 'bundled-keymaps-loaded', => @loadPlatformItems() - atom.packages.on 'activated', => @sortPackagesMenu() + atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() + atom.packages.onDidActivateAll => @sortPackagesMenu() # Public: Adds the given items to the application menu. # diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 4935804dd..f95073e10 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -1,14 +1,16 @@ path = require 'path' _ = require 'underscore-plus' -{Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' fs = require 'fs-plus' Q = require 'q' +{deprecate} = require 'grim' Package = require './package' ThemePackage = require './theme-package' -# Public: Package manager for coordinating the lifecycle of Atom packages. +# Extended: Package manager for coordinating the lifecycle of Atom packages. # # An instance of this class is always available as the `atom.packages` global. # @@ -25,9 +27,10 @@ ThemePackage = require './theme-package' # settings and also by calling `enablePackage()/disablePackage()`. module.exports = class PackageManager - Emitter.includeInto(this) + EmitterMixin.includeInto(this) constructor: ({configDirPath, devMode, safeMode, @resourcePath}) -> + @emitter = new Emitter @packageDirPaths = [] unless safeMode if devMode @@ -41,6 +44,40 @@ class PackageManager @packageActivators = [] @registerPackageActivator(this, ['atom', 'textmate']) + ### + Section: Event Subscription + ### + + # Essential: Invoke the given callback when all packages have been activated. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidLoadAll: (callback) -> + @emitter.on 'did-load-all', callback + + # Essential: Invoke the given callback when all packages have been activated. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivateAll: (callback) -> + @emitter.on 'did-activate-all', callback + + on: (eventName) -> + switch eventName + when 'loaded' + deprecate 'Use PackageManager::onDidLoadAll instead' + when 'activated' + deprecate 'Use PackageManager::onDidActivateAll instead' + else + deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + ### + Section: Instance Methods + ### + # Extended: Get the path to the apm command. # # Return a {String} file path to apm. @@ -83,6 +120,7 @@ class PackageManager packages = @getLoadedPackagesForTypes(types) activator.activatePackages(packages) @emit 'activated' + @emitter.emit 'did-activate-all' # another type of package manager can handle other package types. # See ThemeManager @@ -159,8 +197,11 @@ class PackageManager packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath) @loadPackage(packagePath) for packagePath in packagePaths @emit 'loaded' + @emitter.emit 'did-load-all' loadPackage: (nameOrPath) -> + return pack if pack = @getLoadedPackage(nameOrPath) + if packagePath = @resolvePackagePath(nameOrPath) name = path.basename(nameOrPath) return pack if pack = @getLoadedPackage(name) diff --git a/src/package.coffee b/src/package.coffee index 707e7b591..c005ae664 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -4,8 +4,10 @@ _ = require 'underscore-plus' async = require 'async' CSON = require 'season' fs = require 'fs-plus' -{Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' Q = require 'q' +{deprecate} = require 'grim' $ = null # Defer require in case this is in the window-less browser process ScopedProperties = require './scoped-properties' @@ -14,7 +16,7 @@ ScopedProperties = require './scoped-properties' # stylesheets, keymaps, grammar, editor properties, and menus. module.exports = class Package - Emitter.includeInto(this) + EmitterMixin.includeInto(this) @stylesheetsDir: 'stylesheets' @@ -37,11 +39,40 @@ class Package resolvedMainModulePath: false mainModule: null + ### + Section: Construction + ### + constructor: (@path, @metadata) -> + @emitter = new Emitter @metadata ?= Package.loadMetadata(@path) @name = @metadata?.name ? path.basename(@path) @reset() + ### + Section: Event Subscription + ### + + # Essential: Invoke the given callback when all packages have been activated. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDeactivate: (callback) -> + @emitter.on 'did-deactivate', callback + + on: (eventName) -> + switch eventName + when 'deactivated' + deprecate 'Use Package::onDidDeactivate instead' + else + deprecate 'Package::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + ### + Section: Instance Methods + ### + enable: -> atom.config.removeAtKeyPath('core.disabledPackages', @name) @@ -242,8 +273,13 @@ class Package @unsubscribeFromActivationEvents() @deactivateResources() @deactivateConfig() - @mainModule?.deactivate?() if @mainActivated - @emit('deactivated') + if @mainActivated + try + @mainModule?.deactivate?() + catch e + console.error "Error deactivating package '#{@name}'", e.stack + @emit 'deactivated' + @emitter.emit 'did-deactivate' deactivateConfig: -> @mainModule?.deactivateConfig?() diff --git a/src/pane-axis-view.coffee b/src/pane-axis-view.coffee index 3039600c0..0018ee6b1 100644 --- a/src/pane-axis-view.coffee +++ b/src/pane-axis-view.coffee @@ -1,11 +1,17 @@ +{CompositeDisposable} = require 'event-kit' {View} = require './space-pen-extensions' PaneView = null module.exports = class PaneAxisView extends View initialize: (@model) -> - @onChildAdded(child) for child in @model.children - @subscribe @model.children, 'changed', @onChildrenChanged + @subscriptions = new CompositeDisposable + + @onChildAdded({child, index}) for child, index in @model.getChildren() + + @subscriptions.add @model.onDidAddChild(@onChildAdded) + @subscriptions.add @model.onDidRemoveChild(@onChildRemoved) + @subscriptions.add @model.onDidReplaceChild(@onChildReplaced) afterAttach: -> @container = @closest('.panes').view() @@ -14,19 +20,22 @@ class PaneAxisView extends View viewClass = model.getViewClass() model._view ?= new viewClass(model) - onChildrenChanged: ({index, removedValues, insertedValues}) => + onChildReplaced: ({index, oldChild, newChild}) => focusedElement = document.activeElement if @hasFocus() - @onChildRemoved(child, index) for child in removedValues - @onChildAdded(child, index + i) for child, i in insertedValues + @onChildRemoved({child: oldChild, index}) + @onChildAdded({child: newChild, index}) focusedElement?.focus() if document.activeElement is document.body - onChildAdded: (child, index) => + onChildAdded: ({child, index}) => view = @viewForModel(child) @insertAt(index, view) - onChildRemoved: (child) => + onChildRemoved: ({child}) => view = @viewForModel(child) view.detach() PaneView ?= require './pane-view' if view instanceof PaneView and view.model.isDestroyed() @container?.trigger 'pane:removed', [view] + + beforeRemove: -> + @subscriptions.dispose() diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index 39657fcf7..9ef95bc8c 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -1,4 +1,5 @@ -{Model, Sequence} = require 'theorist' +{Model} = require 'theorist' +{Emitter, CompositeDisposable} = require 'event-kit' {flatten} = require 'underscore-plus' Serializable = require 'serializable' @@ -10,18 +11,17 @@ class PaneAxis extends Model atom.deserializers.add(this) Serializable.includeInto(this) + parent: null + container: null + orientation: null + constructor: ({@container, @orientation, children}) -> - @children = Sequence.fromArray(children ? []) - - @subscribe @children.onEach (child) => - child.parent = this - child.container = @container - @subscribe child, 'destroyed', => @removeChild(child) - - @subscribe @children.onRemoval (child) => @unsubscribe(child) - - @when @children.$length.becomesLessThan(2), 'reparentLastChild' - @when @children.$length.becomesLessThan(1), 'destroy' + @emitter = new Emitter + @subscriptionsByChild = new WeakMap + @subscriptions = new CompositeDisposable + @children = [] + if children? + @addChild(child) for child in children deserializeParams: (params) -> {container} = params @@ -32,35 +32,93 @@ class PaneAxis extends Model children: @children.map (child) -> child.serialize() orientation: @orientation + getParent: -> @parent + + setParent: (@parent) -> @parent + + getContainer: -> @container + + setContainer: (@container) -> @container + getViewClass: -> if @orientation is 'vertical' PaneColumnView ?= require './pane-column-view' else PaneRowView ?= require './pane-row-view' + getChildren: -> @children.slice() + getPanes: -> flatten(@children.map (child) -> child.getPanes()) - addChild: (child, index=@children.length) -> - @children.splice(index, 0, child) + getItems: -> + flatten(@children.map (child) -> child.getItems()) - removeChild: (child) -> + onDidAddChild: (fn) -> + @emitter.on 'did-add-child', fn + + onDidRemoveChild: (fn) -> + @emitter.on 'did-remove-child', fn + + onDidReplaceChild: (fn) -> + @emitter.on 'did-replace-child', fn + + onDidDestroy: (fn) -> + @emitter.on 'did-destroy', fn + + addChild: (child, index=@children.length) -> + child.setParent(this) + child.setContainer(@container) + + @subscribeToChild(child) + + @children.splice(index, 0, child) + @emitter.emit 'did-add-child', {child, index} + + removeChild: (child, replacing=false) -> index = @children.indexOf(child) throw new Error("Removing non-existent child") if index is -1 + + @unsubscribeFromChild(child) + @children.splice(index, 1) + @emitter.emit 'did-remove-child', {child, index} + @reparentLastChild() if not replacing and @children.length < 2 replaceChild: (oldChild, newChild) -> + @unsubscribeFromChild(oldChild) + @subscribeToChild(newChild) + + newChild.setParent(this) + newChild.setContainer(@container) + index = @children.indexOf(oldChild) - throw new Error("Replacing non-existent child") if index is -1 @children.splice(index, 1, newChild) + @emitter.emit 'did-replace-child', {oldChild, newChild, index} insertChildBefore: (currentChild, newChild) -> index = @children.indexOf(currentChild) - @children.splice(index, 0, newChild) + @addChild(newChild, index) insertChildAfter: (currentChild, newChild) -> index = @children.indexOf(currentChild) - @children.splice(index + 1, 0, newChild) + @addChild(newChild, index + 1) reparentLastChild: -> @parent.replaceChild(this, @children[0]) + @destroy() + + subscribeToChild: (child) -> + subscription = child.onDidDestroy => @removeChild(child) + @subscriptionsByChild.set(child, subscription) + @subscriptions.add(subscription) + + unsubscribeFromChild: (child) -> + subscription = @subscriptionsByChild.get(child) + @subscriptions.remove(subscription) + subscription.dispose() + + destroyed: -> + @subscriptions.dispose() + @emitter.emit 'did-destroy' + @emitter.dispose() diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee index 77be49420..09a890d74 100644 --- a/src/pane-container-view.coffee +++ b/src/pane-container-view.coffee @@ -1,5 +1,6 @@ {deprecate} = require 'grim' Delegator = require 'delegato' +{CompositeDisposable} = require 'event-kit' {$, View} = require './space-pen-extensions' PaneView = require './pane-view' PaneContainer = require './pane-container' @@ -15,13 +16,15 @@ class PaneContainerView extends View @div class: 'panes' initialize: (params) -> + @subscriptions = new CompositeDisposable + if params instanceof PaneContainer @model = params else @model = new PaneContainer({root: params?.root?.model}) - @subscribe @model.$root, @onRootChanged - @subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged + @subscriptions.add @model.observeRoot(@onRootChanged) + @subscriptions.add @model.onDidChangeActivePaneItem(@onActivePaneItemChanged) viewForModel: (model) -> if model? @@ -88,7 +91,7 @@ class PaneContainerView extends View @viewForModel(@model.activePane) getActivePaneItem: -> - @model.activePaneItem + @model.getActivePaneItem() getActiveView: -> @getActivePaneView()?.activeView @@ -153,3 +156,6 @@ class PaneContainerView extends View getPanes: -> deprecate("Use PaneContainerView::getPaneViews() instead") @getPaneViews() + + beforeRemove: -> + @subscriptions.dispose() diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 07083263a..14eeae077 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -1,5 +1,6 @@ -{find} = require 'underscore-plus' +{find, flatten} = require 'underscore-plus' {Model} = require 'theorist' +{Emitter, CompositeDisposable} = require 'event-kit' Serializable = require 'serializable' Pane = require './pane' @@ -11,10 +12,9 @@ class PaneContainer extends Model @version: 1 @properties - root: -> new Pane activePane: null - previousRoot: null + root: null @behavior 'activePaneItem', -> @$activePane @@ -23,9 +23,16 @@ class PaneContainer extends Model constructor: (params) -> super - @subscribe @$root, @onRootChanged + + @emitter = new Emitter + @subscriptions = new CompositeDisposable + + @setRoot(params?.root ? new Pane) @destroyEmptyPanes() if params?.destroyEmptyPanes + @monitorActivePaneItem() + @monitorPaneItems() + deserializeParams: (params) -> params.root = atom.deserializers.deserialize(params.root, container: this) params.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes') @@ -36,16 +43,75 @@ class PaneContainer extends Model root: @root?.serialize() activePaneId: @activePane.id + onDidChangeRoot: (fn) -> + @emitter.on 'did-change-root', fn + + observeRoot: (fn) -> + fn(@getRoot()) + @onDidChangeRoot(fn) + + onDidAddPane: (fn) -> + @emitter.on 'did-add-pane', fn + + observePanes: (fn) -> + fn(pane) for pane in @getPanes() + @onDidAddPane ({pane}) -> fn(pane) + + onDidChangeActivePane: (fn) -> + @emitter.on 'did-change-active-pane', fn + + observeActivePane: (fn) -> + fn(@getActivePane()) + @onDidChangeActivePane(fn) + + onDidAddPaneItem: (fn) -> + @emitter.on 'did-add-pane-item', fn + + observePaneItems: (fn) -> + fn(item) for item in @getPaneItems() + @onDidAddPaneItem ({item}) -> fn(item) + + onDidChangeActivePaneItem: (fn) -> + @emitter.on 'did-change-active-pane-item', fn + + observeActivePaneItem: (fn) -> + fn(@getActivePaneItem()) + @onDidChangeActivePaneItem(fn) + + onDidDestroyPaneItem: (fn) -> + @emitter.on 'did-destroy-pane-item', fn + + getRoot: -> @root + + setRoot: (@root) -> + @root.setParent(this) + @root.setContainer(this) + @emitter.emit 'did-change-root', @root + if not @getActivePane()? and @root instanceof Pane + @setActivePane(@root) + replaceChild: (oldChild, newChild) -> throw new Error("Replacing non-existent child") if oldChild isnt @root - @root = newChild + @setRoot(newChild) getPanes: -> - @root?.getPanes() ? [] + @getRoot().getPanes() + + getPaneItems: -> + @getRoot().getItems() getActivePane: -> @activePane + setActivePane: (activePane) -> + if activePane isnt @activePane + @activePane = activePane + @emitter.emit 'did-change-active-pane', @activePane + @activePane + + getActivePaneItem: -> + @getActivePane().getActiveItem() + paneForUri: (uri) -> find @getPanes(), (pane) -> pane.itemForUri(uri)? @@ -73,26 +139,37 @@ class PaneContainer extends Model else false - onRootChanged: (root) => - @unsubscribe(@previousRoot) if @previousRoot? - @previousRoot = root - - unless root? - @activePane = null - return - - root.parent = this - root.container = this - - - @activePane ?= root if root instanceof Pane - destroyEmptyPanes: -> pane.destroy() for pane in @getPanes() when pane.items.length is 0 - itemDestroyed: (item) -> - @emit 'item-destroyed', item + paneItemDestroyed: (item) -> + @emitter.emit 'did-destroy-pane-item', item + + didAddPane: (pane) -> + @emitter.emit 'did-add-pane', pane # Called by Model superclass when destroyed destroyed: -> pane.destroy() for pane in @getPanes() + @subscriptions.dispose() + @emitter.dispose() + + monitorActivePaneItem: -> + childSubscription = null + @subscriptions.add @observeActivePane (activePane) => + if childSubscription? + @subscriptions.remove(childSubscription) + childSubscription.dispose() + + childSubscription = activePane.observeActiveItem (activeItem) => + @emitter.emit 'did-change-active-pane-item', activeItem + + @subscriptions.add(childSubscription) + + monitorPaneItems: -> + @subscriptions.add @observePanes (pane) => + for item, index in pane.getItems() + @emitter.emit 'did-add-pane-item', {item, pane, index} + + pane.onDidAddItem ({item, index}) => + @emitter.emit 'did-add-pane-item', {item, pane, index} diff --git a/src/pane-view.coffee b/src/pane-view.coffee index 0d641582e..21f4c8cfa 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -1,6 +1,7 @@ {$, View} = require './space-pen-extensions' Delegator = require 'delegato' {deprecate} = require 'grim' +{CompositeDisposable} = require 'event-kit' PropertyAccessors = require 'property-accessors' Pane = require './pane' @@ -33,6 +34,8 @@ class PaneView extends View previousActiveItem: null initialize: (args...) -> + @subscriptions = new CompositeDisposable + if args[0] instanceof Pane @model = args[0] else @@ -44,13 +47,13 @@ class PaneView extends View @handleEvents() handleEvents: -> - @subscribe @model.$activeItem, @onActiveItemChanged - @subscribe @model, 'item-added', @onItemAdded - @subscribe @model, 'item-removed', @onItemRemoved - @subscribe @model, 'item-moved', @onItemMoved - @subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed - @subscribe @model, 'activated', @onActivated - @subscribe @model.$active, @onActiveStatusChanged + @subscriptions.add @model.observeActiveItem(@onActiveItemChanged) + @subscriptions.add @model.onDidAddItem(@onItemAdded) + @subscriptions.add @model.onDidRemoveItem(@onItemRemoved) + @subscriptions.add @model.onDidMoveItem(@onItemMoved) + @subscriptions.add @model.onWillDestroyItem(@onBeforeItemDestroyed) + @subscriptions.add @model.onDidActivate(@onActivated) + @subscriptions.add @model.observeActive(@onActiveStatusChanged) @subscribe this, 'focusin', => @model.focus() @subscribe this, 'focusout', => @model.blur() @@ -72,15 +75,18 @@ class PaneView extends View @command 'pane:show-item-8', => @activateItemAtIndex(7) @command 'pane:show-item-9', => @activateItemAtIndex(8) - @command 'pane:split-left', => @splitLeft(@copyActiveItem()) - @command 'pane:split-right', => @splitRight(@copyActiveItem()) - @command 'pane:split-up', => @splitUp(@copyActiveItem()) - @command 'pane:split-down', => @splitDown(@copyActiveItem()) + @command 'pane:split-left', => @model.splitLeft(copyActiveItem: true) + @command 'pane:split-right', => @model.splitRight(copyActiveItem: true) + @command 'pane:split-up', => @model.splitUp(copyActiveItem: true) + @command 'pane:split-down', => @model.splitDown(copyActiveItem: true) @command 'pane:close', => @model.destroyItems() @model.destroy() @command 'pane:close-other-items', => @destroyInactiveItems() + # Essential: Returns the {Pane} model underlying this pane view + getModel: -> @model + # Deprecated: Use ::destroyItem removeItem: (item) -> deprecate("Use PaneView::destroyItem instead") @@ -141,6 +147,9 @@ class PaneView extends View @activeItem onActiveItemChanged: (item) => + @activeItemDisposables.dispose() if @activeItemDisposables? + @activeItemDisposables = new CompositeDisposable() + if @previousActiveItem?.off? @previousActiveItem.off 'title-changed', @activeItemTitleChanged @previousActiveItem.off 'modified-status-changed', @activeItemModifiedChanged @@ -148,22 +157,36 @@ class PaneView extends View return unless item? - hasFocus = @hasFocus() - if item.on? - item.on 'title-changed', @activeItemTitleChanged - item.on 'modified-status-changed', @activeItemModifiedChanged + if item.onDidChangeTitle? + disposable = item.onDidChangeTitle(@activeItemTitleChanged) + 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.' + disposable = item.on('title-changed', @activeItemTitleChanged) + @activeItemDisposables.add(disposable) if disposable?.dispose? + + if item.onDidChangeModified? + disposable = item.onDidChangeModified(@activeItemModifiedChanged) + deprecate 'Please return a Disposable object from your ::onDidChangeModified 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 support modified behavior, please implement a ::onDidChangeModified() method.' + item.on('modified-status-changed', @activeItemModifiedChanged) + @activeItemDisposables.add(disposable) if disposable?.dispose? + view = @viewForItem(item) otherView.hide() for otherView in @itemViews.children().not(view).views() @itemViews.append(view) unless view.parent().is(@itemViews) view.show() if @attached - view.focus() if hasFocus + view.focus() if @hasFocus() @trigger 'pane:active-item-changed', [item] - onItemAdded: (item, index) => + onItemAdded: ({item, index}) => @trigger 'pane:item-added', [item, index] - onItemRemoved: (item, index, destroyed) => + onItemRemoved: ({item, index, destroyed}) => if item instanceof $ viewToRemove = item else if viewToRemove = @viewsByItem.get(item) @@ -177,7 +200,7 @@ class PaneView extends View @trigger 'pane:item-removed', [item, index] - onItemMoved: (item, newIndex) => + onItemMoved: ({item, newIndex}) => @trigger 'pane:item-moved', [item, newIndex] onBeforeItemDestroyed: (item) => @@ -219,6 +242,7 @@ class PaneView extends View @closest('.panes').view() beforeRemove: -> + @subscriptions.dispose() @model.destroy() unless @model.isDestroyed() remove: (selector, keepData) -> diff --git a/src/pane.coffee b/src/pane.coffee index 678e2156e..c6351f656 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,47 +1,16 @@ {find, compact, extend, last} = require 'underscore-plus' -{Model, Sequence} = require 'theorist' +{Model} = require 'theorist' +{Emitter} = require 'event-kit' Serializable = require 'serializable' +Grim = require 'grim' PaneAxis = require './pane-axis' Editor = require './editor' PaneView = null -# Extended: A container for multiple items, one of which is *active* at a given -# time. With the default packages, a tab is displayed for each item and the -# active item's view is displayed. -# -# ## Events -# ### activated -# -# Extended: Emit when this pane as been activated -# -# ### item-added -# -# Extended: Emit when an item was added to the pane -# -# * `item` The pane item that has been added -# * `index` {Number} Index in the pane -# -# ### before-item-destroyed -# -# Extended: Emit before the item is destroyed -# -# * `item` The pane item that will be destoryed -# -# ### item-removed -# -# Extended: Emit when the item was removed from the pane -# -# * `item` The pane item that was removed -# * `index` {Number} Index in the pane -# * `destroying` {Boolean} `true` when the item is being removed because of destruction -# -# ### item-moved -# -# Extended: Emit when an item was moved within the pane -# -# * `item` The pane item that was moved -# * `newIndex` {Number} Index that the item was moved to -# +# Extended: A container for presenting content in the center of the workspace. +# Panes can contain multiple items, one of which is *active* at a given time. +# The view corresponding to the active item is displayed in the interface. In +# the default configuration, tabs are also displayed for each item. module.exports = class Pane extends Model atom.deserializers.add(this) @@ -64,15 +33,11 @@ class Pane extends Model constructor: (params) -> super - @items = Sequence.fromArray(compact(params?.items ? [])) - @activeItem ?= @items[0] + @emitter = new Emitter + @items = [] - @subscribe @items.onEach (item) => - if typeof item.on is 'function' - @subscribe item, 'destroyed', => @removeItem(item, true) - - @subscribe @items.onRemoval (item, index) => - @unsubscribe item if typeof item.on is 'function' + @addItems(compact(params?.items ? [])) + @setActiveItem(@items[0]) unless @getActiveItem()? # Called by the Serializable mixin during serialization. serializeParams: -> @@ -91,7 +56,174 @@ class Pane extends Model # Called by the view layer to construct a view for this model. getViewClass: -> PaneView ?= require './pane-view' - isActive: -> @active + getParent: -> @parent + + setParent: (@parent) -> @parent + + getContainer: -> @container + + setContainer: (container) -> + container.didAddPane({pane: this}) unless container is @container + @container = container + + ### + Section: Event Subscription + ### + + # Public: Invoke the given callback when the pane is activated. + # + # The given callback will be invoked whenever {::activate} is called on the + # pane, even if it is already active at the time. + # + # * `callback` {Function} to be called when the pane is activated. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivate: (callback) -> + @emitter.on 'did-activate', callback + + # Public: Invoke the given callback when the pane is destroyed. + # + # * `callback` {Function} to be called when the pane is destroyed. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + # Public: Invoke the given callback when the value of the {::isActive} + # property changes. + # + # * `callback` {Function} to be called when the value of the {::isActive} + # property changes. + # * `active` {Boolean} indicating whether the pane is active. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActive: (callback) -> + @container.onDidChangeActivePane (activePane) => + callback(this is activePane) + + # Public: Invoke the given callback with the current and future values of the + # {::isActive} property. + # + # * `callback` {Function} to be called with the current and future values of + # the {::isActive} property. + # * `active` {Boolean} indicating whether the pane is active. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActive: (callback) -> + callback(@isActive()) + @onDidChangeActive(callback) + + # Public: Invoke the given callback when an item is added to the pane. + # + # * `callback` {Function} to be called with when items are added. + # * `event` {Object} with the following keys: + # * `item` The added pane item. + # * `index` {Number} indicating where the item is located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddItem: (callback) -> + @emitter.on 'did-add-item', callback + + # Public: Invoke the given callback when an item is removed from the pane. + # + # * `callback` {Function} to be called with when items are removed. + # * `event` {Object} with the following keys: + # * `item` The removed pane item. + # * `index` {Number} indicating where the item was located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveItem: (callback) -> + @emitter.on 'did-remove-item', callback + + # Public: Invoke the given callback when an item is moved within the pane. + # + # * `callback` {Function} to be called with when items are moved. + # * `event` {Object} with the following keys: + # * `item` The removed pane item. + # * `oldIndex` {Number} indicating where the item was located. + # * `newIndex` {Number} indicating where the item is now located. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidMoveItem: (callback) -> + @emitter.on 'did-move-item', callback + + # Public: Invoke the given callback with all current and future items. + # + # * `callback` {Function} to be called with current and future items. + # * `item` An item that is present in {::getItems} at the time of + # subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeItems: (callback) -> + callback(item) for item in @getItems() + @onDidAddItem ({item}) -> callback(item) + + # Public: Invoke the given callback when the value of {::getActiveItem} + # changes. + # + # * `callback` {Function} to be called with when the active item changes. + # * `activeItem` The current active item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActiveItem: (callback) -> + @emitter.on 'did-change-active-item', callback + + # Public: Invoke the given callback with the current and future values of + # {::getActiveItem}. + # + # * `callback` {Function} to be called with the current and future active + # items. + # * `activeItem` The current active item. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActiveItem: (callback) -> + callback(@getActiveItem()) + @onDidChangeActiveItem(callback) + + # Public: Invoke the given callback before items are destroyed. + # + # * `callback` {Function} to be called before items are destroyed. + # * `event` {Object} with the following keys: + # * `item` The item that will be destroyed. + # * `index` The location of the item. + # + # Returns a {Disposable} on which `.dispose()` can be called to + # unsubscribe. + onWillDestroyItem: (callback) -> + @emitter.on 'will-destroy-item', callback + + on: (eventName) -> + switch eventName + when 'activated' + Grim.deprecate("Use Pane::onDidActivate instead") + when 'destroyed' + Grim.deprecate("Use Pane::onDidDestroy instead") + when 'item-added' + Grim.deprecate("Use Pane::onDidAddItem instead") + when 'item-removed' + Grim.deprecate("Use Pane::onDidRemoveItem instead") + when 'item-moved' + Grim.deprecate("Use Pane::onDidMoveItem instead") + when 'before-item-destroyed' + Grim.deprecate("Use Pane::onWillDestroyItem instead") + else + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + super + + behavior: (behaviorName) -> + switch behaviorName + when 'active' + Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.") + when 'container' + Grim.deprecate("The $container behavior property is deprecated.") + when 'activeItem' + Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.") + when 'focused' + Grim.deprecate("The $focused behavior property is deprecated.") + else + Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.") + + super # Called by the view layer to indicate that the pane has gained focus. focus: -> @@ -103,14 +235,12 @@ class Pane extends Model @focused = false true # if this is called from an event handler, don't cancel it - # Public: Makes this pane the *active* pane, causing it to gain focus - # immediately. - activate: -> - @container?.activePane = this - @emit 'activated' - getPanes: -> [this] + ### + Section: Items + ### + # Public: Get the items in this pane. # # Returns an {Array} of items. @@ -120,15 +250,23 @@ class Pane extends Model # Public: Get the active pane item in this pane. # # Returns a pane item. - getActiveItem: -> + getActiveItem: -> @activeItem + + setActiveItem: (activeItem) -> + unless activeItem is @activeItem + @activeItem = activeItem + @emitter.emit 'did-change-active-item', @activeItem @activeItem - # Public: Returns an {Editor} if the pane item is an {Editor}, or null - # otherwise. + # Return an {Editor} if the pane item is an {Editor}, or null otherwise. getActiveEditor: -> @activeItem if @activeItem instanceof Editor - # Public: Returns the item at the specified index. + # Public: Return the item at the given index. + # + # * `index` {Number} + # + # Returns an item or `null` if no item exists at the given index. itemAtIndex: (index) -> @items[index] @@ -148,86 +286,115 @@ class Pane extends Model else @activateItemAtIndex(@items.length - 1) - # Returns the index of the current active item. + # Public: Get the index of the active item. + # + # Returns a {Number}. getActiveItemIndex: -> @items.indexOf(@activeItem) - # Makes the item at the given index active. + # Public: Activate the item at the given index. + # + # * `index` {Number} activateItemAtIndex: (index) -> @activateItem(@itemAtIndex(index)) - # Makes the given item active, adding the item if necessary. + # Public: Make the given item *active*, causing it to be displayed by + # the pane's view. activateItem: (item) -> if item? @addItem(item) - @activeItem = item + @setActiveItem(item) - # Public: Adds the item to the pane. + # Public: Add the given item to the pane. # - # * `item` The item to add. It can be a model with an associated view or a view. - # * `index` (optional) {Number} at which to add the item. If omitted, the item is - # added after the current active item. + # * `item` The item to add. It can be a model with an associated view or a + # view. + # * `index` (optional) {Number} indicating the index at which to add the item. + # If omitted, the item is added after the current active item. # - # Returns the added item + # Returns the added item. addItem: (item, index=@getActiveItemIndex() + 1) -> return if item in @items + if typeof item.on is 'function' + @subscribe item, 'destroyed', => @removeItem(item, true) + @items.splice(index, 0, item) @emit 'item-added', item, index - @activeItem ?= item + @emitter.emit 'did-add-item', {item, index} + @setActiveItem(item) unless @getActiveItem()? item - # Public: Adds the given items to the pane. + # Public: Add the given items to the pane. # - # * `items` An {Array} of items to add. Items can be models with associated - # views or views. Any items that are already present in items will - # not be added. - # * `index` (optional) {Number} index at which to add the item. If omitted, the item is - # added after the current active item. + # * `items` An {Array} of items to add. Items can be views or models with + # associated views. Any objects that are already present in the pane's + # current items will not be added again. + # * `index` (optional) {Number} index at which to add the items. If omitted, + # the item is # added after the current active item. # - # Returns an {Array} of the added items + # Returns an {Array} of added items. addItems: (items, index=@getActiveItemIndex() + 1) -> items = items.filter (item) => not (item in @items) @addItem(item, index + i) for item, i in items items - removeItem: (item, destroying) -> + removeItem: (item, destroyed=false) -> index = @items.indexOf(item) return if index is -1 + + if typeof item.on is 'function' + @unsubscribe item + if item is @activeItem if @items.length is 1 - @activeItem = undefined + @setActiveItem(undefined) else if index is 0 @activateNextItem() else @activatePreviousItem() @items.splice(index, 1) - @emit 'item-removed', item, index, destroying - @container?.itemDestroyed(item) if destroying + @emit 'item-removed', item, index, destroyed + @emitter.emit 'did-remove-item', {item, index, destroyed} + @container?.paneItemDestroyed(item) if destroyed @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes') - # Public: Moves the given item to the specified index. + # Public: Move the given item to the given index. + # + # * `item` The item to move. + # * `index` {Number} indicating the index to which to move the item. moveItem: (item, newIndex) -> oldIndex = @items.indexOf(item) @items.splice(oldIndex, 1) @items.splice(newIndex, 0, item) @emit 'item-moved', item, newIndex + @emitter.emit 'did-move-item', {item, oldIndex, newIndex} - # Public: Moves the given item to the given index at another pane. + # Public: Move the given item to the given index on another pane. + # + # * `item` The item to move. + # * `pane` {Pane} to which to move the item. + # * `index` {Number} indicating the index to which to move the item in the + # given pane. moveItemToPane: (item, pane, index) -> pane.addItem(item, index) @removeItem(item) - # Public: Destroys the currently active item and make the next item active. + # Public: Destroy the active item and activate the next item. destroyActiveItem: -> @destroyItem(@activeItem) false - # Public: Destroys the given item. If it is the active item, activate the next - # one. If this is the last item, also destroys the pane. + # Public: Destroy the given item. + # + # If the item is active, the next item will be activated. If the item is the + # last item, the pane will be destroyed if the `core.destroyEmptyPanes` config + # setting is `true`. destroyItem: (item) -> - if item? + index = @items.indexOf(item) + if index isnt -1 @emit 'before-item-destroyed', item + @emitter.emit 'will-destroy-item', {item, index} if @promptToSaveItem(item) @removeItem(item, true) item.destroy?() @@ -235,27 +402,14 @@ class Pane extends Model else false - # Public: Destroys all items and destroys the pane. + # Public: Destroy all items. destroyItems: -> @destroyItem(item) for item in @getItems() - # Public: Destroys all items but the active one. + # Public: Destroy all items except for the active item. destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem - destroy: -> - if @container?.isAlive() and @container.getPanes().length is 1 - @destroyItems() - else - super - - # Called by model superclass. - destroyed: -> - @container.activateNextPane() if @isActive() - item.destroy?() for item in @items.slice() - - # Public: Prompts the user to save the given item if it can be saved and is - # currently unsaved. promptToSaveItem: (item) -> return true unless item.shouldPromptToSave?() @@ -270,18 +424,23 @@ class Pane extends Model when 1 then false when 2 then true - # Public: Saves the active item. - saveActiveItem: -> - @saveItem(@activeItem) + # Public: Save the active item. + saveActiveItem: (nextAction) -> + @saveItem(@getActiveItem(), nextAction) - # Public: Saves the active item at a prompted-for location. - saveActiveItemAs: -> - @saveItemAs(@activeItem) + # Public: Prompt the user for a location and save the active item with the + # path they select. + # + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. + saveActiveItemAs: (nextAction) -> + @saveItemAs(@getActiveItem(), nextAction) - # Public: Saves the specified item. + # Public: Save the given item. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is saved. + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. saveItem: (item, nextAction) -> if item?.getUri?() item.save?() @@ -289,10 +448,12 @@ class Pane extends Model else @saveItemAs(item, nextAction) - # Public: Saves the given item at a prompted-for location. + # Public: Prompt the user for a location and save the active item with the + # path they select. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is saved. + # * `nextAction` (optional) {Function} which will be called after the item is + # successfully saved. saveItemAs: (item, nextAction) -> return unless item?.saveAs? @@ -302,17 +463,20 @@ class Pane extends Model item.saveAs(newItemPath) nextAction?() - # Public: Saves all items. + # Public: Save all items. saveItems: -> @saveItem(item) for item in @getItems() - # Public: Returns the first item that matches the given URI or undefined if + # Public: Return the first item that matches the given URI or undefined if # none exists. + # + # * `uri` {String} containing a URI. itemForUri: (uri) -> find @items, (item) -> item.getUri?() is uri - # Public: Activates the first item that matches the given URI. Returns a - # boolean indicating whether a matching item was found. + # Public: Activate the first item that matches the given URI. + # + # Returns a {Boolean} indicating whether an item matching the URI was found. activateItemForUri: (uri) -> if item = @itemForUri(uri) @activateItem(item) @@ -324,19 +488,57 @@ class Pane extends Model if @activeItem? @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) - # Public: Creates a new pane to the left of the receiver. + ### + Section: Lifecycle + ### + + # Public: Determine whether the pane is active. # - # * `params` {Object} with keys - # * `items` (optional) {Array} of items with which to construct the new pane. + # Returns a {Boolean}. + isActive: -> + @container?.getActivePane() is this + + # Public: Makes this pane the *active* pane, causing it to gain focus. + activate: -> + @container?.setActivePane(this) + @emit 'activated' + @emitter.emit 'did-activate' + + # Public: Close the pane and destroy all its items. + # + # If this is the last pane, all the items will be destroyed but the pane + # itself will not be destroyed. + destroy: -> + if @container?.isAlive() and @container.getPanes().length is 1 + @destroyItems() + else + super + + # Called by model superclass. + destroyed: -> + @container.activateNextPane() if @isActive() + @emitter.emit 'did-destroy' + item.destroy?() for item in @items.slice() + + ### + Section: Splitting + ### + + # Public: Create a new pane to the left of this pane. + # + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. + # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane # # Returns the new {Pane}. splitLeft: (params) -> @split('horizontal', 'before', params) - # Public: Creates a new pane to the right of the receiver. + # Public: Create a new pane to the right of this pane. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. + # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane # # Returns the new {Pane}. splitRight: (params) -> @@ -344,8 +546,9 @@ class Pane extends Model # Public: Creates a new pane above the receiver. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. + # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane # # Returns the new {Pane}. splitUp: (params) -> @@ -353,14 +556,19 @@ class Pane extends Model # Public: Creates a new pane below the receiver. # - # * `params` {Object} with keys: - # * `items` (optional) {Array} of items with which to construct the new pane. + # * `params` (optional) {Object} with the following keys: + # * `items` (optional) {Array} of items to add to the new pane. + # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane # # Returns the new {Pane}. splitDown: (params) -> @split('vertical', 'after', params) split: (orientation, side, params) -> + if params?.copyActiveItem + params.items ?= [] + params.items.push(@copyActiveItem()) + if @parent.orientation isnt orientation @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) diff --git a/src/project.coffee b/src/project.coffee index bf2adf2c3..371a7af3e 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -18,19 +18,6 @@ Git = require './git' # Extended: Represents a project that's opened in Atom. # # An instance of this class is always available as the `atom.project` global. -# -# ## Events -# -# ### path-changed -# -# Extended: Emit when the project's path has changed. Use {::getPath} to get the new path -# -# ### buffer-created -# -# Extended: Emit when a buffer is created. For example, when {::open} is called, this is fired. -# -# * `buffer` {TextBuffer} the new buffer that was created. -# module.exports = class Project extends Model atom.deserializers.add(this) @@ -49,7 +36,7 @@ class Project extends Model for buffer in @buffers do (buffer) => - buffer.once 'destroyed', => @removeBuffer(buffer) + buffer.onDidDestroy => @removeBuffer(buffer) @setPath(path) @@ -216,11 +203,11 @@ class Project extends Model addBuffer: (buffer, options={}) -> @addBufferAtIndex(buffer, @buffers.length, options) - buffer.once 'destroyed', => @removeBuffer(buffer) + buffer.onDidDestroy => @removeBuffer(buffer) addBufferAtIndex: (buffer, index, options={}) -> @buffers.splice(index, 0, buffer) - buffer.once 'destroyed', => @removeBuffer(buffer) + buffer.onDidDestroy => @removeBuffer(buffer) @emit 'buffer-created', buffer buffer diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index b9227df18..ae5ffdc93 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -51,7 +51,7 @@ class SelectListView extends View # This method can be overridden by subclasses but `super` should always # be called. initialize: -> - @filterEditorView.getEditor().getBuffer().on 'changed', => + @filterEditorView.getEditor().getBuffer().onDidChange => @schedulePopulateList() @filterEditorView.hiddenInput.on 'focusout', => @cancel() unless @cancelling diff --git a/src/selection.coffee b/src/selection.coffee index a0db277c1..c381d4994 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -1,21 +1,10 @@ {Point, Range} = require 'text-buffer' {Model} = require 'theorist' {pick} = require 'underscore-plus' +{Emitter} = require 'event-kit' +Grim = require 'grim' # Extended: Represents a selection in the {Editor}. -# -# ## Events -# -# ### screen-range-changed -# -# Extended: Emit when the selection was moved. -# -# * `screenRange` {Range} indicating the new screenrange -# -# ### destroyed -# -# Extended: Emit when the selection was destroyed -# module.exports = class Selection extends Model cursor: null @@ -26,15 +15,54 @@ class Selection extends Model needsAutoscroll: null constructor: ({@cursor, @marker, @editor, id}) -> + @emitter = new Emitter + @assignId(id) @cursor.selection = this @decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection') - @marker.on 'changed', => @screenRangeChanged() - @marker.on 'destroyed', => - @destroyed = true - @editor.removeSelection(this) - @emit 'destroyed' unless @editor.isDestroyed() + @marker.onDidChange => @screenRangeChanged() + @marker.onDidDestroy => + unless @editor.isDestroyed() + @destroyed = true + @editor.removeSelection(this) + @emit 'destroyed' + @emitter.emit 'did-destroy' + @emitter.dispose() + + ### + Section: Event Subscription + ### + + # Extended: Calls your `callback` when the selection was moved. + # + # * `callback` {Function} + # * `screenRange` {Range} indicating the new screenrange + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeRange: (callback) -> + @emitter.on 'did-change-range', callback + + # Extended: Calls your `callback` when the selection was destroyed + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy: (callback) -> + @emitter.on 'did-destroy', callback + + on: (eventName) -> + switch eventName + when 'screen-range-changed' + Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.") + when 'destroyed' + Grim.deprecate("Use Selection::onDidDestroy instead.") + + super + + ### + Section: Methods + ### destroy: -> @marker.destroy() @@ -198,18 +226,26 @@ class Selection extends Model @modifySelection => @cursor.setBufferPosition(position) # Public: Selects the text one position right of the cursor. - selectRight: -> - @modifySelection => @cursor.moveRight() + # + # * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectRight: (columnCount) -> + @modifySelection => @cursor.moveRight(columnCount) # Public: Selects the text one position left of the cursor. - selectLeft: -> - @modifySelection => @cursor.moveLeft() + # + # * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectLeft: (columnCount) -> + @modifySelection => @cursor.moveLeft(columnCount) # Public: Selects all the text one position above the cursor. + # + # * `rowCount` (optional) {Number} number of rows to select (default: 1) selectUp: (rowCount) -> @modifySelection => @cursor.moveUp(rowCount) # Public: Selects all the text one position below the cursor. + # + # * `rowCount` (optional) {Number} number of rows to select (default: 1) selectDown: (rowCount) -> @modifySelection => @cursor.moveDown(rowCount) @@ -657,6 +693,6 @@ class Selection extends Model @getBufferRange().compare(otherSelection.getBufferRange()) screenRangeChanged: -> - screenRange = @getScreenRange() - @emit 'screen-range-changed', screenRange - @editor.selectionScreenRangeChanged(this) + @emit 'screen-range-changed', @getScreenRange() + @emitter.emit 'did-change-range' + @editor.selectionRangeChanged(this) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index fa254f31f..23c630043 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -1,48 +1,93 @@ path = require 'path' _ = require 'underscore-plus' -{Emitter} = require 'emissary' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' +{File} = require 'pathwatcher' fs = require 'fs-plus' Q = require 'q' +{deprecate} = require 'grim' -{$} = require './space-pen-extensions' Package = require './package' -{File} = require 'pathwatcher' # Extended: Handles loading and activating available themes. # # An instance of this class is always available as the `atom.themes` global. -# -# ## Events -# -# ### reloaded -# -# Extended: Emit when all styles have been reloaded. -# -# ### stylesheet-added -# -# Extended: Emit when a stylesheet has been added. -# -# * `stylesheet` {StyleSheet} object that was removed -# -# ### stylesheet-removed -# -# Extended: Emit when a stylesheet has been removed. -# -# * `stylesheet` {StyleSheet} object that was removed -# -# ### stylesheets-changed -# -# Extended: Emit anytime any style sheet is added or removed from the editor -# module.exports = class ThemeManager - Emitter.includeInto(this) + EmitterMixin.includeInto(this) constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) -> + @emitter = new Emitter @lessCache = null + @initialLoadComplete = false @packageManager.registerPackageActivator(this, ['theme']) + ### + Section: Event Subscription + ### + + # Essential: Invoke `callback` when all styles have been reloaded. + # + # * `callback` {Function} + onDidReloadAll: (callback) -> + @emitter.on 'did-reload-all', callback + + # Essential: Invoke `callback` when a stylesheet has been added to the dom. + # + # * `callback` {Function} + # * `stylesheet` {StyleSheet} the style node + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddStylesheet: (callback) -> + @emitter.on 'did-add-stylesheet', callback + + # Essential: Invoke `callback` when a stylesheet has been removed from the dom. + # + # * `callback` {Function} + # * `stylesheet` {StyleSheet} the style node + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveStylesheet: (callback) -> + @emitter.on 'did-remove-stylesheet', callback + + # Essential: Invoke `callback` when a stylesheet has been updated. + # + # * `callback` {Function} + # * `stylesheet` {StyleSheet} the style node + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidUpdateStylesheet: (callback) -> + @emitter.on 'did-update-stylesheet', callback + + # Essential: Invoke `callback` when any stylesheet has been updated, added, or removed. + # + # * `callback` {Function} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStylesheets: (callback) -> + @emitter.on 'did-change-stylesheets', callback + + on: (eventName) -> + switch eventName + when 'reloaded' + deprecate 'Use ThemeManager::onDidReloadAll instead' + when 'stylesheet-added' + deprecate 'Use ThemeManager::onDidAddStylesheet instead' + when 'stylesheet-removed' + deprecate 'Use ThemeManager::onDidRemoveStylesheet instead' + when 'stylesheet-updated' + deprecate 'Use ThemeManager::onDidUpdateStylesheet instead' + when 'stylesheets-changed' + deprecate 'Use ThemeManager::onDidChangeStylesheets instead' + else + deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + ### + Section: Instance Methods + ### + getAvailableNames: -> # TODO: Maybe should change to list all the available themes out there? @getLoadedNames() @@ -63,7 +108,7 @@ class ThemeManager getLoadedThemes: -> pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() - activatePackages: (themePackages) -> @activateThemes() + activatePackages: -> @activateThemes() # Get the enabled theme names from the config. # @@ -121,7 +166,9 @@ class ThemeManager @refreshLessCache() # Update cache again now that @getActiveThemes() is populated @loadUserStylesheet() @reloadBaseStylesheets() + @initialLoadComplete = true @emit 'reloaded' + @emitter.emit 'did-reload-all' deferred.resolve() deferred.promise @@ -132,6 +179,8 @@ class ThemeManager @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes() null + isInitialLoadComplete: -> @initialLoadComplete + addActiveThemeClasses: -> for pack in @getActiveThemes() atom.workspaceView?[0]?.classList.add("theme-#{pack.name}") @@ -197,8 +246,8 @@ class ThemeManager if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) @requireStylesheet(nativeStylesheetPath) - stylesheetElementForId: (id, htmlElement=$('html')) -> - htmlElement.find("""head style[id="#{id}"]""") + stylesheetElementForId: (id) -> + document.head.querySelector("""style[id="#{id}"]""") resolveStylesheet: (stylesheetPath) -> if path.extname(stylesheetPath).length > 0 @@ -208,16 +257,16 @@ class ThemeManager # Public: Resolve and apply the stylesheet specified by the path. # - # This supports both CSS and LESS stylsheets. + # 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', htmlElement) -> + requireStylesheet: (stylesheetPath, type='bundled') -> if fullPath = @resolveStylesheet(stylesheetPath) content = @loadStylesheet(fullPath) - @applyStylesheet(fullPath, content, type = 'bundled', htmlElement) + @applyStylesheet(fullPath, content, type) else throw new Error("Could not find a file at path '#{stylesheetPath}'") @@ -244,11 +293,11 @@ class ThemeManager @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n')) else @lessCache.read(lessStylesheetPath) - catch e + catch error console.error """ - Error compiling less stylesheet: #{lessStylesheetPath} - Line number: #{e.line} - #{e.message} + Error compiling Less stylesheet: #{lessStylesheetPath} + Line number: #{error.line} + #{error.message} """ stringToId: (string) -> @@ -257,23 +306,49 @@ class ThemeManager removeStylesheet: (stylesheetPath) -> fullPath = @resolveStylesheet(stylesheetPath) ? stylesheetPath element = @stylesheetElementForId(@stringToId(fullPath)) - if element.length > 0 - stylesheet = element[0].sheet + if element? + {sheet} = element element.remove() - @emit 'stylesheet-removed', stylesheet + @emit 'stylesheet-removed', sheet + @emitter.emit 'did-remove-stylesheet', sheet @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' - applyStylesheet: (path, text, type = 'bundled', htmlElement=$('html')) -> - styleElement = @stylesheetElementForId(@stringToId(path), htmlElement) - if styleElement.length - @emit 'stylesheet-removed', styleElement[0].sheet - styleElement.text(text) + 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 = $("") - if htmlElement.find("head style.#{type}").length - htmlElement.find("head style.#{type}:last").after(styleElement) - else - htmlElement.find("head").append(styleElement) + styleElement = document.createElement('style') + styleElement.setAttribute('class', type) + styleElement.setAttribute('id', styleId) + styleElement.textContent = text - @emit 'stylesheet-added', styleElement[0].sheet + 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 {}') + styleNode = @stylesheetElementForId('global-editor-styles') + + {sheet} = styleNode + editorRule = sheet.cssRules[0] + editorRule.style[property] = value + + @emit 'stylesheet-updated', sheet + @emitter.emit 'did-update-stylesheet', sheet + @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 79dcba5f7..8872b6e01 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -1,9 +1,12 @@ _ = require 'underscore-plus' {Model} = require 'theorist' +EmitterMixin = require('emissary').Emitter +{Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' Serializable = require 'serializable' TokenizedLine = require './tokenized-line' Token = require './token' +Grim = require 'grim' module.exports = class TokenizedBuffer extends Model @@ -20,20 +23,15 @@ class TokenizedBuffer extends Model visible: false constructor: ({@buffer, @tabLength, @invisibles}) -> + @emitter = new Emitter + @tabLength ?= atom.config.getPositiveInt('editor.tabLength', 2) - @subscribe atom.syntax, 'grammar-added grammar-updated', (grammar) => - if grammar.injectionSelector? - @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector) - else - newScore = grammar.getScore(@buffer.getPath(), @buffer.getText()) - @setGrammar(grammar, newScore) if newScore > @currentGrammarScore + @subscribe atom.syntax.onDidAddGrammar(@grammarAddedOrUpdated) + @subscribe atom.syntax.onDidUpdateGrammar(@grammarAddedOrUpdated) - @on 'grammar-changed grammar-updated', => @retokenizeLines() - @subscribe @buffer, "changed", (e) => @handleBufferChange(e) - @subscribe @buffer, "path-changed", => - @bufferPath = @buffer.getPath() - @reloadGrammar() + @subscribe @buffer.onDidChange (e) => @handleBufferChange(e) + @subscribe @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() @subscribe @$tabLength.changes, (tabLength) => @retokenizeLines() @@ -51,13 +49,44 @@ class TokenizedBuffer extends Model params.buffer = atom.project.bufferForPathSync(params.bufferPath) params + onDidChangeGrammar: (callback) -> + @emitter.on 'did-change-grammar', callback + + onDidChange: (callback) -> + @emitter.on 'did-change', callback + + onDidTokenize: (callback) -> + @emitter.on 'did-tokenize', callback + + on: (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use TokenizedBuffer::onDidChange instead") + when 'grammar-changed' + Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead") + when 'tokenized' + Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead") + else + Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) + + grammarAddedOrUpdated: (grammar) => + if grammar.injectionSelector? + @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector) + else + newScore = grammar.getScore(@buffer.getPath(), @buffer.getText()) + @setGrammar(grammar, newScore) if newScore > @currentGrammarScore + setGrammar: (grammar, score) -> return if grammar is @grammar @unsubscribe(@grammar) if @grammar @grammar = grammar @currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText()) - @subscribe @grammar, 'grammar-updated', => @retokenizeLines() + @subscribe @grammar.onDidUpdate => @retokenizeLines() + @retokenizeLines() @emit 'grammar-changed', grammar + @emitter.emit 'did-change-grammar', grammar reloadGrammar: -> if grammar = atom.syntax.selectGrammar(@buffer.getPath(), @buffer.getText()) @@ -77,7 +106,9 @@ class TokenizedBuffer extends Model @invalidRows = [] @invalidateRow(0) @fullyTokenized = false - @emit "changed", {start: 0, end: lastRow, delta: 0} + event = {start: 0, end: lastRow, delta: 0} + @emit 'changed', event + @emitter.emit 'did-change', event setVisible: (@visible) -> @tokenizeInBackground() if @visible @@ -127,12 +158,16 @@ class TokenizedBuffer extends Model @validateRow(row) @invalidateRow(row + 1) unless filledRegion - @emit "changed", { start: invalidRow, end: row, delta: 0 } + event = { start: invalidRow, end: row, delta: 0 } + @emit 'changed', event + @emitter.emit 'did-change', event if @firstInvalidRow()? @tokenizeInBackground() else - @emit "tokenized" unless @fullyTokenized + unless @fullyTokenized + @emit 'tokenized' + @emitter.emit 'did-tokenize' @fullyTokenized = true firstInvalidRow: -> @@ -173,7 +208,9 @@ class TokenizedBuffer extends Model if newEndStack and not _.isEqual(newEndStack, previousEndStack) @invalidateRow(end + delta + 1) - @emit "changed", { start, end, delta, bufferChange: e } + event = { start, end, delta, bufferChange: e } + @emit 'changed', event + @emitter.emit 'did-change', event retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) -> line = @tokenizedLines[row] diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 5f2b09b62..5fe4a9ec6 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -1,3 +1,4 @@ +path = require 'path' {$} = require './space-pen-extensions' _ = require 'underscore-plus' ipc = require 'ipc' @@ -28,9 +29,11 @@ class WindowEventHandler @subscribe $(window), 'blur', -> document.body.classList.add('is-blurred') @subscribe $(window), 'window:open-path', (event, {pathToOpen, initialLine, initialColumn}) -> - if fs.isDirectorySync(pathToOpen) - atom.project.setPath(pathToOpen) unless atom.project.getPath() - else + unless atom.project?.getPath() + if fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen)) + atom.project?.setPath(pathToOpen) + + unless fs.isDirectorySync(pathToOpen) atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) @subscribe $(window), 'beforeunload', => diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 9da0d7b90..458d6d311 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -84,7 +84,7 @@ class WorkspaceView extends View @panes.replaceWith(panes) @panes = panes - @subscribe @model, 'uri-opened', => @trigger 'uri-opened' + @subscribe @model.onDidOpen => @trigger 'uri-opened' @subscribe scrollbarStyle, (style) => @removeClass('scrollbars-visible-always scrollbars-visible-when-scrolling') @@ -240,7 +240,8 @@ class WorkspaceView extends View # # Returns an {Array} of {EditorView}s. getEditorViews: -> - @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() + 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. @@ -371,25 +372,14 @@ class WorkspaceView extends View beforeRemove: -> @model.destroy() - setEditorFontSize: (fontSize) => - @setEditorStyle('font-size', fontSize + 'px') + setEditorFontSize: (fontSize) -> + atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') - setEditorFontFamily: (fontFamily) => - @setEditorStyle('font-family', fontFamily) + setEditorFontFamily: (fontFamily) -> + atom.themes.updateGlobalEditorStyle('font-family', fontFamily) - setEditorLineHeight: (lineHeight) => - @setEditorStyle('line-height', lineHeight) - - setEditorStyle: (property, value) -> - unless styleNode = atom.themes.stylesheetElementForId('global-editor-styles')[0] - atom.themes.applyStylesheet('global-editor-styles', '.editor {}') - styleNode = atom.themes.stylesheetElementForId('global-editor-styles')[0] - - {sheet} = styleNode - editorRule = sheet.cssRules[0] - editorRule.style[property] = value - atom.themes.emit 'stylesheet-updated', sheet - atom.themes.emit 'stylesheets-changed' + setEditorLineHeight: (lineHeight) -> + atom.themes.updateGlobalEditorStyle('line-height', lineHeight) # Deprecated eachPane: (callback) -> @@ -409,4 +399,4 @@ class WorkspaceView extends View # Deprecated: Call {Workspace::getActivePaneItem} instead. getActivePaneItem: -> deprecate("Use Workspace::getActivePaneItem instead") - @model.activePaneItem + @model.getActivePaneItem() diff --git a/src/workspace.coffee b/src/workspace.coffee index 85870ef1b..64ea6404b 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -5,6 +5,7 @@ _ = require 'underscore-plus' Q = require 'q' Serializable = require 'serializable' Delegator = require 'delegato' +{Emitter} = require 'event-kit' Editor = require './editor' PaneContainer = require './pane-container' Pane = require './pane' @@ -16,17 +17,6 @@ Pane = require './pane' # editors, and manipulate panes. To add panels, you'll need to use the # {WorkspaceView} class for now until we establish APIs at the model layer. # -# ## Events -# -# ### uri-opened -# -# Extended: Emit when something has been opened. This can be anything, from an -# editor to the settings view. You can get the new item via {::getActivePaneItem} -# -# ### editor-created -# -# Extended: Emit when an editor is created (a file opened). -# # * `editor` {Editor} the new editor # module.exports = @@ -44,9 +34,11 @@ class Workspace extends Model constructor: -> super + @emitter = new Emitter @openers = [] - @subscribe @paneContainer, 'item-destroyed', @onPaneItemDestroyed + @paneContainer.onDidDestroyPaneItem(@onPaneItemDestroyed) + @registerOpener (filePath) => switch filePath when 'atom://.atom/stylesheet' @@ -83,34 +75,163 @@ class Workspace extends Model for scopeName in includedGrammarScopes ? [] addGrammar(atom.syntax.grammarForScopeName(scopeName)) - addGrammar(editor.getGrammar()) for editor in @getEditors() + editors = @getTextEditors() + addGrammar(editor.getGrammar()) for editor in editors + + if editors.length > 0 + for grammar in atom.syntax.getGrammars() when grammar.injectionSelector + addGrammar(grammar) + _.uniq(packageNames) editorAdded: (editor) -> @emit 'editor-created', editor - # Public: Register a function to be called for every current and future - # {Editor} in the workspace. + ### + Section: Event Subscription + ### + + # Extended: Invoke the given callback when a pane is added to the workspace. # - # * `callback` A {Function} with an {Editor} as its only argument. + # * `callback` {Function} to be called panes are added. + # * `event` {Object} with the following keys: + # * `pane` The added pane. # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane: (callback) -> @paneContainer.onDidAddPane(callback) + + # Extended: Invoke the given callback with all current and future panes in the + # workspace. + # + # * `callback` {Function} to be called with current and future panes. + # * `pane` A {Pane} that is present in {::getPanes} at the time of + # subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes: (callback) -> @paneContainer.observePanes(callback) + + # Extended: Invoke the given callback when the active pane changes. + # + # * `callback` {Function} to be called when the active pane changes. + # * `pane` A {Pane} that is the current return value of {::getActivePane}. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane: (callback) -> @paneContainer.onDidChangeActivePane(callback) + + # Extended: Invoke the given callback with the current active pane and when + # the active pane changes. + # + # * `callback` {Function} to be called with the current and future active# + # panes. + # * `pane` A {Pane} that is the current return value of {::getActivePane}. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane: (callback) -> @paneContainer.observeActivePane(callback) + + # Extended: Invoke the given callback when a pane item is added to the + # workspace. + # + # * `callback` {Function} to be called when panes are added. + # * `event` {Object} with the following keys: + # * `item` The added pane item. + # * `pane` {Pane} containing the added item. + # * `index` {Number} indicating the index of the added item in its pane. + # + # 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. + # + # * `callback` {Function} to be called panes are added. + # * `event` {Object} with the following keys: + # * `textEditor` {Editor} that was added. + # * `pane` {Pane} containing the added text editor. + # * `index` {Number} indicating the index of the added text editor in its + # pane. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddTextEditor: (callback) -> + @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") + callback(editor) for editor in @getEditors() @subscribe this, 'editor-created', (editor) -> callback(editor) - # Public: Get all current editors in the workspace. - # - # Returns an {Array} of {Editor}s. getEditors: -> + deprecate("Use Workspace::getTextEditors instead") + editors = [] for pane in @paneContainer.getPanes() editors.push(item) for item in pane.getItems() when item instanceof Editor editors - # Public: Open a given a URI in Atom asynchronously. + on: (eventName) -> + switch eventName + when 'editor-created' + deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.") + when 'uri-opened' + deprecate("Use Workspace::onDidAddPaneItem instead.") + else + deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + + super + + ### + Section: Opening + ### + + # Essential: Open a given a URI in Atom asynchronously. # # * `uri` A {String} containing a URI. # * `options` (optional) {Object} @@ -137,11 +258,11 @@ class Workspace extends Model pane = @paneContainer.paneForUri(uri) if searchAllPanes pane ?= switch split when 'left' - @activePane.findLeftmostSibling() + @getActivePane().findLeftmostSibling() when 'right' - @activePane.findOrCreateRightmostSibling() + @getActivePane().findOrCreateRightmostSibling() else - @activePane + @getActivePane() @openUriInPane(uri, pane, options) @@ -195,12 +316,14 @@ class Workspace extends Model @itemOpened(item) pane.activateItem(item) pane.activate() if changeFocus + index = pane.getActiveItemIndex() @emit "uri-opened" + @emitter.emit 'did-open', {uri, pane, item, index} item .catch (error) -> console.error(error.stack ? error) - # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been + # Extended: 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 @@ -216,7 +339,7 @@ class Workspace extends Model if uri = @destroyedItemUris.pop() @openSync(uri) - # Public: Register an opener for a uri. + # Extended: Register an opener for a uri. # # An {Editor} will be used if no openers return a value. # @@ -232,52 +355,52 @@ class Workspace extends Model registerOpener: (opener) -> @openers.push(opener) - # Public: Unregister an opener registered with {::registerOpener}. + # Extended: Unregister an opener registered with {::registerOpener}. unregisterOpener: (opener) -> _.remove(@openers, opener) getOpeners: -> @openers - # Public: Get the active {Pane}. + ### + Section: Pane Items + ### + + # Essential: Get all pane items in the workspace. # - # Returns a {Pane}. - getActivePane: -> - @paneContainer.activePane + # Returns an {Array} of items. + getPaneItems: -> + @paneContainer.getPaneItems() - # Public: Get all {Pane}s. - # - # Returns an {Array} of {Pane}s. - getPanes: -> - @paneContainer.getPanes() - - # Public: Save all pane items. - saveAll: -> - @paneContainer.saveAll() - - # Public: Make the next pane active. - activateNextPane: -> - @paneContainer.activateNextPane() - - # Public: Make the previous pane active. - activatePreviousPane: -> - @paneContainer.activatePreviousPane() - - # Public: Get the first pane {Pane} with an item for the given URI. - # - # * `uri` {String} uri - # - # Returns a {Pane} or `undefined` if no pane exists for the given URI. - paneForUri: (uri) -> - @paneContainer.paneForUri(uri) - - # Public: Get the active {Pane}'s active item. + # Essential: Get the active {Pane}'s active item. # # Returns an pane item {Object}. getActivePaneItem: -> - @paneContainer.getActivePane().getActiveItem() + @paneContainer.getActivePaneItem() - # Public: Save the active pane item. + # Essential: Get all text editors in the workspace. + # + # Returns an {Array} of {Editor}s. + getTextEditors: -> + @getPaneItems().filter (item) -> item instanceof Editor + + # Essential: Get the active item if it is an {Editor}. + # + # Returns an {Editor} or `undefined` if the current active item is not an + # {Editor}. + getActiveTextEditor: -> + activeItem = @getActivePaneItem() + activeItem if activeItem instanceof Editor + + # Deprecated: + getActiveEditor: -> + @activePane?.getActiveEditor() + + # Extended: Save all pane items. + saveAll: -> + @paneContainer.saveAll() + + # Save the active pane item. # # If the active pane item currently has a URI according to the item's # `.getUri` method, calls `.save` on the item. Otherwise @@ -286,7 +409,7 @@ class Workspace extends Model saveActivePaneItem: -> @activePane?.saveActiveItem() - # Public: Prompt the user for a path and save the active pane item to it. + # Prompt the user for a path and save the active pane item to it. # # Opens a native dialog where the user selects a path on disk, then calls # `.saveAs` on the item with the selected path. This method does nothing if @@ -294,34 +417,59 @@ class Workspace extends Model saveActivePaneItemAs: -> @activePane?.saveActiveItemAs() - # Public: Destroy (close) the active pane item. + # Destroy (close) the active pane item. # # Removes the active pane item and calls the `.destroy` method on it if one is # defined. destroyActivePaneItem: -> @activePane?.destroyActiveItem() - # Public: Destroy (close) the active pane. + ### + Section: Panes + ### + + # Extended: Get all panes in the workspace. + # + # Returns an {Array} of {Pane}s. + getPanes: -> + @paneContainer.getPanes() + + # Extended: Get the active {Pane}. + # + # Returns a {Pane}. + getActivePane: -> + @paneContainer.getActivePane() + + # Extended: Make the next pane active. + activateNextPane: -> + @paneContainer.activateNextPane() + + # Extended: Make the previous pane active. + activatePreviousPane: -> + @paneContainer.activatePreviousPane() + + # Extended: Get the first pane {Pane} with an item for the given URI. + # + # * `uri` {String} uri + # + # Returns a {Pane} or `undefined` if no pane exists for the given URI. + paneForUri: (uri) -> + @paneContainer.paneForUri(uri) + + # Destroy (close) the active pane. destroyActivePane: -> @activePane?.destroy() - # Public: Get the active item if it is an {Editor}. - # - # Returns an {Editor} or `undefined` if the current active item is not an - # {Editor}. - getActiveEditor: -> - @activePane?.getActiveEditor() - - # Public: Increase the editor font size by 1px. + # Increase the editor font size by 1px. increaseFontSize: -> atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) - # Public: Decrease the editor font size by 1px. + # Decrease the editor font size by 1px. decreaseFontSize: -> fontSize = atom.config.get("editor.fontSize") atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - # Public: Restore to a default editor font size. + # Restore to a default editor font size. resetFontSize: -> atom.config.restoreDefault("editor.fontSize") diff --git a/vendor/jasmine-jquery.js b/vendor/jasmine-jquery.js index 02599fd3c..5fe347b9f 100644 --- a/vendor/jasmine-jquery.js +++ b/vendor/jasmine-jquery.js @@ -138,6 +138,9 @@ jasmine.JQuery.matchersClass = {}; var builtInMatcher = jasmine.Matchers.prototype[methodName]; jasmine.JQuery.matchersClass[methodName] = function() { + if (this.actual instanceof HTMLElement) { + this.actual = jQuery(this.actual); + } if (this.actual instanceof jQuery) { var result = jQueryMatchers[methodName].apply(this, arguments); this.actual = jasmine.JQuery.elementToString(this.actual);