diff --git a/.travis.yml b/.travis.yml index 993787aee..ab65307ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,11 @@ git: depth: 10 env: - - NODE_VERSION=0.12 + global: + - ATOM_ACCESS_TOKEN=da809a6077bb1b0aa7c5623f7b2d5f1fec2faae4 + + matrix: + - NODE_VERSION=0.12 os: - linux diff --git a/README.md b/README.md index cafeca707..8958f303d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Atom is a hackable text editor for the 21st century, built on [Electron](https:/ Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https://discuss.atom.io). +Follow [@AtomEditor](https://twitter.com/atomeditor) on Twitter for important +announcements. + Visit [issue #3684](https://github.com/atom/atom/issues/3684) to learn more about the Atom 1.0 roadmap. diff --git a/apm/package.json b/apm/package.json index 3cd607758..1dce7e90b 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.161.0" + "atom-package-manager": "0.167.0" } } diff --git a/atom.sh b/atom.sh index ecd7da052..236f49ff6 100755 --- a/atom.sh +++ b/atom.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ "$(uname)" == 'Darwin' ]; then OS='Mac' diff --git a/build/package.json b/build/package.json index 82feaad92..c60bdbae5 100644 --- a/build/package.json +++ b/build/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "asar": "^0.4.4", + "asar": "^0.5.0", "async": "~0.2.9", "donna": "1.0.10", "formidable": "~1.0.14", diff --git a/build/tasks/compile-packages-slug-task.coffee b/build/tasks/compile-packages-slug-task.coffee index a56ed6e18..d0ed51fd6 100644 --- a/build/tasks/compile-packages-slug-task.coffee +++ b/build/tasks/compile-packages-slug-task.coffee @@ -2,6 +2,7 @@ path = require 'path' CSON = require 'season' fs = require 'fs-plus' _ = require 'underscore-plus' +normalizePackageData = require 'normalize-package-data' OtherPlatforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'].filter (platform) -> platform isnt process.platform @@ -32,6 +33,7 @@ module.exports = (grunt) -> modulesDirectory = path.join(appDir, 'node_modules') packages = {} + invalidPackages = false for moduleDirectory in fs.listSync(modulesDirectory) continue if path.basename(moduleDirectory) is '.bin' @@ -40,6 +42,13 @@ module.exports = (grunt) -> metadata = grunt.file.readJSON(metadataPath) continue unless metadata?.engines?.atom? + reportPackageError = (msg) -> + invalidPackages = true + grunt.log.error("#{metadata.name}: #{msg}") + normalizePackageData metadata, reportPackageError, true + if metadata.repository?.type is 'git' + metadata.repository.url = metadata.repository.url?.replace(/^git\+/, '') + moduleCache = metadata._atomModuleCache ? {} rm metadataPath @@ -79,3 +88,4 @@ module.exports = (grunt) -> metadata._atomKeymaps = getKeymaps(appDir) grunt.file.write(path.join(appDir, 'package.json'), JSON.stringify(metadata)) + not invalidPackages diff --git a/build/tasks/generate-asar-task.coffee b/build/tasks/generate-asar-task.coffee index a71d88dfe..3de5f73b6 100644 --- a/build/tasks/generate-asar-task.coffee +++ b/build/tasks/generate-asar-task.coffee @@ -14,6 +14,7 @@ module.exports = (grunt) -> 'ctags-darwin' 'ctags-linux' 'ctags-win32.exe' + '**/node_modules/spellchecker/**' ] unpack = "{#{unpack.join(',')}}" diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 2367c9a5f..a3c9bbe79 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -54,14 +54,14 @@ module.exports = (grunt) -> if process.platform in ['darwin', 'linux'] options = cmd: appPath - args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}"] + args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", '--one'] opts: cwd: packagePath env: _.extend({}, process.env, ATOM_PATH: rootDir) else if process.platform is 'win32' options = cmd: process.env.comspec - args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", "--log-file=ci.log"] + args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", "--log-file=ci.log", '--one'] opts: cwd: packagePath env: _.extend({}, process.env, ATOM_PATH: rootDir) diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 7b95e8a17..4874489ab 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -38,7 +38,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ### openSUSE -* `sudo zypper install nodejs make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` +* `sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` ## Instructions diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 656d9ec8e..5d3629386 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -77,6 +77,15 @@ 'ctrl-k ctrl-down': 'window:focus-pane-below' 'ctrl-k ctrl-left': 'window:focus-pane-on-left' 'ctrl-k ctrl-right': 'window:focus-pane-on-right' + 'alt-1': 'pane:show-item-1' + 'alt-2': 'pane:show-item-2' + 'alt-3': 'pane:show-item-3' + 'alt-4': 'pane:show-item-4' + 'alt-5': 'pane:show-item-5' + 'alt-6': 'pane:show-item-6' + 'alt-7': 'pane:show-item-7' + 'alt-8': 'pane:show-item-8' + 'alt-9': 'pane:show-item-9' 'atom-workspace atom-text-editor': # Platform Bindings diff --git a/menus/darwin.cson b/menus/darwin.cson index effd19197..17072e14f 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -81,6 +81,8 @@ { label: 'Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of Word', command: 'editor:delete-to-end-of-word' } + { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } + { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: 'Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: 'Transpose', command: 'editor:transpose' } @@ -168,6 +170,10 @@ ] } { type: 'separator' } + { label: 'Increase Font Size', command: 'window:increase-font-size' } + { label: 'Decrease Font Size', command: 'window:decrease-font-size' } + { label: 'Reset Font Size', command: 'window:reset-font-size' } + { type: 'separator' } { label: 'Toggle Soft Wrap', command: 'editor:toggle-soft-wrap' } ] } diff --git a/menus/linux.cson b/menus/linux.cson index 2cfb17871..9363d02e5 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -55,6 +55,8 @@ { label: '&Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' } + { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } + { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: '&Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: '&Transpose', command: 'editor:transpose' } @@ -125,6 +127,10 @@ ] } { type: 'separator' } + { label: '&Increase Font Size', command: 'window:increase-font-size' } + { label: '&Decrease Font Size', command: 'window:decrease-font-size' } + { label: 'Re&set Font Size', command: 'window:reset-font-size' } + { type: 'separator' } { label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' } ] } diff --git a/menus/win32.cson b/menus/win32.cson index 2bd013ce3..068817888 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -62,6 +62,8 @@ { label: '&Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' } + { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } + { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: '&Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: '&Transpose', command: 'editor:transpose' } @@ -124,6 +126,10 @@ ] } { type: 'separator' } + { label: '&Increase Font Size', command: 'window:increase-font-size' } + { label: '&Decrease Font Size', command: 'window:decrease-font-size' } + { label: 'Re&set Font Size', command: 'window:reset-font-size' } + { type: 'separator' } { label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' } ] } diff --git a/package.json b/package.json index 42e39c463..98d763eb9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.195.0", + "version": "0.200.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -27,23 +27,24 @@ "clear-cut": "^2.0.1", "coffee-cash": "0.8.0", "coffee-script": "1.8.0", - "coffeestack": "^1.1.1", + "coffeestack": "^1.1.2", "color": "^0.7.3", "delegato": "^1", "emissary": "^1.3.3", - "event-kit": "^1.1", + "event-kit": "^1.1.1", "first-mate": "^3.1", - "fs-plus": "^2.7.1", + "fs-plus": "^2.8.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^3.0.0", - "grim": "1.2.2", + "grim": "1.4.1", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", "jquery": "^2.1.1", "less-cache": "0.22", "marked": "^0.3.3", "mixto": "^1", + "normalize-package-data": "^2.0.0", "nslog": "^2.0.0", "oniguruma": "^4.1", "pathwatcher": "^4.4", @@ -63,96 +64,99 @@ "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", - "text-buffer": "^5.2", + "text-buffer": "6.0.0", "theorist": "^1.0.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": ">=3.7.2 <4.0" }, "packageDependencies": { - "atom-dark-syntax": "0.26.0", + "atom-dark-syntax": "0.27.0", "atom-dark-ui": "0.49.0", - "atom-light-syntax": "0.26.0", + "atom-light-syntax": "0.28.0", "atom-light-ui": "0.41.0", - "base16-tomorrow-dark-theme": "0.25.0", - "base16-tomorrow-light-theme": "0.8.0", - "one-dark-ui": "0.6.3", - "one-dark-syntax": "0.3.0", - "one-light-syntax": "0.4.0", - "one-light-ui": "0.5.3", - "solarized-dark-syntax": "0.32.0", - "solarized-light-syntax": "0.19.0", - "archive-view": "0.55.0", - "autocomplete": "0.44.0", - "autoflow": "0.22.0", + "base16-tomorrow-dark-theme": "0.26.0", + "base16-tomorrow-light-theme": "0.9.0", + "one-dark-ui": "0.8.1", + "one-dark-syntax": "0.5.0", + "one-light-syntax": "0.6.0", + "one-light-ui": "0.8.1", + "solarized-dark-syntax": "0.35.0", + "solarized-light-syntax": "0.21.0", + "archive-view": "0.57.0", + "autocomplete-atom-api": "0.9.0", + "autocomplete-css": "0.7.2", + "autocomplete-html": "0.7.2", + "autocomplete-plus": "2.17.0", + "autocomplete-snippets": "1.6.1", + "autoflow": "0.23.0", "autosave": "0.20.0", "background-tips": "0.24.0", "bookmarks": "0.35.0", - "bracket-matcher": "0.73.0", - "command-palette": "0.35.0", - "deprecation-cop": "0.40.0", - "dev-live-reload": "0.45.0", - "encoding-selector": "0.19.0", + "bracket-matcher": "0.74.0", + "command-palette": "0.36.0", + "deprecation-cop": "0.48.0", + "dev-live-reload": "0.46.0", + "encoding-selector": "0.20.0", "exception-reporting": "0.24.0", - "feedback": "0.38.0", - "find-and-replace": "0.160.0", - "fuzzy-finder": "0.81.0", - "git-diff": "0.54.0", + "find-and-replace": "0.166.0", + "fuzzy-finder": "0.85.0", + "git-diff": "0.55.0", "go-to-line": "0.30.0", - "grammar-selector": "0.46.0", + "grammar-selector": "0.47.0", "image-view": "0.54.0", "incompatible-packages": "0.24.0", - "keybinding-resolver": "0.31.0", + "keybinding-resolver": "0.32.0", "link": "0.30.0", - "markdown-preview": "0.148.0", - "metrics": "0.45.0", - "notifications": "0.41.0", - "open-on-github": "0.36.0", - "package-generator": "0.38.0", + "markdown-preview": "0.149.0", + "metrics": "0.48.0", + "notifications": "0.47.0", + "open-on-github": "0.37.0", + "package-generator": "0.39.0", "release-notes": "0.52.0", - "settings-view": "0.195.0", - "snippets": "0.88.0", - "spell-check": "0.55.0", - "status-bar": "0.69.0", + "settings-view": "0.199.0", + "snippets": "0.89.0", + "spell-check": "0.58.0", + "status-bar": "0.72.0", "styleguide": "0.44.0", - "symbols-view": "0.94.0", - "tabs": "0.67.0", + "symbols-view": "0.97.0", + "tabs": "0.68.0", "timecop": "0.31.0", "tree-view": "0.171.0", - "update-package-dependencies": "0.9.0", + "update-package-dependencies": "0.10.0", "welcome": "0.27.0", "whitespace": "0.29.0", - "wrap-guide": "0.32.0", + "wrap-guide": "0.33.0", "language-c": "0.44.0", - "language-clojure": "0.14.0", - "language-coffee-script": "0.39.0", + "language-clojure": "0.15.0", + "language-coffee-script": "0.40.0", "language-csharp": "0.5.0", - "language-css": "0.28.0", - "language-gfm": "0.69.0", + "language-css": "0.29.0", + "language-gfm": "0.75.0", "language-git": "0.10.0", - "language-go": "0.25.0", - "language-html": "0.32.0", - "language-hyperlink": "0.12.2", - "language-java": "0.14.0", - "language-javascript": "0.74.0", + "language-go": "0.26.0", + "language-html": "0.37.0", + "language-hyperlink": "0.13.0", + "language-java": "0.15.0", + "language-javascript": "0.77.0", "language-json": "0.14.0", - "language-less": "0.26.0", + "language-less": "0.27.0", "language-make": "0.14.0", "language-mustache": "0.11.0", "language-objective-c": "0.15.0", - "language-perl": "0.23.0", - "language-php": "0.22.0", + "language-perl": "0.24.0", + "language-php": "0.23.0", "language-property-list": "0.8.0", - "language-python": "0.34.0", - "language-ruby": "0.52.0", + "language-python": "0.35.0", + "language-ruby": "0.53.0", "language-ruby-on-rails": "0.21.0", - "language-sass": "0.36.0", - "language-shellscript": "0.13.0", + "language-sass": "0.38.0", + "language-shellscript": "0.14.0", "language-source": "0.9.0", "language-sql": "0.15.0", "language-text": "0.6.0", - "language-todo": "0.18.0", - "language-toml": "0.15.0", + "language-todo": "0.21.0", + "language-toml": "0.16.0", "language-xml": "0.28.0", "language-yaml": "0.22.0" }, diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index ea9abe001..04cff0b6d 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -13,21 +13,44 @@ describe "BufferedProcess", -> window.onerror = oldOnError describe "when there is an error handler specified", -> - it "calls the error handler and does not throw an exception", -> - process = new BufferedProcess - command: 'bad-command-nope' - args: ['nothing'] - options: {} + describe "when an error event is emitted by the process", -> + it "calls the error handler and does not throw an exception", -> + process = new BufferedProcess + command: 'bad-command-nope' + args: ['nothing'] + options: {} - errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() - process.onWillThrowError(errorSpy) + errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() + process.onWillThrowError(errorSpy) - waitsFor -> errorSpy.callCount > 0 + waitsFor -> errorSpy.callCount > 0 - runs -> - expect(window.onerror).not.toHaveBeenCalled() - expect(errorSpy).toHaveBeenCalled() - expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT' + runs -> + expect(window.onerror).not.toHaveBeenCalled() + expect(errorSpy).toHaveBeenCalled() + expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT' + + describe "when an error is thrown spawning the process", -> + it "calls the error handler and does not throw an exception", -> + spyOn(ChildProcess, 'spawn').andCallFake -> + error = new Error('Something is really wrong') + error.code = 'EAGAIN' + throw error + + process = new BufferedProcess + command: 'ls' + args: [] + options: {} + + errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle() + process.onWillThrowError(errorSpy) + + waitsFor -> errorSpy.callCount > 0 + + runs -> + expect(window.onerror).not.toHaveBeenCalled() + expect(errorSpy).toHaveBeenCalled() + expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong' describe "when there is not an error handler specified", -> it "calls the error handler and does not throw an exception", -> @@ -73,3 +96,21 @@ describe "BufferedProcess", -> expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c' expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"' + + it "calls the specified stdout, stderr, and exit callbacks ", -> + stdout = '' + stderr = '' + exitCallback = jasmine.createSpy('exit callback') + process = new BufferedProcess + command: atom.packages.getApmPath() + args: ['-h'] + options: {} + stdout: (lines) -> stdout += lines + stderr: (lines) -> stderr += lines + exit: exitCallback + + waitsFor -> exitCallback.callCount is 1 + + runs -> + expect(stderr).toContain 'apm - Atom Package Manager' + expect(stdout).toEqual '' diff --git a/spec/custom-gutter-component-spec.coffee b/spec/custom-gutter-component-spec.coffee index 21db68653..4b7d81fba 100644 --- a/spec/custom-gutter-component-spec.coffee +++ b/spec/custom-gutter-component-spec.coffee @@ -30,15 +30,12 @@ describe "CustomGutterComponent", -> buildTestState = (customDecorations) -> mockTestState = - gutters: + content: if customDecorations then customDecorations else {} + styles: scrollHeight: 100 scrollTop: 10 backgroundColor: 'black' - sortedDescriptions: [{gutter, visible: true}] - customDecorations: customDecorations - lineNumberGutter: - maxLineNumberDigits: 10 - lineNumbers: {} + mockTestState it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", -> @@ -53,7 +50,7 @@ describe "CustomGutterComponent", -> expect(decorationsWrapperNode.style.backgroundColor).not.toBe '' it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", -> - customDecorations = 'test-gutter': + customDecorations = 'decoration-id-1': top: 0 height: 10 @@ -75,7 +72,7 @@ describe "CustomGutterComponent", -> expect(decorationItem).toBe decorationItem1 it "updates the existing DOM node for a decoration that existed but has new properties", -> - initialCustomDecorations = 'test-gutter': + initialCustomDecorations = 'decoration-id-1': top: 0 height: 10 @@ -86,7 +83,7 @@ describe "CustomGutterComponent", -> # Change the dimensions and item, remove the class. decorationItem2 = document.createElement('div') - changedCustomDecorations = 'test-gutter': + changedCustomDecorations = 'decoration-id-1': top: 10 height: 20 @@ -103,7 +100,7 @@ describe "CustomGutterComponent", -> expect(decorationItem).toBe decorationItem2 # Remove the item, add a class. - changedCustomDecorations = 'test-gutter': + changedCustomDecorations = 'decoration-id-1': top: 10 height: 20 @@ -118,7 +115,7 @@ describe "CustomGutterComponent", -> expect(changedDecorationNode.children.length).toBe 0 it "removes any decorations that existed previously but aren't in the latest update", -> - customDecorations = 'test-gutter': + customDecorations = 'decoration-id-1': top: 0 height: 10 @@ -127,6 +124,6 @@ describe "CustomGutterComponent", -> decorationsWrapperNode = gutterComponent.getDomNode().children.item(0) expect(decorationsWrapperNode.children.length).toBe 1 - emptyCustomDecorations = 'test-gutter': {} + emptyCustomDecorations = {} gutterComponent.updateSync(buildTestState(emptyCustomDecorations)) expect(decorationsWrapperNode.children.length).toBe 0 diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index 236283d27..69370a77b 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -1,5 +1,6 @@ DefaultDirectoryProvider = require "../src/default-directory-provider" path = require "path" +fs = require 'fs-plus' temp = require "temp" describe "DefaultDirectoryProvider", -> diff --git a/spec/fixtures/packages/package-with-empty-activation-commands/package.json b/spec/fixtures/packages/package-with-empty-activation-commands/package.json index 8b58333ab..46741f3e3 100644 --- a/spec/fixtures/packages/package-with-empty-activation-commands/package.json +++ b/spec/fixtures/packages/package-with-empty-activation-commands/package.json @@ -1,5 +1,5 @@ { - "name": "no events", + "name": "package-with-empty-activation-commands", "version": "0.1.0", "activationCommands": {"atom-workspace": []} } diff --git a/spec/fixtures/packages/package-with-incompatible-native-module/package.json b/spec/fixtures/packages/package-with-incompatible-native-module/package.json index eec67175a..857bc7221 100644 --- a/spec/fixtures/packages/package-with-incompatible-native-module/package.json +++ b/spec/fixtures/packages/package-with-incompatible-native-module/package.json @@ -1,5 +1,5 @@ { "name": "package-with-incompatible-native-module", - "version": "1.0", + "version": "1.0.0", "main": "./main.js" } diff --git a/spec/fixtures/packages/package-with-invalid-url-package-json/package.json b/spec/fixtures/packages/package-with-invalid-url-package-json/package.json new file mode 100644 index 000000000..cc119ffaa --- /dev/null +++ b/spec/fixtures/packages/package-with-invalid-url-package-json/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-with-invalid-url-package-json", + "repository": "foo" +} diff --git a/spec/fixtures/packages/package-with-short-url-package-json/package.json b/spec/fixtures/packages/package-with-short-url-package-json/package.json new file mode 100644 index 000000000..5da40ab08 --- /dev/null +++ b/spec/fixtures/packages/package-with-short-url-package-json/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-with-short-url-package-json", + "repository": "example/repo" +} diff --git a/spec/gutter-container-component-spec.coffee b/spec/gutter-container-component-spec.coffee index 218d8aae2..5d815fea8 100644 --- a/spec/gutter-container-component-spec.coffee +++ b/spec/gutter-container-component-spec.coffee @@ -5,17 +5,20 @@ describe "GutterContainerComponent", -> [gutterContainerComponent] = [] mockGutterContainer = {} - buildTestState = (sortedDescriptions) -> - mockTestState = - gutters: - scrollHeight: 100 - scrollTop: 10 - backgroundColor: 'black' - sortedDescriptions: sortedDescriptions - customDecorations: {} - lineNumberGutter: - maxLineNumberDigits: 10 - lineNumbers: {} + buildTestState = (gutters) -> + styles = + scrollHeight: 100 + scrollTop: 10 + backgroundColor: 'black' + + mockTestState = {gutters: []} + for gutter in gutters + if gutter.name is 'line-number' + content = {maxLineNumberDigits: 10, lineNumbers: {}} + else + content = {} + mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible}) + mockTestState beforeEach -> @@ -30,7 +33,7 @@ describe "GutterContainerComponent", -> describe "when updated with state that contains a new line-number gutter", -> it "adds a LineNumberGutterComponent to its children", -> lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - testState = buildTestState([{gutter: lineNumberGutter, visible: true}]) + testState = buildTestState([lineNumberGutter]) expect(gutterContainerComponent.getDomNode().children.length).toBe 0 gutterContainerComponent.updateSync(testState) @@ -45,7 +48,7 @@ describe "GutterContainerComponent", -> describe "when updated with state that contains a new custom gutter", -> it "adds a CustomGutterComponent to its children", -> customGutter = new Gutter(mockGutterContainer, {name: 'custom'}) - testState = buildTestState([{gutter: customGutter, visible: true}]) + testState = buildTestState([customGutter]) expect(gutterContainerComponent.getDomNode().children.length).toBe 0 gutterContainerComponent.updateSync(testState) @@ -57,15 +60,16 @@ describe "GutterContainerComponent", -> describe "when updated with state that contains a new gutter that is not visible", -> it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", -> - customGutter = new Gutter(mockGutterContainer, {name: 'custom'}) - testState = buildTestState([{gutter: customGutter, visible: false}]) + customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false}) + testState = buildTestState([customGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 1 expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) expect(expectedCustomGutterNode.style.display).toBe 'none' - testState = buildTestState([{gutter: customGutter, visible: true}]) + customGutter.show() + testState = buildTestState([customGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 1 expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) @@ -74,20 +78,20 @@ describe "GutterContainerComponent", -> describe "when updated with a gutter that already exists", -> it "reuses the existing gutter view, instead of recreating it", -> customGutter = new Gutter(mockGutterContainer, {name: 'custom'}) - testState = buildTestState([{gutter: customGutter, visible: true}]) + testState = buildTestState([customGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 1 expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) - testState = buildTestState([{gutter: customGutter, visible: true}]) + testState = buildTestState([customGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 1 expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode it "removes a gutter from the DOM if it does not appear in the latest state update", -> lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) - testState = buildTestState([{gutter: lineNumberGutter, visible: true}]) + testState = buildTestState([lineNumberGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 1 @@ -99,7 +103,7 @@ describe "GutterContainerComponent", -> it "positions (and repositions) the gutters to match the order they appear in each state update", -> lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100}) - testState = buildTestState([{gutter: customGutter1, visible: true}, {gutter: lineNumberGutter, visible: true}]) + testState = buildTestState([customGutter1, lineNumberGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 2 @@ -110,11 +114,7 @@ describe "GutterContainerComponent", -> # Add a gutter. customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10}) - testState = buildTestState([ - {gutter: customGutter1, visible: true}, - {gutter: customGutter2, visible: true}, - {gutter: lineNumberGutter, visible: true} - ]) + testState = buildTestState([customGutter1, customGutter2, lineNumberGutter]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 3 expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0) @@ -125,12 +125,9 @@ describe "GutterContainerComponent", -> expect(expectedLineNumbersNode).toBe atom.views.getView(lineNumberGutter) # Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter. + customGutter2.hide() customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100}) - testState = buildTestState([ - {gutter: customGutter2, visible: false}, - {gutter: customGutter1, visible: true}, - {gutter: customGutter3, visible: true} - ]) + testState = buildTestState([customGutter2, customGutter1, customGutter3]) gutterContainerComponent.updateSync(testState) expect(gutterContainerComponent.getDomNode().children.length).toBe 3 expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 0de6644b3..646281171 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -15,15 +15,23 @@ ChromedriverPort = 9515 ChromedriverURLBase = "/wd/hub" ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status" -pollChromeDriver = (done) -> +chromeDriverUp = (done) -> checkStatus = -> - http.get(ChromedriverStatusURL, (response) -> - if response.statusCode is 200 - done() - else - pollChromeDriver(done) - ).on("error", -> pollChromeDriver(done)) + http + .get ChromedriverStatusURL, (response) -> + if response.statusCode is 200 + done() + else + chromeDriverUp(done) + .on("error", -> chromeDriverUp(done)) + setTimeout(checkStatus, 100) +chromeDriverDown = (done) -> + checkStatus = -> + http + .get ChromedriverStatusURL, (response) -> + chromeDriverDown(done) + .on("error", done) setTimeout(checkStatus, 100) buildAtomClient = (args, env) -> @@ -137,9 +145,9 @@ module.exports = (args, env, fn) -> chromedriver.stderr.on "close", -> resolve(errorCode) - waitsFor("webdriver to start", pollChromeDriver, 15000) + waitsFor("webdriver to start", chromeDriverUp, 15000) - waitsFor("webdriver to finish", (done) -> + waitsFor("tests to run", (done) -> finish = once -> client .simulateQuit() @@ -162,3 +170,5 @@ module.exports = (args, env, fn) -> fn(client.init()).then(finish) , 30000) + + waitsFor("webdriver to stop", chromeDriverDown, 15000) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index b20427954..54817fedc 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -9,7 +9,7 @@ return if process.env.TRAVIS fs = require "fs" path = require "path" temp = require("temp").track() -runAtom = require("./helpers/start-atom") +runAtom = require "./helpers/start-atom" describe "Starting Atom", -> [tempDirPath, otherTempDirPath, atomHome] = [] @@ -41,6 +41,69 @@ describe "Starting Atom", -> .then ({value}) -> expect(value).toBe "Hello!" .dispatchCommand("editor:delete-line") + it "opens the file to the specified line number", -> + filePath = path.join(fs.realpathSync(tempDirPath), "new-file") + fs.writeFileSync filePath, """ + 1 + 2 + 3 + 4 + """ + + runAtom ["#{filePath}:3"], {ATOM_HOME: atomHome}, (client) -> + client + .waitForWindowCount(1, 1000) + .waitForExist("atom-workspace", 5000) + .waitForPaneItemCount(1, 1000) + .waitForExist("atom-text-editor", 5000) + .then (exists) -> expect(exists).toBe true + + .execute -> atom.workspace.getActiveTextEditor().getPath() + .then ({value}) -> expect(value).toBe filePath + + .execute -> atom.workspace.getActiveTextEditor().getCursorBufferPosition() + .then ({value}) -> + expect(value.row).toBe 2 + expect(value.column).toBe 0 + + it "opens the file to the specified line number and column number", -> + filePath = path.join(fs.realpathSync(tempDirPath), "new-file") + fs.writeFileSync filePath, """ + 1 + 2 + 3 + 4 + """ + + runAtom ["#{filePath}:2:2"], {ATOM_HOME: atomHome}, (client) -> + client + .waitForWindowCount(1, 1000) + .waitForExist("atom-workspace", 5000) + .waitForPaneItemCount(1, 1000) + .waitForExist("atom-text-editor", 5000) + .then (exists) -> expect(exists).toBe true + + .execute -> atom.workspace.getActiveTextEditor().getPath() + .then ({value}) -> expect(value).toBe filePath + + .execute -> atom.workspace.getActiveTextEditor().getCursorBufferPosition() + .then ({value}) -> + expect(value.row).toBe 1 + expect(value.column).toBe 1 + + it "removes all trailing whitespace and colons from the specified path", -> + filePath = path.join(tempDirPath, "new-file") + runAtom ["#{filePath}: "], {ATOM_HOME: atomHome}, (client) -> + client + .waitForWindowCount(1, 1000) + .waitForExist("atom-workspace", 5000) + .waitForPaneItemCount(1, 1000) + .waitForExist("atom-text-editor", 5000) + .then (exists) -> expect(exists).toBe true + + .execute -> atom.workspace.getActiveTextEditor().getPath() + .then ({value}) -> expect(value).toBe filePath + describe "when there is already a window open", -> it "reuses that window when opening files, but not when opening directories", -> tempFilePath = path.join(temp.mkdirSync("a-third-dir"), "a-file") @@ -150,10 +213,12 @@ describe "Starting Atom", -> .waitForWindowCount(2, 10000) .then ({value: windowHandles}) -> @window(windowHandles[0]) + .waitForExist("atom-workspace") .treeViewRootDirectories() .then ({value: directories}) -> windowProjectPaths.push(directories) .window(windowHandles[1]) + .waitForExist("atom-workspace") .treeViewRootDirectories() .then ({value: directories}) -> windowProjectPaths.push(directories) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 8dcc6bc37..1244ab984 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -41,6 +41,15 @@ describe "PackageManager", -> expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package") + it "normalizes short repository urls in package.json", -> + {metadata} = atom.packages.loadPackage("package-with-short-url-package-json") + expect(metadata.repository.type).toBe "git" + expect(metadata.repository.url).toBe "https://github.com/example/repo.git" + + {metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json") + expect(metadata.repository.type).toBe "git" + expect(metadata.repository.url).toBe "foo" + it "returns null if the package is not found in any package directory", -> spyOn(console, 'warn') expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull() @@ -815,3 +824,37 @@ describe "PackageManager", -> expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + describe "deleting non-bundled autocomplete packages", -> + [autocompleteCSSPath, autocompletePlusPath] = [] + fs = require 'fs-plus' + path = require 'path' + + beforeEach -> + fixturePath = path.resolve(__dirname, './fixtures/packages') + autocompleteCSSPath = path.join(fixturePath, 'autocomplete-css') + autocompletePlusPath = path.join(fixturePath, 'autocomplete-plus') + + try + fs.mkdirSync(autocompleteCSSPath) + fs.writeFileSync(path.join(autocompleteCSSPath, 'package.json'), '{}') + fs.symlinkSync(path.join(fixturePath, 'package-with-main'), autocompletePlusPath, 'dir') + + expect(fs.isDirectorySync(autocompleteCSSPath)).toBe true + expect(fs.isSymbolicLinkSync(autocompletePlusPath)).toBe true + + jasmine.unspy(atom.packages, 'uninstallAutocompletePlus') + + afterEach -> + try + fs.unlink autocompletePlusPath, -> + + it "removes the packages", -> + atom.packages.loadPackages() + + waitsFor -> + not fs.isDirectorySync(autocompleteCSSPath) + + runs -> + expect(fs.isDirectorySync(autocompleteCSSPath)).toBe false + expect(fs.isSymbolicLinkSync(autocompletePlusPath)).toBe true diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index 0a5b11b39..400904f5c 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -132,3 +132,25 @@ describe "PaneContainerElement", -> # dynamically close pane, the pane's flexscale will recorver to origin value lowerPane.close() expectPaneScale [leftPane, 0.5], [rightPane, 1.5] + + it "unsubscribes from mouse events when the pane is detached", -> + container.getActivePane().splitRight() + element = getResizeElement(0) + spyOn(document, 'addEventListener').andCallThrough() + spyOn(document, 'removeEventListener').andCallThrough() + spyOn(element, 'resizeStopped').andCallThrough() + + element.dispatchEvent(new MouseEvent('mousedown', + view: window + bubbles: true + button: 0 + )) + + waitsFor -> + document.addEventListener.callCount is 2 + + runs -> + expect(element.resizeStopped.callCount).toBe 0 + container.destroy() + expect(element.resizeStopped.callCount).toBe 1 + expect(document.removeEventListener.callCount).toBe 2 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 0112042e2..15874ff6f 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -140,6 +140,8 @@ beforeEach -> spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text spyOn(clipboard, 'readText').andCallFake -> clipboardContent + spyOn(atom.packages, 'uninstallAutocompletePlus') + addCustomMatchers(this) afterEach -> @@ -311,7 +313,7 @@ window.waitsForPromise = (args...) -> else promise.then(moveOn) promise.catch.call promise, (error) -> - jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}") + jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with: #{error?.message} #{jasmine.pp(error)}") moveOn() window.resetTimeouts = -> diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index a89bee1fc..12d189035 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -2266,13 +2266,13 @@ describe "TextEditorComponent", -> editor.setText("") componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - currentTime += 99 + currentTime += 100 componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - currentTime += 99 + currentTime += 100 componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) - currentTime += 100 + currentTime += 101 componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) expect(editor.getText()).toBe "xy\nxy\nxy" diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index ba71cb236..435b3a4a5 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -65,6 +65,35 @@ describe "TextEditorElement", -> element.getModel().destroy() expect(component.mounted).toBe false + describe "when the editor is detached from the DOM and then reattached", -> + it "does not render duplicate line numbers", -> + editor = new TextEditor + editor.setText('1\n2\n3') + element = atom.views.getView(editor) + + jasmine.attachToDOM(element) + + initialCount = element.shadowRoot.querySelectorAll('.line-number').length + + element.remove() + jasmine.attachToDOM(element) + expect(element.shadowRoot.querySelectorAll('.line-number').length).toBe initialCount + + it "does not render duplicate decorations in custom gutters", -> + editor = new TextEditor + editor.setText('1\n2\n3') + editor.addGutter({name: 'test-gutter'}) + marker = editor.markBufferRange([[0,0],[2,0]]) + editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'}) + element = atom.views.getView(editor) + + jasmine.attachToDOM(element) + initialDecorationCount = element.shadowRoot.querySelectorAll('.decoration').length + + element.remove() + jasmine.attachToDOM(element) + expect(element.shadowRoot.querySelectorAll('.decoration').length).toBe initialDecorationCount + describe "focus and blur handling", -> describe "when the editor.useShadowDOM config option is true", -> it "proxies focus/blur events to/from the hidden input inside the shadow root", -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 5a50a49a9..7da866ab4 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -32,6 +32,7 @@ describe "TextEditorPresenter", -> windowWidth: 500 windowHeight: 130 boundingClientRect: {left: 0, top: 0, width: 500, height: 130} + gutterWidth: 0 lineHeight: 10 baseCharacterWidth: 10 horizontalScrollbarHeight: 10 @@ -498,6 +499,11 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + it "isn't clipped to 0 when the longest line is folded (regression)", -> + presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) + editor.foldBufferRow(0) + expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + describe ".scrollTop", -> it "tracks the value of ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) @@ -1628,7 +1634,7 @@ describe "TextEditorPresenter", -> marker = editor.markBufferPosition([0, 26], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) expectStateUpdate presenter, -> presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) @@ -1654,7 +1660,7 @@ describe "TextEditorPresenter", -> marker = editor.markBufferPosition([5, 0], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) expectStateUpdate presenter, -> presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) @@ -1682,7 +1688,7 @@ describe "TextEditorPresenter", -> marker = cursor.marker decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) expectStateUpdate presenter, -> presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) @@ -1715,7 +1721,7 @@ describe "TextEditorPresenter", -> marker = editor.markBufferPosition([1, 0], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) expectStateUpdate presenter, -> presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) @@ -1736,7 +1742,7 @@ describe "TextEditorPresenter", -> marker = editor.markBufferPosition([0, 0], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) expectStateUpdate presenter, -> presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) @@ -1766,313 +1772,6 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 10, left: 0} } - describe ".lineNumberGutter", -> - describe ".maxLineNumberDigits", -> - it "is set to the number of digits used by the greatest line number", -> - presenter = buildPresenter() - expect(editor.getLastBufferRow()).toBe 12 - expect(presenter.getState().gutters.lineNumberGutter.maxLineNumberDigits).toBe 2 - - editor.setText("1\n2\n3") - expect(presenter.getState().gutters.lineNumberGutter.maxLineNumberDigits).toBe 1 - - describe ".lineNumbers", -> - lineNumberStateForScreenRow = (presenter, screenRow) -> - editor = presenter.model - bufferRow = editor.bufferRowForScreenRow(screenRow) - wrapCount = screenRow - editor.screenRowForBufferRow(bufferRow) - if wrapCount > 0 - key = bufferRow + '-' + wrapCount - else - key = bufferRow - - presenter.getState().gutters.lineNumberGutter.lineNumbers[key] - - it "contains states for line numbers that are visible on screen, plus and minus the overdraw margin", -> - editor.foldBufferRow(4) - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, lineOverdrawMargin: 1) - - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 2 * 10} - expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 3 * 10} - expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 4 * 10} - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 5 * 10} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 6 * 10} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 7 * 10} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - it "includes states for all line numbers if no ::explicitHeight is assigned", -> - presenter = buildPresenter(explicitHeight: null) - expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 12)).toBeDefined() - - it "updates when ::scrollTop changes", -> - editor.foldBufferRow(4) - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) - - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setScrollTop(20) - - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 1), {bufferRow: 1} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} - expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() - - it "updates when ::explicitHeight changes", -> - editor.foldBufferRow(4) - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) - - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setExplicitHeight(35) - - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined() - - it "updates when ::lineHeight changes", -> - editor.foldBufferRow(4) - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineOverdrawMargin: 0) - - expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} - expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} - expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setLineHeight(5) - - expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} - expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} - expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined() - - it "updates when the editor's content changes", -> - editor.foldBufferRow(4) - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, lineOverdrawMargin: 0) - - expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - expectStateUpdate presenter, -> - editor.getBuffer().insert([3, Infinity], new Array(25).join("x ")) - - expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 25, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) - - expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 5)).toBeUndefined() - - presenter.setMouseWheelScreenRow(0) - expectStateUpdate presenter, -> presenter.setScrollTop(35) - - expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - expectStateUpdate presenter, -> advanceClock(200) - - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - - it "correctly handles the first screen line being soft-wrapped", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(30) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 50) - - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 3, softWrapped: true} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false} - - describe ".decorationClasses", -> - it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> - marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a') - presenter = buildPresenter() - marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') - decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b') - - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') - expect(marker1.isValid()).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> editor.undo() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> decoration1.destroy() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> marker2.destroy() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - - it "honors the 'onlyEmpty' option on line-number decorations", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 1]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true) - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> marker.clearTail() - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "honors the 'onlyNonEmpty' option on line-number decorations", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true) - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - expectStateUpdate presenter, -> marker.clearTail() - - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "honors the 'onlyHead' option on line-number decorations", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true) - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - - it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", -> - presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 0]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') - - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - - it "does not apply line-number decorations to mini editors", -> - editor.setMini(true) - presenter = buildPresenter() - marker = editor.markBufferRange([[0, 0], [0, 0]]) - decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toBeNull() - - expectStateUpdate presenter, -> editor.setMini(false) - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'cursor-line-no-selection', 'a'] - - expectStateUpdate presenter, -> editor.setMini(true) - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toBeNull() - - it "only applies line-number decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> - editor.setText("a line that wraps, ok") - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(16) - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, type: 'line-number', class: 'a') - presenter = buildPresenter(explicitHeight: 10) - - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - - marker.setBufferRange([[0, 0], [0, Infinity]]) - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' - - describe ".foldable", -> - it "marks line numbers at the start of a foldable region as foldable", -> - presenter = buildPresenter() - expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe false - - it "updates the foldable class on the correct line numbers when the foldable positions change", -> - presenter = buildPresenter() - editor.getBuffer().insert([0, 0], '\n') - expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe false - expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe true - expect(lineNumberStateForScreenRow(presenter, 6).foldable).toBe false - - it "updates the foldable class on a line number that becomes foldable", -> - presenter = buildPresenter() - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false - - editor.getBuffer().insert([11, 44], '\n fold me') - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe true - - editor.undo() - expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false - describe ".height", -> it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", -> presenter = buildPresenter(explicitHeight: null, autoHeight: true) @@ -2100,130 +1799,70 @@ describe "TextEditorPresenter", -> expect(presenter.getState().focused).toBe false describe ".gutters", -> - describe ".scrollHeight", -> - it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> + getStateForGutterWithName = (presenter, gutterName) -> + gutterDescriptions = presenter.getState().gutters + for description in gutterDescriptions + gutter = description.gutter + return description if gutter.name is gutterName + + describe "the array itself, an array of gutter descriptions", -> + it "updates when gutters are added to the editor model, and keeps the gutters sorted by priority", -> presenter = buildPresenter() - expect(presenter.getState().gutters.scrollHeight).toBe editor.getScreenLineCount() * 10 + gutter1 = editor.addGutter({name: 'test-gutter-1', priority: -100, visible: true}) + gutter2 = editor.addGutter({name: 'test-gutter-2', priority: 100, visible: false}) - presenter = buildPresenter(explicitHeight: 500) - expect(presenter.getState().gutters.scrollHeight).toBe 500 + expectedGutterOrder = [gutter1, editor.gutterWithName('line-number'), gutter2] + for gutterDescription, index in presenter.getState().gutters + expect(gutterDescription.gutter).toEqual expectedGutterOrder[index] - it "updates when the ::lineHeight changes", -> + it "updates when the visibility of a gutter changes", -> presenter = buildPresenter() - expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(presenter.getState().gutters.scrollHeight).toBe editor.getScreenLineCount() * 20 + gutter = editor.addGutter({name: 'test-gutter', visible: true}) + expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe true + gutter.hide() + expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe false - it "updates when the line count changes", -> + it "updates when a gutter is removed", -> presenter = buildPresenter() - expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(presenter.getState().gutters.scrollHeight).toBe editor.getScreenLineCount() * 10 + gutter = editor.addGutter({name: 'test-gutter', visible: true}) + expect(getStateForGutterWithName(presenter, 'test-gutter').visible).toBe true + gutter.destroy() + expect(getStateForGutterWithName(presenter, 'test-gutter')).toBeUndefined() - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter() - expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(presenter.getState().gutters.scrollHeight).toBe 500 + describe "for a gutter description that corresponds to the line-number gutter", -> + getLineNumberGutterState = (presenter) -> + gutterDescriptions = presenter.getState().gutters + for description in gutterDescriptions + gutter = description.gutter + return description if gutter.name is 'line-number' - it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().gutters.scrollHeight).toBe presenter.contentHeight - - expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true) - expect(presenter.getState().gutters.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().gutters.scrollHeight).toBe presenter.contentHeight - - describe ".scrollTop", -> - it "tracks the value of ::scrollTop", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 20) - expect(presenter.getState().gutters.scrollTop).toBe 10 - expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(presenter.getState().gutters.scrollTop).toBe 50 - - it "never exceeds the computed scrollHeight minus the computed clientHeight", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(presenter.getState().gutters.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(presenter.getState().gutters.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(presenter.getState().gutters.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(presenter.getState().gutters.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight - - # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop - expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(presenter.getState().gutters.scrollTop).toBe scrollTopBefore - - it "never goes negative", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(presenter.getState().gutters.scrollTop).toBe 0 - - it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> - presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().gutters.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - atom.config.set("editor.scrollPastEnd", true) - expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().gutters.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) - - expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().gutters.scrollTop).toBe presenter.contentHeight - presenter.clientHeight - - describe ".backgroundColor", -> - it "is assigned to ::gutterBackgroundColor if present, and to ::backgroundColor otherwise", -> - presenter = buildPresenter(backgroundColor: "rgba(255, 0, 0, 0)", gutterBackgroundColor: "rgba(0, 255, 0, 0)") - expect(presenter.getState().gutters.backgroundColor).toBe "rgba(0, 255, 0, 0)" - - expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 255, 0)") - expect(presenter.getState().gutters.backgroundColor).toBe "rgba(0, 0, 255, 0)" - - expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 0, 0)") - expect(presenter.getState().gutters.backgroundColor).toBe "rgba(255, 0, 0, 0)" - - expectStateUpdate presenter, -> presenter.setBackgroundColor("rgba(0, 0, 255, 0)") - expect(presenter.getState().gutters.backgroundColor).toBe "rgba(0, 0, 255, 0)" - - describe ".sortedDescriptions", -> - gutterDescriptionWithName = (presenter, name) -> - for gutterDesc in presenter.getState().gutters.sortedDescriptions - return gutterDesc if gutterDesc.gutter.name is name - undefined - - describe "the line-number gutter", -> - it "is present iff the editor isn't mini, ::isLineNumberGutterVisible is true on the editor, and 'editor.showLineNumbers' is enabled in config", -> + describe ".visible", -> + it "is true iff the editor isn't mini, ::isLineNumberGutterVisible is true on the editor, and the 'editor.showLineNumbers' config is enabled", -> presenter = buildPresenter() expect(editor.isLineNumberGutterVisible()).toBe true - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe true + expect(getLineNumberGutterState(presenter).visible).toBe true expectStateUpdate presenter, -> editor.setMini(true) - expect(gutterDescriptionWithName(presenter, 'line-number')).toBeUndefined() + expect(getLineNumberGutterState(presenter)).toBeUndefined() expectStateUpdate presenter, -> editor.setMini(false) - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe true + expect(getLineNumberGutterState(presenter).visible).toBe true expectStateUpdate presenter, -> editor.setLineNumberGutterVisible(false) - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe false + expect(getLineNumberGutterState(presenter).visible).toBe false expectStateUpdate presenter, -> editor.setLineNumberGutterVisible(true) - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe true + expect(getLineNumberGutterState(presenter).visible).toBe true expectStateUpdate presenter, -> atom.config.set('editor.showLineNumbers', false) - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe false + expect(getLineNumberGutterState(presenter).visible).toBe false it "gets updated when the editor's grammar changes", -> presenter = buildPresenter() atom.config.set('editor.showLineNumbers', false, scopeSelector: '.source.js') - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe true + expect(getLineNumberGutterState(presenter).visible).toBe true stateUpdated = false presenter.onDidUpdateState -> stateUpdated = true @@ -2231,263 +1870,674 @@ describe "TextEditorPresenter", -> runs -> expect(stateUpdated).toBe true - expect(gutterDescriptionWithName(presenter, 'line-number').visible).toBe false + expect(getLineNumberGutterState(presenter).visible).toBe false - it "updates when gutters are added to the editor model, and keeps the gutters sorted by priority", -> - presenter = buildPresenter() - gutter1 = editor.addGutter({name: 'test-gutter-1', priority: -100, visible: true}) - gutter2 = editor.addGutter({name: 'test-gutter-2', priority: 100, visible: false}) - expectedState = [ - {gutter: gutter1, visible: true}, - {gutter: editor.gutterWithName('line-number'), visible: true}, - {gutter: gutter2, visible: false}, - ] - expect(presenter.getState().gutters.sortedDescriptions).toEqual expectedState + describe ".content.maxLineNumberDigits", -> + it "is set to the number of digits used by the greatest line number", -> + presenter = buildPresenter() + expect(editor.getLastBufferRow()).toBe 12 + expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 2 - it "updates when the visibility of a gutter changes", -> - presenter = buildPresenter() - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - expect(gutterDescriptionWithName(presenter, 'test-gutter').visible).toBe true - gutter.hide() - expect(gutterDescriptionWithName(presenter, 'test-gutter').visible).toBe false + editor.setText("1\n2\n3") + expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 1 - it "updates when a gutter is removed", -> - presenter = buildPresenter() - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - expect(gutterDescriptionWithName(presenter, 'test-gutter').visible).toBe true - gutter.destroy() - expect(gutterDescriptionWithName(presenter, 'test-gutter')).toBeUndefined() + describe ".content.lineNumbers", -> + lineNumberStateForScreenRow = (presenter, screenRow) -> + editor = presenter.model + bufferRow = editor.bufferRowForScreenRow(screenRow) + wrapCount = screenRow - editor.screenRowForBufferRow(bufferRow) + if wrapCount > 0 + key = bufferRow + '-' + wrapCount + else + key = bufferRow - describe ".customDecorations", -> - [presenter, gutter, decorationItem, decorationParams] = [] - [marker1, decoration1, marker2, decoration2, marker3, decoration3] = [] + getLineNumberGutterState(presenter).content.lineNumbers[key] - # Set the scrollTop to 0 to show the very top of the file. - # Set the explicitHeight to make 10 lines visible. - scrollTop = 0 - lineHeight = 10 - explicitHeight = lineHeight * 10 - lineOverdrawMargin = 1 + it "contains states for line numbers that are visible on screen, plus and minus the overdraw margin", -> + editor.foldBufferRow(4) + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, lineOverdrawMargin: 1) - decorationStateForGutterName = (presenter, gutterName) -> - presenter.getState().gutters.customDecorations[gutterName] + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 2 * 10} + expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 3 * 10} + expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 4 * 10} + expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 5 * 10} + expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 6 * 10} + expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 7 * 10} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - beforeEach -> - # At the beginning of each test, decoration1 and decoration2 are in visible range, - # but not decoration3. - presenter = buildPresenter({explicitHeight, scrollTop, lineHeight, lineOverdrawMargin}) - gutter = editor.addGutter({name: 'test-gutter', visible: true}) - decorationItem = document.createElement('div') - decorationItem.class = 'decoration-item' - decorationParams = - type: 'gutter' - gutterName: 'test-gutter' - class: 'test-class' - item: decorationItem - marker1 = editor.markBufferRange([[0,0],[1,0]]) - decoration1 = editor.decorateMarker(marker1, decorationParams) - marker2 = editor.markBufferRange([[9,0],[12,0]]) - decoration2 = editor.decorateMarker(marker2, decorationParams) - marker3 = editor.markBufferRange([[13,0],[14,0]]) - decoration3 = editor.decorateMarker(marker3, decorationParams) + it "includes states for all line numbers if no ::explicitHeight is assigned", -> + presenter = buildPresenter(explicitHeight: null) + expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 12)).toBeDefined() - # Clear any batched state updates. - presenter.getState() + it "updates when ::scrollTop changes", -> + editor.foldBufferRow(4) + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) - it "contains all decorations within the visible buffer range", -> - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row - expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() - expect(decorationState[decoration1.id].item).toBe decorationItem - expect(decorationState[decoration1.id].class).toBe 'test-class' + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row - expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration2.id].class).toBe 'test-class' + expectStateUpdate presenter, -> presenter.setScrollTop(20) - expect(decorationState[decoration3.id]).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 1), {bufferRow: 1} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() - it "updates when ::scrollTop changes", -> - # This update will scroll decoration1 out of view, and decoration3 into view. - expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) + it "updates when ::explicitHeight changes", -> + editor.foldBufferRow(4) + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - it "updates when ::explicitHeight changes", -> - # This update will make all three decorations visible. - expectStateUpdate presenter, -> presenter.setExplicitHeight(explicitHeight + lineHeight * 5) + expectStateUpdate presenter, -> presenter.setExplicitHeight(35) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} + expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined() - it "updates when ::lineHeight changes", -> - # This update will make all three decorations visible. - expectStateUpdate presenter, -> presenter.setLineHeight(Math.ceil(1.0 * explicitHeight / marker3.getBufferRange().end.row)) + it "updates when ::lineHeight changes", -> + editor.foldBufferRow(4) + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineOverdrawMargin: 0) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id].top).toBeDefined() + expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined() - it "updates when the editor's content changes", -> - # This update will add enough lines to push decoration2 out of view. - expectStateUpdate presenter, -> editor.setTextInBufferRange([[8,0],[9,0]],'\n\n\n\n\n') + expectStateUpdate presenter, -> presenter.setLineHeight(5) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} + expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} + expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined() - it "updates when a decoration's marker is modified", -> - # This update will move decoration1 out of view. - expectStateUpdate presenter, -> - newRange = new Range([13,0],[14,0]) - marker1.setBufferRange(newRange) + it "updates when the editor's content changes", -> + editor.foldBufferRow(4) + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(50) + presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, lineOverdrawMargin: 0) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() - describe "when a decoration's properties are modified", -> - it "updates the item applied to the decoration, if the decoration item is changed", -> - # This changes the decoration class. The visibility of the decoration should not be affected. - newItem = document.createElement('div') - newItem.class = 'new-decoration-item' - newDecorationParams = + expectStateUpdate presenter, -> + editor.getBuffer().insert([3, Infinity], new Array(25).join("x ")) + + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 4), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + + it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> + presenter = buildPresenter(explicitHeight: 25, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) + + expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 4)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 5)).toBeUndefined() + + presenter.setMouseWheelScreenRow(0) + expectStateUpdate presenter, -> presenter.setScrollTop(35) + + expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + + expectStateUpdate presenter, -> advanceClock(200) + + expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + + it "correctly handles the first screen line being soft-wrapped", -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(30) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 50) + + expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 3, softWrapped: true} + expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true} + expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false} + + describe ".decorationClasses", -> + it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> + marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') + decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a') + presenter = buildPresenter() + marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') + decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b') + + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') + expect(marker1.isValid()).toBe false + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> editor.undo() + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> decoration1.destroy() + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> marker2.destroy() + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + + it "honors the 'onlyEmpty' option on line-number decorations", -> + presenter = buildPresenter() + marker = editor.markBufferRange([[4, 0], [6, 1]]) + decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true) + + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> marker.clearTail() + + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + + it "honors the 'onlyNonEmpty' option on line-number decorations", -> + presenter = buildPresenter() + marker = editor.markBufferRange([[4, 0], [6, 2]]) + decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true) + + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + + expectStateUpdate presenter, -> marker.clearTail() + + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + + it "honors the 'onlyHead' option on line-number decorations", -> + presenter = buildPresenter() + marker = editor.markBufferRange([[4, 0], [6, 2]]) + decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true) + + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + + it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", -> + presenter = buildPresenter() + marker = editor.markBufferRange([[4, 0], [6, 0]]) + decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') + + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + + it "does not apply line-number decorations to mini editors", -> + editor.setMini(true) + presenter = buildPresenter() + marker = editor.markBufferRange([[0, 0], [0, 0]]) + decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') + # A mini editor will have no gutters. + expect(getLineNumberGutterState(presenter)).toBeUndefined() + + expectStateUpdate presenter, -> editor.setMini(false) + expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'cursor-line-no-selection', 'a'] + + expectStateUpdate presenter, -> editor.setMini(true) + expect(getLineNumberGutterState(presenter)).toBeUndefined() + + it "only applies line-number decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> + editor.setText("a line that wraps, ok") + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(16) + marker = editor.markBufferRange([[0, 0], [0, 2]]) + editor.decorateMarker(marker, type: 'line-number', class: 'a') + presenter = buildPresenter(explicitHeight: 10) + + expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() + + marker.setBufferRange([[0, 0], [0, Infinity]]) + expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' + + describe ".foldable", -> + it "marks line numbers at the start of a foldable region as foldable", -> + presenter = buildPresenter() + expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe false + expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false + expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe false + + it "updates the foldable class on the correct line numbers when the foldable positions change", -> + presenter = buildPresenter() + editor.getBuffer().insert([0, 0], '\n') + expect(lineNumberStateForScreenRow(presenter, 0).foldable).toBe false + expect(lineNumberStateForScreenRow(presenter, 1).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 2).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 3).foldable).toBe false + expect(lineNumberStateForScreenRow(presenter, 4).foldable).toBe false + expect(lineNumberStateForScreenRow(presenter, 5).foldable).toBe true + expect(lineNumberStateForScreenRow(presenter, 6).foldable).toBe false + + it "updates the foldable class on a line number that becomes foldable", -> + presenter = buildPresenter() + expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false + + editor.getBuffer().insert([11, 44], '\n fold me') + expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe true + + editor.undo() + expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false + + describe "for a gutter description that corresponds to a custom gutter", -> + describe ".content", -> + getContentForGutterWithName = (presenter, gutterName) -> + fullState = getStateForGutterWithName(presenter, gutterName) + return fullState.content if fullState + + [presenter, gutter, decorationItem, decorationParams] = [] + [marker1, decoration1, marker2, decoration2, marker3, decoration3] = [] + + # Set the scrollTop to 0 to show the very top of the file. + # Set the explicitHeight to make 10 lines visible. + scrollTop = 0 + lineHeight = 10 + explicitHeight = lineHeight * 10 + lineOverdrawMargin = 1 + + beforeEach -> + # At the beginning of each test, decoration1 and decoration2 are in visible range, + # but not decoration3. + presenter = buildPresenter({explicitHeight, scrollTop, lineHeight, lineOverdrawMargin}) + gutter = editor.addGutter({name: 'test-gutter', visible: true}) + decorationItem = document.createElement('div') + decorationItem.class = 'decoration-item' + decorationParams = type: 'gutter' gutterName: 'test-gutter' class: 'test-class' - item: newItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + item: decorationItem + marker1 = editor.markBufferRange([[0,0],[1,0]]) + decoration1 = editor.decorateMarker(marker1, decorationParams) + marker2 = editor.markBufferRange([[9,0],[12,0]]) + decoration2 = editor.decorateMarker(marker2, decorationParams) + marker3 = editor.markBufferRange([[13,0],[14,0]]) + decoration3 = editor.decorateMarker(marker3, decorationParams) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].item).toBe newItem + # Clear any batched state updates. + presenter.getState() + + it "contains all decorations within the visible buffer range", -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row + expect(decorationState[decoration1.id].height).toBe lineHeight * marker1.getScreenRange().getRowCount() + expect(decorationState[decoration1.id].item).toBe decorationItem + expect(decorationState[decoration1.id].class).toBe 'test-class' + + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration3.id]).toBeUndefined() - - it "updates the class applied to the decoration, if the decoration class is changed", -> - # This changes the decoration item. The visibility of the decoration should not be affected. - newDecorationParams = - type: 'gutter' - gutterName: 'test-gutter' - class: 'new-test-class' - item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].class).toBe 'new-test-class' expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id]).toBeUndefined() - it "updates the type of the decoration, if the decoration type is changed", -> - # This changes the type of the decoration. This should remove the decoration from the gutter. - newDecorationParams = - type: 'line' - gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter. - class: 'test-class' - item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + it "updates when ::scrollTop changes", -> + # This update will scroll decoration1 out of view, and decoration3 into view. + expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id].top).toBeDefined() + + it "updates when ::explicitHeight changes", -> + # This update will make all three decorations visible. + expectStateUpdate presenter, -> presenter.setExplicitHeight(explicitHeight + lineHeight * 5) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id].top).toBeDefined() + + it "updates when ::lineHeight changes", -> + # This update will make all three decorations visible. + expectStateUpdate presenter, -> presenter.setLineHeight(Math.ceil(1.0 * explicitHeight / marker3.getBufferRange().end.row)) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id].top).toBeDefined() + + it "updates when the editor's content changes", -> + # This update will add enough lines to push decoration2 out of view. + expectStateUpdate presenter, -> editor.setTextInBufferRange([[8,0],[9,0]],'\n\n\n\n\n') + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() + expect(decorationState[decoration2.id]).toBeUndefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates when a decoration's marker is modified", -> + # This update will move decoration1 out of view. + expectStateUpdate presenter, -> + newRange = new Range([13,0],[14,0]) + marker1.setBufferRange(newRange) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') expect(decorationState[decoration1.id]).toBeUndefined() expect(decorationState[decoration2.id].top).toBeDefined() expect(decorationState[decoration3.id]).toBeUndefined() - it "updates the gutter the decoration targets, if the decoration gutterName is changed", -> - # This changes which gutter this decoration applies to. Since this gutter does not exist, - # the decoration should not appear in the customDecorations state. - newDecorationParams = + describe "when a decoration's properties are modified", -> + it "updates the item applied to the decoration, if the decoration item is changed", -> + # This changes the decoration class. The visibility of the decoration should not be affected. + newItem = document.createElement('div') + newItem.class = 'new-decoration-item' + newDecorationParams = + type: 'gutter' + gutterName: 'test-gutter' + class: 'test-class' + item: newItem + expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].item).toBe newItem + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates the class applied to the decoration, if the decoration class is changed", -> + # This changes the decoration item. The visibility of the decoration should not be affected. + newDecorationParams = + type: 'gutter' + gutterName: 'test-gutter' + class: 'new-test-class' + item: decorationItem + expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].class).toBe 'new-test-class' + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates the type of the decoration, if the decoration type is changed", -> + # This changes the type of the decoration. This should remove the decoration from the gutter. + newDecorationParams = + type: 'line' + gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter. + class: 'test-class' + item: decorationItem + expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates the gutter the decoration targets, if the decoration gutterName is changed", -> + # This changes which gutter this decoration applies to. Since this gutter does not exist, + # the decoration should not appear in the customDecorations state. + newDecorationParams = + type: 'gutter' + gutterName: 'test-gutter-2' + class: 'new-test-class' + item: decorationItem + expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + # After adding the targeted gutter, the decoration will appear in the state for that gutter, + # since it should be visible. + expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) + newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2') + expect(newGutterDecorationState[decoration1.id].top).toBeDefined() + expect(newGutterDecorationState[decoration2.id]).toBeUndefined() + expect(newGutterDecorationState[decoration3.id]).toBeUndefined() + oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(oldGutterDecorationState[decoration1.id]).toBeUndefined() + expect(oldGutterDecorationState[decoration2.id].top).toBeDefined() + expect(oldGutterDecorationState[decoration3.id]).toBeUndefined() + + it "updates when the editor's mini state changes, and is cleared when the editor is mini", -> + expectStateUpdate presenter, -> editor.setMini(true) + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState).toBeUndefined() + + # The decorations should return to the original state. + expectStateUpdate presenter, -> editor.setMini(false) + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates when a gutter's visibility changes, and is cleared when the gutter is not visible", -> + expectStateUpdate presenter, -> gutter.hide() + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id]).toBeUndefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + # The decorations should return to the original state. + expectStateUpdate presenter, -> gutter.show() + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBeDefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() + + it "updates when a gutter is added to the editor", -> + decorationParams = type: 'gutter' gutterName: 'test-gutter-2' - class: 'new-test-class' - item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) - - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() - - # After adding the targeted gutter, the decoration will appear in the state for that gutter, - # since it should be visible. + class: 'test-class' + marker4 = editor.markBufferRange([[0,0],[1,0]]) + decoration4 = editor.decorateMarker(marker4, decorationParams) expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) - newGutterDecorationState = decorationStateForGutterName(presenter, 'test-gutter-2') - expect(newGutterDecorationState[decoration1.id].top).toBeDefined() - expect(newGutterDecorationState[decoration2.id]).toBeUndefined() - expect(newGutterDecorationState[decoration3.id]).toBeUndefined() - oldGutterDecorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(oldGutterDecorationState[decoration1.id]).toBeUndefined() - expect(oldGutterDecorationState[decoration2.id].top).toBeDefined() - expect(oldGutterDecorationState[decoration3.id]).toBeUndefined() - it "updates when the editor's mini state changes, and is cleared when the editor is mini", -> - expectStateUpdate presenter, -> editor.setMini(true) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState).toBeUndefined() + decorationState = getContentForGutterWithName(presenter, 'test-gutter-2') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id]).toBeUndefined() + expect(decorationState[decoration3.id]).toBeUndefined() + expect(decorationState[decoration4.id].top).toBeDefined() - # The decorations should return to the original state. - expectStateUpdate presenter, -> editor.setMini(false) - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + it "updates when editor lines are folded", -> + oldDimensionsForDecoration1 = + top: lineHeight * marker1.getScreenRange().start.row + height: lineHeight * marker1.getScreenRange().getRowCount() + oldDimensionsForDecoration2 = + top: lineHeight * marker2.getScreenRange().start.row + height: lineHeight * marker2.getScreenRange().getRowCount() - it "updates when a gutter's visibility changes, and is cleared when the gutter is not visible", -> - expectStateUpdate presenter, -> gutter.hide() - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() + # Based on the contents of sample.js, this should affect all but the top + # part of decoration1. + expectStateUpdate presenter, -> editor.foldBufferRow(0) - # The decorations should return to the original state. - expectStateUpdate presenter, -> gutter.show() - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBeDefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBe oldDimensionsForDecoration1.top + expect(decorationState[decoration1.id].height).not.toBe oldDimensionsForDecoration1.height + # Due to the issue described here: https://github.com/atom/atom/issues/6454, decoration2 + # will be bumped up to the row that was folded and still made visible, instead of being + # entirely collapsed. (The same thing will happen to decoration3.) + expect(decorationState[decoration2.id].top).not.toBe oldDimensionsForDecoration2.top + expect(decorationState[decoration2.id].height).not.toBe oldDimensionsForDecoration2.height - it "updates when a gutter is added to the editor", -> - decorationParams = - type: 'gutter' - gutterName: 'test-gutter-2' - class: 'test-class' - marker4 = editor.markBufferRange([[0,0],[1,0]]) - decoration4 = editor.decorateMarker(marker4, decorationParams) - expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) + describe "regardless of what kind of gutter a gutter description corresponds to", -> + [customGutter] = [] - decorationState = decorationStateForGutterName(presenter, 'test-gutter-2') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() - expect(decorationState[decoration4.id].top).toBeDefined() + getStylesForGutterWithName = (presenter, gutterName) -> + fullState = getStateForGutterWithName(presenter, gutterName) + return fullState.styles if fullState - it "updates when editor lines are folded", -> - oldDimensionsForDecoration1 = - top: lineHeight * marker1.getScreenRange().start.row - height: lineHeight * marker1.getScreenRange().getRowCount() - oldDimensionsForDecoration2 = - top: lineHeight * marker2.getScreenRange().start.row - height: lineHeight * marker2.getScreenRange().getRowCount() + beforeEach -> + customGutter = editor.addGutter({name: 'test-gutter', priority: -1, visible: true}) - # Based on the contents of sample.js, this should affect all but the top - # part of decoration1. - expectStateUpdate presenter, -> editor.foldBufferRow(0) + afterEach -> + customGutter.destroy() - decorationState = decorationStateForGutterName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].top).toBe oldDimensionsForDecoration1.top - expect(decorationState[decoration1.id].height).not.toBe oldDimensionsForDecoration1.height - # Due to the issue described here: https://github.com/atom/atom/issues/6454, decoration2 - # will be bumped up to the row that was folded and still made visible, instead of being - # entirely collapsed. (The same thing will happen to decoration3.) - expect(decorationState[decoration2.id].top).not.toBe oldDimensionsForDecoration2.top - expect(decorationState[decoration2.id].height).not.toBe oldDimensionsForDecoration2.height + describe ".scrollHeight", -> + it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> + presenter = buildPresenter() + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 10 + + presenter = buildPresenter(explicitHeight: 500) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe 500 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe 500 + + it "updates when the ::lineHeight changes", -> + presenter = buildPresenter() + expectStateUpdate presenter, -> presenter.setLineHeight(20) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 20 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 20 + + it "updates when the line count changes", -> + presenter = buildPresenter() + expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe editor.getScreenLineCount() * 10 + + it "updates when ::explicitHeight changes", -> + presenter = buildPresenter() + expectStateUpdate presenter, -> presenter.setExplicitHeight(500) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe 500 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe 500 + + it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> + presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) + expectStateUpdate presenter, -> presenter.setScrollTop(300) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight + + expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) + + expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe presenter.contentHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollHeight).toBe presenter.contentHeight + + describe ".scrollTop", -> + it "tracks the value of ::scrollTop", -> + presenter = buildPresenter(scrollTop: 10, explicitHeight: 20) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 10 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 10 + expectStateUpdate presenter, -> presenter.setScrollTop(50) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 50 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 50 + + it "never exceeds the computed scrollHeight minus the computed clientHeight", -> + presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) + expectStateUpdate presenter, -> presenter.setScrollTop(100) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + + expectStateUpdate presenter, -> presenter.setExplicitHeight(60) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + + expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + + expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + + # Scroll top only gets smaller when needed as dimensions change, never bigger + scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop + expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe scrollTopBefore + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe scrollTopBefore + + it "never goes negative", -> + presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) + expectStateUpdate presenter, -> presenter.setScrollTop(-100) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe 0 + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe 0 + + it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> + presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) + expectStateUpdate presenter, -> presenter.setScrollTop(300) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - presenter.clientHeight + + atom.config.set("editor.scrollPastEnd", true) + expectStateUpdate presenter, -> presenter.setScrollTop(300) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) + + expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.contentHeight - presenter.clientHeight + + describe ".backgroundColor", -> + it "is assigned to ::gutterBackgroundColor if present, and to ::backgroundColor otherwise", -> + presenter = buildPresenter(backgroundColor: "rgba(255, 0, 0, 0)", gutterBackgroundColor: "rgba(0, 255, 0, 0)") + expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 255, 0, 0)" + expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 255, 0, 0)" + + expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 255, 0)") + expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 0, 255, 0)" + expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 0, 255, 0)" + + expectStateUpdate presenter, -> presenter.setGutterBackgroundColor("rgba(0, 0, 0, 0)") + expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(255, 0, 0, 0)" + expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(255, 0, 0, 0)" + + expectStateUpdate presenter, -> presenter.setBackgroundColor("rgba(0, 0, 255, 0)") + expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 0, 255, 0)" + expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 0, 255, 0)" # disabled until we fix an issue with display buffer markers not updating when # they are moved on screen but not in the buffer diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 4e990b914..d1d311088 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -36,23 +36,21 @@ describe "TextEditor", -> it "preserves the invisibles setting", -> atom.config.set('editor.showInvisibles', true) - previousInvisibles = editor.displayBuffer.invisibles + previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles editor2 = editor.testSerialization() - expect(editor2.displayBuffer.invisibles).toEqual previousInvisibles - expect(editor2.displayBuffer.tokenizedBuffer.invisibles).toEqual previousInvisibles + expect(previousInvisibles).toBeDefined() + expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles it "updates invisibles if the settings have changed between serialization and deserialization", -> atom.config.set('editor.showInvisibles', true) - previousInvisibles = editor.displayBuffer.invisibles state = editor.serialize() atom.config.set('editor.invisibles', eol: '?') editor2 = TextEditor.deserialize(state) - expect(editor2.displayBuffer.invisibles.eol).toBe '?' - expect(editor2.displayBuffer.tokenizedBuffer.invisibles.eol).toBe '?' + expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?' describe "when the editor is constructed with an initialLine option", -> it "positions the cursor on the specified line", -> @@ -2343,6 +2341,62 @@ describe "TextEditor", -> editor.backspace() expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' + describe ".deleteToPreviousWordBoundary()", -> + describe "when no text is selected", -> + it "deletes to the previous word boundary", -> + editor.setCursorBufferPosition([0, 16]) + editor.addCursorAtBufferPosition([1, 21]) + [cursor1, cursor2] = editor.getCursors() + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' + expect(buffer.lineForRow(1)).toBe ' var sort = (items) {' + expect(cursor1.getBufferPosition()).toEqual [0, 15] + expect(cursor2.getBufferPosition()).toEqual [1, 13] + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe 'var quicksort function () {' + expect(buffer.lineForRow(1)).toBe ' var sort =(items) {' + expect(cursor1.getBufferPosition()).toEqual [0, 14] + expect(cursor2.getBufferPosition()).toEqual [1, 12] + + describe "when text is selected", -> + it "deletes only selected text", -> + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + + describe ".deleteToNextWordBoundary()", -> + describe "when no text is selected", -> + it "deletes to the next word boundary", -> + editor.setCursorBufferPosition([0, 15]) + editor.addCursorAtBufferPosition([1, 24]) + [cursor1, cursor2] = editor.getCursors() + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + expect(cursor1.getBufferPosition()).toEqual [0, 15] + expect(cursor2.getBufferPosition()).toEqual [1, 24] + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe 'var quicksort = () {' + expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' + expect(cursor1.getBufferPosition()).toEqual [0, 15] + expect(cursor2.getBufferPosition()).toEqual [1, 24] + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe 'var quicksort =() {' + expect(buffer.lineForRow(1)).toBe ' var sort = function(it{' + expect(cursor1.getBufferPosition()).toEqual [0, 15] + expect(cursor2.getBufferPosition()).toEqual [1, 24] + + describe "when text is selected", -> + it "deletes only selected text", -> + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' + describe ".deleteToBeginningOfWord()", -> describe "when no text is selected", -> it "deletes all text between the cursor and the beginning of the word", -> diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 3cd776c2b..9d92335af 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -611,7 +611,8 @@ describe "TokenizedBuffer", -> tokenizedBuffer = new TokenizedBuffer({buffer}) fullyTokenize(tokenizedBuffer) - tokenizedBuffer.setInvisibles(space: 'S', tab: 'T') + atom.config.set("editor.showInvisibles", true) + atom.config.set("editor.invisibles", space: 'S', tab: 'T') fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "SST Sa line with tabsTand T spacesSTS" @@ -623,7 +624,7 @@ describe "TokenizedBuffer", -> tokenizedBuffer = new TokenizedBuffer({buffer}) atom.config.set('editor.showInvisibles', true) - tokenizedBuffer.setInvisibles(cr: 'R', eol: 'N') + atom.config.set("editor.invisibles", cr: 'R', eol: 'N') fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N'] @@ -634,7 +635,7 @@ describe "TokenizedBuffer", -> expect(left.endOfLineInvisibles).toBe null expect(right.endOfLineInvisibles).toEqual ['R', 'N'] - tokenizedBuffer.setInvisibles(cr: 'R', eol: false) + atom.config.set("editor.invisibles", cr: 'R', eol: false) expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R'] expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual [] @@ -688,7 +689,8 @@ describe "TokenizedBuffer", -> it "sets leading and trailing whitespace correctly on a line with invisible characters that is copied", -> buffer.setText(" \t a line with tabs\tand \tspaces \t ") - tokenizedBuffer.setInvisibles(space: 'S', tab: 'T') + atom.config.set("editor.showInvisibles", true) + atom.config.set("editor.invisibles", space: 'S', tab: 'T') fullyTokenize(tokenizedBuffer) line = tokenizedBuffer.tokenizedLineForRow(0).copy() @@ -696,7 +698,8 @@ describe "TokenizedBuffer", -> expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0 it "sets the ::firstNonWhitespaceIndex and ::firstTrailingWhitespaceIndex correctly when tokens are split for soft-wrapping", -> - tokenizedBuffer.setInvisibles(space: 'S') + atom.config.set("editor.showInvisibles", true) + atom.config.set("editor.invisibles", space: 'S') buffer.setText(" token ") fullyTokenize(tokenizedBuffer) token = tokenizedBuffer.tokenizedLines[0].tokens[0] diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee new file mode 100644 index 000000000..1771b2363 --- /dev/null +++ b/spec/workspace-element-spec.coffee @@ -0,0 +1,44 @@ +ipc = require 'ipc' +path = require 'path' +temp = require('temp').track() + +describe "WorkspaceElement", -> + workspaceElement = null + + beforeEach -> + workspaceElement = atom.views.getView(atom.workspace) + + describe "the 'window:run-package-specs' command", -> + it "runs the package specs for the active item's project path, or the first project path", -> + spyOn(ipc, 'send') + + # No project paths. Don't try to run specs. + atom.commands.dispatch(workspaceElement, "window:run-package-specs") + expect(ipc.send).not.toHaveBeenCalledWith("run-package-specs") + + projectPaths = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")] + atom.project.setPaths(projectPaths) + + # No active item. Use first project directory. + atom.commands.dispatch(workspaceElement, "window:run-package-specs") + expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) + ipc.send.reset() + + # Active item doesn't implement ::getPath(). Use first project directory. + item = document.createElement("div") + atom.workspace.getActivePane().activateItem(item) + atom.commands.dispatch(workspaceElement, "window:run-package-specs") + expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) + ipc.send.reset() + + # Active item has no path. Use first project directory. + item.getPath = -> null + atom.commands.dispatch(workspaceElement, "window:run-package-specs") + expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) + ipc.send.reset() + + # Active item has path. Use project path for item path. + item.getPath = -> path.join(projectPaths[1], "a-file.txt") + atom.commands.dispatch(workspaceElement, "window:run-package-specs") + expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec")) + ipc.send.reset() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index dac2849c0..0b32ff31f 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -426,6 +426,35 @@ describe "Workspace", -> workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe 1 + describe "::resetFontSize()", -> + it "resets the font size to the window's starting font size", -> + originalFontSize = atom.config.get('editor.fontSize') + + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + 1 + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize - 1 + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + + it "does nothing if the font size has not been changed", -> + originalFontSize = atom.config.get('editor.fontSize') + + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + + it "resets the font size when the editor's font size changes", -> + originalFontSize = atom.config.get('editor.fontSize') + + atom.config.set('editor.fontSize', originalFontSize + 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + atom.config.set('editor.fontSize', originalFontSize - 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe originalFontSize + describe "::openLicense()", -> it "opens the license as plain-text in a buffer", -> waitsForPromise -> workspace.openLicense() diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index 586e72fe8..a160cdb39 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -242,10 +242,15 @@ describe "WorkspaceView", -> describe "the scrollbar visibility class", -> it "has a class based on the style of the scrollbar", -> + style = 'legacy' scrollbarStyle = require 'scrollbar-style' - scrollbarStyle.emitValue 'legacy' + spyOn(scrollbarStyle, 'getPreferredScrollbarStyle').andCallFake -> style + + atom.workspaceView.element.observeScrollbarStyle() expect(atom.workspaceView).toHaveClass 'scrollbars-visible-always' - scrollbarStyle.emitValue 'overlay' + + style = 'overlay' + atom.workspaceView.element.observeScrollbarStyle() expect(atom.workspaceView).toHaveClass 'scrollbars-visible-when-scrolling' describe "editor font styling", -> diff --git a/src/atom.coffee b/src/atom.coffee index e2544640d..0d029b61d 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -151,7 +151,7 @@ class Atom extends Model # Public: A {TooltipManager} instance tooltips: null - # Experimental: A {NotificationManager} instance + # Public: A {NotificationManager} instance notifications: null # Public: A {Project} instance @@ -223,6 +223,8 @@ class Atom extends Model @disposables?.dispose() @disposables = new CompositeDisposable + @displayWindow() unless @inSpecMode() + @setBodyPlatformClass() @loadTime = null @@ -483,22 +485,27 @@ class Atom extends Model # Extended: Set the full screen state of the current window. setFullScreen: (fullScreen=false) -> ipc.send('call-window-method', 'setFullScreen', fullScreen) - if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen") + if fullScreen + document.body.classList.add("fullscreen") + else + document.body.classList.remove("fullscreen") # Extended: Toggle the full screen state of the current window. toggleFullScreen: -> @setFullScreen(not @isFullScreen()) - # Schedule the window to be shown and focused on the next tick. + # Restore the window to its previous dimensions and show it. # - # This is done in a next tick to prevent a white flicker from occurring - # if called synchronously. - displayWindow: ({maximize}={}) -> + # Also restores the full screen and maximized state on the next tick to + # prevent resize glitches. + displayWindow: -> + dimensions = @restoreWindowDimensions() + @show() + setImmediate => - @show() @focus() - @setFullScreen(true) if @workspace.fullScreen - @maximize() if maximize + @setFullScreen(true) if @workspace?.fullScreen + @maximize() if dimensions?.maximized and process.platform isnt 'darwin' # Get the dimensions of this window. # @@ -572,6 +579,13 @@ class Atom extends Model dimensions = @getWindowDimensions() @state.windowDimensions = dimensions if @isValidDimensions(dimensions) + storeWindowBackground: -> + return if @inSpecMode() + + workspaceElement = @views.getView(@workspace) + backgroundColor = window.getComputedStyle(workspaceElement)['background-color'] + window.localStorage.setItem('atom:window-background-color', backgroundColor) + # Call this method when establishing a real application window. startEditorWindow: -> {safeMode} = @getLoadSettings() @@ -582,7 +596,6 @@ class Atom extends Model CommandInstaller.installApmCommand false, (error) -> console.warn error.message if error? - dimensions = @restoreWindowDimensions() @loadConfig() @keymaps.loadBundledKeymaps() @themes.loadBaseStylesheets() @@ -602,12 +615,10 @@ class Atom extends Model @openInitialEmptyEditorIfNecessary() - maximize = dimensions?.maximized and process.platform isnt 'darwin' - @displayWindow({maximize}) - unloadEditorWindow: -> return if not @project + @storeWindowBackground() @state.grammars = @grammars.serialize() @state.project = @project.serialize() @state.workspace = @workspace.serialize() @@ -747,7 +758,7 @@ class Atom extends Model # Only reload stylesheets from non-theme packages for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' pack.reloadStylesheets?() - null + return # Notify the browser project of the window's current project path watchProjectPath: -> diff --git a/src/babel.coffee b/src/babel.coffee index 286ee45ef..7058c85c2 100644 --- a/src/babel.coffee +++ b/src/babel.coffee @@ -32,11 +32,6 @@ defaultOptions = # Target a version of the regenerator runtime that # supports yield so the transpiled code is cleaner/smaller. 'asyncToGenerator' - - # Because Atom is currently packaged with a fork of React v0.11, - # it makes sense to use the reactCompat transform so the React - # JSX transformer produces pre-v0.12 code. - 'reactCompat' ] # Includes support for es7 features listed at: diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index d88314c89..74da80e43 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -9,11 +9,10 @@ _ = require 'underscore-plus' # and maintain the state of all menu items. module.exports = class ApplicationMenu - constructor: (@version) -> + constructor: (@version, @autoUpdateManager) -> @windowTemplates = new WeakMap() @setActiveTemplate(@getDefaultTemplate()) - global.atomApplication.autoUpdateManager.on 'state-changed', (state) => - @showUpdateMenuItem(state) + @autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state) # Public: Updates the entire menu with the given keybindings. # @@ -33,7 +32,7 @@ class ApplicationMenu @menu = Menu.buildFromTemplate(_.deepClone(template)) Menu.setApplicationMenu(@menu) - @showUpdateMenuItem(global.atomApplication.autoUpdateManager.getState()) + @showUpdateMenuItem(@autoUpdateManager.getState()) # Register a BrowserWindow with this application menu. addWindow: (window) -> @@ -162,8 +161,8 @@ class ApplicationMenu firstKeystroke = keystrokesByCommand[command]?[0] return null unless firstKeystroke - modifiers = firstKeystroke.split('-') - key = modifiers.pop() + modifiers = firstKeystroke.split(/-(?=.)/) + key = modifiers.pop().toUpperCase().replace('+', 'Plus') modifiers = modifiers.map (modifier) -> modifier.replace(/shift/ig, "Shift") @@ -171,5 +170,5 @@ class ApplicationMenu .replace(/ctrl/ig, "Ctrl") .replace(/alt/ig, "Alt") - keys = modifiers.concat([key.toUpperCase()]) + keys = modifiers.concat([key]) keys.join("+") diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index dbe29caab..d81660a49 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -56,6 +56,7 @@ class AtomApplication atomProtocolHandler: null resourcePath: null version: null + quitting: false exit: (status) -> app.exit(status) @@ -71,8 +72,8 @@ class AtomApplication @pathsToOpen ?= [] @windows = [] - @autoUpdateManager = new AutoUpdateManager(@version) - @applicationMenu = new ApplicationMenu(@version) + @autoUpdateManager = new AutoUpdateManager(@version, options.test) + @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @listenForArgumentsFromNewProcess() @@ -85,20 +86,26 @@ class AtomApplication else @loadState() or @openPath(options) - openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile}) -> + openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile, profileStartup}) -> if test - @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) + @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile, apiPreviewMode}) else if pathsToOpen.length > 0 - @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode}) + @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup}) else if urlsToOpen.length > 0 @openUrl({urlToOpen, devMode, safeMode, apiPreviewMode}) for urlToOpen in urlsToOpen else - @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode}) # Always open a editor window if this is the first instance of Atom. + # Always open a editor window if this is the first instance of Atom. + @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup}) # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> @windows.splice @windows.indexOf(window), 1 - @applicationMenu?.enableWindowSpecificItems(false) if @windows.length is 0 + if @windows.length is 0 + @applicationMenu?.enableWindowSpecificItems(false) + if process.platform in ['win32', 'linux'] + app.quit() + return + @saveState() unless window.isSpec or @quitting # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> @@ -109,10 +116,14 @@ class AtomApplication unless window.isSpec focusHandler = => @lastFocusedWindow = window + blurHandler = => @saveState() window.browserWindow.on 'focus', focusHandler + window.browserWindow.on 'blur', blurHandler window.browserWindow.once 'closed', => @lastFocusedWindow = null if window is @lastFocusedWindow window.browserWindow.removeListener 'focus', focusHandler + window.browserWindow.removeListener 'blur', blurHandler + window.browserWindow.webContents.once 'did-finish-load', => @saveState() # Creates server to listen for additional atom application launches. # @@ -175,7 +186,7 @@ class AtomApplication @on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues') @on 'application:search-issues', -> require('shell').openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom') - @on 'application:install-update', -> @autoUpdateManager.install() + @on 'application:install-update', => @autoUpdateManager.install() @on 'application:check-for-update', => @autoUpdateManager.check() if process.platform is 'darwin' @@ -198,17 +209,15 @@ class AtomApplication @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) - app.on 'window-all-closed', -> - app.quit() if process.platform in ['win32', 'linux'] - app.on 'before-quit', => - @saveState() + @quitting = true app.on 'will-quit', => @killAllProcesses() @deleteSocketFile() app.on 'will-exit', => + @saveState() unless @windows.every (window) -> window.isSpec @killAllProcesses() @deleteSocketFile() @@ -343,9 +352,10 @@ class AtomApplication # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode. + # :profileStartup - Boolean to control creating a profile of the startup time. # :window - {AtomWindow} to open file paths in. - openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) -> - @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) + openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup, window}) -> + @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, profileStartup, window}) # Public: Opens multiple paths, in existing windows if possible. # @@ -358,7 +368,7 @@ class AtomApplication # :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode. # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. - openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, window}={}) -> + openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, profileStartup, window}={}) -> pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) @@ -388,7 +398,7 @@ class AtomApplication bootstrapScript ?= require.resolve('../window-bootstrap') resourcePath ?= @resourcePath - openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, apiPreviewMode, windowDimensions}) + openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, apiPreviewMode, windowDimensions, profileStartup}) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -420,14 +430,9 @@ class AtomApplication saveState: -> states = [] for window in @windows - if loadSettings = window.getLoadSettings() - unless loadSettings.isSpec - states.push(_.pick(loadSettings, - 'initialPaths' - 'devMode' - 'safeMode' - 'apiPreviewMode' - )) + unless window.isSpec + if loadSettings = window.getLoadSettings() + states.push(initialPaths: loadSettings.initialPaths) @storageFolder.store('application.json', states) loadState: -> @@ -436,9 +441,9 @@ class AtomApplication @openWithOptions({ pathsToOpen: state.initialPaths urlsToOpen: [] - devMode: state.devMode - safeMode: state.safeMode - apiPreviewMode: state.apiPreviewMode + devMode: @devMode + safeMode: @safeMode + apiPreviewMode: @apiPreviewMode }) true else @@ -484,7 +489,7 @@ class AtomApplication # :specPath - The directory to load specs from. # :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages # and ~/.atom/dev/packages, defaults to false. - runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode}) -> + runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode, apiPreviewMode}) -> if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) resourcePath = @resourcePath @@ -496,7 +501,8 @@ class AtomApplication isSpec = true devMode = true safeMode ?= false - new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode}) + apiPreviewMode ?= false + new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode, apiPreviewMode}) runBenchmarks: ({exitWhenDone, specDirectory}={}) -> try @@ -514,13 +520,15 @@ class AtomApplication return {pathToOpen} unless pathToOpen return {pathToOpen} if fs.existsSync(pathToOpen) + pathToOpen = pathToOpen.replace(/[:\s]+$/, '') + [fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':') return {pathToOpen} unless initialLine - return {pathToOpen} unless parseInt(initialLine) > 0 + return {pathToOpen} unless parseInt(initialLine) >= 0 # Convert line numbers to a base of 0 - initialLine -= 1 if initialLine - initialColumn -= 1 if initialColumn + initialLine = Math.max(0, initialLine - 1) if initialLine + initialColumn = Math.max(0, initialColumn - 1) if initialColumn pathToOpen = path.join(path.dirname(pathToOpen), fileToOpen) {pathToOpen, initialLine, initialColumn} diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 358390889..a2c239789 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -15,7 +15,7 @@ module.exports = class AutoUpdateManager _.extend @prototype, EventEmitter.prototype - constructor: (@version) -> + constructor: (@version, @testMode) -> @state = IdleState if process.platform is 'win32' # Squirrel for Windows can't handle query params @@ -53,7 +53,7 @@ class AutoUpdateManager @emitUpdateAvailableEvent(@getWindows()...) # Only released versions should check for updates. - @check(hidePopups: true) unless /\w{7}/.test(@version) + @scheduleUpdateCheck() unless /\w{7}/.test(@version) switch process.platform when 'win32' @@ -75,15 +75,21 @@ class AutoUpdateManager getState: -> @state + scheduleUpdateCheck: -> + checkForUpdates = => @check(hidePopups: true) + fourHours = 1000 * 60 * 60 * 4 + setInterval(checkForUpdates, fourHours) + checkForUpdates() + check: ({hidePopups}={}) -> unless hidePopups autoUpdater.once 'update-not-available', @onUpdateNotAvailable autoUpdater.once 'error', @onUpdateError - autoUpdater.checkForUpdates() + autoUpdater.checkForUpdates() unless @testMode install: -> - autoUpdater.quitAndInstall() + autoUpdater.quitAndInstall() unless @testMode onUpdateNotAvailable: => autoUpdater.removeListener 'error', @onUpdateError diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 908501a1c..68b57a88e 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -110,6 +110,7 @@ parseCommandLine = -> options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.') options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.') options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.') + options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which to run package specs (default: Atom\'s spec directory).') options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.') @@ -132,13 +133,13 @@ parseCommandLine = -> safeMode = args['safe'] apiPreviewMode = args['one'] pathsToOpen = args._ - pathsToOpen = [executedFrom] if executedFrom and pathsToOpen.length is 0 test = args['test'] specDirectory = args['spec-directory'] newWindow = args['new-window'] pidToKillWhenClosed = args['pid'] if args['wait'] logFile = args['log-file'] socketPath = args['socket-path'] + profileStartup = args['profile-startup'] if args['resource-path'] devMode = true @@ -165,6 +166,6 @@ parseCommandLine = -> {resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, apiPreviewMode, safeMode, newWindow, specDirectory, logFile, - socketPath} + socketPath, profileStartup} start() diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index 9748e5557..c7097d711 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -48,6 +48,7 @@ class BufferedProcess constructor: ({command, args, options, stdout, stderr, exit}={}) -> @emitter = new Emitter options ?= {} + @command = command # Related to joyent/node#2318 if process.platform is 'win32' # Quote all arguments and escapes inner quotes @@ -69,50 +70,12 @@ class BufferedProcess cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""] cmdOptions = _.clone(options) cmdOptions.windowsVerbatimArguments = true - @process = ChildProcess.spawn(@getCmdPath(), cmdArgs, cmdOptions) + @spawn(@getCmdPath(), cmdArgs, cmdOptions) else - @process = ChildProcess.spawn(command, args, options) + @spawn(command, args, options) + @killed = false - - stdoutClosed = true - stderrClosed = true - processExited = true - exitCode = 0 - triggerExitCallback = -> - return if @killed - if stdoutClosed and stderrClosed and processExited - exit?(exitCode) - - if stdout - stdoutClosed = false - @bufferStream @process.stdout, stdout, -> - stdoutClosed = true - triggerExitCallback() - - if stderr - stderrClosed = false - @bufferStream @process.stderr, stderr, -> - stderrClosed = true - triggerExitCallback() - - if exit - processExited = false - @process.on 'exit', (code) -> - exitCode = code - processExited = true - triggerExitCallback() - - @process.on 'error', (error) => - handled = false - handle = -> handled = true - - @emitter.emit 'will-throw-error', {error, handle} - - if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0 - error = new Error("Failed to spawn command `#{command}`. Make sure `#{command}` is installed and on your PATH", error.path) - error.name = 'BufferedProcessError' - - throw error unless handled + @handleEvents(stdout, stderr, exit) ### Section: Event Subscription @@ -164,6 +127,8 @@ class BufferedProcess # This is required since killing the cmd.exe does not terminate child # processes. killOnWindows: -> + return unless @process? + parentPid = @process.pid cmd = 'wmic' args = [ @@ -174,7 +139,12 @@ class BufferedProcess 'processid' ] - wmicProcess = ChildProcess.spawn(cmd, args) + try + wmicProcess = ChildProcess.spawn(cmd, args) + catch spawnError + @killProcess() + return + wmicProcess.on 'error', -> # ignore errors output = '' wmicProcess.stdout.on 'data', (data) -> output += data @@ -220,3 +190,55 @@ class BufferedProcess @killProcess() undefined + + spawn: (command, args, options) -> + try + @process = ChildProcess.spawn(command, args, options) + catch spawnError + process.nextTick => @handleError(spawnError) + + handleEvents: (stdout, stderr, exit) -> + return unless @process? + + stdoutClosed = true + stderrClosed = true + processExited = true + exitCode = 0 + triggerExitCallback = -> + return if @killed + if stdoutClosed and stderrClosed and processExited + exit?(exitCode) + + if stdout + stdoutClosed = false + @bufferStream @process.stdout, stdout, -> + stdoutClosed = true + triggerExitCallback() + + if stderr + stderrClosed = false + @bufferStream @process.stderr, stderr, -> + stderrClosed = true + triggerExitCallback() + + if exit + processExited = false + @process.on 'exit', (code) -> + exitCode = code + processExited = true + triggerExitCallback() + + @process.on 'error', (error) => @handleError(error) + return + + handleError: (error) -> + handled = false + handle = -> handled = true + + @emitter.emit 'will-throw-error', {error, handle} + + if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0 + error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path) + error.name = 'BufferedProcessError' + + throw error unless handled diff --git a/src/config-schema.coffee b/src/config-schema.coffee index ba8150a26..548b35fa8 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -98,9 +98,6 @@ module.exports = type: ['string', 'null'] # These can be used as globals or scoped, thus defaults. - completions: - type: ['array', 'object'] - default: [] fontFamily: type: 'string' default: '' @@ -108,6 +105,7 @@ module.exports = type: 'integer' default: 16 minimum: 1 + maximum: 100 lineHeight: type: ['string', 'number'] default: 1.3 diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index bd5ff943e..0fdcf4ab6 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -105,9 +105,9 @@ class ContextMenuManager # Detect deprecated file path as first argument if itemsBySelector? and typeof itemsBySelector isnt 'object' Grim.deprecate """ - ContextMenuManager::add has changed to take a single object as its + `ContextMenuManager::add` has changed to take a single object as its argument. Please see - https://atom.io/docs/api/latest/ContextMenuManager for more info. + https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info. """ itemsBySelector = arguments[1] devMode = arguments[2]?.devMode @@ -116,9 +116,9 @@ class ContextMenuManager for key, value of itemsBySelector unless _.isArray(value) Grim.deprecate """ - ContextMenuManager::add has changed to take a single object as its + `ContextMenuManager::add` has changed to take a single object as its argument. Please see - https://atom.io/docs/api/latest/ContextMenuManager for more info. + https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info. """ itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode) diff --git a/src/custom-gutter-component.coffee b/src/custom-gutter-component.coffee index 380322e8b..39f5a80a1 100644 --- a/src/custom-gutter-component.coffee +++ b/src/custom-gutter-component.coffee @@ -13,6 +13,8 @@ class CustomGutterComponent @domNode = atom.views.getView(@gutter) @decorationsNode = @domNode.firstChild + # Clear the contents in case the domNode is being reused. + @decorationsNode.innerHTML = '' getDomNode: -> @domNode @@ -27,13 +29,14 @@ class CustomGutterComponent @domNode.style.removeProperty('display') @visible = true + # `state` is a subset of the TextEditorPresenter state that is specific + # to this line number gutter. updateSync: (state) -> @oldDimensionsAndBackgroundState ?= {} - newDimensionsAndBackgroundState = state.gutters - setDimensionsAndBackground(@oldDimensionsAndBackgroundState, newDimensionsAndBackgroundState, @decorationsNode) + setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode) @oldDecorationPositionState ?= {} - decorationState = state.gutters.customDecorations[@gutter.name] + decorationState = state.content updatedDecorationIds = new Set for decorationId, decorationInfo of decorationState diff --git a/src/decoration.coffee b/src/decoration.coffee index ca239421e..bc3a21748 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -201,6 +201,6 @@ if Grim.includeDeprecatedAPIs Grim.deprecate 'Use Decoration::getProperties instead' @getProperties() - Decoration::update = -> (newProperties) -> + Decoration::update = (newProperties) -> Grim.deprecate 'Use Decoration::setProperties instead' @setProperties(newProperties) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index d86fd13be..17b11a4c4 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -24,13 +24,13 @@ class DisplayBuffer extends Model horizontalScrollMargin: 6 scopedCharacterWidthsChangeCount: 0 - constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @invisibles}={}) -> + constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles}={}) -> super @emitter = new Emitter @disposables = new CompositeDisposable - @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles}) + @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, ignoreInvisibles}) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @markers = {} @@ -39,9 +39,9 @@ class DisplayBuffer extends Model @decorationsByMarkerId = {} @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange - @disposables.add @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated @updateAllScreenLines() + @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id}) @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) subscribeToScopedConfigSettings: => @@ -86,14 +86,13 @@ class DisplayBuffer extends Model scrollTop: @scrollTop scrollLeft: @scrollLeft tokenizedBuffer: @tokenizedBuffer.serialize() - invisibles: _.clone(@invisibles) deserializeParams: (params) -> params.tokenizedBuffer = TokenizedBuffer.deserialize(params.tokenizedBuffer) params copy: -> - newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @invisibles}) + newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()}) newDisplayBuffer.setScrollTop(@getScrollTop()) newDisplayBuffer.setScrollLeft(@getScrollLeft()) @@ -153,12 +152,12 @@ class DisplayBuffer extends Model @emitter.on 'did-update-markers', callback emitDidChange: (eventProperties, refreshMarkers=true) -> - if refreshMarkers - @pauseMarkerChangeEvents() - @refreshMarkerScreenPositions() @emit 'changed', eventProperties if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', eventProperties - @resumeMarkerChangeEvents() + if refreshMarkers + @refreshMarkerScreenPositions() + @emit 'markers-updated' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-update-markers' updateWrappedScreenLines: -> start = 0 @@ -428,8 +427,8 @@ class DisplayBuffer extends Model setTabLength: (tabLength) -> @tokenizedBuffer.setTabLength(tabLength) - setInvisibles: (@invisibles) -> - @tokenizedBuffer.setInvisibles(@invisibles) + setIgnoreInvisibles: (ignoreInvisibles) -> + @tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles) setSoftWrapped: (softWrapped) -> if softWrapped isnt @softWrapped @@ -886,7 +885,7 @@ class DisplayBuffer extends Model getDecorations: (propertyFilter) -> allDecorations = [] for markerId, decorations of @decorationsByMarkerId - allDecorations = allDecorations.concat(decorations) if decorations? + allDecorations.push(decorations...) if decorations? if propertyFilter? allDecorations = allDecorations.filter (decoration) -> for key, value of propertyFilter @@ -1075,17 +1074,11 @@ class DisplayBuffer extends Model findFoldMarkers: (attributes) -> @buffer.findMarkers(@getFoldMarkerAttributes(attributes)) - getFoldMarkerAttributes: (attributes={}) -> - _.extend(attributes, class: 'fold', displayBufferId: @id) - - pauseMarkerChangeEvents: -> - marker.pauseChangeEvents() for marker in @getMarkers() - return - - resumeMarkerChangeEvents: -> - marker.resumeChangeEvents() for marker in @getMarkers() - @emit 'markers-updated' if Grim.includeDeprecatedAPIs - @emitter.emit 'did-update-markers' + getFoldMarkerAttributes: (attributes) -> + if attributes + _.extend(attributes, @foldMarkerAttributes) + else + @foldMarkerAttributes refreshMarkerScreenPositions: -> for marker in @getMarkers() @@ -1109,7 +1102,7 @@ class DisplayBuffer extends Model handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange - @updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?) + @updateScreenLines(start, end + 1, delta, refreshMarkers: false) @setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if delta < 0 updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> @@ -1120,7 +1113,7 @@ class DisplayBuffer extends Model {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta) screenDelta = screenLines.length - (endScreenRow - startScreenRow) - @screenLines[startScreenRow...endScreenRow] = screenLines + _.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000) @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions) @findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta) @@ -1132,11 +1125,7 @@ class DisplayBuffer extends Model screenDelta: screenDelta bufferDelta: bufferDelta - if options.delayChangeEvent - @pauseMarkerChangeEvents() - @pendingChangeEvent = changeEvent - else - @emitDidChange(changeEvent, options.refreshMarkers) + @emitDidChange(changeEvent, options.refreshMarkers) buildScreenLines: (startBufferRow, endBufferRow) -> screenLines = [] @@ -1216,11 +1205,6 @@ class DisplayBuffer extends Model @scrollWidth += 1 unless @isSoftWrapped() @setScrollLeft(Math.min(@getScrollLeft(), @getMaxScrollLeft())) - handleBufferMarkersUpdated: => - if event = @pendingChangeEvent - @pendingChangeEvent = null - @emitDidChange(event, false) - handleBufferMarkerCreated: (textBufferMarker) => @createFoldForMarker(textBufferMarker) if textBufferMarker.matchesParams(@getFoldMarkerAttributes()) if marker = @getMarker(textBufferMarker.id) diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee index e7fec35d5..5fa2f85f4 100644 --- a/src/gutter-container-component.coffee +++ b/src/gutter-container-component.coffee @@ -1,3 +1,4 @@ +_ = require 'underscore-plus' CustomGutterComponent = require './custom-gutter-component' LineNumberGutterComponent = require './line-number-gutter-component' @@ -26,11 +27,11 @@ class GutterContainerComponent updateSync: (state) -> # The GutterContainerComponent expects the gutters to be sorted in the order # they should appear. - newState = state.gutters.sortedDescriptions + newState = state.gutters newGutterComponents = [] newGutterComponentsByGutterName = {} - for {gutter, visible} in newState + for {gutter, visible, styles, content} in newState gutterComponent = @gutterComponentsByGutterName[gutter.name] if not gutterComponent if gutter.name is 'line-number' @@ -38,8 +39,20 @@ class GutterContainerComponent @lineNumberGutterComponent = gutterComponent else gutterComponent = new CustomGutterComponent({gutter}) + if visible then gutterComponent.showNode() else gutterComponent.hideNode() - gutterComponent.updateSync(state) + # Pass the gutter only the state that it needs. + if gutter.name is 'line-number' + # For ease of use in the line number gutter component, set the shared + # 'styles' as a field under the 'content'. + gutterSubstate = _.clone(content) + gutterSubstate.styles = styles + else + # Custom gutter 'content' is keyed on gutter name, so we cannot set + # 'styles' as a subfield directly under it. + gutterSubstate = {content, styles} + gutterComponent.updateSync(gutterSubstate) + newGutterComponents.push({ name: gutter.name, component: gutterComponent, diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee index 9c77b6647..351275f63 100644 --- a/src/line-number-gutter-component.coffee +++ b/src/line-number-gutter-component.coffee @@ -13,6 +13,7 @@ class LineNumberGutterComponent @domNode = atom.views.getView(@gutter) @lineNumbersNode = @domNode.firstChild + @lineNumbersNode.innerHTML = '' @domNode.addEventListener 'click', @onClick @domNode.addEventListener 'mousedown', @onMouseDown @@ -30,19 +31,25 @@ class LineNumberGutterComponent @domNode.style.removeProperty('display') @visible = true + # `state` is a subset of the TextEditorPresenter state that is specific + # to this line number gutter. updateSync: (state) -> - @newState = state.gutters.lineNumberGutter - @oldState ?= {lineNumbers: {}} + @newState = state + @oldState ?= + lineNumbers: {} + styles: {} @appendDummyLineNumber() unless @dummyLineNumberNode? - newDimensionsAndBackgroundState = state.gutters - setDimensionsAndBackground(@oldState, newDimensionsAndBackgroundState, @lineNumbersNode) + setDimensionsAndBackground(@oldState.styles, @newState.styles, @lineNumbersNode) if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits @updateDummyLineNumber() node.remove() for id, node of @lineNumberNodesById - @oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}} + @oldState = + maxLineNumberDigits: @newState.maxLineNumberDigits + lineNumbers: {} + styles: {} @lineNumberNodesById = {} @updateLineNumbers() diff --git a/src/marker.coffee b/src/marker.coffee index 5d1e35570..942e25606 100644 --- a/src/marker.coffee +++ b/src/marker.coffee @@ -48,7 +48,6 @@ class Marker oldTailBufferPosition: null oldTailScreenPosition: null wasValid: true - deferredChangeEvents: null ### Section: Construction and Destruction @@ -332,11 +331,11 @@ class Marker newTailScreenPosition = @getTailScreenPosition() isValid = @isValid() - return if _.isEqual(isValid, @wasValid) and - _.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and - _.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and - _.isEqual(newTailBufferPosition, @oldTailBufferPosition) and - _.isEqual(newTailScreenPosition, @oldTailScreenPosition) + return if isValid is @wasValid and + newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and + newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and + newTailBufferPosition.isEqual(@oldTailBufferPosition) and + newTailScreenPosition.isEqual(@oldTailScreenPosition) changeEvent = { @oldHeadScreenPosition, newHeadScreenPosition, @@ -347,11 +346,8 @@ class Marker isValid } - if @deferredChangeEvents? - @deferredChangeEvents.push(changeEvent) - else - @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change', changeEvent + @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs + @emitter.emit 'did-change', changeEvent @oldHeadBufferPosition = newHeadBufferPosition @oldHeadScreenPosition = newHeadScreenPosition @@ -359,18 +355,6 @@ class Marker @oldTailScreenPosition = newTailScreenPosition @wasValid = isValid - pauseChangeEvents: -> - @deferredChangeEvents = [] - - resumeChangeEvents: -> - if deferredChangeEvents = @deferredChangeEvents - @deferredChangeEvents = null - - for event in deferredChangeEvents - @emit 'changed', event if Grim.includeDeprecatedAPIs - @emitter.emit 'did-change', event - return - getPixelRange: -> @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false) diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee index f811a501f..f4ebd97db 100644 --- a/src/notification-manager.coffee +++ b/src/notification-manager.coffee @@ -1,8 +1,8 @@ {Emitter, Disposable} = require 'event-kit' Notification = require '../src/notification' -# Experimental: Allows messaging the user. This will likely change, dont use -# quite yet! +# Public: A notification manager used to create {Notification}s to be shown +# to the user. module.exports = class NotificationManager constructor: -> @@ -13,6 +13,12 @@ class NotificationManager Section: Events ### + # Public: Invoke the given callback after a notification has been added. + # + # * `callback` {Function} to be called after the notification is added. + # * `notification` The {Notification} that was added. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddNotification: (callback) -> @emitter.on 'did-add-notification', callback @@ -20,18 +26,43 @@ class NotificationManager Section: Adding Notifications ### + # Public: Add a success notification. + # + # * `message` A {String} message + # * `options` An options {Object} with optional keys such as: + # * `detail` A {String} with additional details about the notification addSuccess: (message, options) -> @addNotification(new Notification('success', message, options)) + # Public: Add an informational notification. + # + # * `message` A {String} message + # * `options` An options {Object} with optional keys such as: + # * `detail` A {String} with additional details about the notification addInfo: (message, options) -> @addNotification(new Notification('info', message, options)) + # Public: Add a warning notification. + # + # * `message` A {String} message + # * `options` An options {Object} with optional keys such as: + # * `detail` A {String} with additional details about the notification addWarning: (message, options) -> @addNotification(new Notification('warning', message, options)) + # Public: Add an error notification. + # + # * `message` A {String} message + # * `options` An options {Object} with optional keys such as: + # * `detail` A {String} with additional details about the notification addError: (message, options) -> @addNotification(new Notification('error', message, options)) + # Public: Add a fatal error notification. + # + # * `message` A {String} message + # * `options` An options {Object} with optional keys such as: + # * `detail` A {String} with additional details about the notification addFatalError: (message, options) -> @addNotification(new Notification('fatal', message, options)) @@ -47,7 +78,10 @@ class NotificationManager Section: Getting Notifications ### - getNotifications: -> @notifications + # Public: Get all the notifications. + # + # Returns an {Array} of {Notifications}s. + getNotifications: -> @notifications.slice() ### Section: Managing Notifications diff --git a/src/notification.coffee b/src/notification.coffee index 43044c182..9dfffc59a 100644 --- a/src/notification.coffee +++ b/src/notification.coffee @@ -1,6 +1,6 @@ {Emitter} = require 'event-kit' -# Experimental: This will likely change, do not use. +# Public: A notification to the user containing a message and type. module.exports = class Notification constructor: (@type, @message, @options={}) -> @@ -18,8 +18,10 @@ class Notification getOptions: -> @options + # Public: Retrieves the {String} type. getType: -> @type + # Public: Retrieves the {String} message. getMessage: -> @message getTimestamp: -> @timestamp diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 8527188d3..8d2f4d663 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -303,6 +303,9 @@ class PackageManager # of the first package isn't skewed by being the first to require atom require '../exports/atom' + # TODO: remove after a few atom versions. + @uninstallAutocompletePlus() + packagePaths = @getAvailablePackagePaths() packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath)) packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath) @@ -409,6 +412,39 @@ class PackageManager message = "Failed to load the #{path.basename(packagePath)} package" atom.notifications.addError(message, {stack, detail, dismissable: true}) + # TODO: remove these autocomplete-plus specific helpers after a few versions. + uninstallAutocompletePlus: -> + packageDir = null + devDir = path.join("dev", "packages") + for packageDirPath in @packageDirPaths + if not packageDirPath.endsWith(devDir) + packageDir = packageDirPath + break + + if packageDir? + dirsToRemove = [ + path.join(packageDir, 'autocomplete-plus') + path.join(packageDir, 'autocomplete-atom-api') + path.join(packageDir, 'autocomplete-css') + path.join(packageDir, 'autocomplete-html') + path.join(packageDir, 'autocomplete-snippets') + ] + for dirToRemove in dirsToRemove + @uninstallDirectory(dirToRemove) + return + + uninstallDirectory: (directory) -> + symlinkPromise = new Promise (resolve) -> + fs.isSymbolicLink directory, (isSymLink) -> resolve(isSymLink) + + dirPromise = new Promise (resolve) -> + fs.isDirectory directory, (isDir) -> resolve(isDir) + + Promise.all([symlinkPromise, dirPromise]).then (values) -> + [isSymLink, isDir] = values + if not isSymLink and isDir + fs.remove directory, -> + if Grim.includeDeprecatedAPIs EmitterMixin = require('emissary').Emitter EmitterMixin.includeInto(PackageManager) diff --git a/src/package.coffee b/src/package.coffee index 8003d2d8d..b48ed8014 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -1,4 +1,5 @@ path = require 'path' +normalizePackageData = null _ = require 'underscore-plus' async = require 'async' @@ -24,6 +25,13 @@ class Package @resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}" packagePath?.startsWith(@resourcePathWithTrailingSlash) + @normalizeMetadata: (metadata) -> + unless metadata?._id + normalizePackageData ?= require 'normalize-package-data' + normalizePackageData(metadata) + if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string' + metadata.repository.url = metadata.repository.url.replace(/^git\+/, '') + @loadMetadata: (packagePath, ignoreErrors=false) -> packageName = path.basename(packagePath) if @isBundledPackagePath(packagePath) @@ -32,8 +40,10 @@ class Package if metadataPath = CSON.resolve(path.join(packagePath, 'package')) try metadata = CSON.readFileSync(metadataPath) + @normalizeMetadata(metadata) catch error throw error unless ignoreErrors + metadata ?= {} metadata.name = packageName @@ -161,9 +171,9 @@ class Package if @mainModule.config? and typeof @mainModule.config is 'object' atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} else if includeDeprecatedAPIs and @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' - deprecate """Use a config schema instead. See the configuration section + deprecate("""Use a config schema instead. See the configuration section of https://atom.io/docs/latest/hacking-atom-package-word-count and - https://atom.io/docs/api/latest/Config for more details""" + https://atom.io/docs/api/latest/Config for more details""", {packageName: @name}) atom.config.setDefaults(@name, @mainModule.configDefaults) @mainModule.activateConfig?() @configActivated = true @@ -191,7 +201,20 @@ class Package for [menuPath, map] in @menus when map['context-menu']? try - @activationDisposables.add(atom.contextMenu.add(map['context-menu'])) + itemsBySelector = map['context-menu'] + + if includeDeprecatedAPIs + # Detect deprecated format for items object + for key, value of itemsBySelector + unless _.isArray(value) + deprecate(""" + The context menu CSON format has changed. Please see + https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format + for more info. + """, {packageName: @name}) + itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector) + + @activationDisposables.add(atom.contextMenu.add(itemsBySelector)) catch error if error.code is 'EBADSELECTOR' error.message += " in #{menuPath}" @@ -465,7 +488,7 @@ class Package @activationCommands[selector].push(commands...) if includeDeprecatedAPIs and @metadata.activationEvents? - deprecate """ + deprecate(""" Use `activationCommands` instead of `activationEvents` in your package.json Commands should be grouped by selector as follows: ```json @@ -474,7 +497,7 @@ class Package "atom-text-editor": ["foo:quux"] } ``` - """ + """, {packageName: @name}) if _.isArray(@metadata.activationEvents) for eventName in @metadata.activationEvents @activationCommands['atom-workspace'] ?= [] diff --git a/src/pane-resize-handle-element.coffee b/src/pane-resize-handle-element.coffee index 47548bc60..078bb44ac 100644 --- a/src/pane-resize-handle-element.coffee +++ b/src/pane-resize-handle-element.coffee @@ -12,6 +12,9 @@ class PaneResizeHandleElement extends HTMLElement @isHorizontal = @parentElement.classList.contains("horizontal") @classList.add if @isHorizontal then 'horizontal' else 'vertical' + detachedCallback: -> + @resizeStopped() + resizeToFitContent: -> # clear flex-grow css style of both pane @previousSibling.model.setFlexScale(1) @@ -43,6 +46,7 @@ class PaneResizeHandleElement extends HTMLElement resizePane: ({clientX, clientY, which}) -> return @resizeStopped() unless which is 1 + return @resizeStopped() unless @previousSibling? and @nextSibling? if @isHorizontal totalWidth = @previousSibling.clientWidth + @nextSibling.clientWidth diff --git a/src/selection.coffee b/src/selection.coffee index f806acf43..6ec874203 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -404,6 +404,20 @@ class Selection extends Model @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow()) @deleteSelectedText() + # Public: Removes the selection or, if nothing is selected, then all + # characters from the start of the selection back to the previous word + # boundary. + deleteToPreviousWordBoundary: -> + @selectToPreviousWordBoundary() if @isEmpty() + @deleteSelectedText() + + # Public: Removes the selection or, if nothing is selected, then all + # characters from the start of the selection up to the next word + # boundary. + deleteToNextWordBoundary: -> + @selectToNextWordBoundary() if @isEmpty() + @deleteSelectedText() + # Public: Removes from the start of the selection to the beginning of the # current word if the selection is empty otherwise it deletes the selection. deleteToBeginningOfWord: -> diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index e284516fa..eb01e0f23 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -70,7 +70,7 @@ class TextEditorComponent @scrollViewNode.classList.add('scroll-view') @domNode.appendChild(@scrollViewNode) - @mountGutterContainerComponent() if @presenter.getState().gutters.sortedDescriptions.length + @mountGutterContainerComponent() if @presenter.getState().gutters.length @hiddenInputComponent = new InputComponent @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode()) @@ -137,7 +137,7 @@ class TextEditorComponent else @domNode.style.height = '' - if @newState.gutters.sortedDescriptions.length + if @newState.gutters.length @mountGutterContainerComponent() unless @gutterContainerComponent? @gutterContainerComponent.updateSync(@newState) else @@ -349,15 +349,15 @@ class TextEditorComponent if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally - previousScrollLeft = @editor.getScrollLeft() + previousScrollLeft = @presenter.getScrollLeft() @presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)) - event.preventDefault() unless previousScrollLeft is @editor.getScrollLeft() + event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() else # Scrolling vertically @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) - previousScrollTop = @presenter.scrollTop + previousScrollTop = @presenter.getScrollTop() @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) - event.preventDefault() unless previousScrollTop is @editor.getScrollTop() + event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() onScrollViewScroll: => if @mounted @@ -619,6 +619,7 @@ class TextEditorComponent if clientWidth > 0 @presenter.setContentFrameWidth(clientWidth) + @presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0) @presenter.setBoundingClientRect(@hostElement.getBoundingClientRect()) measureWindowSize: -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 95c60f9a0..ab18be8bf 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -281,6 +281,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo( 'core:cut': -> @cutSelectedText() 'core:copy': -> @copySelectedText() 'core:paste': -> @pasteText() + 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() + 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 55b88f2b8..70c26a1a3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -13,12 +13,13 @@ class TextEditorPresenter overlayDimensions: {} constructor: (params) -> - {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight} = params + {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth + @gutterWidth ?= 0 @disposables = new CompositeDisposable @emitter = new Emitter @@ -55,18 +56,6 @@ class TextEditorPresenter isBatching: -> @updating is false - # Private: Executes `fn` if `isBatching()` is false, otherwise sets `@[flagName]` to `true` for later processing. In either cases, it calls `emitDidUpdateState`. - # * `flagName` {String} name of a property of this presenter - # * `fn` {Function} to call when not batching. - batch: (flagName, fn) -> - if @isBatching() - @[flagName] = true - else - fn.apply(this) - @[flagName] = false - - @emitDidUpdateState() - # Public: Gets this presenter's state, updating it just in time before returning from this function. # Returns a state {Object}, useful for rendering to screen. getState: -> @@ -94,40 +83,68 @@ class TextEditorPresenter @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState @updating = false + @resetTrackedUpdates() + @state + resetTrackedUpdates: -> + @shouldUpdateFocusedState = false + @shouldUpdateHeightState = false + @shouldUpdateVerticalScrollState = false + @shouldUpdateHorizontalScrollState = false + @shouldUpdateScrollbarsState = false + @shouldUpdateHiddenInputState = false + @shouldUpdateContentState = false + @shouldUpdateDecorations = false + @shouldUpdateLinesState = false + @shouldUpdateCursorsState = false + @shouldUpdateOverlaysState = false + @shouldUpdateLineNumberGutterState = false + @shouldUpdateLineNumbersState = false + @shouldUpdateGutterOrderState = false + @shouldUpdateCustomGutterDecorationState = false + observeModel: -> @disposables.add @model.onDidChange => @updateContentDimensions() @updateEndRow() - @updateHeightState() - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateScrollbarsState() - @updateContentState() - @updateDecorations() - @updateLinesState() - @updateLineNumberGutterState() - @updateLineNumbersState() - @updateGutterOrderState() - @updateCustomGutterDecorationState() + @shouldUpdateHeightState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateLineNumberGutterState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true + + @emitDidUpdateState() @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) - @disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this)) + @disposables.add @model.onDidChangePlaceholderText => + @shouldUpdateContentState = true + + @emitDidUpdateState() @disposables.add @model.onDidChangeMini => + @shouldUpdateScrollbarsState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateLineNumberGutterState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true @updateScrollbarDimensions() - @updateScrollbarsState() - @updateContentState() - @updateDecorations() - @updateLinesState() - @updateLineNumberGutterState() - @updateLineNumbersState() @updateCommonGutterState() - @updateGutterOrderState() - @updateCustomGutterDecorationState() + + @emitDidUpdateState() @disposables.add @model.onDidChangeLineNumberGutterVisible => - @updateLineNumberGutterState() + @shouldUpdateLineNumberGutterState = true + @shouldUpdateGutterOrderState = true @updateCommonGutterState() - @updateGutterOrderState() + + @emitDidUpdateState() @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this)) @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) @disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this)) @@ -153,24 +170,32 @@ class TextEditorPresenter @configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) => @showIndentGuide = newValue - @updateContentState() + @shouldUpdateContentState = true + + @emitDidUpdateState() @configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) => @scrollPastEnd = newValue + @shouldUpdateVerticalScrollState = true + @shouldUpdateScrollbarsState = true @updateScrollHeight() - @updateVerticalScrollState() - @updateScrollbarsState() + + @emitDidUpdateState() @configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) => @showLineNumbers = newValue - @updateLineNumberGutterState() + @shouldUpdateLineNumberGutterState = true + @shouldUpdateGutterOrderState = true @updateCommonGutterState() - @updateGutterOrderState() + + @emitDidUpdateState() didChangeGrammar: -> @observeConfig() - @updateContentState() - @updateLineNumberGutterState() + @shouldUpdateContentState = true + @shouldUpdateLineNumberGutterState = true + @shouldUpdateGutterOrderState = true @updateCommonGutterState() - @updateGutterOrderState() + + @emitDidUpdateState() buildState: -> @state = @@ -183,11 +208,13 @@ class TextEditorPresenter lines: {} highlights: {} overlays: {} - gutters: - sortedDescriptions: [] - customDecorations: {} - lineNumberGutter: - lineNumbers: {} + gutters: [] + # Shared state that is copied into ``@state.gutters`. + @sharedGutterStyles = {} + @customGutterDecorations = {} + @lineNumberGutter = + lineNumbers: {} + @updateState() updateState: -> @@ -213,32 +240,34 @@ class TextEditorPresenter @updateGutterOrderState() @updateCustomGutterDecorationState() - updateFocusedState: -> @batch "shouldUpdateFocusedState", -> + @resetTrackedUpdates() + + updateFocusedState: -> @state.focused = @focused - updateHeightState: -> @batch "shouldUpdateHeightState", -> + updateHeightState: -> if @autoHeight @state.height = @contentHeight else @state.height = null - updateVerticalScrollState: -> @batch "shouldUpdateVerticalScrollState", -> + updateVerticalScrollState: -> @state.content.scrollHeight = @scrollHeight - @state.gutters.scrollHeight = @scrollHeight + @sharedGutterStyles.scrollHeight = @scrollHeight @state.verticalScrollbar.scrollHeight = @scrollHeight @state.content.scrollTop = @scrollTop - @state.gutters.scrollTop = @scrollTop + @sharedGutterStyles.scrollTop = @scrollTop @state.verticalScrollbar.scrollTop = @scrollTop - updateHorizontalScrollState: -> @batch "shouldUpdateHorizontalScrollState", -> + updateHorizontalScrollState: -> @state.content.scrollWidth = @scrollWidth @state.horizontalScrollbar.scrollWidth = @scrollWidth @state.content.scrollLeft = @scrollLeft @state.horizontalScrollbar.scrollLeft = @scrollLeft - updateScrollbarsState: -> @batch "shouldUpdateScrollbarsState", -> + updateScrollbarsState: -> @state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0 @state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight @state.horizontalScrollbar.right = @verticalScrollbarWidth @@ -247,7 +276,7 @@ class TextEditorPresenter @state.verticalScrollbar.width = @measuredVerticalScrollbarWidth @state.verticalScrollbar.bottom = @horizontalScrollbarHeight - updateHiddenInputState: -> @batch "shouldUpdateHiddenInputState", -> + updateHiddenInputState: -> return unless lastCursor = @model.getLastCursor() {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange()) @@ -264,14 +293,14 @@ class TextEditorPresenter @state.hiddenInput.height = height @state.hiddenInput.width = Math.max(width, 2) - updateContentState: -> @batch "shouldUpdateContentState", -> + updateContentState: -> @state.content.scrollWidth = @scrollWidth @state.content.scrollLeft = @scrollLeft @state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null - updateLinesState: -> @batch "shouldUpdateLinesState", -> + updateLinesState: -> return unless @startRow? and @endRow? and @lineHeight? visibleLineIds = {} @@ -316,7 +345,7 @@ class TextEditorPresenter top: row * @lineHeight decorationClasses: @lineDecorationClassesForRow(row) - updateCursorsState: -> @batch "shouldUpdateCursorsState", -> + updateCursorsState: -> @state.content.cursors = {} @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation return @@ -334,7 +363,7 @@ class TextEditorPresenter @emitDidUpdateState() - updateOverlaysState: -> @batch "shouldUpdateOverlaysState", -> + updateOverlaysState: -> return unless @hasOverlayPositionRequirements() visibleDecorationIds = {} @@ -351,10 +380,9 @@ class TextEditorPresenter pixelPosition = @pixelPositionForScreenPosition(screenPosition) {scrollTop, scrollLeft} = @state.content - gutterWidth = @boundingClientRect.width - @contentFrameWidth top = pixelPosition.top + @lineHeight - scrollTop - left = pixelPosition.left + gutterWidth - scrollLeft + left = pixelPosition.left + @gutterWidth - scrollLeft if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions @@ -383,11 +411,11 @@ class TextEditorPresenter return - updateLineNumberGutterState: -> @batch "shouldUpdateLineNumberGutterState", -> - @state.gutters.lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length + updateLineNumberGutterState: -> + @lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length updateCommonGutterState: -> - @state.gutters.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)" + @sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)" @gutterBackgroundColor else @backgroundColor @@ -395,30 +423,45 @@ class TextEditorPresenter didAddGutter: (gutter) -> gutterDisposables = new CompositeDisposable gutterDisposables.add gutter.onDidChangeVisible => - @updateGutterOrderState() - @updateCustomGutterDecorationState() + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true + + @emitDidUpdateState() gutterDisposables.add gutter.onDidDestroy => @disposables.remove(gutterDisposables) gutterDisposables.dispose() - @updateGutterOrderState() + @shouldUpdateGutterOrderState = true + + @emitDidUpdateState() # It is not necessary to @updateCustomGutterDecorationState here. # The destroyed gutter will be removed from the list of gutters in @state, # and thus will be removed from the DOM. @disposables.add(gutterDisposables) - @updateGutterOrderState() - @updateCustomGutterDecorationState() + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true + + @emitDidUpdateState() updateGutterOrderState: -> - @batch "shouldUpdateGutterOrderState", -> - @state.gutters.sortedDescriptions = [] - if @model.isMini() - return - for gutter in @model.getGutters() - isVisible = @gutterIsVisible(gutter) - @state.gutters.sortedDescriptions.push({gutter, visible: isVisible}) + @state.gutters = [] + if @model.isMini() + return + for gutter in @model.getGutters() + isVisible = @gutterIsVisible(gutter) + if gutter.name is 'line-number' + content = @lineNumberGutter + else + @customGutterDecorations[gutter.name] ?= {} + content = @customGutterDecorations[gutter.name] + @state.gutters.push({ + gutter, + visible: isVisible, + styles: @sharedGutterStyles, + content, + }) # Updates the decoration state for the gutter with the given gutterName. - # @state.gutters.customDecorations is an {Object}, with the form: + # @customGutterDecorations is an {Object}, with the form: # * gutterName : { # decoration.id : { # top: # of pixels from top @@ -428,25 +471,44 @@ class TextEditorPresenter # } # } updateCustomGutterDecorationState: -> - @batch 'shouldUpdateCustomGutterDecorationState', => - return unless @startRow? and @endRow? and @lineHeight? + return unless @startRow? and @endRow? and @lineHeight? - @state.gutters.customDecorations = {} - return if @model.isMini() + if @model.isMini() + # Mini editors have no gutter decorations. + # We clear instead of reassigning to preserve the reference. + @clearAllCustomGutterDecorations() - for gutter in @model.getGutters() - gutterName = gutter.name - @state.gutters.customDecorations[gutterName] = {} - return if not @gutterIsVisible(gutter) + for gutter in @model.getGutters() + gutterName = gutter.name + gutterDecorations = @customGutterDecorations[gutterName] + if gutterDecorations + # Clear the gutter decorations; they are rebuilt. + # We clear instead of reassigning to preserve the reference. + @clearDecorationsForCustomGutterName(gutterName) + else + @customGutterDecorations[gutterName] = {} + return if not @gutterIsVisible(gutter) - relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1) - relevantDecorations.forEach (decoration) => - decorationRange = decoration.getMarker().getScreenRange() - @state.gutters.customDecorations[gutterName][decoration.id] = - top: @lineHeight * decorationRange.start.row - height: @lineHeight * decorationRange.getRowCount() - item: decoration.getProperties().item - class: decoration.getProperties().class + relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1) + relevantDecorations.forEach (decoration) => + decorationRange = decoration.getMarker().getScreenRange() + @customGutterDecorations[gutterName][decoration.id] = + top: @lineHeight * decorationRange.start.row + height: @lineHeight * decorationRange.getRowCount() + item: decoration.getProperties().item + class: decoration.getProperties().class + + clearAllCustomGutterDecorations: -> + allGutterNames = Object.keys(@customGutterDecorations) + for gutterName in allGutterNames + @clearDecorationsForCustomGutterName(gutterName) + + clearDecorationsForCustomGutterName: (gutterName) -> + gutterDecorations = @customGutterDecorations[gutterName] + if gutterDecorations + allDecorationIds = Object.keys(gutterDecorations) + for decorationId in allDecorationIds + delete gutterDecorations[decorationId] gutterIsVisible: (gutterModel) -> isVisible = gutterModel.isVisible() @@ -454,7 +516,7 @@ class TextEditorPresenter isVisible = isVisible and @showLineNumbers isVisible - updateLineNumbersState: -> @batch "shouldUpdateLineNumbersState", -> + updateLineNumbersState: -> return unless @startRow? and @endRow? and @lineHeight? visibleLineNumberIds = {} @@ -484,7 +546,7 @@ class TextEditorPresenter decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - @state.gutters.lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} + @lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} visibleLineNumberIds[id] = true if @mouseWheelScreenRow? @@ -494,8 +556,8 @@ class TextEditorPresenter id += '-' + wrapCount if wrapCount > 0 visibleLineNumberIds[id] = true - for id of @state.gutters.lineNumberGutter.lineNumbers - delete @state.gutters.lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id] + for id of @lineNumberGutter.lineNumbers + delete @lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id] return @@ -541,7 +603,8 @@ class TextEditorPresenter if @baseCharacterWidth? oldContentWidth = @contentWidth - @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), Infinity]).left + clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() + @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width if @contentHeight isnt oldContentHeight @@ -670,8 +733,10 @@ class TextEditorPresenter @startBlinkingCursors() else @stopBlinkingCursors(false) - @updateFocusedState() - @updateHiddenInputState() + @shouldUpdateFocusedState = true + @shouldUpdateHiddenInputState = true + + @emitDidUpdateState() setScrollTop: (scrollTop) -> scrollTop = @constrainScrollTop(scrollTop) @@ -682,14 +747,19 @@ class TextEditorPresenter @updateStartRow() @updateEndRow() @didStartScrolling() - @updateVerticalScrollState() - @updateHiddenInputState() - @updateDecorations() - @updateLinesState() - @updateCursorsState() - @updateLineNumbersState() - @updateCustomGutterDecorationState() - @updateOverlaysState() + @shouldUpdateVerticalScrollState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateCustomGutterDecorationState = true + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() + + getScrollTop: -> + @scrollTop didStartScrolling: -> if @stoppedScrollingTimeoutId? @@ -703,11 +773,11 @@ class TextEditorPresenter @state.content.scrollingVertically = false if @mouseWheelScreenRow? @mouseWheelScreenRow = null - @updateLinesState() - @updateLineNumbersState() - @updateCustomGutterDecorationState() - else - @emitDidUpdateState() + @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateCustomGutterDecorationState = true + + @emitDidUpdateState() setScrollLeft: (scrollLeft) -> scrollLeft = @constrainScrollLeft(scrollLeft) @@ -715,10 +785,15 @@ class TextEditorPresenter oldScrollLeft = @scrollLeft @scrollLeft = scrollLeft @model.setScrollLeft(scrollLeft) - @updateHorizontalScrollState() - @updateHiddenInputState() - @updateCursorsState() unless oldScrollLeft? - @updateOverlaysState() + @shouldUpdateHorizontalScrollState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateCursorsState = true unless oldScrollLeft? + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() + + getScrollLeft: -> + @scrollLeft setHorizontalScrollbarHeight: (horizontalScrollbarHeight) -> unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight @@ -726,10 +801,12 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @model.setHorizontalScrollbarHeight(horizontalScrollbarHeight) @updateScrollbarDimensions() - @updateScrollbarsState() - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateCursorsState() unless oldHorizontalScrollbarHeight? + @shouldUpdateScrollbarsState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateCursorsState = true unless oldHorizontalScrollbarHeight? + + @emitDidUpdateState() setVerticalScrollbarWidth: (verticalScrollbarWidth) -> unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth @@ -737,28 +814,34 @@ class TextEditorPresenter @measuredVerticalScrollbarWidth = verticalScrollbarWidth @model.setVerticalScrollbarWidth(verticalScrollbarWidth) @updateScrollbarDimensions() - @updateScrollbarsState() - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateCursorsState() unless oldVerticalScrollbarWidth? + @shouldUpdateScrollbarsState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateCursorsState = true unless oldVerticalScrollbarWidth? + + @emitDidUpdateState() setAutoHeight: (autoHeight) -> unless @autoHeight is autoHeight @autoHeight = autoHeight - @updateHeightState() + @shouldUpdateHeightState = true + + @emitDidUpdateState() setExplicitHeight: (explicitHeight) -> unless @explicitHeight is explicitHeight @explicitHeight = explicitHeight @model.setHeight(explicitHeight) @updateHeight() - @updateVerticalScrollState() - @updateScrollbarsState() - @updateDecorations() - @updateLinesState() - @updateCursorsState() - @updateLineNumbersState() - @updateCustomGutterDecorationState() + @shouldUpdateVerticalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateCustomGutterDecorationState = true + + @emitDidUpdateState() updateHeight: -> height = @explicitHeight ? @contentHeight @@ -776,18 +859,22 @@ class TextEditorPresenter @model.setWidth(contentFrameWidth) @updateScrollbarDimensions() @updateClientWidth() - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateScrollbarsState() - @updateContentState() - @updateDecorations() - @updateLinesState() - @updateCursorsState() unless oldContentFrameWidth? + @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true unless oldContentFrameWidth? + + @emitDidUpdateState() setBoundingClientRect: (boundingClientRect) -> unless @clientRectsEqual(@boundingClientRect, boundingClientRect) @boundingClientRect = boundingClientRect - @updateOverlaysState() + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() clientRectsEqual: (clientRectA, clientRectB) -> clientRectA? and clientRectB? and @@ -800,22 +887,33 @@ class TextEditorPresenter if @windowWidth isnt width or @windowHeight isnt height @windowWidth = width @windowHeight = height - @updateOverlaysState() + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() setBackgroundColor: (backgroundColor) -> unless @backgroundColor is backgroundColor @backgroundColor = backgroundColor - @updateContentState() - @updateLineNumberGutterState() + @shouldUpdateContentState = true + @shouldUpdateLineNumberGutterState = true @updateCommonGutterState() - @updateGutterOrderState() + @shouldUpdateGutterOrderState = true + + @emitDidUpdateState() setGutterBackgroundColor: (gutterBackgroundColor) -> unless @gutterBackgroundColor is gutterBackgroundColor @gutterBackgroundColor = gutterBackgroundColor - @updateLineNumberGutterState() + @shouldUpdateLineNumberGutterState = true @updateCommonGutterState() - @updateGutterOrderState() + @shouldUpdateGutterOrderState = true + + @emitDidUpdateState() + + setGutterWidth: (gutterWidth) -> + if @gutterWidth isnt gutterWidth + @gutterWidth = gutterWidth + @updateOverlaysState() setLineHeight: (lineHeight) -> unless @lineHeight is lineHeight @@ -826,17 +924,19 @@ class TextEditorPresenter @updateHeight() @updateStartRow() @updateEndRow() - @updateHeightState() - @updateHorizontalScrollState() - @updateVerticalScrollState() - @updateScrollbarsState() - @updateHiddenInputState() - @updateDecorations() - @updateLinesState() - @updateCursorsState() - @updateLineNumbersState() - @updateCustomGutterDecorationState() - @updateOverlaysState() + @shouldUpdateHeightState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateCustomGutterDecorationState = true + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() setMouseWheelScreenRow: (mouseWheelScreenRow) -> unless @mouseWheelScreenRow is mouseWheelScreenRow @@ -876,15 +976,17 @@ class TextEditorPresenter characterWidthsChanged: -> @updateContentDimensions() - @updateHorizontalScrollState() - @updateVerticalScrollState() - @updateScrollbarsState() - @updateHiddenInputState() - @updateContentState() - @updateDecorations() - @updateLinesState() - @updateCursorsState() - @updateOverlaysState() + @shouldUpdateHorizontalScrollState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() clearScopedCharacterWidths: -> @characterWidthsByScope = {} @@ -972,11 +1074,11 @@ class TextEditorPresenter intersectsVisibleRowRange = true if intersectsVisibleRowRange - @updateLinesState() if decoration.isType('line') + @shouldUpdateLinesState = true if decoration.isType('line') if decoration.isType('line-number') - @updateLineNumbersState() + @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') - @updateCustomGutterDecorationState() + @shouldUpdateCustomGutterDecorationState = true if decoration.isType('highlight') return if change.textChanged @@ -984,7 +1086,9 @@ class TextEditorPresenter @updateHighlightState(decoration) if decoration.isType('overlay') - @updateOverlaysState() + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() decorationPropertiesDidChange: (decoration, event) -> {oldProperties} = event @@ -995,29 +1099,33 @@ class TextEditorPresenter decoration.getMarker().getScreenRange()) @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) if decoration.isType('line') or Decoration.isType(oldProperties, 'line') - @updateLinesState() + @shouldUpdateLinesState = true if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number') - @updateLineNumbersState() + @shouldUpdateLineNumbersState = true if (decoration.isType('gutter') and not decoration.isType('line-number')) or (Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number')) - @updateCustomGutterDecorationState() + @shouldUpdateCustomGutterDecorationState = true else if decoration.isType('overlay') - @updateOverlaysState() + @shouldUpdateOverlaysState = true else if decoration.isType('highlight') @updateHighlightState(decoration, event) + @emitDidUpdateState() + didDestroyDecoration: (decoration) -> if decoration.isType('line') or decoration.isType('gutter') @removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @updateLinesState() if decoration.isType('line') + @shouldUpdateLinesState = true if decoration.isType('line') if decoration.isType('line-number') - @updateLineNumbersState() + @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') - @updateCustomGutterDecorationState(decoration.getProperties().gutterName) + @shouldUpdateCustomGutterDecorationState = true if decoration.isType('highlight') @updateHighlightState(decoration) if decoration.isType('overlay') - @updateOverlaysState() + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() highlightDidFlash: (decoration) -> flash = decoration.consumeNextFlash() @@ -1032,17 +1140,19 @@ class TextEditorPresenter if decoration.isType('line') or decoration.isType('gutter') @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @updateLinesState() if decoration.isType('line') + @shouldUpdateLinesState = true if decoration.isType('line') if decoration.isType('line-number') - @updateLineNumbersState() + @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') - @updateCustomGutterDecorationState() + @shouldUpdateCustomGutterDecorationState = true else if decoration.isType('highlight') @updateHighlightState(decoration) else if decoration.isType('overlay') - @updateOverlaysState() + @shouldUpdateOverlaysState = true - updateDecorations: -> @batch "shouldUpdateDecorations", -> + @emitDidUpdateState() + + updateDecorations: -> @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} @@ -1197,14 +1307,18 @@ class TextEditorPresenter overlayState.itemWidth = itemWidth overlayState.itemHeight = itemHeight overlayState.contentMargin = contentMargin - @updateOverlaysState() + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => - @updateHiddenInputState() if cursor.isLastCursor() + @shouldUpdateHiddenInputState = true if cursor.isLastCursor() @pauseCursorBlinking() @updateCursorState(cursor) + @emitDidUpdateState() + didChangeVisibilityDisposable = cursor.onDidChangeVisibility => @updateCursorState(cursor) @@ -1212,19 +1326,23 @@ class TextEditorPresenter @disposables.remove(didChangePositionDisposable) @disposables.remove(didChangeVisibilityDisposable) @disposables.remove(didDestroyDisposable) - @updateHiddenInputState() + @shouldUpdateHiddenInputState = true @updateCursorState(cursor, true) + @emitDidUpdateState() + @disposables.add(didChangePositionDisposable) @disposables.add(didChangeVisibilityDisposable) @disposables.add(didDestroyDisposable) didAddCursor: (cursor) -> @observeCursor(cursor) - @updateHiddenInputState() + @shouldUpdateHiddenInputState = true @pauseCursorBlinking() @updateCursorState(cursor) + @emitDidUpdateState() + startBlinkingCursors: -> unless @toggleCursorBlinkHandle @state.content.cursorsVisible = true diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 861f6231e..d2bd77522 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -75,7 +75,7 @@ class TextEditor extends Model 'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows', toProperty: 'languageMode' - constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible}) -> + constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible}={}) -> super @emitter = new Emitter @@ -84,12 +84,10 @@ class TextEditor extends Model @selections = [] buffer ?= new TextBuffer - @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped}) + @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini}) @buffer = @displayBuffer.buffer @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true - @updateInvisibles() - for marker in @findMarkers(@getSelectionMarkerAttributes()) marker.setProperties(preserveFolds: true) @addSelection(marker) @@ -170,21 +168,9 @@ class TextEditor extends Model @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration - @subscribeToScopedConfigSettings() - - subscribeToScopedConfigSettings: -> - @scopedConfigSubscriptions?.dispose() - @scopedConfigSubscriptions = subscriptions = new CompositeDisposable - - scopeDescriptor = @getRootScopeDescriptor() - - subscriptions.add atom.config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, => @updateInvisibles() - subscriptions.add atom.config.onDidChange 'editor.invisibles', scope: scopeDescriptor, => @updateInvisibles() - destroyed: -> @unsubscribe() if includeDeprecatedAPIs @disposables.dispose() - @scopedConfigSubscriptions.dispose() selection.destroy() for selection in @getSelections() @buffer.release() @displayBuffer.destroy() @@ -488,7 +474,7 @@ class TextEditor extends Model setMini: (mini) -> if mini isnt @mini @mini = mini - @updateInvisibles() + @displayBuffer.setIgnoreInvisibles(@mini) @emitter.emit 'did-change-mini', @mini @mini @@ -1057,6 +1043,16 @@ class TextEditor extends Model deleteToBeginningOfWord: -> @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() + # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the + # previous word boundary. + deleteToPreviousWordBoundary: -> + @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary() + + # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the + # next word boundary. + deleteToNextWordBoundary: -> + @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() + # Extended: For each selection, if the selection is empty, delete all characters # of the containing line that precede the cursor. Otherwise delete the # selected text. @@ -2769,15 +2765,6 @@ class TextEditor extends Model shouldAutoIndentOnPaste: -> atom.config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor()) - shouldShowInvisibles: -> - not @mini and atom.config.get('editor.showInvisibles', scope: @getRootScopeDescriptor()) - - updateInvisibles: -> - if @shouldShowInvisibles() - @displayBuffer.setInvisibles(atom.config.get('editor.invisibles', scope: @getRootScopeDescriptor())) - else - @displayBuffer.setInvisibles(null) - ### Section: Event Handlers ### @@ -2786,8 +2773,6 @@ class TextEditor extends Model @softTabs = @usesSoftTabs() ? @softTabs handleGrammarChange: -> - @updateInvisibles() - @subscribeToScopedConfigSettings() @unfoldAll() @emit 'grammar-changed' if includeDeprecatedAPIs @emitter.emit 'did-change-grammar', @getGrammar() diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index d9f1bcba7..6d8f0c018 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -20,8 +20,9 @@ class TokenizedBuffer extends Model chunkSize: 50 invalidRows: null visible: false + configSettings: null - constructor: ({@buffer, @tabLength, @invisibles}) -> + constructor: ({@buffer, @tabLength, @ignoreInvisibles}) -> @emitter = new Emitter @disposables = new CompositeDisposable @@ -39,7 +40,7 @@ class TokenizedBuffer extends Model serializeParams: -> bufferPath: @buffer.getPath() tabLength: @tabLength - invisibles: _.clone(@invisibles) + ignoreInvisibles: @ignoreInvisibles deserializeParams: (params) -> params.buffer = atom.project.bufferForPathSync(params.bufferPath) @@ -76,13 +77,25 @@ class TokenizedBuffer extends Model @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() @disposables.add(@grammarUpdateDisposable) - @configSettings = tabLength: atom.config.get('editor.tabLength', scope: @rootScopeDescriptor) + scopeOptions = {scope: @rootScopeDescriptor} + @configSettings = + tabLength: atom.config.get('editor.tabLength', scopeOptions) + invisibles: atom.config.get('editor.invisibles', scopeOptions) + showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions) - @grammarTabLengthSubscription?.dispose() - @grammarTabLengthSubscription = atom.config.onDidChange 'editor.tabLength', scope: @rootScopeDescriptor, ({newValue}) => + if @configSubscriptions? + @configSubscriptions.dispose() + @disposables.remove(@configSubscriptions) + @configSubscriptions = new CompositeDisposable + @configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) => @configSettings.tabLength = newValue @retokenizeLines() - @disposables.add(@grammarTabLengthSubscription) + ['invisibles', 'showInvisibles'].forEach (key) => + @configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) => + oldInvisibles = @getInvisiblesToShow() + @configSettings[key] = newValue + @retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles) + @disposables.add(@configSubscriptions) @retokenizeLines() @@ -123,10 +136,11 @@ class TokenizedBuffer extends Model @tabLength = tabLength @retokenizeLines() - setInvisibles: (invisibles) -> - unless _.isEqual(invisibles, @invisibles) - @invisibles = invisibles - @retokenizeLines() + setIgnoreInvisibles: (ignoreInvisibles) -> + if ignoreInvisibles isnt @ignoreInvisibles + @ignoreInvisibles = ignoreInvisibles + if @configSettings.showInvisibles and @configSettings.invisibles? + @retokenizeLines() tokenizeInBackground: -> return if not @visible or @pendingChunk or not @isAlive() @@ -302,7 +316,7 @@ class TokenizedBuffer extends Model tabLength = @getTabLength() indentLevel = @indentLevelForRow(row) lineEnding = @buffer.lineEndingForRow(row) - new TokenizedLine({tokens, tabLength, indentLevel, @invisibles, lineEnding}) + new TokenizedLine({tokens, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding}) buildTokenizedLineForRow: (row, ruleStack) -> @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack) @@ -312,7 +326,13 @@ class TokenizedBuffer extends Model tabLength = @getTabLength() indentLevel = @indentLevelForRow(row) {tokens, ruleStack} = @grammar.tokenizeLine(line, ruleStack, row is 0) - new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, @invisibles}) + new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow()}) + + getInvisiblesToShow: -> + if @configSettings.showInvisibles and not @ignoreInvisibles + @configSettings.invisibles + else + null tokenizedLineForRow: (bufferRow) -> @tokenizedLines[bufferRow] diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index afde69db9..ee2054b5a 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -87,8 +87,9 @@ class TooltipManager new Disposable -> tooltip = $target.data('bs.tooltip') - tooltip.leave(currentTarget: target) - tooltip.hide() + if tooltip? + tooltip.leave(currentTarget: target) + tooltip.hide() $target.tooltip('destroy') humanizeKeystrokes = (keystroke) -> diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 2d0cb4e49..4f5dd6c5d 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -113,7 +113,10 @@ class WorkspaceElement extends HTMLElement focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight() runPackageSpecs: -> - [projectPath] = atom.project.getPaths() + if activePath = atom.workspace.getActivePaneItem()?.getPath?() + [projectPath] = atom.project.relativizePath(activePath) + else + [projectPath] = atom.project.getPaths() ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath atom.commands.add 'atom-workspace', diff --git a/src/workspace.coffee b/src/workspace.coffee index 50c4117fb..0e6fb7e40 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -75,6 +75,8 @@ class Workspace extends Model atom.views.addViewProvider Panel, (model) -> new PanelElement().initialize(model) + @subscribeToFontSize() + # Called by the Serializable mixin during deserialization deserializeParams: (params) -> for packageName in params.packagesWithActiveGrammars ? [] @@ -619,9 +621,14 @@ class Workspace extends Model fontSize = atom.config.get("editor.fontSize") atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - # Restore to a default editor font size. + # Restore to the window's original editor font size. resetFontSize: -> - atom.config.unset("editor.fontSize") + if @originalFontSize + atom.config.set("editor.fontSize", @originalFontSize) + + subscribeToFontSize: -> + atom.config.onDidChange 'editor.fontSize', ({oldValue}) => + @originalFontSize ?= oldValue # Removes the item's uri from the list of potential items to reopen. itemOpened: (item) -> diff --git a/static/index.html b/static/index.html index 5559058dc..84e8d57d4 100644 --- a/static/index.html +++ b/static/index.html @@ -1,8 +1,6 @@ - + - - diff --git a/static/index.js b/static/index.js index 2db98a382..f41d41cd9 100644 --- a/static/index.js +++ b/static/index.js @@ -1,6 +1,11 @@ +(function() { + var fs = require('fs'); var path = require('path'); +var loadSettings = null; +var loadSettingsError = null; + window.onload = function() { try { var startTime = Date.now(); @@ -12,66 +17,81 @@ window.onload = function() { // Ensure ATOM_HOME is always set before anything else is required setupAtomHome(); - var cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache'); - // Use separate compile cache when sudo'ing as root to avoid permission issues - if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) { - cacheDir = path.join(cacheDir, 'root'); - } - - var rawLoadSettings = decodeURIComponent(location.hash.substr(1)); - var loadSettings; - try { - loadSettings = JSON.parse(rawLoadSettings); - } catch (error) { - console.error("Failed to parse load settings: " + rawLoadSettings); - throw error; - } - // Normalize to make sure drive letter case is consistent on Windows process.resourcesPath = path.normalize(process.resourcesPath); + if (loadSettingsError) { + throw loadSettingsError; + } + var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep); - setupCoffeeCache(cacheDir); - - ModuleCache = require('../src/module-cache'); - ModuleCache.register(loadSettings); - ModuleCache.add(loadSettings.resourcePath); - - require('grim').includeDeprecatedAPIs = !loadSettings.apiPreviewMode; - - // Start the crash reporter before anything else. - require('crash-reporter').start({ - productName: 'Atom', - companyName: 'GitHub', - // By explicitly passing the app version here, we could save the call - // of "require('remote').require('app').getVersion()". - extra: {_version: loadSettings.appVersion} - }); - - setupVmCompatibility(); - setupCsonCache(cacheDir); - setupSourceMapCache(cacheDir); - setupBabel(cacheDir); - setupTypeScript(cacheDir); - - require(loadSettings.bootstrapScript); - require('ipc').sendChannel('window-command', 'window:loaded'); - - if (global.atom) { - global.atom.loadTime = Date.now() - startTime; - console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms'); + if (loadSettings.profileStartup) { + profileStartup(loadSettings, Date.now() - startTime); + } else { + setupWindow(loadSettings); + setLoadTime(Date.now() - startTime); } } catch (error) { - var currentWindow = require('remote').getCurrentWindow(); - currentWindow.setSize(800, 600); - currentWindow.center(); - currentWindow.show(); - currentWindow.openDevTools(); - console.error(error.stack || error); + handleSetupError(error); } } +var getCacheDirectory = function() { + var cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache'); + // Use separate compile cache when sudo'ing as root to avoid permission issues + if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) { + cacheDir = path.join(cacheDir, 'root'); + } + return cacheDir; +} + +var setLoadTime = function(loadTime) { + if (global.atom) { + global.atom.loadTime = loadTime; + console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms'); + } +} + +var handleSetupError = function(error) { + var currentWindow = require('remote').getCurrentWindow(); + currentWindow.setSize(800, 600); + currentWindow.center(); + currentWindow.show(); + currentWindow.openDevTools(); + console.error(error.stack || error); +} + +var setupWindow = function(loadSettings) { + var cacheDir = getCacheDirectory(); + + setupCoffeeCache(cacheDir); + + ModuleCache = require('../src/module-cache'); + ModuleCache.register(loadSettings); + ModuleCache.add(loadSettings.resourcePath); + + require('grim').includeDeprecatedAPIs = !loadSettings.apiPreviewMode; + + // Start the crash reporter before anything else. + require('crash-reporter').start({ + productName: 'Atom', + companyName: 'GitHub', + // By explicitly passing the app version here, we could save the call + // of "require('remote').require('app').getVersion()". + extra: {_version: loadSettings.appVersion} + }); + + setupVmCompatibility(); + setupCsonCache(cacheDir); + setupSourceMapCache(cacheDir); + setupBabel(cacheDir); + setupTypeScript(cacheDir); + + require(loadSettings.bootstrapScript); + require('ipc').sendChannel('window-command', 'window:loaded'); +} + var setupCoffeeCache = function(cacheDir) { var CoffeeCache = require('coffee-cash'); CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee')); @@ -118,6 +138,73 @@ var setupSourceMapCache = function(cacheDir) { var setupVmCompatibility = function() { var vm = require('vm'); - if (!vm.Script.createContext) + if (!vm.Script.createContext) { vm.Script.createContext = vm.createContext; + } } + +var profileStartup = function(loadSettings, initialTime) { + var profile = function() { + console.profile('startup'); + try { + var startTime = Date.now() + setupWindow(loadSettings); + setLoadTime(Date.now() - startTime + initialTime); + } catch (error) { + handleSetupError(error); + } finally { + console.profileEnd('startup'); + console.log("Switch to the Profiles tab to view the created startup profile") + } + }; + + var currentWindow = require('remote').getCurrentWindow(); + if (currentWindow.devToolsWebContents) { + profile(); + } else { + currentWindow.openDevTools(); + currentWindow.once('devtools-opened', function() { + setTimeout(profile, 100); + }); + } +} + +var parseLoadSettings = function() { + var rawLoadSettings = decodeURIComponent(location.hash.substr(1)); + try { + loadSettings = JSON.parse(rawLoadSettings); + } catch (error) { + console.error("Failed to parse load settings: " + rawLoadSettings); + loadSettingsError = error; + } +} + +var setupWindowBackground = function() { + if (loadSettings && loadSettings.isSpec) { + return; + } + + var backgroundColor = window.localStorage.getItem('atom:window-background-color'); + if (!backgroundColor) { + return; + } + + var backgroundStylesheet = document.createElement('style'); + backgroundStylesheet.type = 'text/css'; + backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }'; + document.head.appendChild(backgroundStylesheet); + + // Remove once the page loads + window.addEventListener("load", function loadWindow() { + window.removeEventListener("load", loadWindow, false); + setTimeout(function() { + backgroundStylesheet.remove(); + backgroundStylesheet = null; + }, 1000); + }, false); +} + +parseLoadSettings(); +setupWindowBackground(); + +})(); diff --git a/static/text-editor-light.less b/static/text-editor-light.less index 3ba4e8313..9d87c61ca 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -4,7 +4,7 @@ atom-text-editor { display: block; - font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; + font-family: Inconsolata, Monaco, Consolas, 'DejaVu Sans Mono', monospace; line-height: 1.3; }