diff --git a/CHANGELOG.md b/CHANGELOG.md index e36b3f59e..8823bd9cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1 @@ See https://atom.io/releases - -## 1.4.0 - -* Switching encoding is now fast also with large files. -* Fixed an issue where disabling and re-enabling a package caused custom keymaps to be overridden. -* Fixed restoring untitled editors on restart. The new behavior never prompts to save new/changed files when closing a window or quitting Atom. - -## 1.3.0 - -* The tree-view now sorts directory entries more naturally, in a locale-sensitive way. -* Lines can now be moved up and down with multiple cursors. -* Improved the performance of marker-dependent code paths such as spell-check and find and replace. -* Fixed copying and pasting in native input fields. -* By default, windows with no pane items are now closed via the `core:close` command. The previous behavior can be restored via the `Close Empty Windows` option in settings. -* Fixed an issue where characters were inserted when toggling the settings view on some keyboard layouts. -* Modules can now temporarily override `Error.prepareStackTrace`. There is also an `Error.prototype.getRawStack()` method if you just need access to the raw v8 trace structure. -* Fixed a problem that caused blurry fonts on monitors that have a slightly higher resolution than 96 DPI. diff --git a/apm/package.json b/apm/package.json index b8dda21ea..2e6b0b8ea 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.5.0" + "atom-package-manager": "1.6.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 26d9c2f42..60d5916b8 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -14,6 +14,8 @@ _ = require 'underscore-plus' packageJson = require '../package.json' module.exports = (grunt) -> + require('time-grunt')(grunt) + grunt.loadNpmTasks('grunt-babel') grunt.loadNpmTasks('grunt-coffeelint') grunt.loadNpmTasks('grunt-lesslint') @@ -36,7 +38,6 @@ module.exports = (grunt) -> buildDir = grunt.option('build-dir') buildDir ?= path.join(os.tmpdir(), 'atom-build') buildDir = path.resolve(buildDir) - disableAutoUpdate = grunt.option('no-auto-update') ? false channel = grunt.option('channel') releasableBranches = ['stable', 'beta'] @@ -179,7 +180,7 @@ module.exports = (grunt) -> pkg: grunt.file.readJSON('package.json') atom: { - appName, channel, metadata, disableAutoUpdate, + appName, channel, metadata, appFileName, apmFileName, appDir, buildDir, contentsDir, installDir, shellAppDir, symbolsDir, } @@ -255,7 +256,7 @@ module.exports = (grunt) -> outputDir: 'electron' downloadDir: electronDownloadDir rebuild: true # rebuild native modules after electron is updated - token: process.env.ATOM_ACCESS_TOKEN + token: process.env.ATOM_ACCESS_TOKEN ? 'da809a6077bb1b0aa7c5623f7b2d5f1fec2faae4' 'create-windows-installer': installer: diff --git a/build/package.json b/build/package.json index fd7d29d80..d5c780c08 100644 --- a/build/package.json +++ b/build/package.json @@ -37,6 +37,7 @@ "runas": "^3.1", "tello": "1.0.5", "temp": "~0.8.1", + "time-grunt": "1.2.2", "underscore-plus": "1.x", "unzip": "~0.1.9", "vm-compatibility-layer": "~0.1.0", diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index 213aa0da4..a86c7c1f4 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -186,5 +186,4 @@ module.exports = (grunt) -> dependencies = ['compile', 'generate-license:save', 'generate-module-cache', 'compile-packages-slug'] dependencies.push('copy-info-plist') if process.platform is 'darwin' dependencies.push('set-exe-icon') if process.platform is 'win32' - dependencies.push('disable-autoupdate') if grunt.config.get('atom.disableAutoUpdate') grunt.task.run(dependencies...) diff --git a/build/tasks/disable-autoupdate-task.coffee b/build/tasks/disable-autoupdate-task.coffee deleted file mode 100644 index 7517543da..000000000 --- a/build/tasks/disable-autoupdate-task.coffee +++ /dev/null @@ -1,12 +0,0 @@ -fs = require 'fs' -path = require 'path' - -module.exports = (grunt) -> - - grunt.registerTask 'disable-autoupdate', 'Set up disableAutoUpdate field in package.json file', -> - appDir = fs.realpathSync(grunt.config.get('atom.appDir')) - - metadata = grunt.file.readJSON(path.join(appDir, 'package.json')) - metadata._disableAutoUpdate = grunt.config.get('atom.disableAutoUpdate') - - grunt.file.write(path.join(appDir, 'package.json'), JSON.stringify(metadata)) diff --git a/dot-atom/.gitignore b/dot-atom/.gitignore index 81af3f689..e5c80ce23 100644 --- a/dot-atom/.gitignore +++ b/dot-atom/.gitignore @@ -1,5 +1,6 @@ -storage +blob-store compile-cache dev -.npm +storage .node-gyp +.npm diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 28f43a8fc..536a1d75d 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -13,7 +13,7 @@ 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-pageup': 'pane:move-item-left' 'ctrl-shift-pagedown': 'pane:move-item-right' - 'F11': 'window:toggle-full-screen' + 'f11': 'window:toggle-full-screen' # Sublime Parity 'ctrl-,': 'application:show-settings' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index f052a64fc..cb291f493 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -19,7 +19,7 @@ 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' - 'F11': 'window:toggle-full-screen' + 'f11': 'window:toggle-full-screen' # Sublime Parity 'ctrl-,': 'application:show-settings' diff --git a/menus/darwin.cson b/menus/darwin.cson index 52b7a5bc8..a2636887d 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -11,11 +11,12 @@ { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: 'Preferences…', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: 'Install Shell Commands', command: 'window:install-shell-commands' } { type: 'separator' } @@ -110,36 +111,9 @@ ] } - { - label: 'Selection' - submenu: [ - { label: 'Add Selection Above', command: 'editor:add-selection-above' } - { label: 'Add Selection Below', command: 'editor:add-selection-below' } - { label: 'Single Selection', command: 'editor:consolidate-selections'} - { label: 'Split into Lines', command: 'editor:split-selections-into-lines'} - { type: 'separator' } - { label: 'Select to Top', command: 'core:select-to-top' } - { label: 'Select to Bottom', command: 'core:select-to-bottom' } - { type: 'separator' } - { label: 'Select Line', command: 'editor:select-line' } - { label: 'Select Word', command: 'editor:select-word' } - { label: 'Select to Beginning of Word', command: 'editor:select-to-beginning-of-word' } - { label: 'Select to Beginning of Line', command: 'editor:select-to-beginning-of-line' } - { label: 'Select to First Character of Line', command: 'editor:select-to-first-character-of-line' } - { label: 'Select to End of Word', command: 'editor:select-to-end-of-word' } - { label: 'Select to End of Line', command: 'editor:select-to-end-of-line' } - ] - } - - { - label: 'Find' - submenu: [] - } - { label: 'View' submenu: [ - { label: 'Reload', command: 'window:reload' } { label: 'Toggle Full Screen', command: 'window:toggle-full-screen' } { label: 'Panes' @@ -164,6 +138,7 @@ label: 'Developer' submenu: [ { label: 'Open In Dev Mode…', command: 'application:open-dev' } + { label: 'Reload Window', command: 'window:reload' } { label: 'Run Package Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' } ] @@ -177,6 +152,32 @@ ] } + { + label: 'Selection' + submenu: [ + { label: 'Add Selection Above', command: 'editor:add-selection-above' } + { label: 'Add Selection Below', command: 'editor:add-selection-below' } + { label: 'Single Selection', command: 'editor:consolidate-selections'} + { label: 'Split into Lines', command: 'editor:split-selections-into-lines'} + { type: 'separator' } + { label: 'Select to Top', command: 'core:select-to-top' } + { label: 'Select to Bottom', command: 'core:select-to-bottom' } + { type: 'separator' } + { label: 'Select Line', command: 'editor:select-line' } + { label: 'Select Word', command: 'editor:select-word' } + { label: 'Select to Beginning of Word', command: 'editor:select-to-beginning-of-word' } + { label: 'Select to Beginning of Line', command: 'editor:select-to-beginning-of-line' } + { label: 'Select to First Character of Line', command: 'editor:select-to-first-character-of-line' } + { label: 'Select to End of Word', command: 'editor:select-to-end-of-word' } + { label: 'Select to End of Line', command: 'editor:select-to-end-of-line' } + ] + } + + { + label: 'Find' + submenu: [] + } + { label: 'Packages' submenu: [] diff --git a/menus/linux.cson b/menus/linux.cson index fa831b4a4..1276748d8 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -83,11 +83,12 @@ } { type: 'separator' } { label: '&Preferences', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } ] } @@ -95,7 +96,6 @@ { label: '&View' submenu: [ - { label: '&Reload', command: 'window:reload' } { label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' } { label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' } { @@ -121,6 +121,7 @@ label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } + { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] diff --git a/menus/win32.cson b/menus/win32.cson index 04da3d388..a7d41b28f 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -10,11 +10,12 @@ { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } - { label: 'Open Your Config', command: 'application:open-your-config' } - { label: 'Open Your Init Script', command: 'application:open-your-init-script' } - { label: 'Open Your Keymap', command: 'application:open-your-keymap' } - { label: 'Open Your Snippets', command: 'application:open-your-snippets' } - { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } + { type: 'separator' } + { label: 'Config…', command: 'application:open-your-config' } + { label: 'Init Script…', command: 'application:open-your-init-script' } + { label: 'Keymap…', command: 'application:open-your-keymap' } + { label: 'Snippets…', command: 'application:open-your-snippets' } + { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As…', command: 'core:save-as' } @@ -94,7 +95,6 @@ { label: '&View' submenu: [ - { label: '&Reload', command: 'window:reload' } { label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' } { label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' } { @@ -120,6 +120,7 @@ label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } + { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] diff --git a/package.json b/package.json index dd661ccd4..bafa45f18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.5.0-dev", + "version": "1.6.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -28,13 +28,14 @@ "fs-plus": "^2.8.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "^4.0.7", + "git-utils": "^4.1.0", "grim": "1.5.0", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", - "jquery": "^2.1.1", + "jquery": "2.1.4", "key-path-helpers": "^0.4.0", "less-cache": "0.22", + "line-top-index": "0.2.0", "marked": "^0.3.4", "normalize-package-data": "^2.0.0", "nslog": "^3", @@ -52,7 +53,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "8.1.3", + "text-buffer": "8.1.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -65,12 +66,12 @@ "base16-tomorrow-dark-theme": "1.1.0", "base16-tomorrow-light-theme": "1.1.1", "one-dark-ui": "1.1.9", - "one-dark-syntax": "1.1.1", - "one-light-syntax": "1.1.1", "one-light-ui": "1.1.9", + "one-dark-syntax": "1.1.2", + "one-light-syntax": "1.1.2", "solarized-dark-syntax": "0.39.0", "solarized-light-syntax": "0.23.0", - "about": "1.1.0", + "about": "1.3.0", "archive-view": "0.61.0", "autocomplete-atom-api": "0.9.2", "autocomplete-css": "0.11.0", @@ -80,7 +81,7 @@ "autoflow": "0.26.0", "autosave": "0.23.0", "background-tips": "0.26.0", - "bookmarks": "0.38.0", + "bookmarks": "0.38.2", "bracket-matcher": "0.79.0", "command-palette": "0.38.0", "deprecation-cop": "0.54.0", @@ -102,16 +103,15 @@ "notifications": "0.62.1", "open-on-github": "0.40.0", "package-generator": "0.41.0", - "release-notes": "0.53.0", - "settings-view": "0.232.1", + "settings-view": "0.232.3", "snippets": "1.0.1", - "spell-check": "0.63.0", + "spell-check": "0.65.0", "status-bar": "0.80.0", "styleguide": "0.45.0", "symbols-view": "0.110.1", "tabs": "0.88.0", "timecop": "0.33.0", - "tree-view": "0.198.0", + "tree-view": "0.198.1", "update-package-dependencies": "0.10.0", "welcome": "0.33.0", "whitespace": "0.32.1", @@ -121,24 +121,24 @@ "language-coffee-script": "0.46.0", "language-csharp": "0.11.0", "language-css": "0.36.0", - "language-gfm": "0.82.0", - "language-git": "0.11.0", - "language-go": "0.41.0", - "language-html": "0.43.1", + "language-gfm": "0.83.0", + "language-git": "0.12.1", + "language-go": "0.42.0", + "language-html": "0.44.0", "language-hyperlink": "0.16.0", "language-java": "0.17.0", - "language-javascript": "0.104.0", - "language-json": "0.17.2", + "language-javascript": "0.105.0", + "language-json": "0.17.3", "language-less": "0.29.0", "language-make": "0.21.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.1", "language-perl": "0.32.0", - "language-php": "0.34.0", + "language-php": "0.36.0", "language-property-list": "0.8.0", - "language-python": "0.42.1", - "language-ruby": "0.65.0", - "language-ruby-on-rails": "0.24.0", + "language-python": "0.43.0", + "language-ruby": "0.68.0", + "language-ruby-on-rails": "0.25.0", "language-sass": "0.45.0", "language-shellscript": "0.21.0", "language-source": "0.9.0", diff --git a/script/bootstrap b/script/bootstrap index 8314b9cb0..5f9241a3d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -5,8 +5,16 @@ var verifyRequirements = require('./utils/verify-requirements'); var safeExec = require('./utils/child-process-wrapper.js').safeExec; var path = require('path'); +var t0, t1 + // Executes an array of commands one by one. function executeCommands(commands, done, index) { + if (index != undefined) { + t1 = Date.now() + console.log("=> Took " + (t1 - t0) + "ms."); + console.log(); + } + index = (index == undefined ? 0 : index); if (index < commands.length) { var command = commands[index]; @@ -17,6 +25,7 @@ function executeCommands(commands, done, index) { options = command.options; command = command.command; } + t0 = Date.now() safeExec(command, options, executeCommands.bind(this, commands, done, index + 1)); } else @@ -96,7 +105,10 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - apmPath + ' clean' + apmFlags, + { + command: apmPath + ' clean' + apmFlags, + message: 'Deleting old packages...' + }, moduleInstallCommand, dedupeApmCommand + ' ' + packagesToDedupe.join(' '), ]; diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index caaaed9f2..3c4a4fe4b 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -1,4 +1,26 @@ +# Users may have this environment variable set. Currently, it causes babel to +# log to stderr, which causes errors on Windows. +# See https://github.com/atom/electron/issues/2033 +process.env.DEBUG='*' + +path = require('path') +temp = require('temp').track() +CompileCache = require('../src/compile-cache') + describe "Babel transpiler support", -> + originalCacheDir = null + + beforeEach -> + originalCacheDir = CompileCache.getCacheDirectory() + CompileCache.setCacheDirectory(temp.mkdirSync('compile-cache')) + for cacheKey in Object.keys(require.cache) + if cacheKey.startsWith(path.join(__dirname, 'fixtures', 'babel')) + console.log('deleting', cacheKey) + delete require.cache[cacheKey] + + afterEach -> + CompileCache.setCacheDirectory(originalCacheDir) + describe 'when a .js file starts with /** @babel */;', -> it "transpiles it using babel", -> transpiled = require('./fixtures/babel/babel-comment.js') @@ -17,3 +39,12 @@ describe "Babel transpiler support", -> describe "when a .js file does not start with 'use babel';", -> it "does not transpile it using babel", -> expect(-> require('./fixtures/babel/invalid.js')).toThrow() + + it "does not try to log to stdout or stderr while parsing the file", -> + spyOn(process.stderr, 'write') + spyOn(process.stdout, 'write') + + transpiled = require('./fixtures/babel/babel-double-quotes.js') + + expect(process.stdout.write).not.toHaveBeenCalled() + expect(process.stderr.write).not.toHaveBeenCalled() diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index eab2f6f04..c431fea5f 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -872,6 +872,26 @@ describe "Config", -> atom.config.loadUserConfig() expect(atom.config.get("foo.bar")).toBe "baz" + describe "when the config file fails to load", -> + addErrorHandler = null + + beforeEach -> + atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() + spyOn(fs, "existsSync").andCallFake -> + error = new Error() + error.code = 'EPERM' + throw error + + it "creates a notification and does not try to save later changes to disk", -> + load = -> atom.config.loadUserConfig() + expect(load).not.toThrow() + expect(addErrorHandler.callCount).toBe 1 + + atom.config.set("foo.bar", "baz") + advanceClock(100) + expect(atom.config.save).not.toHaveBeenCalled() + expect(atom.config.get("foo.bar")).toBe "baz" + describe ".observeUserConfig()", -> updatedHandler = null diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 8c4adca44..0246008a4 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1253,6 +1253,13 @@ describe "DisplayBuffer", -> decoration.destroy() expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined() + it "does not allow destroyed markers to be decorated", -> + marker.destroy() + expect(-> + displayBuffer.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')}) + ).toThrow("Cannot decorate a destroyed marker") + expect(displayBuffer.getOverlayDecorations()).toEqual [] + describe "when a decoration is updated via Decoration::update()", -> it "emits an 'updated' event containing the new and old params", -> decoration.onDidChangeProperties updatedSpy = jasmine.createSpy() diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index da5f8327e..38716ab3e 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -2,7 +2,7 @@ module.exports = class FakeLinesYardstick - constructor: (@model) -> + constructor: (@model, @lineTopIndex) -> @characterWidthsByScope = {} getScopedCharacterWidth: (scopeNames, char) -> @@ -19,15 +19,14 @@ class FakeLinesYardstick setScopedCharacterWidth: (scopeNames, character, width) -> @getScopedCharacterWidths(scopeNames)[character] = width - pixelPositionForScreenPosition: (screenPosition, clip=true) -> + pixelPositionForScreenPosition: (screenPosition) -> screenPosition = Point.fromObject(screenPosition) - screenPosition = @model.clipScreenPosition(screenPosition) if clip targetRow = screenPosition.row targetColumn = screenPosition.column baseCharacterWidth = @model.getDefaultCharWidth() - top = targetRow * @model.getLineHeightInPixels() + top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow) left = 0 column = 0 diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 799c7685f..da4e08ae2 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -125,6 +125,27 @@ describe "Starting Atom", -> .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([otherTempDirPath]) + it "opens the new window offset from the other window", -> + runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> + win0Position = null + win1Position = null + client + .waitForWindowCount(1, 10000) + .execute -> atom.getPosition() + .then ({value}) -> win0Position = value + .waitForNewWindow(-> + @startAnotherAtom([path.join(temp.mkdirSync("a-third-dir"), "a-file")], ATOM_HOME: atomHome) + , 5000) + .waitForWindowCount(2, 10000) + .execute -> atom.getPosition() + .then ({value}) -> win1Position = value + .then -> + expect(win1Position.x).toBeGreaterThan(win0Position.x) + # Ideally we'd test the y coordinate too, but if the window's + # already as tall as it can be, then OS X won't move it down outside + # the screen. + # expect(win1Position.y).toBeGreaterThan(win0Position.y) + describe "reopening a directory that was previously opened", -> it "remembers the state of the window", -> runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 74f5fca6a..46935510f 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -1,5 +1,7 @@ -LinesYardstick = require "../src/lines-yardstick" +LinesYardstick = require '../src/lines-yardstick' +LineTopIndex = require 'line-top-index' {toArray} = require 'underscore-plus' +{Point} = require 'text-buffer' describe "LinesYardstick", -> [editor, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = [] @@ -44,7 +46,10 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, atom.grammars) + lineTopIndex = new LineTopIndex({ + defaultLineHeight: editor.getLineHeightInPixels() + }) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) afterEach -> lineNode.remove() for lineNode in createdLineNodes @@ -62,12 +67,12 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition([0, 0])).toEqual({left: 0, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([0, 1])).toEqual({left: 7, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual({left: 37.78125, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([1, 6])).toEqual({left: 43.171875, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([1, 9])).toEqual({left: 72.171875, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, Infinity])).toEqual({left: 287.859375, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 37.78125, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43.171875, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72.171875, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.859375, top: 28}) it "reuses already computed pixel positions unless it is invalidated", -> atom.styles.addStyleSheet """ @@ -77,9 +82,9 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 19.203125, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 57.609375, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 95.609375, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70}) atom.styles.addStyleSheet """ * { @@ -87,15 +92,15 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 19.203125, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 57.609375, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 95.609375, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70}) linesYardstick.invalidateCache() - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 24, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 72, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 120, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 24, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70}) it "correctly handles RTL characters", -> atom.styles.addStyleSheet """ @@ -106,13 +111,13 @@ describe "LinesYardstick", -> """ editor.setText("السلام عليكم") - expect(linesYardstick.pixelPositionForScreenPosition([0, 0]).left).toBe 0 - expect(linesYardstick.pixelPositionForScreenPosition([0, 1]).left).toBe 8 - expect(linesYardstick.pixelPositionForScreenPosition([0, 2]).left).toBe 16 - expect(linesYardstick.pixelPositionForScreenPosition([0, 5]).left).toBe 33 - expect(linesYardstick.pixelPositionForScreenPosition([0, 7]).left).toBe 50 - expect(linesYardstick.pixelPositionForScreenPosition([0, 9]).left).toBe 67 - expect(linesYardstick.pixelPositionForScreenPosition([0, 11]).left).toBe 84 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0)).left).toBe 0 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1)).left).toBe 8 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 2)).left).toBe 16 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5)).left).toBe 33 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 7)).left).toBe 50 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 9)).left).toBe 67 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 11)).left).toBe 84 it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", -> # This spec documents what seems to be a bug in Chromium, because we'd @@ -137,9 +142,9 @@ describe "LinesYardstick", -> editor.setText(text) - expect(linesYardstick.pixelPositionForScreenPosition([0, 35]).left).toBe 230.90625 - expect(linesYardstick.pixelPositionForScreenPosition([0, 36]).left).toBe 237.5 - expect(linesYardstick.pixelPositionForScreenPosition([0, 37]).left).toBe 244.09375 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 230.90625 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 237.5 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 244.09375 describe "::screenPositionForPixelPosition(pixelPosition)", -> it "converts pixel positions to screen positions", -> diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index e6848ef03..46d1d11ee 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -1026,6 +1026,16 @@ describe "PackageManager", -> expect(atom.packages.enablePackage("this-doesnt-exist")).toBeNull() expect(console.warn.callCount).toBe 1 + it "does not disable an already disabled package", -> + packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain packageName + + atom.packages.disablePackage(packageName) + packagesDisabled = atom.config.get('core.disabledPackages').filter((pack) -> pack is packageName) + expect(packagesDisabled.length).toEqual 1 + describe "with themes", -> didChangeActiveThemesHandler = null diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d5e9f5425..9663c6222 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1651,6 +1651,214 @@ describe('TextEditorComponent', function () { }) }) + describe('block decorations rendering', function () { + function createBlockDecorationBeforeScreenRow(screenRow, {className}) { + let item = document.createElement("div") + item.className = className || "" + let blockDecoration = editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + {type: "block", item: item, position: "before"} + ) + return [item, blockDecoration] + } + + function createBlockDecorationAfterScreenRow(screenRow, {className}) { + let item = document.createElement("div") + item.className = className || "" + let blockDecoration = editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + {type: "block", item: item, position: "after"} + ) + return [item, blockDecoration] + } + + beforeEach(async function () { + wrapperNode.style.height = 5 * lineHeightInPixels + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + }) + + afterEach(function () { + atom.themes.removeStylesheet('test') + }) + + it("renders visible and yet-to-be-measured block decorations, inserting them between the appropriate lines and refreshing them as needed", async function () { + let [item1, blockDecoration1] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) + let [item2, blockDecoration2] = createBlockDecorationBeforeScreenRow(2, {className: "decoration-2"}) + let [item3, blockDecoration3] = createBlockDecorationBeforeScreenRow(4, {className: "decoration-3"}) + let [item4, blockDecoration4] = createBlockDecorationBeforeScreenRow(7, {className: "decoration-4"}) + let [item5, blockDecoration5] = createBlockDecorationAfterScreenRow(7, {className: "decoration-5"}) + + atom.styles.addStyleSheet( + `atom-text-editor .decoration-1 { width: 30px; height: 80px; } + atom-text-editor .decoration-2 { width: 30px; height: 40px; } + atom-text-editor .decoration-3 { width: 30px; height: 100px; } + atom-text-editor .decoration-4 { width: 30px; height: 120px; } + atom-text-editor .decoration-5 { width: 30px; height: 42px; }`, + {context: 'atom-text-editor'} + ) + await nextAnimationFramePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 80 + 40 + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBe(item1) + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() + + expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) + + editor.setCursorScreenPosition([0, 0]) + editor.insertNewline() + blockDecoration1.destroy() + + await nextAnimationFramePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() + + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) + + atom.styles.addStyleSheet( + 'atom-text-editor .decoration-2 { height: 60px; }', + {context: 'atom-text-editor'} + ) + + await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles + await nextAnimationFramePromise() // applies the changes + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() + + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) + + item2.style.height = "20px" + wrapperNode.invalidateBlockDecorationDimensions(blockDecoration2) + await nextAnimationFramePromise() + await nextAnimationFramePromise() + + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBe(item5) + + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) + expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) + expect(item5.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100 + 120 + lineHeightInPixels) + }) + + it("correctly sets screen rows on elements, both initially and when decorations move", async function () { + let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) + atom.styles.addStyleSheet( + 'atom-text-editor .decoration-1 { width: 30px; height: 80px; }', + {context: 'atom-text-editor'} + ) + + await nextAnimationFramePromise() + + let tileNode, contentElements + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + await nextAnimationFramePromise() + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + + blockDecoration.getMarker().setHeadBufferPosition([2, 0]) + await nextAnimationFramePromise() + + tileNode = component.tileNodesForLines()[0] + contentElements = tileNode.querySelectorAll("content") + + expect(contentElements.length).toBe(1) + expect(contentElements[0].dataset.screenRow).toBe("2") + expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") + expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") + expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + }) + + it('measures block decorations taking into account both top and bottom margins', async function () { + let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) + atom.styles.addStyleSheet( + 'atom-text-editor .decoration-1 { width: 30px; height: 30px; margin-top: 10px; margin-bottom: 5px; }', + {context: 'atom-text-editor'} + ) + + await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles + await nextAnimationFramePromise() // applies the changes + + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 30 + 10 + 5 + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + }) + }) + describe('highlight decoration rendering', function () { let decoration, marker, scrollViewClientLeft @@ -3433,6 +3641,40 @@ describe('TextEditorComponent', function () { }) }) + describe('when the mousewheel event\'s target is a block decoration', function () { + it('keeps it on the DOM if it is scrolled off-screen', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await nextViewUpdatePromise() + + let item = document.createElement("div") + item.style.width = "30px" + item.style.height = "30px" + item.className = "decoration-1" + editor.decorateMarker( + editor.markScreenPosition([0, 0], {invalidate: "never"}), + {type: "block", item: item} + ) + + await nextViewUpdatePromise() + + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -500 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return item + } + }) + componentNode.dispatchEvent(wheelEvent) + await nextAnimationFramePromise() + + expect(component.getTopmostDOMNode().contains(item)).toBe(true) + }) + }) + it('only prevents the default action of the mousewheel event if it actually lead to scrolling', async function () { spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' @@ -4567,6 +4809,13 @@ describe('TextEditorComponent', function () { }) }) + describe('::pixelPositionForScreenPosition()', () => { + it('returns the correct horizontal position, even if it is on a row that has not yet been rendered (regression)', () => { + editor.setTextInBufferRange([[5, 0], [6, 0]], 'hello world\n') + expect(wrapperNode.pixelPositionForScreenPosition([5, Infinity]).left).toBeGreaterThan(0) + }) + }) + describe('middle mouse paste on Linux', function () { let originalPlatform diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 380f1a5fc..05b0e2708 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,6 +5,7 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' FakeLinesYardstick = require './fake-lines-yardstick' +LineTopIndex = require 'line-top-index' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. @@ -29,13 +30,29 @@ describe "TextEditorPresenter", -> presenter.getPreMeasurementState() presenter.getPostMeasurementState() + addBlockDecorationBeforeScreenRow = (screenRow, item) -> + editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", item: item, position: "before" + ) + + addBlockDecorationAfterScreenRow = (screenRow, item) -> + editor.decorateMarker( + editor.markScreenPosition([screenRow, 0], invalidate: "never"), + type: "block", item: item, position: "after" + ) + buildPresenterWithoutMeasurements = (params={}) -> + lineTopIndex = new LineTopIndex({ + defaultLineHeight: editor.getLineHeightInPixels() + }) _.defaults params, model: editor config: atom.config contentFrameWidth: 500 + lineTopIndex: lineTopIndex presenter = new TextEditorPresenter(params) - presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter)) + presenter.setLinesYardstick(new FakeLinesYardstick(editor, lineTopIndex)) presenter buildPresenter = (params={}) -> @@ -164,6 +181,99 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[12]).toBeUndefined() + describe "when there are block decorations", -> + it "computes each tile's height and scrollTop based on block decorations' height", -> + presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) + + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(5) + blockDecoration4 = addBlockDecorationAfterScreenRow(5) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 1) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 40) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 50) + + expect(stateFn(presenter).tiles[0].height).toBe(2 * 10 + 1) + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40 + 50) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40 + 50) + expect(stateFn(presenter).tiles[8]).toBeUndefined() + + presenter.setScrollTop(21) + + expect(stateFn(presenter).tiles[0]).toBeUndefined() + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 40 + 50) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 40 + 50 - 21) + expect(stateFn(presenter).tiles[8]).toBeUndefined() + + blockDecoration3.getMarker().setHeadScreenPosition([6, 0]) + + expect(stateFn(presenter).tiles[0]).toBeUndefined() + expect(stateFn(presenter).tiles[2].height).toBe(2 * 10 + 30) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 1 - 21) + expect(stateFn(presenter).tiles[4].height).toBe(2 * 10 + 50) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 1 + 30 - 21) + expect(stateFn(presenter).tiles[6].height).toBe(2 * 10 + 40) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 1 + 30 + 50 - 21) + expect(stateFn(presenter).tiles[8]).toBeUndefined() + + it "works correctly when soft wrapping is enabled", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(4) + blockDecoration3 = addBlockDecorationBeforeScreenRow(8) + + presenter = buildPresenter(explicitHeight: 330, lineHeight: 10, tileSize: 2, baseCharacterWidth: 5) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) + + editor.setSoftWrapped(true) + presenter.setContentFrameWidth(5 * 25) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[14].top).toBe(14 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[16].top).toBe(16 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[18].top).toBe(18 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[20].top).toBe(20 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[22].top).toBe(22 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[24].top).toBe(24 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[26].top).toBe(26 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[28].top).toBe(28 * 10 + 10 + 20 + 30) + + editor.setSoftWrapped(false) + + expect(stateFn(presenter).tiles[0].top).toBe(0 * 10) + expect(stateFn(presenter).tiles[2].top).toBe(2 * 10 + 10) + expect(stateFn(presenter).tiles[4].top).toBe(4 * 10 + 10) + expect(stateFn(presenter).tiles[6].top).toBe(6 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[8].top).toBe(8 * 10 + 10 + 20) + expect(stateFn(presenter).tiles[10].top).toBe(10 * 10 + 10 + 20 + 30) + expect(stateFn(presenter).tiles[12].top).toBe(12 * 10 + 10 + 20 + 30) + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() @@ -363,7 +473,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -482,6 +592,32 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500 + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(7) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "updates when the ::lineHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setLineHeight(20) @@ -608,7 +744,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(getState(presenter).hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -630,6 +766,32 @@ describe "TextEditorPresenter", -> expect(getState(presenter).content.maxHeight).toBe(50) describe ".scrollHeight", -> + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(7) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(getState(presenter).content.scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "is initialized based on the lineHeight, the number of lines, and the height", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 10 @@ -705,7 +867,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(getState(presenter).content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -811,6 +973,9 @@ describe "TextEditorPresenter", -> editor.insertNewline() expect(getState(presenter).content.scrollTop).toBe(10) + editor.insertNewline() + expect(getState(presenter).content.scrollTop).toBe(20) + it "never exceeds the computed scroll height minus the computed client height", -> didChangeScrollTopSpy = jasmine.createSpy() presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) @@ -1165,6 +1330,142 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')] expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')] + describe ".blockDecorations", -> + it "contains all block decorations that are present before/after a line, both initially and when decorations change", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + presenter = buildPresenter() + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(7) + blockDecoration4 = addBlockDecorationAfterScreenRow(7) + + waitsForStateToUpdate presenter + runs -> + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([blockDecoration3]) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([blockDecoration4]) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + + waitsForStateToUpdate presenter, -> + blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) + blockDecoration2.getMarker().setHeadBufferPosition([9, 0]) + blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) + blockDecoration4.getMarker().setHeadBufferPosition([8, 0]) + + runs -> + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([blockDecoration4]) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([blockDecoration2, blockDecoration3]) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + + waitsForStateToUpdate presenter, -> + blockDecoration4.destroy() + blockDecoration3.destroy() + blockDecoration1.getMarker().setHeadBufferPosition([0, 0]) + + runs -> + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + + waitsForStateToUpdate presenter, -> + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + + runs -> + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([blockDecoration1]) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([blockDecoration2]) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') @@ -1360,6 +1661,45 @@ describe "TextEditorPresenter", -> presenter.setHorizontalScrollbarHeight(10) expect(getState(presenter).content.cursors).not.toEqual({}) + it "updates when block decorations change", -> + editor.setSelectedBufferRanges([ + [[1, 2], [1, 2]], + [[2, 4], [2, 4]], + [[3, 4], [3, 5]] + [[5, 12], [5, 12]], + [[8, 4], [8, 4]] + ]) + presenter = buildPresenter(explicitHeight: 80, scrollTop: 0) + + expect(stateForCursor(presenter, 0)).toEqual {top: 10, left: 2 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 20, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} + + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(1) + + waitsForStateToUpdate presenter, -> + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + + runs -> + expect(stateForCursor(presenter, 0)).toEqual {top: 50, left: 2 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 60, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toBeUndefined() + expect(stateForCursor(presenter, 3)).toBeUndefined() + expect(stateForCursor(presenter, 4)).toBeUndefined() + + waitsForStateToUpdate presenter, -> + blockDecoration2.destroy() + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + editor.setCursorBufferPosition([0, 0]) + + runs -> + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 0, width: 10, height: 10} + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], @@ -1434,12 +1774,12 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'v', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} it "updates when cursors are added, moved, hidden, shown, or destroyed", -> @@ -1785,7 +2125,7 @@ describe "TextEditorPresenter", -> } expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) - presenter.characterWidthsChanged() + presenter.measurementsChanged() expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] } @@ -1897,6 +2237,223 @@ describe "TextEditorPresenter", -> flashCount: 2 } + describe ".blockDecorations", -> + stateForBlockDecoration = (presenter, decoration) -> + getState(presenter).content.blockDecorations[decoration.id] + + it "contains state for measured block decorations that are not visible when they are on ::mouseWheelScreenRow", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0, stoppedScrollingDelay: 200) + getState(presenter) # flush pending state + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 0) + + presenter.setScrollTop(100) + presenter.setMouseWheelScreenRow(0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: true + } + + advanceClock(presenter.stoppedScrollingDelay) + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + it "invalidates block decorations that intersect a change in the buffer", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(9) + blockDecoration2 = addBlockDecorationBeforeScreenRow(10) + blockDecoration3 = addBlockDecorationBeforeScreenRow(11) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 9 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 10 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + + editor.setSelectedScreenRange([[10, 0], [12, 0]]) + editor.delete() + presenter.setScrollTop(0) # deleting the buffer causes the editor to autoscroll + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 10 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 10 + isVisible: false + } + + it "invalidates all block decorations when content frame width, window size or bounding client rect change", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(11) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setBoundingClientRect({top: 0, left: 0, width: 50, height: 30}) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setContentFrameWidth(100) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + presenter.setWindowSize(100, 200) + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 11 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + + it "contains state for on-screen and unmeasured block decorations, both initially and when they are updated or destroyed", -> + item = {} + blockDecoration1 = addBlockDecorationBeforeScreenRow(0, item) + blockDecoration2 = addBlockDecorationBeforeScreenRow(4, item) + blockDecoration3 = addBlockDecorationBeforeScreenRow(4, item) + blockDecoration4 = addBlockDecorationBeforeScreenRow(10, item) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: true + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + isVisible: true + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 4 + isVisible: true + } + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: false + } + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 20) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: true + } + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 4 + isVisible: false + } + expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() + + blockDecoration3.getMarker().setHeadScreenPosition([5, 0]) + presenter.setScrollTop(90) + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration2), { + decoration: blockDecoration2 + screenRow: 4 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration3), { + decoration: blockDecoration3 + screenRow: 5 + isVisible: false + } + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } + + presenter.invalidateBlockDecorationDimensions(blockDecoration1) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + + expectValues stateForBlockDecoration(presenter, blockDecoration1), { + decoration: blockDecoration1 + screenRow: 0 + isVisible: false + } + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } + + blockDecoration1.destroy() + + expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expectValues stateForBlockDecoration(presenter, blockDecoration4), { + decoration: blockDecoration4 + screenRow: 10 + isVisible: true + } + + it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> + blockDecoration = addBlockDecorationBeforeScreenRow(0) + presenter = buildPresenter() + + blockDecoration.destroy() + presenter.setBlockDecorationDimensions(blockDecoration, 30, 30) + + expect(getState(presenter).content.blockDecorations).toEqual({}) + describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> @@ -2396,6 +2953,76 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 5, softWrapped: false} expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 6, softWrapped: false} + describe ".blockDecorationsHeight", -> + it "adds the sum of all block decorations' heights to the relevant line number state objects, both initially and when decorations change", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + presenter = buildPresenter(tileSize: 2, explicitHeight: 300) + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(3) + blockDecoration4 = addBlockDecorationBeforeScreenRow(7) + blockDecoration5 = addBlockDecorationAfterScreenRow(7) + blockDecoration6 = addBlockDecorationAfterScreenRow(10) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 30) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 35) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40) + presenter.setBlockDecorationDimensions(blockDecoration5, 0, 50) + presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60) + + waitsForStateToUpdate presenter + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(20 + 30) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) + + waitsForStateToUpdate presenter, -> + blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) + blockDecoration2.getMarker().setHeadBufferPosition([5, 0]) + blockDecoration3.getMarker().setHeadBufferPosition([9, 0]) + + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(10) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(30) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) + + waitsForStateToUpdate presenter, -> + blockDecoration1.destroy() + blockDecoration3.destroy() + + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 2).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 3).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 4).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 5).blockDecorationsHeight).toBe(20) + expect(lineNumberStateForScreenRow(presenter, 6).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 7).blockDecorationsHeight).toBe(40) + expect(lineNumberStateForScreenRow(presenter, 8).blockDecorationsHeight).toBe(0) # 0 because we're at the start of a tile. + expect(lineNumberStateForScreenRow(presenter, 9).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 10).blockDecorationsHeight).toBe(0) + expect(lineNumberStateForScreenRow(presenter, 11).blockDecorationsHeight).toBe(60) + describe ".decorationClasses", -> it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') @@ -2625,6 +3252,56 @@ describe "TextEditorPresenter", -> expect(decorationState[decoration1.id]).toBeUndefined() expect(decorationState[decoration3.id].top).toBeDefined() + it "updates when block decorations are added, changed or removed", -> + # block decoration before decoration1 + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 3) + # block decoration between decoration1 and decoration2 + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 5) + # block decoration between decoration2 and decoration3 + blockDecoration3 = addBlockDecorationBeforeScreenRow(10) + blockDecoration4 = addBlockDecorationAfterScreenRow(10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 7) + presenter.setBlockDecorationDimensions(blockDecoration4, 0, 11) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].top).toBe lineHeight * marker1.getScreenRange().start.row + 3 + 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 + 3 + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id]).toBeUndefined() + + presenter.setScrollTop(scrollTop + lineHeight * 5) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 3 + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 3 + 5 + 7 + 11 + expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() + expect(decorationState[decoration3.id].item).toBe decorationItem + expect(decorationState[decoration3.id].class).toBe 'test-class' + + waitsForStateToUpdate presenter, -> blockDecoration1.destroy() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBe lineHeight * marker2.getScreenRange().start.row + 5 + expect(decorationState[decoration2.id].height).toBe lineHeight * marker2.getScreenRange().getRowCount() + 7 + 11 + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id].top).toBe lineHeight * marker3.getScreenRange().start.row + 5 + 7 + 11 + expect(decorationState[decoration3.id].height).toBe lineHeight * marker3.getScreenRange().getRowCount() + expect(decorationState[decoration3.id].item).toBe decorationItem + expect(decorationState[decoration3.id].class).toBe 'test-class' + it "updates when ::scrollTop changes", -> # This update will scroll decoration1 out of view, and decoration3 into view. expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5) @@ -2648,6 +3325,7 @@ describe "TextEditorPresenter", -> 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() @@ -2830,6 +3508,32 @@ describe "TextEditorPresenter", -> customGutter.destroy() describe ".scrollHeight", -> + it "updates when new block decorations are measured, changed or destroyed", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + + blockDecoration1 = addBlockDecorationBeforeScreenRow(0) + blockDecoration2 = addBlockDecorationBeforeScreenRow(3) + blockDecoration3 = addBlockDecorationBeforeScreenRow(7) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 35.8) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 50.3) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 95.2) + + linesHeight = editor.getScreenLineCount() * 10 + blockDecorationsHeight = Math.round(35.8 + 50.3 + 95.2) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 100.3) + + blockDecorationsHeight = Math.round(35.8 + 100.3 + 95.2) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + + waitsForStateToUpdate presenter, -> blockDecoration3.destroy() + runs -> + blockDecorationsHeight = Math.round(35.8 + 100.3) + expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe(linesHeight + blockDecorationsHeight) + it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> presenter = buildPresenter() expect(getStylesForGutterWithName(presenter, 'line-number').scrollHeight).toBe editor.getScreenLineCount() * 10 diff --git a/src/babel.js b/src/babel.js index f53dbc758..1f450ff96 100644 --- a/src/babel.js +++ b/src/babel.js @@ -42,6 +42,10 @@ exports.getCachePath = function (sourceCode) { exports.compile = function (sourceCode, filePath) { if (!babel) { babel = require('babel-core') + var Logger = require('babel-core/lib/transformation/file/logger') + var noop = function () {} + Logger.prototype.debug = noop + Logger.prototype.verbose = noop } var options = {filename: filePath} diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee new file mode 100644 index 000000000..0cfa7974f --- /dev/null +++ b/src/block-decorations-component.coffee @@ -0,0 +1,72 @@ +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + +module.exports = +class BlockDecorationsComponent + constructor: (@container, @views, @presenter, @domElementPool) -> + @newState = null + @oldState = null + @blockDecorationNodesById = {} + @domNode = @domElementPool.buildElement("content") + @domNode.setAttribute("select", ".atom--invisible-block-decoration") + @domNode.style.visibility = "hidden" + + getDomNode: -> + @domNode + + updateSync: (state) -> + @newState = state.content + @oldState ?= {blockDecorations: {}, width: 0} + + if @newState.width isnt @oldState.width + @domNode.style.width = @newState.width + "px" + @oldState.width = @newState.width + + for id, blockDecorationState of @oldState.blockDecorations + unless @newState.blockDecorations.hasOwnProperty(id) + @blockDecorationNodesById[id].remove() + delete @blockDecorationNodesById[id] + delete @oldState.blockDecorations[id] + + for id, blockDecorationState of @newState.blockDecorations + if @oldState.blockDecorations.hasOwnProperty(id) + @updateBlockDecorationNode(id) + else + @oldState.blockDecorations[id] = {} + @createAndAppendBlockDecorationNode(id) + + measureBlockDecorations: -> + for decorationId, blockDecorationNode of @blockDecorationNodesById + style = getComputedStyle(blockDecorationNode) + decoration = @newState.blockDecorations[decorationId].decoration + marginBottom = parseInt(style.marginBottom) ? 0 + marginTop = parseInt(style.marginTop) ? 0 + @presenter.setBlockDecorationDimensions( + decoration, + blockDecorationNode.offsetWidth, + blockDecorationNode.offsetHeight + marginTop + marginBottom + ) + + createAndAppendBlockDecorationNode: (id) -> + blockDecorationState = @newState.blockDecorations[id] + blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) + blockDecorationNode.id = "atom--block-decoration-#{id}" + @container.appendChild(blockDecorationNode) + @blockDecorationNodesById[id] = blockDecorationNode + @updateBlockDecorationNode(id) + + updateBlockDecorationNode: (id) -> + newBlockDecorationState = @newState.blockDecorations[id] + oldBlockDecorationState = @oldState.blockDecorations[id] + blockDecorationNode = @blockDecorationNodesById[id] + + if newBlockDecorationState.isVisible + blockDecorationNode.classList.remove("atom--invisible-block-decoration") + else + blockDecorationNode.classList.add("atom--invisible-block-decoration") + + if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + oldBlockDecorationState.screenRow = newBlockDecorationState.screenRow diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 27b9df8e1..74da80e43 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -103,8 +103,6 @@ class ApplicationMenu downloadingUpdateItem.visible = false installUpdateItem.visible = false - return if @autoUpdateManager.isDisabled() - switch state when 'idle', 'error', 'no-update-available' checkForUpdateItem.visible = true @@ -119,9 +117,10 @@ class ApplicationMenu # # Returns an Array of menu item Objects. getDefaultTemplate: -> - template = [ + [ label: "Atom" submenu: [ + {label: "Check for Update", metadata: {autoUpdate: true}} {label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()} {label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()} {label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()} @@ -129,10 +128,6 @@ class ApplicationMenu ] ] - # Add `Check for Update` button if autoUpdateManager is enabled. - template[0].submenu.unshift({label: "Check for Update", metadata: {autoUpdate: true}}) unless @autoUpdateManager.isDisabled() - template - focusedWindow: -> _.find global.atomApplication.windows, (atomWindow) -> atomWindow.isFocused() diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index e720597e3..44848eb72 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -74,8 +74,7 @@ class AtomApplication @pidsToOpenWindows = {} @windows = [] - disableAutoUpdate = require(path.join(@resourcePath, 'package.json'))._disableAutoUpdate ? false - @autoUpdateManager = new AutoUpdateManager(@version, options.test, disableAutoUpdate) + @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath) @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @@ -167,7 +166,7 @@ class AtomApplication safeMode: @focusedWindow()?.safeMode @on 'application:quit', -> app.quit() - @on 'application:new-window', -> @openPath(_.extend(windowDimensions: @focusedWindow()?.getDimensions(), getLoadSettings())) + @on 'application:new-window', -> @openPath(getLoadSettings()) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() @on 'application:open', -> @promptForPathToOpen('all', getLoadSettings()) @on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings()) @@ -229,7 +228,7 @@ class AtomApplication @openUrl({urlToOpen, @devMode, @safeMode}) app.on 'activate-with-no-open-windows', (event) => - event.preventDefault() + event?.preventDefault() @emit('application:new-window') # A request from the associated render process to open a new render process. @@ -360,6 +359,24 @@ class AtomApplication focusedWindow: -> _.find @windows, (atomWindow) -> atomWindow.isFocused() + # Get the platform-specific window offset for new windows. + getWindowOffsetForCurrentPlatform: -> + offsetByPlatform = + darwin: 22 + win32: 26 + offsetByPlatform[process.platform] ? 0 + + # Get the dimensions for opening a new window by cascading as appropriate to + # the platform. + getDimensionsForNewWindow: -> + return if (@focusedWindow() ? @lastFocusedWindow)?.isMaximized() + dimensions = (@focusedWindow() ? @lastFocusedWindow)?.getDimensions() + offset = @getWindowOffsetForCurrentPlatform() + if dimensions? and offset? + dimensions.x += offset + dimensions.y += offset + dimensions + # Public: Opens a single path, in an existing window if possible. # # options - @@ -370,7 +387,7 @@ class AtomApplication # :safeMode - Boolean to control the opened window's safe 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, profileStartup, window}) -> + openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window} = {}) -> @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window}) # Public: Opens multiple paths, in existing windows if possible. @@ -417,6 +434,7 @@ class AtomApplication windowInitializationScript ?= require.resolve('../initialize-application-window') resourcePath ?= @resourcePath + windowDimensions ?= @getDimensionsForNewWindow() openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup}) if pidToKillWhenClosed? diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index c507b634c..6e2d39266 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -212,6 +212,8 @@ class AtomWindow isFocused: -> @browserWindow.isFocused() + isMaximized: -> @browserWindow.isMaximized() + isMinimized: -> @browserWindow.isMinimized() isWebViewFocused: -> @browserWindow.isWebViewFocused() diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 55ab2462b..6a008d44d 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -1,5 +1,6 @@ autoUpdater = null _ = require 'underscore-plus' +Config = require '../config' {EventEmitter} = require 'events' path = require 'path' @@ -15,10 +16,13 @@ module.exports = class AutoUpdateManager _.extend @prototype, EventEmitter.prototype - constructor: (@version, @testMode, @disabled) -> + constructor: (@version, @testMode, resourcePath) -> @state = IdleState @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') @feedUrl = "https://atom.io/api/updates?version=#{@version}" + @config = new Config({configDirPath: process.env.ATOM_HOME, resourcePath, enablePersistence: true}) + @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))} + @config.load() process.nextTick => @setupAutoUpdater() setupAutoUpdater: -> @@ -46,9 +50,13 @@ class AutoUpdateManager @setState(UpdateAvailableState) @emitUpdateAvailableEvent(@getWindows()...) - # Only check for updates periodically if enabled and running in release - # version. - @scheduleUpdateCheck() unless /\w{7}/.test(@version) or @disabled + @config.onDidChange 'core.automaticallyUpdate', ({newValue}) => + if newValue + @scheduleUpdateCheck() + else + @cancelScheduledUpdateCheck() + + @scheduleUpdateCheck() if @config.get 'core.automaticallyUpdate' switch process.platform when 'win32' @@ -56,9 +64,6 @@ class AutoUpdateManager when 'linux' @setState(UnsupportedState) - isDisabled: -> - @disabled - emitUpdateAvailableEvent: (windows...) -> return unless @releaseVersion? for atomWindow in windows @@ -74,10 +79,18 @@ class AutoUpdateManager @state scheduleUpdateCheck: -> - checkForUpdates = => @check(hidePopups: true) - fourHours = 1000 * 60 * 60 * 4 - setInterval(checkForUpdates, fourHours) - checkForUpdates() + # Only schedule update check periodically if running in release version and + # and there is no existing scheduled update check. + unless /\w{7}/.test(@version) or @checkForUpdatesIntervalID + checkForUpdates = => @check(hidePopups: true) + fourHours = 1000 * 60 * 60 * 4 + @checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours) + checkForUpdates() + + cancelScheduledUpdateCheck: -> + if @checkForUpdatesIntervalID + clearInterval(@checkForUpdatesIntervalID) + @checkForUpdatesIntervalID = null check: ({hidePopups}={}) -> unless hidePopups diff --git a/src/config-schema.coffee b/src/config-schema.coffee index d9c0c1d21..88e00c71d 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -104,6 +104,10 @@ module.exports = description: 'Automatically open an empty editor on startup.' type: 'boolean' default: true + automaticallyUpdate: + description: 'Automatically update Atom when a new release is available.' + type: 'boolean' + default: true editor: type: 'object' diff --git a/src/config.coffee b/src/config.coffee index 2e4387732..2a883a57d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -779,9 +779,14 @@ class Config loadUserConfig: -> return if @shouldNotAccessFileSystem() - unless fs.existsSync(@configFilePath) - fs.makeTreeSync(path.dirname(@configFilePath)) - CSON.writeFileSync(@configFilePath, {}) + try + unless fs.existsSync(@configFilePath) + fs.makeTreeSync(path.dirname(@configFilePath)) + CSON.writeFileSync(@configFilePath, {}) + catch error + @configFileHasErrors = true + @notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack) + return try unless @savePending @@ -820,7 +825,7 @@ class Config @watchSubscription = null notifyFailure: (errorMessage, detail) -> - @notificationManager.addError(errorMessage, {detail, dismissable: true}) + @notificationManager?.addError(errorMessage, {detail, dismissable: true}) save: -> return if @shouldNotAccessFileSystem() diff --git a/src/decoration.coffee b/src/decoration.coffee index f57d234d1..11e32236d 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -35,7 +35,6 @@ translateDecorationParamsOldToNew = (decorationParams) -> # the marker. module.exports = class Decoration - # Private: Check if the `decorationProperties.type` matches `type` # # * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` @@ -154,6 +153,13 @@ class Decoration @displayBuffer.scheduleUpdateDecorationsEvent() @emitter.emit 'did-change-properties', {oldProperties, newProperties} + ### + Section: Utility + ### + + inspect: -> + "" + ### Section: Private methods ### diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index f5a7bd853..8b95656f9 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -812,6 +812,7 @@ class DisplayBuffer extends Model decorationsState decorateMarker: (marker, decorationParams) -> + throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed() marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) @decorationsByMarkerId[marker.id] ?= [] diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 1663f9ad4..ee27f87a5 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -463,8 +463,12 @@ class GitRepository refreshStatus: -> @handlerPath ?= require.resolve('./repository-status-handler') + relativeProjectPaths = @project?.getPaths() + .map (path) => @relativize(path) + .filter (path) -> path.length > 0 + @statusTask?.terminate() - @statusTask = Task.once @handlerPath, @getPath(), ({statuses, upstream, branch, submodules}) => + @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream) and _.isEqual(branch, @branch) and diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee index 32dbca0a2..30f13fff2 100644 --- a/src/line-numbers-tile-component.coffee +++ b/src/line-numbers-tile-component.coffee @@ -96,12 +96,13 @@ class LineNumbersTileComponent screenRowForNode: (node) -> parseInt(node.dataset.screenRow) buildLineNumberNode: (lineNumberState) -> - {screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState + {screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex, blockDecorationsHeight} = lineNumberState className = @buildLineNumberClassName(lineNumberState) lineNumberNode = @domElementPool.buildElement("div", className) lineNumberNode.dataset.screenRow = screenRow lineNumberNode.dataset.bufferRow = bufferRow + lineNumberNode.style.marginTop = blockDecorationsHeight + "px" @setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode) lineNumberNode @@ -139,6 +140,10 @@ class LineNumbersTileComponent oldLineNumberState.screenRow = newLineNumberState.screenRow oldLineNumberState.bufferRow = newLineNumberState.bufferRow + unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight + node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px" + oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight + buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) -> className = "line-number" className += " " + decorationClasses.join(' ') if decorationClasses? diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 6b4ac80ba..defcc0d8a 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -20,6 +20,8 @@ class LinesTileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @textNodesByLineId = {} + @insertionPointsBeforeLineById = {} + @insertionPointsAfterLineById = {} @domNode = @domElementPool.buildElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" @@ -80,6 +82,9 @@ class LinesTileComponent removeLineNode: (id) -> @domElementPool.freeElementAndDescendants(@lineNodesByLineId[id]) + @removeBlockDecorationInsertionPointBeforeLine(id) + @removeBlockDecorationInsertionPointAfterLine(id) + delete @lineNodesByLineId[id] delete @textNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] @@ -116,6 +121,71 @@ class LinesTileComponent else @domNode.appendChild(lineNode) + @insertBlockDecorationInsertionPointBeforeLine(id) + @insertBlockDecorationInsertionPointAfterLine(id) + + removeBlockDecorationInsertionPointBeforeLine: (id) -> + if insertionPoint = @insertionPointsBeforeLineById[id] + @domElementPool.freeElementAndDescendants(insertionPoint) + delete @insertionPointsBeforeLineById[id] + + insertBlockDecorationInsertionPointBeforeLine: (id) -> + {hasPrecedingBlockDecorations, screenRow} = @newTileState.lines[id] + + if hasPrecedingBlockDecorations + lineNode = @lineNodesByLineId[id] + insertionPoint = @domElementPool.buildElement("content") + @domNode.insertBefore(insertionPoint, lineNode) + @insertionPointsBeforeLineById[id] = insertionPoint + insertionPoint.dataset.screenRow = screenRow + @updateBlockDecorationInsertionPointBeforeLine(id) + + updateBlockDecorationInsertionPointBeforeLine: (id) -> + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] + insertionPoint = @insertionPointsBeforeLineById[id] + return unless insertionPoint? + + if newLineState.screenRow isnt oldLineState.screenRow + insertionPoint.dataset.screenRow = newLineState.screenRow + + precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') + + if precedingBlockDecorationsSelector isnt oldLineState.precedingBlockDecorationsSelector + insertionPoint.setAttribute("select", precedingBlockDecorationsSelector) + oldLineState.precedingBlockDecorationsSelector = precedingBlockDecorationsSelector + + removeBlockDecorationInsertionPointAfterLine: (id) -> + if insertionPoint = @insertionPointsAfterLineById[id] + @domElementPool.freeElementAndDescendants(insertionPoint) + delete @insertionPointsAfterLineById[id] + + insertBlockDecorationInsertionPointAfterLine: (id) -> + {hasFollowingBlockDecorations, screenRow} = @newTileState.lines[id] + + if hasFollowingBlockDecorations + lineNode = @lineNodesByLineId[id] + insertionPoint = @domElementPool.buildElement("content") + @domNode.insertBefore(insertionPoint, lineNode.nextSibling) + @insertionPointsAfterLineById[id] = insertionPoint + insertionPoint.dataset.screenRow = screenRow + @updateBlockDecorationInsertionPointAfterLine(id) + + updateBlockDecorationInsertionPointAfterLine: (id) -> + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] + insertionPoint = @insertionPointsAfterLineById[id] + return unless insertionPoint? + + if newLineState.screenRow isnt oldLineState.screenRow + insertionPoint.dataset.screenRow = newLineState.screenRow + + followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',') + + if followingBlockDecorationsSelector isnt oldLineState.followingBlockDecorationsSelector + insertionPoint.setAttribute("select", followingBlockDecorationsSelector) + oldLineState.followingBlockDecorationsSelector = followingBlockDecorationsSelector + findNodeNextTo: (node) -> for nextNode, index in @domNode.children continue if index is 0 # skips highlights node @@ -336,12 +406,28 @@ class LinesTileComponent oldLineState.decorationClasses = newLineState.decorationClasses + if not oldLineState.hasPrecedingBlockDecorations and newLineState.hasPrecedingBlockDecorations + @insertBlockDecorationInsertionPointBeforeLine(id) + else if oldLineState.hasPrecedingBlockDecorations and not newLineState.hasPrecedingBlockDecorations + @removeBlockDecorationInsertionPointBeforeLine(id) + + if not oldLineState.hasFollowingBlockDecorations and newLineState.hasFollowingBlockDecorations + @insertBlockDecorationInsertionPointAfterLine(id) + else if oldLineState.hasFollowingBlockDecorations and not newLineState.hasFollowingBlockDecorations + @removeBlockDecorationInsertionPointAfterLine(id) + if newLineState.screenRow isnt oldLineState.screenRow lineNode.dataset.screenRow = newLineState.screenRow - oldLineState.screenRow = newLineState.screenRow @lineIdsByScreenRow[newLineState.screenRow] = id @screenRowsByLineId[id] = newLineState.screenRow + @updateBlockDecorationInsertionPointBeforeLine(id) + @updateBlockDecorationInsertionPointAfterLine(id) + + oldLineState.screenRow = newLineState.screenRow + oldLineState.hasPrecedingBlockDecorations = newLineState.hasPrecedingBlockDecorations + oldLineState.hasFollowingBlockDecorations = newLineState.hasFollowingBlockDecorations + lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index bd8219e81..2373463af 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -3,7 +3,7 @@ TokenIterator = require './token-iterator' module.exports = class LinesYardstick - constructor: (@model, @lineNodesProvider, grammarRegistry) -> + constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) -> @tokenIterator = new TokenIterator({grammarRegistry}) @rangeForMeasurement = document.createRange() @invalidateCache() @@ -20,8 +20,8 @@ class LinesYardstick targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() - row = Math.floor(targetTop / @model.getLineHeightInPixels()) - targetLeft = 0 if row < 0 + row = @lineTopIndex.rowForPixelPosition(targetTop) + targetLeft = 0 if targetTop < 0 targetLeft = Infinity if row > @model.getLastScreenRow() row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) @@ -77,14 +77,11 @@ class LinesYardstick else Point(row, column) - pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @model.clipScreenPosition(screenPosition) if clip - + pixelPositionForScreenPosition: (screenPosition) -> targetRow = screenPosition.row targetColumn = screenPosition.column - top = targetRow * @model.getLineHeightInPixels() + top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow) left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) {top, left} diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 6772178af..1ecdc5448 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -199,7 +199,10 @@ class PackageManager # Returns the {Package} that was disabled or null if it isn't loaded. disablePackage: (name) -> pack = @loadPackage(name) - pack?.disable() + + unless @isPackageDisabled(name) + pack?.disable() + pack # Public: Is the package with the given name disabled? diff --git a/src/pane.coffee b/src/pane.coffee index 239e0eeff..df8889aac 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -731,30 +731,28 @@ class Pane extends Model message = "#{message} '#{itemPath}'" if itemPath @notificationManager.addWarning(message, options) - if error.code is 'EISDIR' or error.message?.endsWith?('is a directory') + customMessage = @getMessageForErrorCode(error.code) + if customMessage? + addWarningWithPath("Unable to save file: #{customMessage}") + else if error.code is 'EISDIR' or error.message?.endsWith?('is a directory') @notificationManager.addWarning("Unable to save file: #{error.message}") - else if error.code is 'EACCES' - addWarningWithPath('Unable to save file: Permission denied') else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN'] addWarningWithPath('Unable to save file', detail: error.message) - else if error.code is 'EROFS' - addWarningWithPath('Unable to save file: Read-only file system') - else if error.code is 'ENOSPC' - addWarningWithPath('Unable to save file: No space left on device') - else if error.code is 'ENXIO' - addWarningWithPath('Unable to save file: No such device or address') - else if error.code is 'ENOTSUP' - addWarningWithPath('Unable to save file: Operation not supported on socket') - else if error.code is 'EIO' - addWarningWithPath('Unable to save file: I/O error writing file') - else if error.code is 'EINTR' - addWarningWithPath('Unable to save file: Interrupted system call') - else if error.code is 'ECONNRESET' - addWarningWithPath('Unable to save file: Connection reset') - else if error.code is 'ESPIPE' - addWarningWithPath('Unable to save file: Invalid seek') else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) fileName = errorMatch[1] @notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") else throw error + + getMessageForErrorCode: (errorCode) -> + switch errorCode + when 'EACCES' then 'Permission denied' + when 'ECONNRESET' then 'Connection reset' + when 'EINTR' then 'Interrupted system call' + when 'EIO' then 'I/O error writing file' + when 'ENOSPC' then 'No space left on device' + when 'ENOTSUP' then 'Operation not supported on socket' + when 'ENXIO' then 'No such device or address' + when 'EROFS' then 'Read-only file system' + when 'ESPIPE' then 'Invalid seek' + when 'ETIMEDOUT' then 'Connection timed out' diff --git a/src/project.coffee b/src/project.coffee index d59c041cb..714511f9a 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -288,7 +288,7 @@ class Project extends Model 'atom.repository-provider', '^0.1.0', (provider) => - @repositoryProviders.push(provider) + @repositoryProviders.unshift(provider) @setPaths(@getPaths()) if null in @repositories new Disposable => @repositoryProviders.splice(@repositoryProviders.indexOf(provider), 1) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 159ea1abc..b4649f70e 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -169,7 +169,8 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) 'editor:log-cursor-scope': -> @logCursorScope() - 'editor:copy-path': -> @copyPathToClipboard() + 'editor:copy-path': -> @copyPathToClipboard(false) + 'editor:copy-project-path': -> @copyPathToClipboard(true) 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': -> @scrollToCursorPosition() diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index d0763fd5a..2fda9a335 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -1,7 +1,7 @@ Git = require 'git-utils' path = require 'path' -module.exports = (repoPath) -> +module.exports = (repoPath, paths = []) -> repo = Git.open(repoPath) upstream = {} @@ -12,7 +12,8 @@ module.exports = (repoPath) -> if repo? # Statuses in main repo workingDirectoryPath = repo.getWorkingDirectory() - for filePath, status of repo.getStatus() + repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus()) + for filePath, status of repoStatus statuses[filePath] = status # Statuses in submodules diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 6a84c8dac..4a51badbd 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -13,6 +13,8 @@ ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' LinesYardstick = require './lines-yardstick' +BlockDecorationsComponent = require './block-decorations-component' +LineTopIndex = require 'line-top-index' module.exports = class TextEditorComponent @@ -48,6 +50,9 @@ class TextEditorComponent @observeConfig() @setScrollSensitivity(@config.get('editor.scrollSensitivity')) + lineTopIndex = new LineTopIndex({ + defaultLineHeight: @editor.getLineHeightInPixels() + }) @presenter = new TextEditorPresenter model: @editor tileSize: tileSize @@ -55,11 +60,11 @@ class TextEditorComponent cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 config: @config + lineTopIndex: lineTopIndex @presenter.onDidUpdateState(@requestUpdate) @domElementPool = new DOMElementPool - @domNode = document.createElement('div') if @useShadowDOM @domNode.classList.add('editor-contents--private') @@ -68,6 +73,7 @@ class TextEditorComponent insertionPoint.setAttribute('select', 'atom-overlay') @domNode.appendChild(insertionPoint) @overlayManager = new OverlayManager(@presenter, @hostElement, @views) + @blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool) else @domNode.classList.add('editor-contents') @overlayManager = new OverlayManager(@presenter, @domNode, @views) @@ -82,7 +88,10 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, @linesComponent, @grammars) + if @blockDecorationsComponent? + @linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode()) + + @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars) @presenter.setLinesYardstick(@linesYardstick) @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @@ -158,6 +167,7 @@ class TextEditorComponent @hiddenInputComponent.updateSync(@newState) @linesComponent.updateSync(@newState) + @blockDecorationsComponent?.updateSync(@newState) @horizontalScrollbarComponent.updateSync(@newState) @verticalScrollbarComponent.updateSync(@newState) @scrollbarCornerComponent.updateSync(@newState) @@ -177,6 +187,7 @@ class TextEditorComponent readAfterUpdateSync: => @overlayManager?.measureOverlays() + @blockDecorationsComponent?.measureBlockDecorations() if @isVisible() mountGutterContainerComponent: -> @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) @@ -279,13 +290,13 @@ class TextEditorComponent observeConfig: -> @disposables.add @config.onDidChange 'editor.fontSize', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() @disposables.add @config.onDidChange 'editor.fontFamily', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() @disposables.add @config.onDidChange 'editor.lineHeight', => @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() onGrammarChanged: => if @scopedConfigDisposables? @@ -434,12 +445,17 @@ class TextEditorComponent getVisibleRowRange: -> @presenter.getVisibleRowRange() - pixelPositionForScreenPosition: (screenPosition, clip) -> + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @editor.clipScreenPosition(screenPosition) if clip + unless @presenter.isRowVisible(screenPosition.row) @presenter.setScreenRowsToMeasure([screenPosition.row]) + + unless @linesComponent.lineNodeForLineIdAndScreenRow(@presenter.lineIdForScreenRow(screenPosition.row), screenPosition.row)? @updateSyncPreMeasurement() - pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition) @presenter.clearScreenRowsToMeasure() pixelPosition @@ -480,6 +496,9 @@ class TextEditorComponent @editor.screenPositionForBufferPosition(bufferPosition) ) + invalidateBlockDecorationDimensions: -> + @presenter.invalidateBlockDecorationDimensions(arguments...) + onMouseDown: (event) => unless event.button is 0 or (event.button is 1 and process.platform is 'linux') # Only handle mouse down events for left mouse button on all platforms @@ -597,7 +616,7 @@ class TextEditorComponent handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() - @invalidateCharacterWidths() + @invalidateMeasurements() handleDragUntilMouseUp: (dragHandler) -> dragging = false @@ -751,7 +770,7 @@ class TextEditorComponent if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true @measureLineHeightAndDefaultCharWidth() - @invalidateCharacterWidths() + @invalidateMeasurements() sampleBackgroundColors: (suppressUpdate) -> {backgroundColor} = getComputedStyle(@hostElement) @@ -861,7 +880,7 @@ class TextEditorComponent setFontSize: (fontSize) -> @getTopmostDOMNode().style.fontSize = fontSize + 'px' @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() getFontFamily: -> getComputedStyle(@getTopmostDOMNode()).fontFamily @@ -869,16 +888,16 @@ class TextEditorComponent setFontFamily: (fontFamily) -> @getTopmostDOMNode().style.fontFamily = fontFamily @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() setLineHeight: (lineHeight) -> @getTopmostDOMNode().style.lineHeight = lineHeight @sampleFontStyling() - @invalidateCharacterWidths() + @invalidateMeasurements() - invalidateCharacterWidths: -> + invalidateMeasurements: -> @linesYardstick.invalidateCache() - @presenter.characterWidthsChanged() + @presenter.measurementsChanged() setShowIndentGuide: (showIndentGuide) -> @config.set("editor.showIndentGuide", showIndentGuide) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 1a55eb002..380417163 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -347,4 +347,13 @@ class TextEditorElement extends HTMLElement getHeight: -> @offsetHeight + # Experimental: Invalidate the passed block {Decoration} dimensions, forcing + # them to be recalculated and the surrounding content to be adjusted on the + # next animation frame. + # + # * {blockDecoration} A {Decoration} representing the block decoration you + # want to update the dimensions of. + invalidateBlockDecorationDimensions: -> + @component.invalidateBlockDecorationDimensions(arguments...) + module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 1912f4c2e..b13bf9036 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -13,7 +13,7 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @config} = params + {@model, @config, @lineTopIndex} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params {@contentFrameWidth} = params @@ -28,6 +28,9 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterName = {} + @observedBlockDecorations = new Set() + @invalidatedDimensionsByBlockDecoration = new Set() + @invalidateAllBlockDecorationsDimensions = false @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -85,6 +88,7 @@ class TextEditorPresenter if @shouldUpdateDecorations @fetchDecorations() @updateLineDecorations() + @updateBlockDecorations() @updateTilesState() @@ -126,7 +130,8 @@ class TextEditorPresenter @shouldUpdateDecorations = true observeModel: -> - @disposables.add @model.onDidChange => + @disposables.add @model.onDidChange ({start, end, screenDelta}) => + @spliceBlockDecorationsInRange(start, end, screenDelta) @shouldUpdateDecorations = true @emitDidUpdateState() @@ -134,6 +139,11 @@ class TextEditorPresenter @shouldUpdateDecorations = true @emitDidUpdateState() + @disposables.add @model.onDidAddDecoration(@didAddBlockDecoration.bind(this)) + + for decoration in @model.getDecorations({type: 'block'}) + this.didAddBlockDecoration(decoration) + @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText(@emitDidUpdateState.bind(this)) @disposables.add @model.onDidChangeMini => @@ -192,6 +202,7 @@ class TextEditorPresenter highlights: {} overlays: {} cursors: {} + blockDecorations: {} gutters: [] # Shared state that is copied into ``@state.gutters`. @sharedGutterStyles = {} @@ -327,6 +338,7 @@ class TextEditorPresenter zIndex = 0 for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize + tileEndRow = @constrainRow(tileStartRow + @tileSize) rowsWithinTile = [] while screenRowIndex >= 0 @@ -337,17 +349,21 @@ class TextEditorPresenter continue if rowsWithinTile.length is 0 + top = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow)) + bottom = Math.round(@lineTopIndex.pixelPositionBeforeBlocksForRow(tileEndRow)) + height = bottom - top + tile = @state.content.tiles[tileStartRow] ?= {} - tile.top = tileStartRow * @lineHeight - @scrollTop + tile.top = top - @scrollTop tile.left = -@scrollLeft - tile.height = @tileSize * @lineHeight + tile.height = height tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} - gutterTile.top = tileStartRow * @lineHeight - @scrollTop - gutterTile.height = @tileSize * @lineHeight + gutterTile.top = top - @scrollTop + gutterTile.height = height gutterTile.display = "block" gutterTile.zIndex = zIndex @@ -380,10 +396,16 @@ class TextEditorPresenter throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true + precedingBlockDecorations = @precedingBlockDecorationsByScreenRow[screenRow] ? [] + followingBlockDecorations = @followingBlockDecorationsByScreenRow[screenRow] ? [] if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) + lineState.precedingBlockDecorations = precedingBlockDecorations + lineState.followingBlockDecorations = followingBlockDecorations + lineState.hasPrecedingBlockDecorations = precedingBlockDecorations.length > 0 + lineState.hasFollowingBlockDecorations = followingBlockDecorations.length > 0 else tileState.lines[line.id] = screenRow: screenRow @@ -400,6 +422,10 @@ class TextEditorPresenter tabLength: line.tabLength fold: line.fold decorationClasses: @lineDecorationClassesForRow(screenRow) + precedingBlockDecorations: precedingBlockDecorations + followingBlockDecorations: followingBlockDecorations + hasPrecedingBlockDecorations: precedingBlockDecorations.length > 0 + hasFollowingBlockDecorations: followingBlockDecorations.length > 0 for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -433,7 +459,7 @@ class TextEditorPresenter else screenPosition = decoration.getMarker().getHeadScreenPosition() - pixelPosition = @pixelPositionForScreenPosition(screenPosition, true) + pixelPosition = @pixelPositionForScreenPosition(screenPosition) top = pixelPosition.top + @lineHeight left = pixelPosition.left + @gutterWidth @@ -536,9 +562,11 @@ class TextEditorPresenter continue unless @gutterIsVisible(gutter) for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] + top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row) + bottom = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) @customGutterDecorations[gutterName][decorationId] = - top: @lineHeight * screenRange.start.row - height: @lineHeight * screenRange.getRowCount() + top: top + height: bottom - top item: properties.item class: properties.class @@ -586,8 +614,13 @@ class TextEditorPresenter line = @model.tokenizedLineForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) + blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) + blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight + if screenRow % @tileSize isnt 0 + blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1) + blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight - tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight} visibleLineNumberIds[line.id] = true for id of tileState.lineNumbers @@ -598,16 +631,15 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - startRow = Math.floor(@scrollTop / @lineHeight) - @startRow = Math.max(0, startRow) + @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop)) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? - startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight)) - visibleLinesCount = Math.ceil(@height / @lineHeight) + 1 - endRow = startRow + visibleLinesCount - @endRow = Math.min(@model.getScreenLineCount(), endRow) + @endRow = Math.min( + @model.getScreenLineCount(), + @lineTopIndex.rowForPixelPosition(@scrollTop + @height + @lineHeight - 1) + 1 + ) updateRowsPerPage: -> rowsPerPage = Math.floor(@getClientHeight() / @lineHeight) @@ -639,7 +671,7 @@ class TextEditorPresenter updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight - @contentHeight = @lineHeight * @model.getScreenLineCount() + @contentHeight = Math.round(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getScreenLineCount())) if @contentHeight isnt oldContentHeight @updateHeight() @@ -649,8 +681,10 @@ class TextEditorPresenter updateHorizontalDimensions: -> if @baseCharacterWidth? oldContentWidth = @contentWidth - clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() - @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left + rightmostPosition = Point(@model.getLongestScreenRow(), @model.getMaxScreenLineLength()) + if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped() + rightmostPosition = @model.clipScreenPosition(rightmostPosition) + @contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width @@ -804,6 +838,7 @@ class TextEditorPresenter didStopScrolling: -> if @mouseWheelScreenRow? @mouseWheelScreenRow = null + @shouldUpdateDecorations = true @emitDidUpdateState() @@ -896,12 +931,15 @@ class TextEditorPresenter @editorWidthInChars = null @updateScrollbarDimensions() @updateClientWidth() + @invalidateAllBlockDecorationsDimensions = true @shouldUpdateDecorations = true @emitDidUpdateState() setBoundingClientRect: (boundingClientRect) -> unless @clientRectsEqual(@boundingClientRect, boundingClientRect) @boundingClientRect = boundingClientRect + @invalidateAllBlockDecorationsDimensions = true + @shouldUpdateDecorations = true @emitDidUpdateState() clientRectsEqual: (clientRectA, clientRectB) -> @@ -915,6 +953,8 @@ class TextEditorPresenter if @windowWidth isnt width or @windowHeight isnt height @windowWidth = width @windowHeight = height + @invalidateAllBlockDecorationsDimensions = true + @shouldUpdateDecorations = true @emitDidUpdateState() @@ -939,6 +979,8 @@ class TextEditorPresenter setLineHeight: (lineHeight) -> unless @lineHeight is lineHeight @lineHeight = lineHeight + @model.setLineHeightInPixels(@lineHeight) + @lineTopIndex.setDefaultLineHeight(@lineHeight) @restoreScrollTopIfNeeded() @model.setLineHeightInPixels(lineHeight) @shouldUpdateDecorations = true @@ -957,18 +999,19 @@ class TextEditorPresenter @koreanCharWidth = koreanCharWidth @model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) @restoreScrollLeftIfNeeded() - @characterWidthsChanged() + @measurementsChanged() - characterWidthsChanged: -> + measurementsChanged: -> + @invalidateAllBlockDecorationsDimensions = true @shouldUpdateDecorations = true @emitDidUpdateState() hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip=true) -> + pixelPositionForScreenPosition: (screenPosition) -> position = - @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip, true) + @linesYardstick.pixelPositionForScreenPosition(screenPosition) position.top -= @getScrollTop() position.left -= @getScrollLeft() @@ -987,14 +1030,14 @@ class TextEditorPresenter lineHeight = @model.getLineHeightInPixels() if screenRange.end.row > screenRange.start.row - top = @linesYardstick.pixelPositionForScreenPosition(screenRange.start, true).top + top = @linesYardstick.pixelPositionForScreenPosition(screenRange.start).top left = 0 height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight width = @getScrollWidth() else - {top, left} = @linesYardstick.pixelPositionForScreenPosition(screenRange.start, false) + {top, left} = @linesYardstick.pixelPositionForScreenPosition(screenRange.start) height = lineHeight - width = @linesYardstick.pixelPositionForScreenPosition(screenRange.end, false).left - left + width = @linesYardstick.pixelPositionForScreenPosition(screenRange.end).left - left {top, left, width, height} @@ -1012,6 +1055,43 @@ class TextEditorPresenter return unless 0 <= @startRow <= @endRow <= Infinity @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) + updateBlockDecorations: -> + @blockDecorationsToRenderById = {} + @precedingBlockDecorationsByScreenRow = {} + @followingBlockDecorationsByScreenRow = {} + visibleDecorationsByMarkerId = @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) + + if @invalidateAllBlockDecorationsDimensions + for decoration in @model.getDecorations(type: 'block') + @invalidatedDimensionsByBlockDecoration.add(decoration) + @invalidateAllBlockDecorationsDimensions = false + + for markerId, decorations of visibleDecorationsByMarkerId + for decoration in decorations when decoration.isType('block') + @updateBlockDecorationState(decoration, true) + + @invalidatedDimensionsByBlockDecoration.forEach (decoration) => + @updateBlockDecorationState(decoration, false) + + for decorationId, decorationState of @state.content.blockDecorations + continue if @blockDecorationsToRenderById[decorationId] + continue if decorationState.screenRow is @mouseWheelScreenRow + + delete @state.content.blockDecorations[decorationId] + + updateBlockDecorationState: (decoration, isVisible) -> + return if @blockDecorationsToRenderById[decoration.getId()] + + screenRow = decoration.getMarker().getHeadScreenPosition().row + if decoration.getProperties().position is "before" + @precedingBlockDecorationsByScreenRow[screenRow] ?= [] + @precedingBlockDecorationsByScreenRow[screenRow].push(decoration) + else + @followingBlockDecorationsByScreenRow[screenRow] ?= [] + @followingBlockDecorationsByScreenRow[screenRow].push(decoration) + @state.content.blockDecorations[decoration.getId()] = {decoration, screenRow, isVisible} + @blockDecorationsToRenderById[decoration.getId()] = true + updateLineDecorations: -> @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @@ -1134,13 +1214,13 @@ class TextEditorPresenter screenRange.end.column = 0 repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - tileStartRow * @lineHeight + region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow) region.left += @scrollLeft buildHighlightRegions: (screenRange) -> lineHeightInPixels = @lineHeight - startPixelPosition = @pixelPositionForScreenPosition(screenRange.start, false) - endPixelPosition = @pixelPositionForScreenPosition(screenRange.end, false) + startPixelPosition = @pixelPositionForScreenPosition(screenRange.start) + endPixelPosition = @pixelPositionForScreenPosition(screenRange.end) spannedRows = screenRange.end.row - screenRange.start.row + 1 regions = [] @@ -1204,6 +1284,73 @@ class TextEditorPresenter @emitDidUpdateState() + setBlockDecorationDimensions: (decoration, width, height) -> + return unless @observedBlockDecorations.has(decoration) + + @lineTopIndex.resizeBlock(decoration.getId(), height) + + @invalidatedDimensionsByBlockDecoration.delete(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + invalidateBlockDecorationDimensions: (decoration) -> + @invalidatedDimensionsByBlockDecoration.add(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + spliceBlockDecorationsInRange: (start, end, screenDelta) -> + return if screenDelta is 0 + + oldExtent = end - start + newExtent = end - start + screenDelta + invalidatedBlockDecorationIds = @lineTopIndex.splice(start, oldExtent, newExtent) + invalidatedBlockDecorationIds.forEach (id) => + decoration = @model.decorationForId(id) + newScreenPosition = decoration.getMarker().getHeadScreenPosition() + @lineTopIndex.moveBlock(id, newScreenPosition.row) + @invalidatedDimensionsByBlockDecoration.add(decoration) + + didAddBlockDecoration: (decoration) -> + return if not decoration.isType('block') or @observedBlockDecorations.has(decoration) + + didMoveDisposable = decoration.getMarker().bufferMarker.onDidChange (markerEvent) => + @didMoveBlockDecoration(decoration, markerEvent) + + didDestroyDisposable = decoration.onDidDestroy => + @disposables.remove(didMoveDisposable) + @disposables.remove(didDestroyDisposable) + didMoveDisposable.dispose() + didDestroyDisposable.dispose() + @didDestroyBlockDecoration(decoration) + + isAfter = decoration.getProperties().position is "after" + @lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row, 0, isAfter) + + @observedBlockDecorations.add(decoration) + @invalidateBlockDecorationDimensions(decoration) + @disposables.add(didMoveDisposable) + @disposables.add(didDestroyDisposable) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + didMoveBlockDecoration: (decoration, markerEvent) -> + # Don't move blocks after a text change, because we already splice on buffer + # change. + return if markerEvent.textChanged + + @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row) + @shouldUpdateDecorations = true + @emitDidUpdateState() + + didDestroyBlockDecoration: (decoration) -> + return unless @observedBlockDecorations.has(decoration) + + @lineTopIndex.removeBlock(decoration.getId()) + @observedBlockDecorations.delete(decoration) + @invalidatedDimensionsByBlockDecoration.delete(decoration) + @shouldUpdateDecorations = true + @emitDidUpdateState() + observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => @pauseCursorBlinking() @@ -1264,7 +1411,7 @@ class TextEditorPresenter @emitDidUpdateState() didChangeFirstVisibleScreenRow: (screenRow) -> - @setScrollTop(screenRow * @lineHeight) + @setScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(screenRow)) getVerticalScrollMarginInPixels: -> Math.round(@model.getVerticalScrollMargin() * @lineHeight) @@ -1285,8 +1432,8 @@ class TextEditorPresenter verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - top = screenRange.start.row * @lineHeight - bottom = (screenRange.end.row + 1) * @lineHeight + top = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.start.row) + bottom = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRange.end.row) + @lineHeight if options?.center desiredScrollCenter = (top + bottom) / 2 @@ -1358,7 +1505,7 @@ class TextEditorPresenter restoreScrollTopIfNeeded: -> unless @scrollTop? - @updateScrollTop(@model.getFirstVisibleScreenRow() * @lineHeight) + @updateScrollTop(@lineTopIndex.pixelPositionAfterBlocksForRow(@model.getFirstVisibleScreenRow())) restoreScrollLeftIfNeeded: -> unless @scrollLeft? @@ -1375,3 +1522,6 @@ class TextEditorPresenter isRowVisible: (row) -> @startRow <= row < @endRow + + lineIdForScreenRow: (screenRow) -> + @model.tokenizedLineForScreenRow(screenRow)?.id diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 7be1bec31..363047c86 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -668,8 +668,9 @@ class TextEditor extends Model isPending: -> Boolean(@pending) # Copies the current file path to the native clipboard. - copyPathToClipboard: -> + copyPathToClipboard: (relative = false) -> if filePath = @getPath() + filePath = atom.project.relativize(filePath) if relative @clipboard.write(filePath) ### @@ -1438,6 +1439,8 @@ class TextEditor extends Model # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter # decorations are created by calling {Gutter::decorateMarker} on the # desired `Gutter` instance. + # * __block__: Positions the view associated with the given item before or + # after the row of the given `TextEditorMarker`. # # ## Arguments # @@ -1457,11 +1460,14 @@ class TextEditor extends Model # property. # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling # {Gutter::decorateMarker} on the desired `Gutter` instance. + # * `block` Positions the view associated with the given item before or + # after the row of the given `TextEditorMarker`, depending on the `position` + # property. # * `class` This CSS class will be applied to the decorated line number, # line, highlight, or overlay. # * `item` (optional) An {HTMLElement} or a model {Object} with a - # corresponding view registered. Only applicable to the `gutter` and - # `overlay` types. + # corresponding view registered. Only applicable to the `gutter`, + # `overlay` and `block` types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to # the head of the `TextEditorMarker`. Only applicable to the `line` and # `line-number` types. @@ -1471,9 +1477,10 @@ class TextEditor extends Model # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied # if the associated `TextEditorMarker` is non-empty. Only applicable to the # `gutter`, `line`, and `line-number` types. - # * `position` (optional) Only applicable to decorations of type `overlay`, - # controls where the overlay view is positioned relative to the `TextEditorMarker`. - # Values can be `'head'` (the default), or `'tail'`. + # * `position` (optional) Only applicable to decorations of type `overlay` and `block`, + # controls where the view is positioned relative to the `TextEditorMarker`. + # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and + # `'before'` (the default) or `'after'` for block decorations. # # Returns a {Decoration} object decorateMarker: (marker, decorationParams) -> @@ -3052,7 +3059,7 @@ class TextEditor extends Model # Essential: Scrolls the editor to the given screen position. # - # * `screenPosition` An object that represents a buffer position. It can be either + # * `screenPosition` An object that represents a screen position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # * `options` (optional) {Object} # * `center` Center the editor around the position if possible. (default: false) diff --git a/src/workspace.coffee b/src/workspace.coffee index a90d314f7..4682c2321 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -475,7 +475,7 @@ class Workspace extends Model when 'EACCES' @notificationManager.addWarning("Permission denied '#{error.path}'") return Promise.resolve() - when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE', 'ENOTDIR' + when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE', 'ENOTDIR', 'EAGAIN' @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) return Promise.resolve() else