diff --git a/.travis.yml b/.travis.yml index d5918dc8d..06416e8e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ git: matrix: include: - os: linux - env: NODE_VERSION=4.4.7 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1 + env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1 sudo: false @@ -19,7 +19,9 @@ install: - npm install -g npm - script/build --create-debian-package --create-rpm-package --compress-artifacts -script: script/test +script: + - script/lint + - script/test cache: directories: diff --git a/README.md b/README.md index dc22ae866..74a5b4cee 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ repeat these steps to upgrade to future releases. ## Building * [Linux](./docs/build-instructions/linux.md) -* [macOS](./docs/build-instructions/macos.md) +* [macOS](./docs/build-instructions/macOS.md) * [FreeBSD](./docs/build-instructions/freebsd.md) * [Windows](./docs/build-instructions/windows.md) diff --git a/apm/package.json b/apm/package.json index 732ab208a..ede2d1bd7 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.15.1" + "atom-package-manager": "1.15.3" } } diff --git a/appveyor.yml b/appveyor.yml index a9a0d7920..3d8c0b274 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,6 +27,7 @@ build_script: - script\build.cmd --code-sign --create-windows-installer --compress-artifacts test_script: + - script\lint.cmd - script\test.cmd deploy: off diff --git a/benchmarks/text-editor-large-file-construction.bench.js b/benchmarks/text-editor-large-file-construction.bench.js index 694729724..64c6f4d94 100644 --- a/benchmarks/text-editor-large-file-construction.bench.js +++ b/benchmarks/text-editor-large-file-construction.bench.js @@ -26,7 +26,7 @@ export default async function ({test}) { console.log(text.length / 1024) let t0 = window.performance.now() - const buffer = new TextBuffer(text) + const buffer = new TextBuffer({text}) const editor = new TextEditor({buffer, largeFileMode: true}) atom.workspace.getActivePane().activateItem(editor) let t1 = window.performance.now() diff --git a/benchmarks/text-editor-long-lines.bench.js b/benchmarks/text-editor-long-lines.bench.js new file mode 100644 index 000000000..a99220d4e --- /dev/null +++ b/benchmarks/text-editor-long-lines.bench.js @@ -0,0 +1,97 @@ +/** @babel */ + +import path from 'path' +import fs from 'fs' +import {TextEditor, TextBuffer} from 'atom' + +const SIZES_IN_KB = [ + 512, + 1024, + 2048 +] +const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '') +const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length)) + +export default async function ({test}) { + const data = [] + + const workspaceElement = atom.views.getView(atom.workspace) + document.body.appendChild(workspaceElement) + + atom.packages.loadPackages() + await atom.packages.activate() + + console.log(atom.getLoadSettings().resourcePath); + + for (let pane of atom.workspace.getPanes()) { + pane.destroy() + } + + for (const sizeInKB of SIZES_IN_KB) { + const text = TEXT.slice(0, sizeInKB * 1024) + console.log(text.length / 1024) + + let t0 = window.performance.now() + const buffer = new TextBuffer({text}) + const editor = new TextEditor({buffer, largeFileMode: true}) + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + atom.workspace.getActivePane().activateItem(editor) + let t1 = window.performance.now() + + data.push({ + name: 'Opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + const tickDurations = [] + for (let i = 0; i < 20; i++) { + await timeout(50) + t0 = window.performance.now() + await timeout(0) + t1 = window.performance.now() + tickDurations[i] = t1 - t0 + } + + data.push({ + name: 'Max time event loop was blocked after opening a large single-line file', + x: sizeInKB, + duration: Math.max(...tickDurations) + }) + + t0 = window.performance.now() + editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({ + top: 100, + left: 30 + })) + t1 = window.performance.now() + + data.push({ + name: 'Clicking the editor after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + t0 = window.performance.now() + editor.element.setScrollTop(editor.element.getScrollTop() + 100) + t1 = window.performance.now() + + data.push({ + name: 'Scrolling down after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + editor.destroy() + buffer.destroy() + await timeout(10000) + } + + workspaceElement.remove() + + return data +} + +function timeout (duration) { + return new Promise((resolve) => setTimeout(resolve, duration)) +} diff --git a/circle.yml b/circle.yml index ee4eafc1f..c264754d4 100644 --- a/circle.yml +++ b/circle.yml @@ -16,8 +16,8 @@ general: dependencies: pre: - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash - - nvm install 4.4.7 - - nvm use 4.4.7 + - nvm install 6.9.4 + - nvm use 6.9.4 - npm install -g npm override: diff --git a/docs/build-instructions/build-status.md b/docs/build-instructions/build-status.md index 9b381527f..6a366b430 100644 --- a/docs/build-instructions/build-status.md +++ b/docs/build-instructions/build-status.md @@ -71,6 +71,7 @@ | [PathWatcher](https://github.com/atom/node-pathwatcher) | [![macOS Build Status](https://travis-ci.org/atom/node-pathwatcher.svg?branch=master)](https://travis-ci.org/atom/node-pathwatcher) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/li8dkoucdrc2ryts/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/node-pathwatcher) | [![Dependency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) | | [Property Accessors](https://github.com/atom/property-accessors) | [![macOS Build Status](https://travis-ci.org/atom/property-accessors.svg?branch=master)](https://travis-ci.org/atom/property-accessors) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ww4d10hi4v5h7kbp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/property-accessors/branch/master) | [![Dependency Status](https://david-dm.org/atom/property-accessors.svg)](https://david-dm.org/atom/property-accessors) | | [Season](https://github.com/atom/season) | [![macOS Build Status](https://travis-ci.org/atom/season.svg?branch=master)](https://travis-ci.org/atom/season) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/v3bth3ooq5q8k8lx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/season) | [![Dependency Status](https://david-dm.org/atom/season.svg)](https://david-dm.org/atom/season) | +| [Superstring](https://github.com/atom/superstring) | [![macOS Build Status](https://travis-ci.org/atom/superstring.svg?branch=master)](https://travis-ci.org/atom/superstring) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/n5pack4yk7w80fso/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/superstring/branch/master) | | [![Dependency Status](https://david-dm.org/atom/superstring.svg)](https://david-dm.org/atom/superstring) | | [TextBuffer](https://github.com/atom/text-buffer) | [![macOS Build Status](https://travis-ci.org/atom/text-buffer.svg?branch=master)](https://travis-ci.org/atom/text-buffer) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/48xl8do1sm2thf5p/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/text-buffer/branch/master) | [![Dependency Status](https://david-dm.org/atom/text-buffer.svg)](https://david-dm.org/atom/text-buffer) | | [Underscore-Plus](https://github.com/atom/underscore-plus) | [![macOS Build Status](https://travis-ci.org/atom/underscore-plus.svg?branch=master)](https://travis-ci.org/atom/underscore-plus) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/c7l8009vgpaojxcd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/underscore-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/underscore-plus.svg)](https://david-dm.org/atom/underscore-plus) | diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 5f0211c0d..1caf740ba 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -7,7 +7,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * OS with 64-bit or 32-bit architecture * C++11 toolchain * Git -* Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) +* Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm)) * npm 3.10.x or later (run `npm install -g npm`) * Ensure node-gyp uses python2 (run `npm config set python /usr/bin/python2 -g`, use `sudo` if you didn't install node via nvm) * Development headers for [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring). diff --git a/docs/build-instructions/macos.md b/docs/build-instructions/macOS.md similarity index 90% rename from docs/build-instructions/macos.md rename to docs/build-instructions/macOS.md index 18169435f..0d9335eea 100644 --- a/docs/build-instructions/macos.md +++ b/docs/build-instructions/macOS.md @@ -3,7 +3,7 @@ ## Requirements * macOS 10.8 or later - * Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) + * Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm)) * npm 3.10.x or later (run `npm install -g npm`) * Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 5c8c189ef..2c231b2dc 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -2,7 +2,7 @@ ## Requirements -* Node.js 4.4.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) +* Node.js 6.x (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) * Python v2.7.x * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27` * 7zip (7z.exe available from the command line) - for creating distribution zip files diff --git a/package.json b/package.json index e7ff9d1c7..92a9920e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.15.0-dev", + "version": "1.16.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -15,10 +15,21 @@ "electronVersion": "1.3.13", "dependencies": { "async": "0.2.6", - "atom-keymap": "7.1.18", - "atom-select-list": "0.0.6", + "atom-keymap": "7.1.20", + "atom-select-list": "0.0.12", "atom-ui": "0.4.1", - "babel-core": "5.8.38", + "babel-core": "6.22.1", + "babel-plugin-add-module-exports": "0.2.1", + "babel-plugin-transform-async-to-generator": "6.22.0", + "babel-plugin-transform-class-properties": "6.23.0", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-transform-do-expressions": "6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "6.23.0", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-plugin-transform-function-bind": "6.22.0", + "babel-plugin-transform-object-rest-spread": "6.23.0", + "babel-plugin-transform-react-jsx": "6.23.0", "cached-run-in-this-context": "0.4.1", "chai": "3.5.0", "chart.js": "^2.3.0", @@ -65,7 +76,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "10.2.3", + "text-buffer": "10.3.11", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -82,85 +93,85 @@ "one-light-ui": "1.9.1", "one-dark-syntax": "1.7.1", "one-light-syntax": "1.7.1", - "solarized-dark-syntax": "1.1.1", - "solarized-light-syntax": "1.1.1", + "solarized-dark-syntax": "1.1.2", + "solarized-light-syntax": "1.1.2", "about": "1.7.2", "archive-view": "0.62.2", "autocomplete-atom-api": "0.10.0", - "autocomplete-css": "0.14.2", + "autocomplete-css": "0.15.0", "autocomplete-html": "0.7.2", "autocomplete-plus": "2.34.2", "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", "autosave": "0.24.0", "background-tips": "0.26.1", - "bookmarks": "0.43.4", + "bookmarks": "0.44.1", "bracket-matcher": "0.85.2", - "command-palette": "0.39.2", - "deprecation-cop": "0.55.1", + "command-palette": "0.40.2", + "deprecation-cop": "0.56.2", "dev-live-reload": "0.47.0", - "encoding-selector": "0.22.0", - "exception-reporting": "0.40.1", - "find-and-replace": "0.206.0", + "encoding-selector": "0.23.1", + "exception-reporting": "0.41.1", + "find-and-replace": "0.206.3", "fuzzy-finder": "1.4.1", - "git-diff": "1.2.0", - "go-to-line": "0.31.2", - "grammar-selector": "0.48.2", - "image-view": "0.60.0", + "git-diff": "1.3.1", + "go-to-line": "0.32.0", + "grammar-selector": "0.49.2", + "image-view": "0.61.0", "incompatible-packages": "0.26.1", - "keybinding-resolver": "0.35.0", - "line-ending-selector": "0.5.1", + "keybinding-resolver": "0.36.1", + "line-ending-selector": "0.6.1", "link": "0.31.2", - "markdown-preview": "0.159.2", - "metrics": "1.1.2", - "notifications": "0.66.1", + "markdown-preview": "0.159.7", + "metrics": "1.2.0", + "notifications": "0.66.2", "open-on-github": "1.2.1", - "package-generator": "1.0.2", - "settings-view": "0.246.0", + "package-generator": "1.1.0", + "settings-view": "0.247.0", "snippets": "1.0.5", - "spell-check": "0.70.2", - "status-bar": "1.7.0", - "styleguide": "0.48.0", + "spell-check": "0.71.0", + "status-bar": "1.8.1", + "styleguide": "0.49.2", "symbols-view": "0.114.0", "tabs": "0.104.1", - "timecop": "0.34.0", - "tree-view": "0.213.1", + "timecop": "0.35.0", + "tree-view": "0.214.1", "update-package-dependencies": "0.10.0", - "welcome": "0.36.0", - "whitespace": "0.36.1", - "wrap-guide": "0.39.0", - "language-c": "0.54.1", - "language-clojure": "0.22.1", - "language-coffee-script": "0.48.2", - "language-csharp": "0.14.0", + "welcome": "0.36.1", + "whitespace": "0.36.2", + "wrap-guide": "0.39.1", + "language-c": "0.56.0", + "language-clojure": "0.22.2", + "language-coffee-script": "0.48.4", + "language-csharp": "0.14.1", "language-css": "0.42.0", "language-gfm": "0.88.0", "language-git": "0.19.0", "language-go": "0.43.1", - "language-html": "0.47.1", + "language-html": "0.47.2", "language-hyperlink": "0.16.1", - "language-java": "0.25.0", - "language-javascript": "0.125.1", + "language-java": "0.26.0", + "language-javascript": "0.126.0", "language-json": "0.18.3", "language-less": "0.30.1", "language-make": "0.22.3", "language-mustache": "0.13.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.37.3", + "language-php": "0.37.4", "language-property-list": "0.9.0", - "language-python": "0.45.1", - "language-ruby": "0.70.4", - "language-ruby-on-rails": "0.25.1", + "language-python": "0.45.2", + "language-ruby": "0.70.5", + "language-ruby-on-rails": "0.25.2", "language-sass": "0.57.1", "language-shellscript": "0.25.0", "language-source": "0.9.0", - "language-sql": "0.25.2", + "language-sql": "0.25.3", "language-text": "0.7.1", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.34.15", - "language-yaml": "0.27.2" + "language-xml": "0.34.16", + "language-yaml": "0.28.0" }, "private": true, "scripts": { diff --git a/resources/linux/redhat/atom.spec.in b/resources/linux/redhat/atom.spec.in index bc2397126..82a5fbf9a 100644 --- a/resources/linux/redhat/atom.spec.in +++ b/resources/linux/redhat/atom.spec.in @@ -7,7 +7,7 @@ URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency Prefix: <%= installDir %> -Requires: lsb-core-noarch, libXss.so.1 +Requires: lsb-core-noarch, libXss.so.1()(64bit) %description <%= description %> diff --git a/script/lib/code-sign-on-mac.js b/script/lib/code-sign-on-mac.js index 80d316566..5e4c67707 100644 --- a/script/lib/code-sign-on-mac.js +++ b/script/lib/code-sign-on-mac.js @@ -5,15 +5,17 @@ const path = require('path') const spawnSync = require('./spawn-sync') module.exports = function (packagedAppPath) { - if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL) { + if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { console.log('Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) return } - try { - const certPath = path.join(os.tmpdir(), 'mac.p12') + let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH; + if (!certPath) { + certPath = path.join(os.tmpdir(), 'mac.p12') downloadFileFromGithub(process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) - + } + try { console.log(`Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) const unlockArgs = ['unlock-keychain'] // For signing on local workstations, password could be entered interactively @@ -31,6 +33,18 @@ module.exports = function (packagedAppPath) { '-T', '/usr/bin/codesign' ]) + + console.log('Running incantation to suppress dialog when signing on macOS Sierra') + try { + spawnSync('security', [ + 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', + '-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, + process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ]) + } catch (e) { + console.log('Incantation failed... maybe this isn\'t Sierra?'); + } + console.log(`Code-signing application at ${packagedAppPath}`) spawnSync('codesign', [ '--deep', '--force', '--verbose', @@ -38,7 +52,9 @@ module.exports = function (packagedAppPath) { '--sign', 'Developer ID Application: GitHub', packagedAppPath ], {stdio: 'inherit'}) } finally { - console.log(`Deleting certificate at ${certPath}`) - fs.removeSync(certPath) + if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { + console.log(`Deleting certificate at ${certPath}`) + fs.removeSync(certPath) + } } } diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index 4b9e0f3a3..8a0dc0f61 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -18,15 +18,19 @@ module.exports = function (packagedAppPath, codeSign) { iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`, loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'), outputDirectory: CONFIG.buildOutputPath, - remoteReleases: `https://atom.io/api/updates${archSuffix}`, + remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.appMetadata.version}`, setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') } - const certPath = path.join(os.tmpdir(), 'win.p12') - const signing = codeSign && process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL + const signing = codeSign && (process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL || process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) + let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH; if (signing) { - downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) + if (!certPath) { + certPath = path.join(os.tmpdir(), 'win.p12') + downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) + } + var signParams = [] signParams.push(`/f ${certPath}`) // Signing cert file signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password @@ -39,7 +43,7 @@ module.exports = function (packagedAppPath, codeSign) { } const cleanUp = function () { - if (fs.existsSync(certPath)) { + if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { console.log(`Deleting certificate at ${certPath}`) fs.removeSync(certPath) } diff --git a/script/package.json b/script/package.json index 1284fd83c..87c43f261 100644 --- a/script/package.json +++ b/script/package.json @@ -3,7 +3,6 @@ "description": "Atom build scripts", "dependencies": { "async": "2.0.1", - "babel-core": "5.8.38", "coffeelint": "1.15.7", "colors": "1.1.2", "csslint": "1.0.2", diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 9b9715a07..d967fb97b 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -142,6 +142,11 @@ describe "AtomEnvironment", -> atom.assert(false, "a == b", (e) -> error = e) expect(error).toBe errors[0] + describe "if passed metadata", -> + it "assigns the metadata on the assertion failure's error object", -> + atom.assert(false, "a == b", {foo: 'bar'}) + expect(errors[0].metadata).toEqual {foo: 'bar'} + describe "if the condition is true", -> it "does nothing", -> result = atom.assert(true, "a == b") diff --git a/spec/atom-paths-spec.js b/spec/atom-paths-spec.js index 4b1fc7902..3e2da4760 100644 --- a/spec/atom-paths-spec.js +++ b/spec/atom-paths-spec.js @@ -8,8 +8,7 @@ import path from 'path' const temp = require('temp').track() describe("AtomPaths", () => { - const portableAtomHomePath = path.join(atomPaths.getAppDirectory(), '.atom') - console.log(portableAtomHomePath) + const portableAtomHomePath = path.join(atomPaths.getAppDirectory(), '..', '.atom') afterEach(() => { atomPaths.setAtomHome(app.getPath('home')) diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 84d8b0440..6f9b2a28d 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -67,6 +67,29 @@ describe "BufferedProcess", -> expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`' expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError' + describe "when autoStart is false", -> + it "doesnt start unless start method is called", -> + stdout = '' + stderr = '' + exitCallback = jasmine.createSpy('exit callback') + apmProcess = new BufferedProcess + autoStart: false + command: atom.packages.getApmPath() + args: ['-h'] + options: {} + stdout: (lines) -> stdout += lines + stderr: (lines) -> stderr += lines + exit: exitCallback + + expect(apmProcess.started).not.toBe(true) + apmProcess.start() + expect(apmProcess.started).toBe(true) + + waitsFor -> exitCallback.callCount is 1 + runs -> + expect(stderr).toContain 'apm - Atom Package Manager' + expect(stdout).toEqual '' + it "calls the specified stdout, stderr, and exit callbacks", -> stdout = '' stderr = '' diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee index 44da440d9..e57660a57 100644 --- a/spec/decoration-manager-spec.coffee +++ b/spec/decoration-manager-spec.coffee @@ -1,13 +1,14 @@ DecorationManager = require '../src/decoration-manager' describe "DecorationManager", -> - [decorationManager, buffer, defaultMarkerLayer] = [] + [decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = [] beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') displayLayer = buffer.addDisplayLayer() - defaultMarkerLayer = displayLayer.addMarkerLayer() - decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer) + markerLayer1 = displayLayer.addMarkerLayer() + markerLayer2 = displayLayer.addMarkerLayer() + decorationManager = new DecorationManager(displayLayer) waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -17,38 +18,53 @@ describe "DecorationManager", -> buffer.release() describe "decorations", -> - [marker, decoration, decorationProperties] = [] + [layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = [] beforeEach -> - marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]]) + layer1Marker = markerLayer1.markBufferRange([[2, 13], [3, 15]]) decorationProperties = {type: 'line-number', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + layer2Marker = markerLayer2.markBufferRange([[2, 13], [3, 15]]) + layer2MarkerDecoration = decorationManager.decorateMarker(layer2Marker, decorationProperties) it "can add decorations associated with markers and remove them", -> - expect(decoration).toBeDefined() - expect(decoration.getProperties()).toBe decorationProperties - expect(decorationManager.decorationForId(decoration.id)).toBe decoration - expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration + expect(layer1MarkerDecoration).toBeDefined() + expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration + expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual { + "#{layer1Marker.id}": [layer1MarkerDecoration], + "#{layer2Marker.id}": [layer2MarkerDecoration] + } - decoration.destroy() - expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined() - expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + layer1MarkerDecoration.destroy() + expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined() + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() + layer2MarkerDecoration.destroy() + expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined() + expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined() it "will not fail if the decoration is removed twice", -> - decoration.destroy() - decoration.destroy() - expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + layer1MarkerDecoration.destroy() + layer1MarkerDecoration.destroy() + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() it "does not allow destroyed markers to be decorated", -> - marker.destroy() + layer1Marker.destroy() expect(-> - decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')}) + decorationManager.decorateMarker(layer1Marker, {type: 'overlay', item: document.createElement('div')}) ).toThrow("Cannot decorate a destroyed marker") expect(decorationManager.getOverlayDecorations()).toEqual [] + it "does not allow destroyed marker layers to be decorated", -> + layer = displayLayer.addMarkerLayer() + layer.destroy() + expect(-> + decorationManager.decorateMarkerLayer(layer, {type: 'highlight'}) + ).toThrow("Cannot decorate a destroyed marker layer") + 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() - decoration.setProperties type: 'line-number', class: 'two' + layer1MarkerDecoration.onDidChangeProperties updatedSpy = jasmine.createSpy() + layer1MarkerDecoration.setProperties type: 'line-number', class: 'two' {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] expect(oldProperties).toEqual decorationProperties @@ -56,29 +72,29 @@ describe "DecorationManager", -> describe "::getDecorations(properties)", -> it "returns decorations matching the given optional properties", -> - expect(decorationManager.getDecorations()).toEqual [decoration] + expect(decorationManager.getDecorations()).toEqual [layer1MarkerDecoration, layer2MarkerDecoration] expect(decorationManager.getDecorations(class: 'two').length).toEqual 0 - expect(decorationManager.getDecorations(class: 'one').length).toEqual 1 + expect(decorationManager.getDecorations(class: 'one').length).toEqual 2 describe "::decorateMarker", -> describe "when decorating gutters", -> - [marker] = [] + [layer1Marker] = [] beforeEach -> - marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]]) + layer1Marker = markerLayer1.markBufferRange([[1, 0], [1, 0]]) it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", -> decorationProperties = {type: 'line-number', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) - expect(decoration.isType('line-number')).toBe true - expect(decoration.isType('gutter')).toBe true - expect(decoration.getProperties().gutterName).toBe 'line-number' - expect(decoration.getProperties().class).toBe 'one' + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + expect(layer1MarkerDecoration.isType('line-number')).toBe true + expect(layer1MarkerDecoration.isType('gutter')).toBe true + expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'line-number' + expect(layer1MarkerDecoration.getProperties().class).toBe 'one' it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", -> decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) - expect(decoration.isType('gutter')).toBe true - expect(decoration.isType('line-number')).toBe false - expect(decoration.getProperties().gutterName).toBe 'test-gutter' - expect(decoration.getProperties().class).toBe 'one' + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + expect(layer1MarkerDecoration.isType('gutter')).toBe true + expect(layer1MarkerDecoration.isType('line-number')).toBe false + expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'test-gutter' + expect(layer1MarkerDecoration.getProperties().class).toBe 'one' diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index 821c278ee..bf23195cf 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -28,6 +28,15 @@ describe "DefaultDirectoryProvider", -> directory = provider.directoryForURISync(nonNormalizedPath) expect(directory.getPath()).toEqual tmp + it "normalizes disk drive letter in path on #win32", -> + provider = new DefaultDirectoryProvider() + nonNormalizedPath = tmp[0].toLowerCase()+tmp.slice(1) + expect(tmp).not.toMatch /^[a-z]:/ + expect(nonNormalizedPath).toMatch /^[a-z]:/ + + directory = provider.directoryForURISync(nonNormalizedPath) + expect(directory.getPath()).toEqual tmp + it "creates a Directory for its parent dir when passed a file", -> provider = new DefaultDirectoryProvider() file = path.join(tmp, "example.txt") diff --git a/spec/dom-element-pool-spec.coffee b/spec/dom-element-pool-spec.coffee deleted file mode 100644 index 2efe80beb..000000000 --- a/spec/dom-element-pool-spec.coffee +++ /dev/null @@ -1,60 +0,0 @@ -DOMElementPool = require '../src/dom-element-pool' -{contains} = require 'underscore-plus' - -describe "DOMElementPool", -> - domElementPool = null - - beforeEach -> - domElementPool = new DOMElementPool - - it "builds DOM nodes, recycling them when they are freed", -> - [div, span1, span2, span3, span4, span5, textNode] = elements = [ - domElementPool.buildElement("div") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildText("Hello world!") - ] - - div.appendChild(span1) - span1.appendChild(span2) - div.appendChild(span3) - span3.appendChild(span4) - span4.appendChild(textNode) - - domElementPool.freeElementAndDescendants(div) - domElementPool.freeElementAndDescendants(span5) - - expect(contains(elements, domElementPool.buildElement("div"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildText("another text"))).toBe(true) - - expect(contains(elements, domElementPool.buildElement("div"))).toBe(false) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(false) - expect(contains(elements, domElementPool.buildText("unexisting"))).toBe(false) - - it "forgets free nodes after being cleared", -> - span = domElementPool.buildElement("span") - div = domElementPool.buildElement("div") - domElementPool.freeElementAndDescendants(span) - domElementPool.freeElementAndDescendants(div) - - domElementPool.clear() - - expect(domElementPool.buildElement("span")).not.toBe(span) - expect(domElementPool.buildElement("div")).not.toBe(div) - - it "throws an error when trying to free the same node twice", -> - div = domElementPool.buildElement("div") - domElementPool.freeElementAndDescendants(div) - expect(-> domElementPool.freeElementAndDescendants(div)).toThrow() - - it "throws an error when trying to free an invalid element", -> - expect(-> domElementPool.freeElementAndDescendants(null)).toThrow() - expect(-> domElementPool.freeElementAndDescendants(undefined)).toThrow() diff --git a/spec/dom-element-pool-spec.js b/spec/dom-element-pool-spec.js new file mode 100644 index 000000000..9de932e27 --- /dev/null +++ b/spec/dom-element-pool-spec.js @@ -0,0 +1,112 @@ +const DOMElementPool = require ('../src/dom-element-pool') + +describe('DOMElementPool', function () { + let domElementPool + + beforeEach(() => { domElementPool = new DOMElementPool() }) + + it('builds DOM nodes, recycling them when they are freed', function () { + let elements + const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [ + domElementPool.buildElement('div', 'foo'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildText('Hello world!') + ]) + + expect(div.className).toBe('foo') + div.textContent = 'testing' + div.style.backgroundColor = 'red' + div.dataset.foo = 'bar' + + expect(textNode.textContent).toBe('Hello world!') + + div.appendChild(span1) + span1.appendChild(span2) + div.appendChild(span3) + span3.appendChild(span4) + span4.appendChild(textNode) + + domElementPool.freeElementAndDescendants(div) + domElementPool.freeElementAndDescendants(span5) + + expect(elements.includes(domElementPool.buildElement('div'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildText('another text'))).toBe(true) + + expect(elements.includes(domElementPool.buildElement('div'))).toBe(false) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(false) + expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false) + + expect(div.className).toBe('') + expect(div.textContent).toBe('') + expect(div.style.backgroundColor).toBe('') + expect(div.dataset.foo).toBeUndefined() + + expect(textNode.textContent).toBe('another text') + }) + + it('forgets free nodes after being cleared', function () { + const span = domElementPool.buildElement('span') + const div = domElementPool.buildElement('div') + domElementPool.freeElementAndDescendants(span) + domElementPool.freeElementAndDescendants(div) + + domElementPool.clear() + + expect(domElementPool.buildElement('span')).not.toBe(span) + expect(domElementPool.buildElement('div')).not.toBe(div) + }) + + it('does not attempt to free nodes that were not created by the pool', () => { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const foreignDiv = document.createElement('div') + const div = domElementPool.buildElement('div') + div.appendChild(foreignDiv) + domElementPool.freeElementAndDescendants(div) + const span = domElementPool.buildElement('span') + span.appendChild(foreignDiv) + domElementPool.freeElementAndDescendants(span) + + expect(assertionFailure).toBeUndefined() + }) + + it('fails an assertion when freeing the same element twice', function () { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const div = domElementPool.buildElement('div') + div.textContent = 'testing' + domElementPool.freeElementAndDescendants(div) + expect(assertionFailure).toBeUndefined() + domElementPool.freeElementAndDescendants(div) + expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') + expect(assertionFailure.metadata.content).toBe('
testing
') + }) + + it('fails an assertion when freeing the same text node twice', function () { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const node = domElementPool.buildText('testing') + domElementPool.freeElementAndDescendants(node) + expect(assertionFailure).toBeUndefined() + domElementPool.freeElementAndDescendants(node) + expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') + expect(assertionFailure.metadata.content).toBe('testing') + }) + + it('throws an error when trying to free an invalid element', function () { + expect(() => domElementPool.freeElementAndDescendants(null)).toThrow() + expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow() + }) +}) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index c1dab6c6b..68fd74804 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -78,10 +78,16 @@ describe "LinesYardstick", -> 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: 38, top: 0}) - if process.platform is 'darwin' # One pixel off on left on Win32 - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.875, top: 28}) + + switch process.platform + when 'darwin' + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.875, top: 28}) + when 'win32' + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 42, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 71, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 280, top: 28}) it "reuses already computed pixel positions unless it is invalidated", -> atom.styles.addStyleSheet """ @@ -134,28 +140,40 @@ describe "LinesYardstick", -> editor.setText(text) - return unless process.platform is 'darwin' # These numbers are 15 higher on win32 and always integer - 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 + switch process.platform + when 'darwin' + 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 + when 'win32' + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 245 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 252 + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 259 - if process.platform is 'darwin' # Expectations fail on win32 - it "handles lines containing a mix of left-to-right and right-to-left characters", -> - editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n') + it "handles lines containing a mix of left-to-right and right-to-left characters", -> + editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n') - atom.styles.addStyleSheet """ - * { - font-size: 14px; - font-family: monospace; - } - """ + atom.styles.addStyleSheet """ + * { + font-size: 14px; + font-family: monospace; + } + """ - lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) - linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0}) + lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) + + switch process.platform + when 'darwin' + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0}) + when 'win32' + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 120, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 496, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 464, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 832, top: 0}) describe "::screenPositionForPixelPosition(pixelPosition)", -> it "converts pixel positions to screen positions", -> @@ -176,9 +194,14 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14]) expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) - return unless process.platform is 'darwin' # Following tests are 1 pixel off on Win32 - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) - expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) + + switch process.platform + when 'darwin' + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) + expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) + when 'win32' + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 30]) + expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 34]) it "overshoots to the nearest character when text nodes are not spatially contiguous", -> atom.styles.addStyleSheet """ diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 77d7987a7..d30900b99 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -196,7 +196,9 @@ describe('AtomApplication', function () { it('persists window state based on the project directories', async function () { const tempDirPath = makeTempDir() const atomApplication = buildAtomApplication() - const window1 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'new-file')])) + const nonExistentFilePath = path.join(tempDirPath, 'new-file') + + const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath])) await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { atom.workspace.observeActivePaneItem(function (textEditor) { if (textEditor) { @@ -205,17 +207,31 @@ describe('AtomApplication', function () { } }) }) + await window1.saveState() window1.close() await window1.closedPromise - const window2 = atomApplication.launch(parseCommandLine([path.join(tempDirPath)])) + // Restore unsaved state when opening the directory itself + const window2 = atomApplication.launch(parseCommandLine([tempDirPath])) + await window2.loadedPromise const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeActivePaneItem(function (textEditor) { - if (textEditor) sendBackToMainProcess(textEditor.getText()) - }) + const textEditor = atom.workspace.getActiveTextEditor() + textEditor.moveToBottom() + textEditor.insertText(' How are you?') + sendBackToMainProcess(textEditor.getText()) }) + assert.equal(window2Text, 'Hello World! How are you?') + await window2.saveState() + window2.close() + await window2.closedPromise - assert.equal(window2Text, 'Hello World!') + // Restore unsaved state when opening a path to a non-existent file in the directory + const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')])) + await window3.loadedPromise + const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) { + sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText())) + }) + assert.include(window3Texts, 'Hello World! How are you?') }) it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () { @@ -260,7 +276,7 @@ describe('AtomApplication', function () { }) assert.equal(window1EditorTitle, 'untitled') - const window2 = atomApplication.launch(parseCommandLine([])) + const window2 = atomApplication.openWithOptions(parseCommandLine([])) await focusWindow(window2) const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) @@ -472,7 +488,7 @@ describe('AtomApplication', function () { } let channelIdCounter = 0 - function evalInWebContents (webContents, source) { + function evalInWebContents (webContents, source, ...args) { const channelId = 'eval-result-' + channelIdCounter++ return new Promise(function (resolve) { electron.ipcMain.on(channelId, receiveResult) diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 862b7f428..4821dbc9b 100644 --- a/spec/main-process/file-recovery-service.test.js +++ b/spec/main-process/file-recovery-service.test.js @@ -112,7 +112,7 @@ describe("FileRecoveryService", () => { const mockWindow = {} const filePath = temp.path() fs.writeFileSync(filePath, "content") - fs.chmodSync(filePath, 0444) + fs.chmodSync(filePath, 0o444) let logs = [] this.stub(console, 'log', (message) => logs.push(message)) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index c2e9e11be..6b4429cb1 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -27,12 +27,12 @@ describe "PackageManager", -> apmPath += ".cmd" expect(atom.packages.getApmPath()).toBe apmPath - describe "when the core.apmPath setting is set", -> - beforeEach -> - atom.config.set("core.apmPath", "/path/to/apm") + describe "when the core.apmPath setting is set", -> + beforeEach -> + atom.config.set("core.apmPath", "/path/to/apm") - it "returns the value of the core.apmPath config setting", -> - expect(atom.packages.getApmPath()).toBe "/path/to/apm" + it "returns the value of the core.apmPath config setting", -> + expect(atom.packages.getApmPath()).toBe "/path/to/apm" describe "::loadPackages()", -> beforeEach -> @@ -57,11 +57,13 @@ describe "PackageManager", -> expect(pack.metadata.name).toBe "package-with-index" it "returns the package if it has an invalid keymap", -> + spyOn(atom, 'inSpecMode').andReturn(false) pack = atom.packages.loadPackage("package-with-broken-keymap") expect(pack instanceof Package).toBe true expect(pack.metadata.name).toBe "package-with-broken-keymap" it "returns the package if it has an invalid stylesheet", -> + spyOn(atom, 'inSpecMode').andReturn(false) pack = atom.packages.loadPackage("package-with-invalid-styles") expect(pack instanceof Package).toBe true expect(pack.metadata.name).toBe "package-with-invalid-styles" @@ -75,6 +77,7 @@ describe "PackageManager", -> expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual "package-with-invalid-styles" it "returns null if the package has an invalid package.json", -> + spyOn(atom, 'inSpecMode').andReturn(false) addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) expect(atom.packages.loadPackage("package-with-broken-package-json")).toBeNull() @@ -107,6 +110,7 @@ describe "PackageManager", -> describe "when the package is deprecated", -> it "returns null", -> + spyOn(console, 'warn') expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull() expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe true expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe true @@ -392,6 +396,7 @@ describe "PackageManager", -> expect(mainModule.activate.callCount).toBe 1 it "adds a notification when the activation commands are invalid", -> + spyOn(atom, 'inSpecMode').andReturn(false) addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) expect(-> atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() @@ -400,6 +405,7 @@ describe "PackageManager", -> expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-activation-commands" it "adds a notification when the context menu is invalid", -> + spyOn(atom, 'inSpecMode').andReturn(false) addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) expect(-> atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() @@ -546,8 +552,9 @@ describe "PackageManager", -> waitsFor -> activatedPackage? runs -> expect(activatedPackage.name).toBe 'package-with-main' - describe "when the package throws an error while loading", -> + describe "when the package's main module throws an error on load", -> it "adds a notification instead of throwing an exception", -> + spyOn(atom, 'inSpecMode').andReturn(false) atom.config.set("core.disabledPackages", []) addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) @@ -556,6 +563,11 @@ describe "PackageManager", -> expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-that-throws-an-exception package") expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-that-throws-an-exception" + it "re-throws the exception in test mode", -> + atom.config.set("core.disabledPackages", []) + addErrorHandler = jasmine.createSpy() + expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).toThrow("This package throws an exception") + describe "when the package is not found", -> it "rejects the promise", -> atom.config.set("core.disabledPackages", []) @@ -893,6 +905,7 @@ describe "PackageManager", -> describe "::serialize", -> it "does not serialize packages that threw an error during activation", -> + spyOn(atom, 'inSpecMode').andReturn(false) spyOn(console, 'warn') badPack = null waitsForPromise -> @@ -940,6 +953,7 @@ describe "PackageManager", -> atom.packages.unloadPackages() it "calls `deactivate` on the package's main module if activate was successful", -> + spyOn(atom, 'inSpecMode').andReturn(false) pack = null waitsForPromise -> atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p @@ -1028,6 +1042,7 @@ describe "PackageManager", -> describe "::activate()", -> beforeEach -> + spyOn(atom, 'inSpecMode').andReturn(false) jasmine.snapshotDeprecations() spyOn(console, 'warn') atom.packages.loadPackages() @@ -1042,6 +1057,7 @@ describe "PackageManager", -> jasmine.restoreDeprecationsSnapshot() it "sets hasActivatedInitialPackages", -> + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) spyOn(atom.packages, 'activatePackages') expect(atom.packages.hasActivatedInitialPackages()).toBe false waitsForPromise -> atom.packages.activate() diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index d548255e5..997f6054e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -614,3 +614,7 @@ describe "Project", -> randomPath = path.join("some", "random", "path") expect(atom.project.contains(randomPath)).toBe false + + describe ".resolvePath(uri)", -> + it "normalizes disk drive letter in passed path on #win32", -> + expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt" diff --git a/spec/style-manager-spec.js b/spec/style-manager-spec.js index 88baac160..e6b8acae6 100644 --- a/spec/style-manager-spec.js +++ b/spec/style-manager-spec.js @@ -98,6 +98,13 @@ describe('StyleManager', () => { ]) }) + it('does not transform CSS rules with invalid syntax', () => { + styleManager.addStyleSheet("atom-text-editor::shadow .class-1 { font-family: inval'id }") + expect(Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + 'atom-text-editor::shadow .class-1' + ]) + }) + it('does not throw exceptions on rules with no selectors', () => { styleManager.addStyleSheet('@media screen {font-size: 10px}', {context: 'atom-text-editor'}) }) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 82ef7dd2a..b26e64e34 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1347,7 +1347,19 @@ describe('TextEditorComponent', function () { expect(cursorsNode.classList.contains('blink-off')).toBe(true) }) - it('does not render cursors that are associated with non-empty selections', function () { + it('renders cursors that are associated with empty selections', function () { + editor.update({showCursorOnSelection: true}) + editor.setSelectedScreenRange([[0, 4], [4, 6]]) + editor.addCursorAtScreenPosition([6, 8]) + runAnimationFrames() + let cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(2) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(6 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') + expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') + }) + + it('does not render cursors that are associated with non-empty selections when showCursorOnSelection is false', function () { + editor.update({showCursorOnSelection: false}) editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) runAnimationFrames() @@ -1735,11 +1747,13 @@ describe('TextEditorComponent', function () { }) describe('block decorations rendering', function () { + let markerLayer + function createBlockDecorationBeforeScreenRow(screenRow, {className}) { let item = document.createElement("div") item.className = className || "" let blockDecoration = editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), {type: "block", item: item, position: "before"} ) return [item, blockDecoration] @@ -1749,13 +1763,14 @@ describe('TextEditorComponent', function () { let item = document.createElement("div") item.className = className || "" let blockDecoration = editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), {type: "block", item: item, position: "after"} ) return [item, blockDecoration] } beforeEach(function () { + markerLayer = editor.addMarkerLayer() wrapperNode.style.height = 5 * lineHeightInPixels + 'px' editor.update({autoHeight: false}) component.measureDimensions() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 63a616cfc..2c4b6dbab 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1583,6 +1583,7 @@ describe "TextEditorPresenter", -> getState(presenter).content.cursors[presenter.model.getCursors()[cursorIndex].id] it "contains pixelRects for empty selections that are visible on screen", -> + editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], [[2, 4], [2, 4]], @@ -1627,6 +1628,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).content.cursors).not.toEqual({}) it "updates when block decorations change", -> + editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], [[2, 4], [2, 4]], @@ -1704,6 +1706,7 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 0)).toEqual {top: 20, left: 10 * 22, width: 10, height: 10} it "updates when ::explicitHeight changes", -> + editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], [[2, 4], [2, 4]], @@ -1757,6 +1760,7 @@ describe "TextEditorPresenter", -> 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", -> + editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], [[3, 4], [3, 5]] diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 51027e63c..ac5183cab 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -436,6 +436,19 @@ describe('TextEditorRegistry', function () { expect(editor.hasAtomicSoftTabs()).toBe(true) }) + it('enables or disables cursor on selection visibility based on the config', async function () { + editor.update({showCursorOnSelection: true}) + expect(editor.getShowCursorOnSelection()).toBe(true) + + atom.config.set('editor.showCursorOnSelection', false) + registry.maintainConfig(editor) + await initialPackageActivation + expect(editor.getShowCursorOnSelection()).toBe(false) + + atom.config.set('editor.showCursorOnSelection', true) + expect(editor.getShowCursorOnSelection()).toBe(true) + }) + it('enables or disables line numbers based on the config', async function () { editor.update({showLineNumbers: true}) expect(editor.showLineNumbers).toBe(true) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 6e6e1b7b1..911270d16 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -89,7 +89,11 @@ describe "TextEditor", -> describe ".copy()", -> it "returns a different editor with the same initial state", -> - editor.update({autoHeight: false, autoWidth: true}) + expect(editor.getAutoHeight()).toBeFalsy() + expect(editor.getAutoWidth()).toBeFalsy() + expect(editor.getShowCursorOnSelection()).toBeTruthy() + + editor.update({autoHeight: true, autoWidth: true, showCursorOnSelection: false}) editor.setSelectedBufferRange([[1, 2], [3, 4]]) editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true) editor.firstVisibleScreenRow = 5 @@ -105,7 +109,8 @@ describe "TextEditor", -> expect(editor2.getFirstVisibleScreenColumn()).toBe 5 expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() expect(editor2.getAutoWidth()).toBeTruthy() - expect(editor2.getAutoHeight()).toBeFalsy() + expect(editor2.getAutoHeight()).toBeTruthy() + expect(editor2.getShowCursorOnSelection()).toBeFalsy() # editor2 can now diverge from its origin edit session editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) @@ -1186,6 +1191,15 @@ describe "TextEditor", -> editor.getLastSelection().destroy() expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + it "doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", -> + callCount = 0 + editor.getLastSelection().destroy() + editor.onDidAddCursor (cursor) -> + callCount++ + editor.getLastSelection() + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + expect(callCount).toBe(1) + describe ".getSelections()", -> it "creates a new selection at (0, 0) if the last selection has been destroyed", -> editor.getLastSelection().destroy() @@ -1858,7 +1872,7 @@ describe "TextEditor", -> [[4, 25], [4, 29]] ] for cursor in editor.getCursors() - expect(cursor.isVisible()).toBeFalsy() + expect(cursor.isVisible()).toBeTruthy() it "skips lines that are too short to create a non-empty selection", -> editor.setSelectedBufferRange([[3, 31], [3, 38]]) @@ -1991,7 +2005,7 @@ describe "TextEditor", -> [[2, 37], [2, 40]] ] for cursor in editor.getCursors() - expect(cursor.isVisible()).toBeFalsy() + expect(cursor.isVisible()).toBeTruthy() it "skips lines that are too short to create a non-empty selection", -> editor.setSelectedBufferRange([[6, 31], [6, 38]]) @@ -2161,6 +2175,54 @@ describe "TextEditor", -> editor.setCursorScreenPosition([3, 3]) expect(selection.isEmpty()).toBeTruthy() + describe "cursor visibility while there is a selection", -> + describe "when showCursorOnSelection is true", -> + it "is visible while there is no selection", -> + expect(selection.isEmpty()).toBeTruthy() + expect(editor.getShowCursorOnSelection()).toBeTruthy() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursors()[0].isVisible()).toBeTruthy() + + it "is visible while there is a selection", -> + expect(selection.isEmpty()).toBeTruthy() + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(selection.isEmpty()).toBeFalsy() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursors()[0].isVisible()).toBeTruthy() + + it "is visible while there are multiple selections", -> + expect(editor.getSelections().length).toBe 1 + editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]]) + expect(editor.getSelections().length).toBe 2 + expect(editor.getCursors().length).toBe 2 + expect(editor.getCursors()[0].isVisible()).toBeTruthy() + expect(editor.getCursors()[1].isVisible()).toBeTruthy() + + describe "when showCursorOnSelection is false", -> + it "is visible while there is no selection", -> + editor.update({showCursorOnSelection: false}) + expect(selection.isEmpty()).toBeTruthy() + expect(editor.getShowCursorOnSelection()).toBeFalsy() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursors()[0].isVisible()).toBeTruthy() + + it "is not visible while there is a selection", -> + editor.update({showCursorOnSelection: false}) + expect(selection.isEmpty()).toBeTruthy() + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(selection.isEmpty()).toBeFalsy() + expect(editor.getCursors().length).toBe 1 + expect(editor.getCursors()[0].isVisible()).toBeFalsy() + + it "is not visible while there are multiple selections", -> + editor.update({showCursorOnSelection: false}) + expect(editor.getSelections().length).toBe 1 + editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]]) + expect(editor.getSelections().length).toBe 2 + expect(editor.getCursors().length).toBe 2 + expect(editor.getCursors()[0].isVisible()).toBeFalsy() + expect(editor.getCursors()[1].isVisible()).toBeFalsy() + it "does not share selections between different edit sessions for the same buffer", -> editor2 = null waitsForPromise -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 40a3160da..795f1b43e 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -4,6 +4,7 @@ temp = require('temp').track() describe "atom.themes", -> beforeEach -> + spyOn(atom, 'inSpecMode').andReturn(false) spyOn(console, 'warn') afterEach -> diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee index 6bcb24eed..a741dbbd4 100644 --- a/spec/workspace-element-spec.coffee +++ b/spec/workspace-element-spec.coffee @@ -74,14 +74,14 @@ describe "WorkspaceElement", -> atom.config.set('editor.fontSize', 12) # Zoom out - editorElement.dispatchEvent(new WheelEvent('mousewheel', { + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaY: -10, ctrlKey: true })) expect(atom.config.get('editor.fontSize')).toBe(11) # Zoom in - editorElement.dispatchEvent(new WheelEvent('mousewheel', { + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaY: 10, ctrlKey: true })) @@ -95,13 +95,13 @@ describe "WorkspaceElement", -> expect(atom.config.get('editor.fontSize')).toBe(12) # No ctrl key - workspaceElement.dispatchEvent(new WheelEvent('mousewheel', { + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaY: 10, })) expect(atom.config.get('editor.fontSize')).toBe(12) atom.config.set('editor.zoomFontWhenCtrlScrolling', false) - editorElement.dispatchEvent(new WheelEvent('mousewheel', { + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaY: 10, ctrlKey: true })) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 08afa6239..153cc5dc3 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -886,8 +886,12 @@ describe "Workspace", -> describe "document.title", -> describe "when there is no item open", -> - it "sets the title to 'untitled'", -> - expect(document.title).toMatch ///^untitled/// + it "sets the title to the project path", -> + expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) + + it "sets the title to 'untitled' if there is no project path", -> + atom.project.setPaths([]) + expect(document.title).toMatch /^untitled/ describe "when the active pane item's path is not inside a project path", -> beforeEach -> @@ -948,10 +952,10 @@ describe "Workspace", -> expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the last pane item is removed", -> - it "updates the title to be untitled", -> + it "updates the title to the project's first path", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toMatch ///^untitled/// + expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) describe "when an inactive pane's item changes", -> it "does not update the title", -> diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 185db5059..766ba7aa8 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -2,10 +2,12 @@ _ = require 'underscore-plus' {screen, ipcRenderer, remote, shell, webFrame} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' -{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers' +getWindowLoadSettings = require './get-window-load-settings' module.exports = class ApplicationDelegate + getWindowLoadSettings: -> getWindowLoadSettings() + open: (params) -> ipcRenderer.send('open', params) @@ -109,10 +111,7 @@ class ApplicationDelegate ipcRenderer.send("add-recent-document", filename) setRepresentedDirectoryPaths: (paths) -> - loadSettings = getWindowLoadSettings() - loadSettings['initialPaths'] = paths - setWindowLoadSettings(loadSettings) - ipcRenderer.send("did-change-paths") + ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths) setAutoHideWindowMenuBar: (autoHide) -> ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide) @@ -149,13 +148,9 @@ class ApplicationDelegate showMessageDialog: (params) -> showSaveDialog: (params) -> - if _.isString(params) - params = defaultPath: params - else - params = _.clone(params) - params.title ?= 'Save File' - params.defaultPath ?= getWindowLoadSettings().initialPaths[0] - remote.dialog.showSaveDialog remote.getCurrentWindow(), params + if typeof params is 'string' + params = {defaultPath: params} + @getCurrentWindow().showSaveDialog(params) playBeepSound: -> shell.beep() diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index e69aeb55e..3133b5af8 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -11,7 +11,6 @@ Model = require './model' WindowEventHandler = require './window-event-handler' StateStore = require './state-store' StorageFolder = require './storage-folder' -{getWindowLoadSettings} = require './window-load-settings-helpers' registerDefaultCommands = require './register-default-commands' {updateProcessEnv} = require './update-process-env' @@ -458,7 +457,7 @@ class AtomEnvironment extends Model # # Returns an {Object} containing all the load setting key/value pairs. getLoadSettings: -> - getWindowLoadSettings() + @applicationDelegate.getWindowLoadSettings() ### Section: Managing The Atom Window @@ -828,12 +827,17 @@ class AtomEnvironment extends Model Section: Private ### - assert: (condition, message, callback) -> + assert: (condition, message, callbackOrMetadata) -> return true if condition error = new Error("Assertion failed: #{message}") Error.captureStackTrace(error, @assert) - callback?(error) + + if callbackOrMetadata? + if typeof callbackOrMetadata is 'function' + callbackOrMetadata?(error) + else + error.metadata = callbackOrMetadata @emitter.emit 'did-fail-assertion', error diff --git a/src/atom-paths.js b/src/atom-paths.js index 6a5c107b3..39a768e91 100644 --- a/src/atom-paths.js +++ b/src/atom-paths.js @@ -17,7 +17,7 @@ const hasWriteAccess = (dir) => { const getAppDirectory = () => { switch (process.platform) { case 'darwin': - return path.join(process.execPath.substring(0, process.execPath.indexOf('.app')), '..') + return process.execPath.substring(0, process.execPath.indexOf('.app') + 4) case 'linux': case 'win32': return path.join(process.execPath, '..') @@ -27,7 +27,7 @@ const getAppDirectory = () => { module.exports = { setAtomHome: (homePath) => { // When a read-writeable .atom folder exists above app use that - const portableHomePath = path.join(getAppDirectory(), '.atom') + const portableHomePath = path.join(getAppDirectory(), '..', '.atom') if (fs.existsSync(portableHomePath)) { if (hasWriteAccess(portableHomePath)) { process.env.ATOM_HOME = portableHomePath diff --git a/src/babel.js b/src/babel.js index a944f2e8c..d72b29ffd 100644 --- a/src/babel.js +++ b/src/babel.js @@ -6,6 +6,7 @@ var defaultOptions = require('../static/babelrc.json') var babel = null var babelVersionDirectory = null +var options = null var PREFIXES = [ '/** @babel */', @@ -47,16 +48,27 @@ exports.compile = function (sourceCode, filePath) { var noop = function () {} Logger.prototype.debug = noop Logger.prototype.verbose = noop + + options = {ast: false, babelrc: false} + for (var key in defaultOptions) { + if (key === 'plugins') { + const plugins = [] + for (const [pluginName, pluginOptions] of defaultOptions[key]) { + plugins.push([require.resolve(`babel-plugin-${pluginName}`), pluginOptions]) + } + options[key] = plugins + } else { + options[key] = defaultOptions[key] + } + } } if (process.platform === 'win32') { filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/') } - var options = {filename: filePath} - for (var key in defaultOptions) { - options[key] = defaultOptions[key] - } + options.filename = filePath + return babel.transform(sourceCode, options).code } diff --git a/src/buffered-process.js b/src/buffered-process.js index 44e501d4d..339bf05c5 100644 --- a/src/buffered-process.js +++ b/src/buffered-process.js @@ -46,18 +46,34 @@ export default class BufferedProcess { // * `exit` {Function} (optional) The callback which receives a single // argument containing the exit status. // * `code` {Number} - constructor ({command, args, options = {}, stdout, stderr, exit} = {}) { + // * `autoStart` {Boolean} (optional) Whether the command will automatically start + // when this BufferedProcess is created. Defaults to true. When set to false you + // must call the `start` method to start the process. + constructor ({command, args, options = {}, stdout, stderr, exit, autoStart = true} = {}) { this.emitter = new Emitter() this.command = command - // Related to joyent/node#2318 - if (process.platform === 'win32' && options.shell === undefined) { - this.spawnWithEscapedWindowsArgs(command, args, options) - } else { - this.spawn(command, args, options) + this.args = args + this.options = options + this.stdout = stdout + this.stderr = stderr + this.exit = exit + if (autoStart === true) { + this.start() } - this.killed = false - this.handleEvents(stdout, stderr, exit) + } + + start () { + if (this.started === true) return + + this.started = true + // Related to joyent/node#2318 + if (process.platform === 'win32' && this.options.shell === undefined) { + this.spawnWithEscapedWindowsArgs(this.command, this.args, this.options) + } else { + this.spawn(this.command, this.args, this.options) + } + this.handleEvents(this.stdout, this.stderr, this.exit) } // Windows has a bunch of special rules that node still doesn't take care of for you diff --git a/src/config-schema.js b/src/config-schema.js index 52102ec58..840f2d7ba 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -68,6 +68,12 @@ const configSchema = { default: true, description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.' }, + closeDeletedFileTabs: { + type: 'boolean', + default: false, + title: 'Close Deleted File Tabs', + description: 'Close corresponding editors when a file is deleted outside Atom.' + }, destroyEmptyPanes: { type: 'boolean', default: true, @@ -84,42 +90,166 @@ const configSchema = { type: 'string', default: 'utf8', enum: [ - 'cp437', - 'cp850', - 'cp866', - 'eucjp', - 'euckr', - 'gbk', - 'iso88591', - 'iso885910', - 'iso885913', - 'iso885914', - 'iso885915', - 'iso885916', - 'iso88592', - 'iso88593', - 'iso88594', - 'iso88595', - 'iso88596', - 'iso88597', - 'iso88597', - 'iso88598', - 'koi8r', - 'koi8u', - 'macroman', - 'shiftjis', - 'utf16be', - 'utf16le', - 'utf8', - 'windows1250', - 'windows1251', - 'windows1252', - 'windows1253', - 'windows1254', - 'windows1255', - 'windows1256', - 'windows1257', - 'windows1258' + { + value: 'iso88596', + description: 'Arabic (ISO 8859-6)' + }, + { + value: 'windows1256', + description: 'Arabic (Windows 1256)' + }, + { + value: 'iso88594', + description: 'Baltic (ISO 8859-4)' + }, + { + value: 'windows1257', + description: 'Baltic (Windows 1257)' + }, + { + value: 'iso885914', + description: 'Celtic (ISO 8859-14)' + }, + { + value: 'iso88592', + description: 'Central European (ISO 8859-2)' + }, + { + value: 'windows1250', + description: 'Central European (Windows 1250)' + }, + { + value: 'gb18030', + description: 'Chinese (GB18030)' + }, + { + value: 'gbk', + description: 'Chinese (GBK)' + }, + { + value: 'cp950', + description: 'Traditional Chinese (Big5)' + }, + { + value: 'big5hkscs', + description: 'Traditional Chinese (Big5-HKSCS)' + }, + { + value: 'cp866', + description: 'Cyrillic (CP 866)' + }, + { + value: 'iso88595', + description: 'Cyrillic (ISO 8859-5)' + }, + { + value: 'koi8r', + description: 'Cyrillic (KOI8-R)' + }, + { + value: 'koi8u', + description: 'Cyrillic (KOI8-U)' + }, + { + value: 'windows1251', + description: 'Cyrillic (Windows 1251)' + }, + { + value: 'cp437', + description: 'DOS (CP 437)' + }, + { + value: 'cp850', + description: 'DOS (CP 850)' + }, + { + value: 'iso885913', + description: 'Estonian (ISO 8859-13)' + }, + { + value: 'iso88597', + description: 'Greek (ISO 8859-7)' + }, + { + value: 'windows1253', + description: 'Greek (Windows 1253)' + }, + { + value: 'iso88598', + description: 'Hebrew (ISO 8859-8)' + }, + { + value: 'windows1255', + description: 'Hebrew (Windows 1255)' + }, + { + value: 'cp932', + description: 'Japanese (CP 932)' + }, + { + value: 'eucjp', + description: 'Japanese (EUC-JP)' + }, + { + value: 'shiftjis', + description: 'Japanese (Shift JIS)' + }, + { + value: 'euckr', + description: 'Korean (EUC-KR)' + }, + { + value: 'iso885910', + description: 'Nordic (ISO 8859-10)' + }, + { + value: 'iso885916', + description: 'Romanian (ISO 8859-16)' + }, + { + value: 'iso88599', + description: 'Turkish (ISO 8859-9)' + }, + { + value: 'windows1254', + description: 'Turkish (Windows 1254)' + }, + { + value: 'utf8', + description: 'Unicode (UTF-8)' + }, + { + value: 'utf16le', + description: 'Unicode (UTF-16 LE)' + }, + { + value: 'utf16be', + description: 'Unicode (UTF-16 BE)' + }, + { + value: 'windows1258', + description: 'Vietnamese (Windows 1258)' + }, + { + value: 'iso88591', + description: 'Western (ISO 8859-1)' + }, + { + value: 'iso88593', + description: 'Western (ISO 8859-3)' + }, + { + value: 'iso885915', + description: 'Western (ISO 8859-15)' + }, + { + value: 'macroman', + description: 'Western (Mac Roman)' + }, + { + value: 'windows1252', + description: 'Western (Windows 1252)' + } ] }, openEmptyEditorOnStart: { @@ -142,6 +272,12 @@ const configSchema = { type: 'boolean', default: true }, + useProxySettingsWhenCallingApm: { + title: 'Use Proxy Settings When Calling APM', + description: 'Use detected proxy settings when calling the `apm` command-line tool.', + type: 'boolean', + default: true + }, allowPendingPaneItems: { description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.', type: 'boolean', @@ -211,6 +347,11 @@ const configSchema = { default: 1.5, description: 'Height of editor lines, as a multiplier of font size.' }, + showCursorOnSelection: { + type: 'boolean', + 'default': true, + description: 'Show cursor while there is a selection.' + }, showInvisibles: { type: 'boolean', default: false, diff --git a/src/config.coffee b/src/config.coffee index 2dc537dd4..e873a1348 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -336,6 +336,31 @@ ScopeDescriptor = require './scope-descriptor' # order: 2 # ``` # +# ## Manipulating values outside your configuration schema +# +# It is possible to manipulate(`get`, `set`, `observe` etc) values that do not +# appear in your configuration schema. For example, if the config schema of the +# package 'some-package' is +# +# ```coffee +# config: +# someSetting: +# type: 'boolean' +# default: false +# ``` +# +# You can still do the following +# +# ```coffee +# let otherSetting = atom.config.get('some-package.otherSetting') +# atom.config.set('some-package.stillAnotherSetting', otherSetting * 5) +# ``` +# +# In other words, if a function asks for a `key-path`, that path doesn't have to +# be described in the config schema for the package or any package. However, as +# highlighted in the best practices section, you are advised against doing the +# above. +# # ## Best practices # # * Don't depend on (or write to) configuration keys outside of your keypath. diff --git a/src/cursor.coffee b/src/cursor.coffee index df91d95c5..47e8c0594 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -12,15 +12,18 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g # of a {DisplayMarker}. module.exports = class Cursor extends Model + showCursorOnSelection: null screenPosition: null bufferPosition: null goalColumn: null visible: true # Instantiated by a {TextEditor} - constructor: ({@editor, @marker, id}) -> + constructor: ({@editor, @marker, @showCursorOnSelection, id}) -> @emitter = new Emitter + @showCursorOnSelection ?= true + @assignId(id) @updateVisibility() @@ -575,7 +578,10 @@ class Cursor extends Model isVisible: -> @visible updateVisibility: -> - @setVisible(@marker.getBufferRange().isEmpty()) + if @showCursorOnSelection + @setVisible(true) + else + @setVisible(@marker.getBufferRange().isEmpty()) ### Section: Comparing to another cursor @@ -645,6 +651,11 @@ class Cursor extends Model Section: Private ### + setShowCursorOnSelection: (value) -> + if value isnt @showCursorOnSelection + @showCursorOnSelection = value + @updateVisibility() + getNonWordCharacters: -> @editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray()) diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee index e1096f572..05935f018 100644 --- a/src/decoration-manager.coffee +++ b/src/decoration-manager.coffee @@ -8,7 +8,7 @@ class DecorationManager extends Model didUpdateDecorationsEventScheduled: false updatedSynchronously: false - constructor: (@displayLayer, @defaultMarkerLayer) -> + constructor: (@displayLayer) -> super @emitter = new Emitter @@ -71,9 +71,11 @@ class DecorationManager extends Model decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> decorationsByMarkerId = {} - for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if decorations = @decorationsByMarkerId[marker.id] - decorationsByMarkerId[marker.id] = decorations + for layerId of @decorationCountsByLayerId + layer = @displayLayer.getMarkerLayer(layerId) + for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) + if decorations = @decorationsByMarkerId[marker.id] + decorationsByMarkerId[marker.id] = decorations decorationsByMarkerId decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> @@ -104,7 +106,14 @@ class DecorationManager extends Model decorationsState decorateMarker: (marker, decorationParams) -> - throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed() + if marker.isDestroyed() + error = new Error("Cannot decorate a destroyed marker") + error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} + if marker.destroyStackTrace? + error.metadata.destroyStackTrace = marker.destroyStackTrace + if marker.bufferMarker?.destroyStackTrace? + error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace + throw error marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) @decorationsByMarkerId[marker.id] ?= [] @@ -117,6 +126,7 @@ class DecorationManager extends Model decoration decorateMarkerLayer: (markerLayer, decorationParams) -> + throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed() decoration = new LayerDecoration(markerLayer, this, decorationParams) @layerDecorationsByMarkerLayerId[markerLayer.id] ?= [] @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index ed4e9ba36..44d5298dd 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -15,7 +15,7 @@ class DefaultDirectoryProvider # * {Directory} if the given URI is compatible with this provider. # * `null` if the given URI is not compatibile with this provider. directoryForURISync: (uri) -> - normalizedPath = path.normalize(uri) + normalizedPath = @normalizePath(uri) {host} = url.parse(uri) directoryPath = if host uri @@ -42,3 +42,17 @@ class DefaultDirectoryProvider # * `null` if the given URI is not compatibile with this provider. directoryForURI: (uri) -> Promise.resolve(@directoryForURISync(uri)) + + # Public: Normalizes path. + # + # * `uri` {String} The path that should be normalized. + # + # Returns a {String} with normalized path. + normalizePath: (uri) -> + # Normalize disk drive letter on Windows to avoid opening two buffers for the same file + pathWithNormalizedDiskDriveLetter = + if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/) + "#{matchData[1].toUpperCase()}#{uri.slice(1)}" + else + uri + path.normalize(pathWithNormalizedDiskDriveLetter) diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee deleted file mode 100644 index f81a537f3..000000000 --- a/src/dom-element-pool.coffee +++ /dev/null @@ -1,55 +0,0 @@ -module.exports = -class DOMElementPool - constructor: -> - @freeElementsByTagName = {} - @freedElements = new Set - - clear: -> - @freedElements.clear() - for tagName, freeElements of @freeElementsByTagName - freeElements.length = 0 - return - - build: (tagName, factory, reset) -> - element = @freeElementsByTagName[tagName]?.pop() - element ?= factory() - reset(element) - @freedElements.delete(element) - element - - buildElement: (tagName, className) -> - factory = -> document.createElement(tagName) - reset = (element) -> - delete element.dataset[dataId] for dataId of element.dataset - element.removeAttribute("style") - if className? - element.className = className - else - element.removeAttribute("class") - @build(tagName, factory, reset) - - buildText: (textContent) -> - factory = -> document.createTextNode(textContent) - reset = (element) -> element.textContent = textContent - @build("#text", factory, reset) - - freeElementAndDescendants: (element) -> - @free(element) - @freeDescendants(element) - - freeDescendants: (element) -> - for descendant in element.childNodes by -1 - @free(descendant) - @freeDescendants(descendant) - return - - free: (element) -> - throw new Error("The element cannot be null or undefined.") unless element? - throw new Error("The element has already been freed!") if @freedElements.has(element) - - tagName = element.nodeName.toLowerCase() - @freeElementsByTagName[tagName] ?= [] - @freeElementsByTagName[tagName].push(element) - @freedElements.add(element) - - element.remove() diff --git a/src/dom-element-pool.js b/src/dom-element-pool.js new file mode 100644 index 000000000..0fef02dee --- /dev/null +++ b/src/dom-element-pool.js @@ -0,0 +1,89 @@ +module.exports = +class DOMElementPool { + constructor () { + this.managedElements = new Set() + this.freeElementsByTagName = new Map() + this.freedElements = new Set() + } + + clear () { + this.managedElements.clear() + this.freedElements.clear() + this.freeElementsByTagName.clear() + } + + buildElement (tagName, className) { + const elements = this.freeElementsByTagName.get(tagName) + let element = elements ? elements.pop() : null + if (element) { + for (let dataId in element.dataset) { delete element.dataset[dataId] } + element.removeAttribute('style') + if (className) { + element.className = className + } else { + element.removeAttribute('class') + } + while (element.firstChild) { + element.removeChild(element.firstChild) + } + this.freedElements.delete(element) + } else { + element = document.createElement(tagName) + if (className) { + element.className = className + } + this.managedElements.add(element) + } + return element + } + + buildText (textContent) { + const elements = this.freeElementsByTagName.get('#text') + let element = elements ? elements.pop() : null + if (element) { + element.textContent = textContent + this.freedElements.delete(element) + } else { + element = document.createTextNode(textContent) + this.managedElements.add(element) + } + return element + } + + freeElementAndDescendants (element) { + this.free(element) + element.remove() + } + + freeDescendants (element) { + while (element.firstChild) { + this.free(element.firstChild) + element.removeChild(element.firstChild) + } + } + + free (element) { + if (element == null) { throw new Error('The element cannot be null or undefined.') } + if (!this.managedElements.has(element)) return + if (this.freedElements.has(element)) { + atom.assert(false, 'The element has already been freed!', { + content: element instanceof window.Text ? element.textContent : element.outerHTML + }) + return + } + + const tagName = element.nodeName.toLowerCase() + let elements = this.freeElementsByTagName.get(tagName) + if (!elements) { + elements = [] + this.freeElementsByTagName.set(tagName, elements) + } + elements.push(element) + this.freedElements.add(element) + + for (let i = element.childNodes.length - 1; i >= 0; i--) { + const descendant = element.childNodes[i] + this.free(descendant) + } + } +} diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js new file mode 100644 index 000000000..7ee465141 --- /dev/null +++ b/src/get-window-load-settings.js @@ -0,0 +1,10 @@ +const {remote} = require('electron') + +let windowLoadSettings = null + +module.exports = () => { + if (!windowLoadSettings) { + windowLoadSettings = remote.getCurrentWindow().loadSettings + } + return windowLoadSettings +} diff --git a/src/history-manager.js b/src/history-manager.js index c5117d00f..f013957b9 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -61,6 +61,19 @@ export class HistoryManager { this.didChangeProjects() } + removeProject (paths) { + if (paths.length === 0) return + + let project = this.getProject(paths) + if (!project) return + + let index = this.projects.indexOf(project) + this.projects.splice(index, 1) + + this.saveState() + this.didChangeProjects() + } + getProject (paths) { for (var i = 0; i < this.projects.length; i++) { if (arrayEquivalent(paths, this.projects[i].paths)) { diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index 7d3a23db7..be13ce6c6 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -3,7 +3,7 @@ module.exports = ({blobStore}) -> {updateProcessEnv} = require('./update-process-env') path = require 'path' require './window' - {getWindowLoadSettings} = require './window-load-settings-helpers' + getWindowLoadSettings = require './get-window-load-settings' {ipcRenderer} = require 'electron' {resourcePath, devMode, env} = getWindowLoadSettings() require './electron-shims' diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index 29a210904..a223e0b03 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -6,7 +6,7 @@ import ipcHelpers from './ipc-helpers' import util from 'util' export default async function () { - const {getWindowLoadSettings} = require('./window-load-settings-helpers') + const getWindowLoadSettings = require('./get-window-load-settings') const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings() try { const Clipboard = require('../src/clipboard') diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index 39a408fea..794db3174 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -18,7 +18,7 @@ module.exports = ({blobStore}) -> try path = require 'path' {ipcRenderer} = require 'electron' - {getWindowLoadSettings} = require './window-load-settings-helpers' + getWindowLoadSettings = require './get-window-load-settings' CompileCache = require './compile-cache' AtomEnvironment = require '../src/atom-environment' ApplicationDelegate = require '../src/application-delegate' diff --git a/src/ipc-helpers.js b/src/ipc-helpers.js index 6a7565968..4be9f9613 100644 --- a/src/ipc-helpers.js +++ b/src/ipc-helpers.js @@ -15,6 +15,7 @@ exports.on = function (emitter, eventName, callback) { exports.call = function (channel, ...args) { if (!ipcRenderer) { ipcRenderer = require('electron').ipcRenderer + ipcRenderer.setMaxListeners(20) } var responseChannel = getResponseChannel(channel) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 4f580be8c..93e9e3395 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -593,8 +593,7 @@ class AtomApplication states = [] for window in @windows unless window.isSpec - if loadSettings = window.getLoadSettings() - states.push(initialPaths: loadSettings.initialPaths) + states.push({initialPaths: window.representedDirectoryPaths}) if states.length > 0 or allowEmpty @storageFolder.storeSync('application.json', states) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 266f5d292..03386d31a 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -52,9 +52,7 @@ class AtomWindow if @shouldHideTitleBar() options.frame = false - @browserWindow = new BrowserWindow options - @atomApplication.addWindow(this) - + @browserWindow = new BrowserWindow(options) @handleEvents() loadSettings = Object.assign({}, settings) @@ -66,11 +64,15 @@ class AtomWindow loadSettings.clearWindowState ?= false loadSettings.initialPaths ?= for {pathToOpen} in locationsToOpen when pathToOpen - if fs.statSyncNoException(pathToOpen).isFile?() - path.dirname(pathToOpen) - else + stat = fs.statSyncNoException(pathToOpen) or null + if stat?.isDirectory() pathToOpen - + else + parentDirectory = path.dirname(pathToOpen) + if stat?.isFile() or fs.existsSync(parentDirectory) + parentDirectory + else + pathToOpen loadSettings.initialPaths.sort() # Only send to the first non-spec window created @@ -78,33 +80,31 @@ class AtomWindow @constructor.includeShellLoadTime = false loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime + @representedDirectoryPaths = loadSettings.initialPaths + @env = loadSettings.env if loadSettings.env? + @browserWindow.loadSettings = loadSettings @browserWindow.on 'window:loaded', => @emit 'window:loaded' @resolveLoadedPromise() - @setLoadSettings(loadSettings) - @env = loadSettings.env if loadSettings.env? + @browserWindow.loadURL url.format + protocol: 'file' + pathname: "#{@resourcePath}/static/index.html" + slashes: true + + @browserWindow.showSaveDialog = @showSaveDialog.bind(this) + @browserWindow.focusOnWebView() if @isSpec @browserWindow.temporaryState = {windowDimensions} if windowDimensions? hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() - setLoadSettings: (loadSettings) -> - @browserWindow.loadURL url.format - protocol: 'file' - pathname: "#{@resourcePath}/static/index.html" - slashes: true - hash: encodeURIComponent(JSON.stringify(loadSettings)) + @atomApplication.addWindow(this) - getLoadSettings: -> - if @browserWindow.webContents? and not @browserWindow.webContents.isLoading() - hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1) - JSON.parse(decodeURIComponent(hash)) - - hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0 + hasProjectPath: -> @representedDirectoryPaths.length > 0 setupContextMenu: -> ContextMenu = require './context-menu' @@ -118,7 +118,7 @@ class AtomWindow true containsPath: (pathToCheck) -> - @getLoadSettings()?.initialPaths?.some (projectPath) -> + @representedDirectoryPaths.some (projectPath) -> if not projectPath false else if not pathToCheck @@ -281,6 +281,13 @@ class AtomWindow @saveState().then => @browserWindow.reload() @loadedPromise + showSaveDialog: (params) -> + params = Object.assign({ + title: 'Save File', + defaultPath: @representedDirectoryPaths[0] + }, params) + dialog.showSaveDialog(this, params) + toggleDevTools: -> @browserWindow.toggleDevTools() openDevTools: -> @browserWindow.openDevTools() @@ -291,4 +298,8 @@ class AtomWindow setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename) + setRepresentedDirectoryPaths: (@representedDirectoryPaths) -> + @representedDirectoryPaths.sort() + @atomApplication.saveState() + copy: -> @browserWindow.copy() diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index 8fdba844d..ff29dd3d6 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -22,7 +22,7 @@ class AutoUpdateManager setupAutoUpdater: -> if process.platform is 'win32' archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch - @feedUrl = "https://atom.io/api/updates#{archSuffix}" + @feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}" autoUpdater = require './auto-updater-win32' else @feedUrl = "https://atom.io/api/updates?version=#{@version}" diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 4227b63ba..3f9f2523a 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -106,14 +106,14 @@ module.exports = function parseCommandLine (processArgs) { if (args['resource-path']) { devMode = true - resourcePath = args['resource-path'] + devResourcePath = args['resource-path'] } if (test) { devMode = true } - if (devMode && !resourcePath) { + if (devMode) { resourcePath = devResourcePath } diff --git a/src/package.coffee b/src/package.coffee index 9fa2dbe63..63efbf02c 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -711,6 +711,9 @@ class Package incompatibleNativeModules handleError: (message, error) -> + if atom.inSpecMode() + throw error + if error.filename and error.location and (error instanceof SyntaxError) location = "#{error.filename}:#{error.location.first_line + 1}:#{error.location.first_column + 1}" detail = "#{error.message} in #{location}" diff --git a/src/project.coffee b/src/project.coffee index 522fbfbc7..a02f27dac 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -21,7 +21,6 @@ class Project extends Model constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) -> @emitter = new Emitter @buffers = [] - @paths = [] @rootDirectories = [] @repositories = [] @directoryProviders = [] @@ -32,7 +31,9 @@ class Project extends Model destroyed: -> buffer.destroy() for buffer in @buffers - @setPaths([]) + repository?.destroy() for repository in @repositories + @rootDirectories = [] + @repositories = [] reset: (packageManager) -> @emitter.dispose() @@ -62,6 +63,9 @@ class Project extends Model fs.closeSync(fs.openSync(bufferState.filePath, 'r')) catch error return unless error.code is 'ENOENT' + unless bufferState.shouldDestroyOnFileDelete? + bufferState.shouldDestroyOnFileDelete = + -> atom.config.get('core.closeDeletedFileTabs') TextBuffer.deserialize(bufferState) @subscribeToBuffer(buffer) for buffer in @buffers @@ -205,7 +209,7 @@ class Project extends Model removePath: (projectPath) -> # The projectPath may be a URI, in which case it should not be normalized. unless projectPath in @getPaths() - projectPath = path.normalize(projectPath) + projectPath = @defaultDirectoryProvider.normalizePath(projectPath) indexToRemove = null for directory, i in @rootDirectories @@ -233,11 +237,10 @@ class Project extends Model uri else if fs.isAbsolute(uri) - path.normalize(fs.resolveHome(uri)) - + @defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) # TODO: what should we do here when there are multiple directories? else if projectPath = @getPaths()[0] - path.normalize(fs.resolveHome(path.join(projectPath, uri))) + @defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) else undefined @@ -360,9 +363,14 @@ class Project extends Model else @buildBuffer(absoluteFilePath) + shouldDestroyBufferOnFileDelete: -> + atom.config.get('core.closeDeletedFileTabs') + # Still needed when deserializing a tokenized buffer buildBufferSync: (absoluteFilePath) -> - buffer = new TextBuffer({filePath: absoluteFilePath}) + buffer = new TextBuffer({ + filePath: absoluteFilePath + shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete}) @addBuffer(buffer) buffer.loadSync() buffer @@ -374,7 +382,9 @@ class Project extends Model # # Returns a {Promise} that resolves to the {TextBuffer}. buildBuffer: (absoluteFilePath) -> - buffer = new TextBuffer({filePath: absoluteFilePath}) + buffer = new TextBuffer({ + filePath: absoluteFilePath + shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete}) @addBuffer(buffer) buffer.load() .then((buffer) -> buffer) diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 8b2a11838..79acbba66 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -19,6 +19,8 @@ export default class ReopenProjectMenuManager { }), commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) }) ) + + this.applyWindowsJumpListRemovals() } reopenProjectCommand (e) { @@ -49,9 +51,30 @@ export default class ReopenProjectMenuManager { this.updateWindowsJumpList() } + static taskDescription (paths) { + return paths.map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`).join(' ') + } + + // Windows users can right-click Atom taskbar and remove project from the jump list. + // We have to honor that or the group stops working. As we only get a partial list + // each time we remove them from history entirely. + applyWindowsJumpListRemovals () { + if (process.platform !== 'win32') return + if (this.app === undefined) { + this.app = require('remote').app + } + + const removed = this.app.getJumpListSettings().removedItems.map(i => i.description) + if (removed.length === 0) return + for (let project of this.historyManager.getProjects()) { + if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) { + this.historyManager.removeProject(project.paths) + } + } + } + updateWindowsJumpList () { if (process.platform !== 'win32') return - if (this.app === undefined) { this.app = require('remote').app } @@ -64,7 +87,7 @@ export default class ReopenProjectMenuManager { ({ type: 'task', title: project.paths.map(ReopenProjectMenuManager.betterBaseName).join(', '), - description: project.paths.map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`).join(' '), + description: ReopenProjectMenuManager.taskDescription(project.paths), program: process.execPath, args: project.paths.map(path => `"${path}"`).join(' '), iconPath: path.join(path.dirname(process.execPath), 'resources', 'cli', 'folder.ico'), diff --git a/src/state-store.js b/src/state-store.js index fe45bc086..b192d8b04 100644 --- a/src/state-store.js +++ b/src/state-store.js @@ -3,6 +3,7 @@ module.exports = class StateStore { constructor (databaseName, version) { + this.connected = false this.dbPromise = new Promise((resolve) => { let dbOpenRequest = indexedDB.open(databaseName, version) dbOpenRequest.onupgradeneeded = (event) => { @@ -10,15 +11,21 @@ class StateStore { db.createObjectStore('states') } dbOpenRequest.onsuccess = () => { + this.connected = true resolve(dbOpenRequest.result) } dbOpenRequest.onerror = (error) => { console.error('Could not connect to indexedDB', error) + this.connected = false resolve(null) } }) } + isConnected () { + return this.connected + } + connect () { return this.dbPromise.then((db) => !!db) } diff --git a/src/style-manager.js b/src/style-manager.js index 7ee11fd6d..0a0b401d3 100644 --- a/src/style-manager.js +++ b/src/style-manager.js @@ -250,59 +250,70 @@ module.exports = class StyleManager { function transformDeprecatedShadowDOMSelectors (css, context) { const transformedSelectors = [] - const transformedSource = postcss.parse(css) - transformedSource.walkRules((rule) => { - const transformedSelector = selectorParser((selectors) => { - selectors.each((selector) => { - const firstNode = selector.nodes[0] - if (context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host') { - const atomTextEditorElementNode = selectorParser.tag({value: 'atom-text-editor'}) - firstNode.replaceWith(atomTextEditorElementNode) - } - - let previousNodeIsAtomTextEditor = false - let targetsAtomTextEditorShadow = context === 'atom-text-editor' - let previousNode - selector.each((node) => { - if (targetsAtomTextEditorShadow && node.type === 'class') { - if (DEPRECATED_SYNTAX_SELECTORS.has(node.value)) { - node.value = `syntax--${node.value}` - } - } else { - if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') { - node.type = 'className' - node.value = '.editor' - targetsAtomTextEditorShadow = true - } - } - - previousNode = node - if (node.type === 'combinator') { - previousNodeIsAtomTextEditor = false - } else if (previousNode.type === 'tag' && previousNode.value === 'atom-text-editor') { - previousNodeIsAtomTextEditor = true - } - }) - }) - }).process(rule.selector, {lossless: true}).result - if (transformedSelector !== rule.selector) { - transformedSelectors.push({before: rule.selector, after: transformedSelector}) - rule.selector = transformedSelector - } - }) - let deprecationMessage - if (transformedSelectors.length > 0) { - deprecationMessage = 'Starting from Atom v1.13.0, the contents of `atom-text-editor` elements ' - deprecationMessage += 'are no longer encapsulated within a shadow DOM boundary. ' - deprecationMessage += 'This means you should stop using `:host` and `::shadow` ' - deprecationMessage += 'pseudo-selectors, and prepend all your syntax selectors with `syntax--`. ' - deprecationMessage += 'To prevent breakage with existing style sheets, Atom will automatically ' - deprecationMessage += 'upgrade the following selectors:\n\n' - deprecationMessage += transformedSelectors - .map((selector) => `* \`${selector.before}\` => \`${selector.after}\``) - .join('\n\n') + '\n\n' - deprecationMessage += 'Automatic translation of selectors will be removed in a few release cycles to minimize startup time. ' - deprecationMessage += 'Please, make sure to upgrade the above selectors as soon as possible.' + let transformedSource + try { + transformedSource = postcss.parse(css) + } catch (e) { + transformedSource = null + } + + if (transformedSource) { + transformedSource.walkRules((rule) => { + const transformedSelector = selectorParser((selectors) => { + selectors.each((selector) => { + const firstNode = selector.nodes[0] + if (context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host') { + const atomTextEditorElementNode = selectorParser.tag({value: 'atom-text-editor'}) + firstNode.replaceWith(atomTextEditorElementNode) + } + + let previousNodeIsAtomTextEditor = false + let targetsAtomTextEditorShadow = context === 'atom-text-editor' + let previousNode + selector.each((node) => { + if (targetsAtomTextEditorShadow && node.type === 'class') { + if (DEPRECATED_SYNTAX_SELECTORS.has(node.value)) { + node.value = `syntax--${node.value}` + } + } else { + if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') { + node.type = 'className' + node.value = '.editor' + targetsAtomTextEditorShadow = true + } + } + + previousNode = node + if (node.type === 'combinator') { + previousNodeIsAtomTextEditor = false + } else if (previousNode.type === 'tag' && previousNode.value === 'atom-text-editor') { + previousNodeIsAtomTextEditor = true + } + }) + }) + }).process(rule.selector, {lossless: true}).result + if (transformedSelector !== rule.selector) { + transformedSelectors.push({before: rule.selector, after: transformedSelector}) + rule.selector = transformedSelector + } + }) + let deprecationMessage + if (transformedSelectors.length > 0) { + deprecationMessage = 'Starting from Atom v1.13.0, the contents of `atom-text-editor` elements ' + deprecationMessage += 'are no longer encapsulated within a shadow DOM boundary. ' + deprecationMessage += 'This means you should stop using `:host` and `::shadow` ' + deprecationMessage += 'pseudo-selectors, and prepend all your syntax selectors with `syntax--`. ' + deprecationMessage += 'To prevent breakage with existing style sheets, Atom will automatically ' + deprecationMessage += 'upgrade the following selectors:\n\n' + deprecationMessage += transformedSelectors + .map((selector) => `* \`${selector.before}\` => \`${selector.after}\``) + .join('\n\n') + '\n\n' + deprecationMessage += 'Automatic translation of selectors will be removed in a few release cycles to minimize startup time. ' + deprecationMessage += 'Please, make sure to upgrade the above selectors as soon as possible.' + } + return {source: transformedSource.toString(), deprecationMessage} + } else { + // CSS was malformed so we don't transform it. + return {source: css} } - return {source: transformedSource.toString(), deprecationMessage} } diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 8c9792916..26e3bae12 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -108,7 +108,10 @@ class TextEditorElement extends HTMLElement buildModel: -> @setModel(@workspace.buildTextEditor( - buffer: new TextBuffer(@textContent) + buffer: new TextBuffer({ + text: @textContent + shouldDestroyOnFileDelete: + -> atom.config.get('core.closeDeletedFileTabs')}) softWrapped: false tabLength: 2 softTabs: true diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 3c4739ca5..1106cee09 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -622,6 +622,18 @@ class TextEditorPresenter return unless @scrollTop? and @lineHeight? @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop)) + atom.assert( + Number.isFinite(@startRow), + 'Invalid start row', + (error) => + error.metadata = { + startRow: @startRow?.toString(), + scrollTop: @scrollTop?.toString(), + scrollHeight: @scrollHeight?.toString(), + clientHeight: @clientHeight?.toString(), + lineHeight: @lineHeight?.toString() + } + ) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index b29a3887c..30600ff08 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -11,6 +11,7 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [ ['editor.showInvisibles', 'showInvisibles'], ['editor.tabLength', 'tabLength'], ['editor.invisibles', 'invisibles'], + ['editor.showCursorOnSelection', 'showCursorOnSelection'], ['editor.showIndentGuide', 'showIndentGuide'], ['editor.showLineNumbers', 'showLineNumbers'], ['editor.softWrap', 'softWrapped'], diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 821322e22..13b8a0bfa 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -16,12 +16,11 @@ TextEditorElement = require './text-editor-element' {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' ZERO_WIDTH_NBSP = '\ufeff' +MAX_SCREEN_LINE_LENGTH = 500 # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. -# If you're manipulating the state of an editor, use this class. If you're -# interested in the visual appearance of editors, use {TextEditorElement} -# instead. +# If you're manipulating the state of an editor, use this class. # # A single {TextBuffer} can belong to multiple editors. For example, if the # same file is open in two different panes, Atom creates a separate editor for @@ -67,6 +66,7 @@ class TextEditor extends Model buffer: null languageMode: null cursors: null + showCursorOnSelection: null selections: null suppressSelectionMerging: false selectionFlashDuration: 500 @@ -133,7 +133,8 @@ class TextEditor extends Model @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars, @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, - @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength + @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, + @showCursorOnSelection } = params @assert ?= (condition) -> condition @@ -153,13 +154,15 @@ class TextEditor extends Model tabLength ?= 2 @autoIndent ?= true @autoIndentOnPaste ?= true + @showCursorOnSelection ?= true @undoGroupingInterval ?= 300 @nonWordCharacters ?= "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" @softWrapped ?= false @softWrapAtPreferredLineLength ?= false @preferredLineLength ?= 80 - @buffer ?= new TextBuffer + @buffer ?= new TextBuffer({shouldDestroyOnFileDelete: -> + atom.config.get('core.closeDeletedFileTabs')}) @tokenizedBuffer ?= new TokenizedBuffer({ grammar, tabLength, @buffer, @largeFileMode, @assert }) @@ -190,8 +193,9 @@ class TextEditor extends Model @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @defaultMarkerLayer = @displayLayer.addMarkerLayer() @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) + @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer) + @decorationManager = new DecorationManager(@displayLayer) @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) for marker in @selectionsMarkerLayer.getMarkers() @@ -342,6 +346,12 @@ class TextEditor extends Model if value isnt @autoWidth @autoWidth = value @presenter?.didChangeAutoWidth() + + when 'showCursorOnSelection' + if value isnt @showCursorOnSelection + @showCursorOnSelection = value + cursor.setShowCursorOnSelection(value) for cursor in @getCursors() + else throw new TypeError("Invalid TextEditor parameter: '#{param}'") @@ -722,7 +732,7 @@ class TextEditor extends Model tabLength: @tokenizedBuffer.getTabLength(), @firstVisibleScreenRow, @firstVisibleScreenColumn, @assert, displayLayer, grammar: @getGrammar(), - @autoWidth, @autoHeight + @autoWidth, @autoHeight, @showCursorOnSelection }) # Controls visibility based on the given {Boolean}. @@ -892,7 +902,7 @@ class TextEditor extends Model # Determine whether the user should be prompted to save before closing # this editor. shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> - if windowCloseRequested and projectHasPaths + if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() false else @isModified() and not @buffer.hasMultipleEditors() @@ -2269,13 +2279,12 @@ class TextEditor extends Model # Add a cursor based on the given {DisplayMarker}. addCursor: (marker) -> - cursor = new Cursor(editor: this, marker: marker) + cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection) @cursors.push(cursor) @cursorsByMarkerId.set(marker.id, cursor) @decorateMarker(marker, type: 'line-number', class: 'cursor-line') @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) - @emitter.emit 'did-add-cursor', cursor cursor moveCursors: (fn) -> @@ -2764,6 +2773,7 @@ class TextEditor extends Model if selection.intersectsBufferRange(selectionBufferRange) return selection else + @emitter.emit 'did-add-cursor', cursor @emitter.emit 'did-add-selection', selection selection @@ -2956,7 +2966,7 @@ class TextEditor extends Model else @getEditorWidthInChars() else - Infinity + MAX_SCREEN_LINE_LENGTH ### Section: Indentation @@ -3466,6 +3476,11 @@ class TextEditor extends Model # Returns a positive {Number}. getScrollSensitivity: -> @scrollSensitivity + # Experimental: Does this editor show cursors while there is a selection? + # + # Returns a positive {Boolean}. + getShowCursorOnSelection: -> @showCursorOnSelection + # Experimental: Are line numbers enabled for this editor? # # Returns a {Boolean} diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 234f82be9..77221f52e 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -8,6 +8,8 @@ ScopeDescriptor = require './scope-descriptor' TokenizedBufferIterator = require './tokenized-buffer-iterator' NullGrammar = require './null-grammar' +MAX_LINE_LENGTH_TO_TOKENIZE = 500 + module.exports = class TokenizedBuffer extends Model grammar: null @@ -251,6 +253,8 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) + if text.length > MAX_LINE_LENGTH_TO_TOKENIZE + text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee deleted file mode 100644 index 639dc751d..000000000 --- a/src/window-load-settings-helpers.coffee +++ /dev/null @@ -1,8 +0,0 @@ -windowLoadSettings = null - -exports.getWindowLoadSettings = -> - windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1))) - -exports.setWindowLoadSettings = (settings) -> - windowLoadSettings = settings - location.hash = encodeURIComponent(JSON.stringify(settings)) diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index be0af81ed..f598bef0b 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -102,7 +102,7 @@ class WorkspaceElement extends HTMLElement getModel: -> @model handleMousewheel: (event) -> - if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling') and event.target.matches('atom-text-editor') + if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling') and event.target.closest('atom-text-editor')? if event.wheelDeltaY > 0 @model.increaseFontSize() else if event.wheelDeltaY < 0 diff --git a/src/workspace.coffee b/src/workspace.coffee index 9871db224..2a46ce57a 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -182,7 +182,7 @@ class Workspace extends Model projectPath = _.find projectPaths, (projectPath) -> itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) itemTitle ?= "untitled" - projectPath ?= if itemPath then path.dirname(itemPath) else null + projectPath ?= if itemPath then path.dirname(itemPath) else projectPaths[0] if projectPath? projectPath = fs.tildify(projectPath) diff --git a/static/babelrc.json b/static/babelrc.json index 26b70dc41..11474dd8d 100644 --- a/static/babelrc.json +++ b/static/babelrc.json @@ -1,7 +1,16 @@ { - "breakConfig": true, "sourceMap": "inline", - "blacklist": ["es6.forOf", "useStrict"], - "optional": ["asyncToGenerator"], - "stage": 0 + "plugins": [ + ["add-module-exports", {}], + ["transform-async-to-generator", {}], + ["transform-decorators-legacy", {}], + ["transform-class-properties", {}], + ["transform-es2015-modules-commonjs", {"strictMode": false}], + ["transform-export-extensions", {}], + ["transform-do-expressions", {}], + ["transform-function-bind", {}], + ["transform-object-rest-spread", {}], + ["transform-flow-strip-types", {}], + ["transform-react-jsx", {}] + ] } diff --git a/static/index.js b/static/index.js index 2966eafdf..aa57a594a 100644 --- a/static/index.js +++ b/static/index.js @@ -2,9 +2,8 @@ var path = require('path') var FileSystemBlobStore = require('../src/file-system-blob-store') var NativeCompileCache = require('../src/native-compile-cache') + var getWindowLoadSettings = require('../src/get-window-load-settings') - var loadSettings = null - var loadSettingsError = null var blobStore = null window.onload = function () { @@ -25,20 +24,16 @@ // Normalize to make sure drive letter case is consistent on Windows process.resourcesPath = path.normalize(process.resourcesPath) - if (loadSettingsError) { - throw loadSettingsError - } - - var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep) + var devMode = getWindowLoadSettings().devMode || !getWindowLoadSettings().resourcePath.startsWith(process.resourcesPath + path.sep) if (devMode) { setupDeprecatedPackages() } - if (loadSettings.profileStartup) { - profileStartup(loadSettings, Date.now() - startTime) + if (getWindowLoadSettings().profileStartup) { + profileStartup(Date.now() - startTime) } else { - setupWindow(loadSettings) + setupWindow() setLoadTime(Date.now() - startTime) } } catch (error) { @@ -61,23 +56,23 @@ console.error(error.stack || error) } - function setupWindow (loadSettings) { + function setupWindow () { var CompileCache = require('../src/compile-cache') CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) var ModuleCache = require('../src/module-cache') - ModuleCache.register(loadSettings) - ModuleCache.add(loadSettings.resourcePath) + ModuleCache.register(getWindowLoadSettings()) + ModuleCache.add(getWindowLoadSettings().resourcePath) // By explicitly passing the app version here, we could save the call // of "require('remote').require('app').getVersion()". var startCrashReporter = require('../src/crash-reporter-start') - startCrashReporter({_version: loadSettings.appVersion}) + startCrashReporter({_version: getWindowLoadSettings().appVersion}) setupVmCompatibility() setupCsonCache(CompileCache.getCacheDirectory()) - var initialize = require(loadSettings.windowInitializationScript) + var initialize = require(getWindowLoadSettings().windowInitializationScript) return initialize({blobStore: blobStore}).then(function () { require('electron').ipcRenderer.send('window-command', 'window:loaded') }) @@ -105,11 +100,11 @@ } } - function profileStartup (loadSettings, initialTime) { + function profileStartup (initialTime) { function profile () { console.profile('startup') var startTime = Date.now() - setupWindow(loadSettings).then(function () { + setupWindow().then(function () { setLoadTime(Date.now() - startTime + initialTime) console.profileEnd('startup') console.log('Switch to the Profiles tab to view the created startup profile') @@ -125,16 +120,6 @@ } } - function parseLoadSettings () { - var rawLoadSettings = decodeURIComponent(window.location.hash.substr(1)) - try { - loadSettings = JSON.parse(rawLoadSettings) - } catch (error) { - console.error('Failed to parse load settings: ' + rawLoadSettings) - loadSettingsError = error - } - } - var setupAtomHome = function () { if (process.env.ATOM_HOME) { return @@ -143,11 +128,10 @@ // Ensure ATOM_HOME is always set before anything else is required // This is because of a difference in Linux not inherited between browser and render processes // https://github.com/atom/atom/issues/5412 - if (loadSettings && loadSettings.atomHome) { - process.env.ATOM_HOME = loadSettings.atomHome + if (getWindowLoadSettings() && getWindowLoadSettings().atomHome) { + process.env.ATOM_HOME = getWindowLoadSettings().atomHome } } - parseLoadSettings() setupAtomHome() })()