diff --git a/.travis.yml b/.travis.yml index 87559ea3a..b8fe0a01a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ cache: - apm/node_modules - script/node_modules - ~/.atom/compile-cache + - ~/.atom/snapshot-cache notifications: email: diff --git a/apm/package.json b/apm/package.json index ede2d1bd7..5e3dcb1e0 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.3" + "atom-package-manager": "1.17.0" } } diff --git a/appveyor.yml b/appveyor.yml index 29606fb4e..93ceee6a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -52,3 +52,4 @@ cache: - '%APPVEYOR_BUILD_FOLDER%\electron' - '%USERPROFILE%\.atom\.apm' - '%USERPROFILE%\.atom\compile-cache' + - '%USERPROFILE%\.atom\snapshot-cache' diff --git a/circle.yml b/circle.yml index 40ab1a6a9..540eb8de8 100644 --- a/circle.yml +++ b/circle.yml @@ -29,6 +29,7 @@ dependencies: - script/node_modules - node_modules - ~/.atom/compile-cache + - ~/.atom/snapshot-cache test: override: diff --git a/docs/README.md b/docs/README.md index e9b6ff120..c555306b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ In this directory you can only find very specific build and API level documentat Instructions for building Atom on various platforms from source. -* [macOS](./build-instructions/macos.md) +* [macOS](./build-instructions/macOS.md) * [Windows](./build-instructions/windows.md) * [Linux](./build-instructions/linux.md) * [FreeBSD](./build-instructions/freebsd.md) diff --git a/menus/linux.cson b/menus/linux.cson index 94fb90a30..2a1ca47f8 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -137,6 +137,7 @@ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } + { label: 'Run &Benchmarks', command: 'window:run-benchmarks' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] } diff --git a/menus/win32.cson b/menus/win32.cson index 70bb1487d..553b6017e 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -136,6 +136,7 @@ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } + { label: 'Run &Benchmarks', command: 'window:run-benchmarks' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] } diff --git a/package.json b/package.json index 0a799e71e..4f72a19ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.16.0-dev", + "version": "1.17.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -12,46 +12,35 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.3.13", + "electronVersion": "1.3.14", "dependencies": { "async": "0.2.6", - "atom-keymap": "7.1.20", - "atom-select-list": "0.0.12", + "atom-keymap": "8.1.0", + "atom-select-list": "0.0.15", "atom-ui": "0.4.1", - "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", + "babel-core": "5.8.38", "cached-run-in-this-context": "0.4.1", "chai": "3.5.0", "chart.js": "^2.3.0", - "clear-cut": "^2.0.1", + "clear-cut": "^2.0.2", "coffee-script": "1.11.1", "color": "^0.7.3", "dedent": "^0.6.0", "devtron": "1.3.0", "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", - "first-mate": "6.1.0", - "fs-plus": "2.9.2", + "first-mate": "7.0.2", + "fs-plus": "^3.0.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "4.1.2", + "git-utils": "5.0.0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", "jquery": "2.1.4", "key-path-helpers": "^0.4.0", - "less-cache": "0.23", + "less-cache": "1.0.0", "line-top-index": "0.2.0", "marked": "^0.3.6", "minimatch": "^3.0.3", @@ -60,23 +49,23 @@ "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "6.1.0", - "pathwatcher": "6.8.0", + "pathwatcher": "7.0.0", "postcss": "5.2.4", "postcss-selector-parser": "2.2.1", "property-accessors": "^1.1.3", "random-words": "0.0.1", "resolve": "^1.1.6", "runas": "^3.1", - "scandal": "2.2.2", + "scandal": "^3.1.0", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", - "season": "^5.4.1", + "season": "^6.0.0", "semver": "^4.3.3", - "service-hub": "^0.7.2", + "service-hub": "^0.7.3", "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "10.3.12", + "text-buffer": "11.4.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -89,90 +78,90 @@ "atom-light-ui": "0.46.0", "base16-tomorrow-dark-theme": "1.5.0", "base16-tomorrow-light-theme": "1.5.0", - "one-dark-ui": "1.9.1", - "one-light-ui": "1.9.1", + "one-dark-ui": "1.9.2", + "one-light-ui": "1.9.2", "one-dark-syntax": "1.7.1", "one-light-syntax": "1.7.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.15.0", - "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.34.2", + "about": "1.7.5", + "archive-view": "0.63.1", + "autocomplete-atom-api": "0.10.1", + "autocomplete-css": "0.16.1", + "autocomplete-html": "0.7.3", + "autocomplete-plus": "2.35.0", "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", - "autosave": "0.24.0", - "background-tips": "0.26.1", - "bookmarks": "0.44.1", - "bracket-matcher": "0.85.3", - "command-palette": "0.40.2", + "autosave": "0.24.1", + "background-tips": "0.26.2", + "bookmarks": "0.44.2", + "bracket-matcher": "0.85.4", + "command-palette": "0.40.3", "dalek": "0.2.0", - "deprecation-cop": "0.56.2", - "dev-live-reload": "0.47.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.3.1", + "deprecation-cop": "0.56.5", + "dev-live-reload": "0.47.1", + "encoding-selector": "0.23.2", + "exception-reporting": "0.41.3", + "find-and-replace": "0.207.3", + "fuzzy-finder": "1.5.2", + "git-diff": "1.3.4", "go-to-line": "0.32.0", - "grammar-selector": "0.49.2", - "image-view": "0.61.0", - "incompatible-packages": "0.27.0", - "keybinding-resolver": "0.36.1", - "line-ending-selector": "0.6.1", - "link": "0.31.2", - "markdown-preview": "0.159.7", - "metrics": "1.2.1", - "notifications": "0.66.2", + "grammar-selector": "0.49.3", + "image-view": "0.61.2", + "incompatible-packages": "0.27.2", + "keybinding-resolver": "0.37.0", + "line-ending-selector": "0.6.2", + "link": "0.31.3", + "markdown-preview": "0.159.10", + "metrics": "1.2.2", + "notifications": "0.67.1", "open-on-github": "1.2.1", - "package-generator": "1.1.0", - "settings-view": "0.247.0", - "snippets": "1.0.5", - "spell-check": "0.71.0", - "status-bar": "1.8.1", - "styleguide": "0.49.2", - "symbols-view": "0.114.0", - "tabs": "0.104.1", + "package-generator": "1.1.1", + "settings-view": "0.249.1", + "snippets": "1.1.3", + "spell-check": "0.71.3", + "status-bar": "1.8.4", + "styleguide": "0.49.4", + "symbols-view": "0.115.3", + "tabs": "0.104.4", "timecop": "0.36.0", - "tree-view": "0.214.1", - "update-package-dependencies": "0.10.0", - "welcome": "0.36.1", + "tree-view": "0.215.3", + "update-package-dependencies": "0.11.0", + "welcome": "0.36.2", "whitespace": "0.36.2", - "wrap-guide": "0.39.1", - "language-c": "0.56.0", + "wrap-guide": "0.40.1", + "language-c": "0.57.0", "language-clojure": "0.22.2", - "language-coffee-script": "0.48.4", + "language-coffee-script": "0.48.6", "language-csharp": "0.14.2", - "language-css": "0.42.0", - "language-gfm": "0.88.0", + "language-css": "0.42.1", + "language-gfm": "0.88.1", "language-git": "0.19.0", "language-go": "0.43.1", "language-html": "0.47.2", "language-hyperlink": "0.16.1", - "language-java": "0.26.0", - "language-javascript": "0.126.0", - "language-json": "0.18.3", - "language-less": "0.30.1", + "language-java": "0.27.0", + "language-javascript": "0.126.1", + "language-json": "0.19.0", + "language-less": "0.32.0", "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.4", - "language-property-list": "0.9.0", + "language-php": "0.37.5", + "language-property-list": "0.9.1", "language-python": "0.45.2", - "language-ruby": "0.70.5", + "language-ruby": "0.71.0", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.57.1", + "language-sass": "0.59.0", "language-shellscript": "0.25.0", "language-source": "0.9.0", - "language-sql": "0.25.3", - "language-text": "0.7.1", + "language-sql": "0.25.4", + "language-text": "0.7.2", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.34.16", - "language-yaml": "0.28.0" + "language-xml": "0.35.0", + "language-yaml": "0.29.0" }, "private": true, "scripts": { diff --git a/resources/linux/redhat/atom.spec.in b/resources/linux/redhat/atom.spec.in index 82a5fbf9a..306f1029e 100644 --- a/resources/linux/redhat/atom.spec.in +++ b/resources/linux/redhat/atom.spec.in @@ -7,7 +7,11 @@ URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency Prefix: <%= installDir %> +%ifarch i386 i486 i586 i686 +Requires: lsb-core-noarch, libXss.so.1 +%else Requires: lsb-core-noarch, libXss.so.1()(64bit) +%endif %description <%= description %> diff --git a/script/build b/script/build index 0fccee6e6..195afba58 100755 --- a/script/build +++ b/script/build @@ -35,6 +35,7 @@ const dumpSymbols = require('./lib/dump-symbols') const generateAPIDocs = require('./lib/generate-api-docs') const generateMetadata = require('./lib/generate-metadata') const generateModuleCache = require('./lib/generate-module-cache') +const generateStartupSnapshot = require('./lib/generate-startup-snapshot') const installApplication = require('./lib/install-application') const packageApplication = require('./lib/package-application') const prebuildLessCache = require('./lib/prebuild-less-cache') @@ -61,6 +62,7 @@ generateMetadata() generateAPIDocs() dumpSymbols() .then(packageApplication) + .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) .then(packagedAppPath => { if (process.platform === 'darwin') { if (argv.codeSign) { diff --git a/script/lib/check-chromedriver-version.js b/script/lib/check-chromedriver-version.js index 90bc220e5..6fd313fc7 100644 --- a/script/lib/check-chromedriver-version.js +++ b/script/lib/check-chromedriver-version.js @@ -7,16 +7,27 @@ const semver = require('semver') module.exports = function () { // Chromedriver should be specified as ~x.y where x and y match Electron major/minor const chromedriverVer = buildMetadata.dependencies['electron-chromedriver'] + const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot'] // Always use tilde on electron-chromedriver so that it can pick up the best patch vesion if (!chromedriverVer.startsWith('~')) { throw new Error(`electron-chromedriver version in script/package.json should start with a tilde to match latest patch version.`) } + if (!mksnapshotVer.startsWith('~')) { + throw new Error(`electron-mksnapshot version in script/package.json should start with a tilde to match latest patch version.`) + } + const electronVer = CONFIG.appMetadata.electronVersion if (!semver.satisfies(electronVer, chromedriverVer)) { throw new Error(`electron-chromedriver ${chromedriverVer} incompatible with electron ${electronVer}.\n` + 'Did you upgrade electron in package.json and forget to upgrade electron-chromedriver in ' + `script/package.json to '~${semver.major(electronVer)}.${semver.minor(electronVer)}' ?`) } + + if (!semver.satisfies(electronVer, mksnapshotVer)) { + throw new Error(`electron-mksnapshot ${mksnapshotVer} incompatible with electron ${electronVer}.\n` + + 'Did you upgrade electron in package.json and forget to upgrade electron-mksnapshot in ' + + `script/package.json to '~${semver.major(electronVer)}.${semver.minor(electronVer)}' ?`) + } } diff --git a/script/lib/create-debian-package.js b/script/lib/create-debian-package.js index 120463f7b..9c545d6d8 100644 --- a/script/lib/create-debian-package.js +++ b/script/lib/create-debian-package.js @@ -98,7 +98,7 @@ module.exports = function (packagedAppPath) { console.log(`Copying icon into "${debianPackageIconsDirPath}"`) fs.copySync( - path.join(packagedAppPath, 'resources', 'app.asar.unpacked', 'resources', 'atom.png'), + path.join(packagedAppPath, 'resources', 'app', 'resources', 'atom.png'), path.join(debianPackageIconsDirPath, `${atomExecutableName}.png`) ) diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index 274dc835d..22a22701c 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -10,7 +10,7 @@ const spawnSync = require('./spawn-sync') const CONFIG = require('../config') -module.exports = function (packagedAppPath, codeSign) { +module.exports = (packagedAppPath, codeSign) => { const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch const options = { appDirectory: packagedAppPath, @@ -42,7 +42,7 @@ module.exports = function (packagedAppPath, codeSign) { console.log('Skipping code-signing. Specify the --code-sign option and provide a ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable to perform code-signing'.gray) } - const cleanUp = function () { + const cleanUp = () => { if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { console.log(`Deleting certificate at ${certPath}`) fs.removeSync(certPath) @@ -57,7 +57,7 @@ module.exports = function (packagedAppPath, codeSign) { } // Squirrel signs its own copy of the executables but we need them for the portable ZIP - const extractSignedExes = function () { + const extractSignedExes = () => { if (signing) { for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*-full.nupkg`)) { if (nupkgPath.includes(CONFIG.appMetadata.version)) { @@ -73,12 +73,9 @@ module.exports = function (packagedAppPath, codeSign) { console.log(`Creating Windows Installer for ${packagedAppPath}`) return electronInstaller.createWindowsInstaller(options) - .then(extractSignedExes, function (error) { - console.log(`Extracting signed executables failed:\n${error}`) - cleanUp() - }) - .then(cleanUp, function (error) { - console.log(`Windows installer creation failed:\n${error}`) + .then(extractSignedExes) + .then(cleanUp, error => { cleanUp() + return Promise.reject(error) }) } diff --git a/script/lib/download-file-from-github.js b/script/lib/download-file-from-github.js index 2969ea2dc..13e04e99e 100644 --- a/script/lib/download-file-from-github.js +++ b/script/lib/download-file-from-github.js @@ -5,7 +5,7 @@ const path = require('path') const syncRequest = require('sync-request') module.exports = function (downloadURL, destinationPath) { - console.log(`Dowloading file from GitHub Repository to ${destinationPath}`) + console.log(`Downloading file from GitHub Repository to ${destinationPath}`) const response = syncRequest('GET', downloadURL, { 'headers': {'Accept': 'application/vnd.github.v3.raw', 'User-Agent': 'Atom Build'} }) diff --git a/script/lib/generate-metadata.js b/script/lib/generate-metadata.js index 8b62db824..6aa58ee70 100644 --- a/script/lib/generate-metadata.js +++ b/script/lib/generate-metadata.js @@ -28,7 +28,7 @@ function buildBundledPackagesMetadata () { const packageMetadataPath = path.join(packagePath, 'package.json') const packageMetadata = JSON.parse(fs.readFileSync(packageMetadataPath, 'utf8')) normalizePackageData(packageMetadata, (msg) => { - throw new Error(`Invalid package metadata. ${packageName}: ${msg}`) + console.warn(`Invalid package metadata. ${packageMetadata.name}: ${msg}`) }, true) if (packageMetadata.repository && packageMetadata.repository.url && packageMetadata.repository.type === 'git') { packageMetadata.repository.url = packageMetadata.repository.url.replace(/^git\+/, '') diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js new file mode 100644 index 000000000..1dfe05f51 --- /dev/null +++ b/script/lib/generate-startup-snapshot.js @@ -0,0 +1,86 @@ +const childProcess = require('child_process') +const fs = require('fs') +const path = require('path') +const electronLink = require('electron-link') +const CONFIG = require('../config') +const vm = require('vm') + +module.exports = function (packagedAppPath) { + const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js') + const coreModules = new Set(['electron', 'atom', 'shell', 'WNdb', 'lapack', 'remote']) + const baseDirPath = path.join(CONFIG.intermediateAppPath, 'static') + let processedFiles = 0 + + return electronLink({ + baseDirPath, + mainPath: path.resolve(baseDirPath, '..', 'src', 'initialize-application-window.js'), + cachePath: path.join(CONFIG.atomHomeDirPath, 'snapshot-cache'), + shouldExcludeModule: (modulePath) => { + if (processedFiles > 0) { + process.stdout.write('\r') + } + process.stdout.write(`Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})`) + + const relativePath = path.relative(baseDirPath, modulePath) + return ( + modulePath.endsWith('.node') || + coreModules.has(modulePath) || + (relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) || + relativePath == path.join('..', 'exports', 'atom.js') || + relativePath == path.join('..', 'src', 'electron-shims.js') || + relativePath == path.join('..', 'src', 'safe-clipboard.js') || + relativePath == path.join('..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js') || + relativePath == path.join('..', 'node_modules', 'babel-core', 'index.js') || + relativePath == path.join('..', 'node_modules', 'cached-run-in-this-context', 'lib', 'main.js') || + relativePath == path.join('..', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') || + relativePath == path.join('..', 'node_modules', 'cson-parser', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') || + relativePath == path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') || + relativePath == path.join('..', 'node_modules', 'debug', 'node.js') || + relativePath == path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') || + relativePath == path.join('..', 'node_modules', 'glob', 'glob.js') || + relativePath == path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') || + relativePath == path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') || + relativePath == path.join('..', 'node_modules', 'iconv-lite', 'encodings', 'index.js') || + relativePath == path.join('..', 'node_modules', 'less', 'index.js') || + relativePath == path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || + relativePath == path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') || + relativePath == path.join('..', 'node_modules', 'less', 'node_modules', 'graceful-fs', 'graceful-fs.js') || + relativePath == path.join('..', 'node_modules', 'superstring', 'index.js') || + relativePath == path.join('..', 'node_modules', 'oniguruma', 'lib', 'oniguruma.js') || + relativePath == path.join('..', 'node_modules', 'request', 'index.js') || + relativePath == path.join('..', 'node_modules', 'resolve', 'index.js') || + relativePath == path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') || + relativePath == path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') || + relativePath == path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') || + relativePath == path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') || + relativePath == path.join('..', 'node_modules', 'tar', 'tar.js') || + relativePath == path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') || + relativePath == path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') + ) + } + }).then((snapshotScript) => { + fs.writeFileSync(snapshotScriptPath, snapshotScript) + process.stdout.write('\n') + + console.log('Verifying if snapshot can be executed via `mksnapshot`') + vm.runInNewContext(snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}) + + const generatedStartupBlobPath = path.join(CONFIG.buildOutputPath, 'snapshot_blob.bin') + console.log(`Generating startup blob at "${generatedStartupBlobPath}"`) + childProcess.execFileSync( + path.join(CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'bin', 'mksnapshot'), + [snapshotScriptPath, '--startup_blob', generatedStartupBlobPath] + ) + + let startupBlobDestinationPath + if (process.platform === 'darwin') { + startupBlobDestinationPath = `${packagedAppPath}/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin` + } else { + startupBlobDestinationPath = path.join(packagedAppPath, 'snapshot_blob.bin') + } + + console.log(`Moving generated startup blob into "${startupBlobDestinationPath}"`) + fs.unlinkSync(startupBlobDestinationPath) + fs.renameSync(generatedStartupBlobPath, startupBlobDestinationPath) + }) +} diff --git a/script/lib/package-application.js b/script/lib/package-application.js index 1e63b8dc0..5e01e5543 100644 --- a/script/lib/package-application.js +++ b/script/lib/package-application.js @@ -19,7 +19,6 @@ module.exports = function () { 'app-copyright': `Copyright © 2014-${(new Date()).getFullYear()} GitHub, Inc. All rights reserved.`, 'app-version': CONFIG.appMetadata.version, 'arch': process.platform === 'darwin' ? 'x64' : process.arch, // OS X is 64-bit only - 'asar': {unpack: buildAsarUnpackGlobExpression()}, 'build-version': CONFIG.appMetadata.version, 'download': {cache: CONFIG.electronDownloadPath}, 'dir': CONFIG.intermediateAppPath, @@ -96,20 +95,6 @@ function chmodNodeFiles (packagedAppPath) { childProcess.execSync(`find "${packagedAppPath}" -type f -name *.node -exec chmod a-x {} \\;`) } -function buildAsarUnpackGlobExpression () { - const unpack = [ - '*.node', - 'ctags-config', - 'ctags-darwin', - 'ctags-linux', - 'ctags-win32.exe', - path.join('**', 'node_modules', 'spellchecker', '**'), - path.join('**', 'resources', 'atom.png') - ] - - return `{${unpack.join(',')}}` -} - function getAppName () { if (process.platform === 'darwin') { return CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' diff --git a/script/package.json b/script/package.json index 87c43f261..540855abb 100644 --- a/script/package.json +++ b/script/package.json @@ -3,17 +3,20 @@ "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", "donna": "1.0.13", "electron-chromedriver": "~1.3", + "electron-link": "0.0.20", + "electron-mksnapshot": "~1.3", "electron-packager": "7.3.0", "electron-winstaller": "2.5.1", "fs-extra": "0.30.0", "glob": "7.0.3", "joanna": "0.0.8", - "legal-eagle": "0.13.0", + "legal-eagle": "0.14.0", "lodash.template": "4.4.0", "minidump": "0.9.0", "mkdirp": "0.5.1", diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index d967fb97b..1d299acd2 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -126,6 +126,7 @@ describe "AtomEnvironment", -> beforeEach -> errors = [] + spyOn(atom, 'isReleasedVersion').andReturn(true) atom.onDidFailAssertion (error) -> errors.push(error) describe "if the condition is false", -> @@ -147,6 +148,11 @@ describe "AtomEnvironment", -> atom.assert(false, "a == b", {foo: 'bar'}) expect(errors[0].metadata).toEqual {foo: 'bar'} + describe "when Atom has been built from source", -> + it "throws an error", -> + atom.isReleasedVersion.andReturn(false) + expect(-> atom.assert(false, 'testing')).toThrow('Assertion failed: testing') + describe "if the condition is true", -> it "does nothing", -> result = atom.assert(true, "a == b") @@ -334,7 +340,8 @@ describe "AtomEnvironment", -> it "saves the BlobStore so it can be loaded after reload", -> configDirPath = temp.mkdirSync('atom-spec-environment') fakeBlobStore = jasmine.createSpyObj("blob store", ["save"]) - atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true, configDirPath, blobStore: fakeBlobStore, window, document}) + atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true}) + atomEnvironment.initialize({configDirPath, blobStore: fakeBlobStore, window, document}) atomEnvironment.unloadEditorWindow() @@ -351,7 +358,8 @@ describe "AtomEnvironment", -> head: document.createElement('head') body: document.createElement('body') } - atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document: fakeDocument}) + atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) + atomEnvironment.initialize({window, document: fakeDocument}) spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn [] spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve() atomEnvironment.startEditorWindow() diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js index b38e7827c..780c9816b 100644 --- a/spec/auto-update-manager-spec.js +++ b/spec/auto-update-manager-spec.js @@ -11,9 +11,8 @@ describe('AutoUpdateManager (renderer)', () => { let autoUpdateManager beforeEach(() => { - autoUpdateManager = new AutoUpdateManager({ - applicationDelegate: atom.applicationDelegate - }) + autoUpdateManager = new AutoUpdateManager({applicationDelegate: atom.applicationDelegate}) + autoUpdateManager.initialize() }) afterEach(() => { diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index a1cf194a8..f0994fc08 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -25,7 +25,8 @@ describe "CommandInstaller on #darwin", -> it "shows an error dialog when installing commands interactively fails", -> appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) - installer = new CommandInstaller("2.0.2", appDelegate) + installer = new CommandInstaller(appDelegate) + installer.initialize("2.0.2") spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback(new Error("an error")) installer.installShellCommandsInteractively() @@ -48,7 +49,8 @@ describe "CommandInstaller on #darwin", -> it "shows a success dialog when installing commands interactively succeeds", -> appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) - installer = new CommandInstaller("2.0.2", appDelegate) + installer = new CommandInstaller(appDelegate) + installer.initialize("2.0.2") spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback() spyOn(installer, "installApmCommand").andCallFake (__, callback) -> callback() @@ -61,7 +63,8 @@ describe "CommandInstaller on #darwin", -> describe "when using a stable version of atom", -> beforeEach -> - installer = new CommandInstaller("2.0.2") + installer = new CommandInstaller() + installer.initialize("2.0.2") it "symlinks the atom command as 'atom'", -> installedAtomPath = path.join(installationPath, 'atom') @@ -91,7 +94,8 @@ describe "CommandInstaller on #darwin", -> describe "when using a beta version of atom", -> beforeEach -> - installer = new CommandInstaller("2.2.0-beta.0") + installer = new CommandInstaller() + installer.initialize("2.2.0-beta.0") it "symlinks the atom command as 'atom-beta'", -> installedAtomPath = path.join(installationPath, 'atom-beta') diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index c39f33cea..f99d7b004 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -5,7 +5,8 @@ describe "ContextMenuManager", -> beforeEach -> {resourcePath} = atom.getLoadSettings() - contextMenu = new ContextMenuManager({resourcePath, keymapManager: atom.keymaps}) + contextMenu = new ContextMenuManager({keymapManager: atom.keymaps}) + contextMenu.initialize({resourcePath}) parent = document.createElement("div") child = document.createElement("div") diff --git a/spec/dom-element-pool-spec.js b/spec/dom-element-pool-spec.js index 9de932e27..91120ee48 100644 --- a/spec/dom-element-pool-spec.js +++ b/spec/dom-element-pool-spec.js @@ -3,7 +3,10 @@ const DOMElementPool = require ('../src/dom-element-pool') describe('DOMElementPool', function () { let domElementPool - beforeEach(() => { domElementPool = new DOMElementPool() }) + beforeEach(() => { + domElementPool = new DOMElementPool() + spyOn(atom, 'isReleasedVersion').andReturn(true) + }) it('builds DOM nodes, recycling them when they are freed', function () { let elements diff --git a/spec/file-system-blob-store-spec.coffee b/spec/file-system-blob-store-spec.coffee index a2ed39014..ff1c81eb6 100644 --- a/spec/file-system-blob-store-spec.coffee +++ b/spec/file-system-blob-store-spec.coffee @@ -14,78 +14,75 @@ describe "FileSystemBlobStore", -> fs.removeSync(storageDirectory) it "is empty when the file doesn't exist", -> - expect(blobStore.get("foo", "invalidation-key-1")).toBeUndefined() - expect(blobStore.get("bar", "invalidation-key-2")).toBeUndefined() + expect(blobStore.get("foo")).toBeUndefined() + expect(blobStore.get("bar")).toBeUndefined() it "allows to read and write buffers from/to memory without persisting them", -> - blobStore.set("foo", "invalidation-key-1", new Buffer("foo")) - blobStore.set("bar", "invalidation-key-2", new Buffer("bar")) + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) - expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo")) - expect(blobStore.get("bar", "invalidation-key-2")).toEqual(new Buffer("bar")) + expect(blobStore.get("foo")).toEqual(new Buffer("foo")) + expect(blobStore.get("bar")).toEqual(new Buffer("bar")) - expect(blobStore.get("foo", "unexisting-key")).toBeUndefined() - expect(blobStore.get("bar", "unexisting-key")).toBeUndefined() + expect(blobStore.get("baz")).toBeUndefined() + expect(blobStore.get("qux")).toBeUndefined() it "persists buffers when saved and retrieves them on load, giving priority to in-memory ones", -> - blobStore.set("foo", "invalidation-key-1", new Buffer("foo")) - blobStore.set("bar", "invalidation-key-2", new Buffer("bar")) + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo")) - expect(blobStore.get("bar", "invalidation-key-2")).toEqual(new Buffer("bar")) - expect(blobStore.get("foo", "unexisting-key")).toBeUndefined() - expect(blobStore.get("bar", "unexisting-key")).toBeUndefined() + expect(blobStore.get("foo")).toEqual(new Buffer("foo")) + expect(blobStore.get("bar")).toEqual(new Buffer("bar")) + expect(blobStore.get("baz")).toBeUndefined() + expect(blobStore.get("qux")).toBeUndefined() - blobStore.set("foo", "new-key", new Buffer("changed")) + blobStore.set("foo", new Buffer("changed")) - expect(blobStore.get("foo", "new-key")).toEqual(new Buffer("changed")) - expect(blobStore.get("foo", "invalidation-key-1")).toBeUndefined() + expect(blobStore.get("foo")).toEqual(new Buffer("changed")) - it "persists both in-memory and previously stored buffers when saved", -> - blobStore.set("foo", "invalidation-key-1", new Buffer("foo")) - blobStore.set("bar", "invalidation-key-2", new Buffer("bar")) + it "persists in-memory and previously stored buffers, and deletes unused keys when saved", -> + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - blobStore.set("bar", "invalidation-key-3", new Buffer("changed")) - blobStore.set("qux", "invalidation-key-4", new Buffer("qux")) + blobStore.set("bar", new Buffer("changed")) + blobStore.set("qux", new Buffer("qux")) blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo")) - expect(blobStore.get("bar", "invalidation-key-3")).toEqual(new Buffer("changed")) - expect(blobStore.get("qux", "invalidation-key-4")).toEqual(new Buffer("qux")) - expect(blobStore.get("foo", "unexisting-key")).toBeUndefined() - expect(blobStore.get("bar", "invalidation-key-2")).toBeUndefined() - expect(blobStore.get("qux", "unexisting-key")).toBeUndefined() + expect(blobStore.get("foo")).toBeUndefined() + expect(blobStore.get("bar")).toEqual(new Buffer("changed")) + expect(blobStore.get("qux")).toEqual(new Buffer("qux")) it "allows to delete keys from both memory and stored buffers", -> - blobStore.set("a", "invalidation-key-1", new Buffer("a")) - blobStore.set("b", "invalidation-key-2", new Buffer("b")) + blobStore.set("a", new Buffer("a")) + blobStore.set("b", new Buffer("b")) blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - blobStore.set("b", "invalidation-key-3", new Buffer("b")) - blobStore.set("c", "invalidation-key-4", new Buffer("c")) + blobStore.get("a") # prevent the key from being deleted on save + blobStore.set("b", new Buffer("b")) + blobStore.set("c", new Buffer("c")) blobStore.delete("b") blobStore.delete("c") blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - expect(blobStore.get("a", "invalidation-key-1")).toEqual(new Buffer("a")) - expect(blobStore.get("b", "invalidation-key-2")).toBeUndefined() - expect(blobStore.get("b", "invalidation-key-3")).toBeUndefined() - expect(blobStore.get("c", "invalidation-key-4")).toBeUndefined() + expect(blobStore.get("a")).toEqual(new Buffer("a")) + expect(blobStore.get("b")).toBeUndefined() + expect(blobStore.get("b")).toBeUndefined() + expect(blobStore.get("c")).toBeUndefined() it "ignores errors when loading an invalid blob store", -> - blobStore.set("a", "invalidation-key-1", new Buffer("a")) - blobStore.set("b", "invalidation-key-2", new Buffer("b")) + blobStore.set("a", new Buffer("a")) + blobStore.set("b", new Buffer("b")) blobStore.save() # Simulate corruption @@ -95,14 +92,14 @@ describe "FileSystemBlobStore", -> blobStore = FileSystemBlobStore.load(storageDirectory) - expect(blobStore.get("a", "invalidation-key-1")).toBeUndefined() - expect(blobStore.get("b", "invalidation-key-2")).toBeUndefined() + expect(blobStore.get("a")).toBeUndefined() + expect(blobStore.get("b")).toBeUndefined() - blobStore.set("a", "invalidation-key-1", new Buffer("x")) - blobStore.set("b", "invalidation-key-2", new Buffer("y")) + blobStore.set("a", new Buffer("x")) + blobStore.set("b", new Buffer("y")) blobStore.save() blobStore = FileSystemBlobStore.load(storageDirectory) - expect(blobStore.get("a", "invalidation-key-1")).toEqual(new Buffer("x")) - expect(blobStore.get("b", "invalidation-key-2")).toEqual(new Buffer("y")) + expect(blobStore.get("a")).toEqual(new Buffer("x")) + expect(blobStore.get("b")).toEqual(new Buffer("y")) diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee index 59e8c4c68..f1c433d76 100644 --- a/spec/git-repository-spec.coffee +++ b/spec/git-repository-spec.coffee @@ -31,11 +31,6 @@ describe "GitRepository", -> expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() describe ".getPath()", -> - it "returns the repository path for a .git directory path with a file", -> - return if process.platform is 'win32' #Win32TestFailures - libgit2 does not detect files in .git folders - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') - it "returns the repository path for a .git directory path with a directory", -> repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 425f1efe0..7e62a69f4 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -4,27 +4,24 @@ import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import {Emitter, Disposable, CompositeDisposable} from 'event-kit' import {HistoryManager, HistoryProject} from '../src/history-manager' +import StateStore from '../src/state-store' describe("HistoryManager", () => { - let historyManager, commandRegistry, project, localStorage, stateStore + let historyManager, commandRegistry, project, stateStore let commandDisposable, projectDisposable - beforeEach(() => { + beforeEach(async () => { commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) commandRegistry.add.andReturn(commandDisposable) - localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem']) - localStorage.items = { - history: JSON.stringify({ - projects: [ - { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, - { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } - ] - }) - } - localStorage.getItem.andCallFake((key) => localStorage.items[key]) - localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value) + stateStore = new StateStore('history-manager-test', 1) + await stateStore.save('history-manager', { + projects: [ + {paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23)}, + {paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13)} + ] + }) projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']) project = jasmine.createSpyObj('Project', ['onDidChangePaths']) @@ -33,14 +30,20 @@ describe("HistoryManager", () => { return projectDisposable }) - historyManager = new HistoryManager({project, commands:commandRegistry, localStorage}) + historyManager = new HistoryManager({stateStore, project, commands: commandRegistry}) + historyManager.initialize(window.localStorage) + await historyManager.loadState() + }) + + afterEach(async () => { + await stateStore.clear() }) describe("constructor", () => { it("registers the 'clear-project-history' command function", () => { expect(commandRegistry.add).toHaveBeenCalled() const cmdCall = commandRegistry.add.calls[0] - expect(cmdCall.args.length).toBe(2) + expect(cmdCall.args.length).toBe(3) expect(cmdCall.args[0]).toBe('atom-workspace') expect(typeof cmdCall.args[1]['application:clear-project-history']).toBe('function') }) @@ -65,33 +68,28 @@ describe("HistoryManager", () => { }) describe("clearProjects", () => { - it("clears the list of projects", () => { + it("clears the list of projects", async () => { expect(historyManager.getProjects().length).not.toBe(0) - historyManager.clearProjects() + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) }) - it("saves the state", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("saves the state", async () => { + await historyManager.clearProjects() + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) - it("fires the onDidChangeProjects event", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("fires the onDidChangeProjects event", async () => { + const didChangeSpy = jasmine.createSpy() + historyManager.onDidChangeProjects(didChangeSpy) + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) + expect(didChangeSpy).toHaveBeenCalled() }) }) - it("loads state", () => { - expect(localStorage.getItem).toHaveBeenCalledWith('history') - }) - it("listens to project.onDidChangePaths adding a new project", () => { const start = new Date() project.didChangePathsListener(['/a/new', '/path/or/two']) @@ -112,61 +110,61 @@ describe("HistoryManager", () => { }) describe("loadState", () => { - it("defaults to an empty array if no state", () => { - localStorage.items.history = null - historyManager.loadState() + it("defaults to an empty array if no state", async () => { + await stateStore.clear() + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) - it("defaults to an empty array if no projects", () => { - localStorage.items.history = JSON.stringify('') - historyManager.loadState() + it("defaults to an empty array if no projects", async () => { + await stateStore.save('history-manager', {}) + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) }) describe("addProject", () => { - it("adds a new project to the end", () => { + it("adds a new project to the end", async () => { const date = new Date(2010, 10, 9, 8, 7, 6) - historyManager.addProject(['/a/b'], date) + await historyManager.addProject(['/a/b'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[2].paths).toEqual(['/a/b']) expect(projects[2].lastOpened).toBe(date) }) - it("adds a new project to the start", () => { + it("adds a new project to the start", async () => { const date = new Date() - historyManager.addProject(['/so/new'], date) + await historyManager.addProject(['/so/new'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[0].paths).toEqual(['/so/new']) expect(projects[0].lastOpened).toBe(date) }) - it("updates an existing project and moves it to the start", () => { + it("updates an existing project and moves it to the start", async () => { const date = new Date() - historyManager.addProject(['/test'], date) + await historyManager.addProject(['/test'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(2) expect(projects[0].paths).toEqual(['/test']) expect(projects[0].lastOpened).toBe(date) }) - it("fires the onDidChangeProjects event when adding a project", () => { + it("fires the onDidChangeProjects event when adding a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test-new'], new Date()) + await historyManager.addProject(['/test-new'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount + 1) }) - it("fires the onDidChangeProjects event when updating a project", () => { + it("fires the onDidChangeProjects event when updating a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test'], new Date()) + await historyManager.addProject(['/test'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount) }) @@ -186,14 +184,12 @@ describe("HistoryManager", () => { }) describe("saveState" ,() => { - it("saves the state", () => { - historyManager.addProject(["/save/state"]) - historyManager.saveState() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') - expect(localStorage.items['history']).toContain('/save/state') - historyManager.loadState() - expect(historyManager.getProjects()[0].paths).toEqual(['/save/state']) + it("saves the state", async () => { + await historyManager.addProject(["/save/state"]) + await historyManager.saveState() + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) + await historyManager2.loadState() + expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) }) }) diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 4821dbc9b..862b7f428 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, 0o444) + fs.chmodSync(filePath, 0444) let logs = [] this.stub(console, 'log', (message) => logs.push(message)) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index c67a2d27c..798aa3766 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -5,11 +5,8 @@ describe "MenuManager", -> menu = null beforeEach -> - menu = new MenuManager( - resourcePath: atom.getLoadSettings().resourcePath - keymapManager: atom.keymaps - packageManager: atom.packages - ) + menu = new MenuManager({keymapManager: atom.keymaps, packageManager: atom.packages}) + menu.initialize({resourcePath: atom.getLoadSettings().resourcePath}) describe "::add(items)", -> it "can add new menus that can be removed with the returned disposable", -> diff --git a/spec/native-compile-cache-spec.coffee b/spec/native-compile-cache-spec.coffee index 1531deaf9..a43cbe815 100644 --- a/spec/native-compile-cache-spec.coffee +++ b/spec/native-compile-cache-spec.coffee @@ -9,16 +9,18 @@ describe "NativeCompileCache", -> beforeEach -> cachedFiles = [] fakeCacheStore = jasmine.createSpyObj("cache store", ["set", "get", "has", "delete"]) - fakeCacheStore.has.andCallFake (cacheKey, invalidationKey) -> - fakeCacheStore.get(cacheKey, invalidationKey)? - fakeCacheStore.get.andCallFake (cacheKey, invalidationKey) -> + + fakeCacheStore.has.andCallFake (cacheKey) -> + fakeCacheStore.get(cacheKey)? + + fakeCacheStore.get.andCallFake (cacheKey) -> for entry in cachedFiles by -1 continue if entry.cacheKey isnt cacheKey - continue if entry.invalidationKey isnt invalidationKey return entry.cacheBuffer return - fakeCacheStore.set.andCallFake (cacheKey, invalidationKey, cacheBuffer) -> - cachedFiles.push({cacheKey, invalidationKey, cacheBuffer}) + + fakeCacheStore.set.andCallFake (cacheKey, cacheBuffer) -> + cachedFiles.push({cacheKey, cacheBuffer}) nativeCompileCache.setCacheStore(fakeCacheStore) nativeCompileCache.setV8Version("a-v8-version") @@ -29,13 +31,10 @@ describe "NativeCompileCache", -> fn2 = require('./fixtures/native-cache/file-2') expect(cachedFiles.length).toBe(2) - - expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-1')) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn1()).toBe(1) - expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-2')) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) expect(fn2()).toBe(2) @@ -51,7 +50,6 @@ describe "NativeCompileCache", -> fn4 = require('./fixtures/native-cache/file-4') expect(cachedFiles.length).toBe(1) - expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-4')) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn4()).toBe("file-4") @@ -61,8 +59,6 @@ describe "NativeCompileCache", -> fn4 = require('./fixtures/native-cache/file-4') expect(cachedFiles.length).toBe(2) - expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-4')) - expect(cachedFiles[1].invalidationKey).not.toBe(cachedFiles[0].invalidationKey) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) @@ -79,7 +75,6 @@ describe "NativeCompileCache", -> fn5 = require('./fixtures/native-cache/file-5') expect(cachedFiles.length).toBe(1) - expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-5')) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn5()).toBe("file-5") @@ -89,8 +84,6 @@ describe "NativeCompileCache", -> fn5 = require('./fixtures/native-cache/file-5') expect(cachedFiles.length).toBe(2) - expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-5')) - expect(cachedFiles[1].invalidationKey).not.toBe(cachedFiles[0].invalidationKey) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) @@ -100,5 +93,5 @@ describe "NativeCompileCache", -> fn3 = require('./fixtures/native-cache/file-3') - expect(fakeCacheStore.delete).toHaveBeenCalledWith(require.resolve('./fixtures/native-cache/file-3')) + expect(fakeCacheStore.delete).toHaveBeenCalled() expect(fn3()).toBe(3) diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index 84c6c4fc9..07b9f4763 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -23,6 +23,7 @@ describe "PaneContainer", -> serialize: -> deserializer: 'Item' containerA = new PaneContainer(params) + containerA.initialize() pane1A = containerA.getActivePane() pane1A.addItem(new Item) pane2A = pane1A.splitRight(items: [new Item]) @@ -33,6 +34,7 @@ describe "PaneContainer", -> expect(pane3A.focused).toBe true containerB = new PaneContainer(params) + containerB.initialize() containerB.deserialize(containerA.serialize(), atom.deserializers) [pane1B, pane2B, pane3B] = containerB.getPanes() expect(pane3B.focused).toBe true @@ -42,6 +44,7 @@ describe "PaneContainer", -> expect(containerA.getActivePane()).toBe pane3A containerB = new PaneContainer(params) + containerB.initialize() containerB.deserialize(containerA.serialize(), atom.deserializers) [pane1B, pane2B, pane3B] = containerB.getPanes() expect(containerB.getActivePane()).toBe pane3B @@ -51,6 +54,7 @@ describe "PaneContainer", -> state = containerA.serialize() state.activePaneId = -22 containerB = new PaneContainer(params) + containerB.initialize() containerB.deserialize(state, atom.deserializers) expect(containerB.getActivePane()).toBe containerB.getPanes()[0] @@ -62,6 +66,7 @@ describe "PaneContainer", -> it "leaves the empty panes intact", -> state = containerA.serialize() containerB = new PaneContainer(params) + containerB.initialize() containerB.deserialize(state, atom.deserializers) [leftPane, column] = containerB.getRoot().getChildren() [topPane, bottomPane] = column.getChildren() @@ -76,6 +81,7 @@ describe "PaneContainer", -> state = containerA.serialize() containerB = new PaneContainer(params) + containerB.initialize() containerB.deserialize(state, atom.deserializers) [leftPane, rightPane] = containerB.getRoot().getChildren() @@ -84,6 +90,7 @@ describe "PaneContainer", -> it "does not allow the root pane to be destroyed", -> container = new PaneContainer(params) + container.initialize() container.getRoot().destroy() expect(container.getRoot()).toBeDefined() expect(container.getRoot().isDestroyed()).toBe false @@ -93,6 +100,7 @@ describe "PaneContainer", -> beforeEach -> container = new PaneContainer(params) + container.initialize() pane1 = container.getRoot() it "returns the first pane if no pane has been made active", -> @@ -122,6 +130,7 @@ describe "PaneContainer", -> beforeEach -> container = new PaneContainer(params) + container.initialize() container.getRoot().addItems([new Object, new Object]) container.getRoot().splitRight(items: [new Object, new Object]) [pane1, pane2] = container.getPanes() @@ -144,6 +153,7 @@ describe "PaneContainer", -> beforeEach -> container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container.initialize() container.getRoot().splitRight(items: [new Object, new Object]) [pane1, pane2] = container.getPanes() @@ -165,6 +175,7 @@ describe "PaneContainer", -> describe "::observePanes()", -> it "invokes observers with all current and future panes", -> container = new PaneContainer(params) + container.initialize() container.getRoot().splitRight() [pane1, pane2] = container.getPanes() @@ -179,6 +190,7 @@ describe "PaneContainer", -> describe "::observePaneItems()", -> it "invokes observers with all current and future pane items", -> container = new PaneContainer(params) + container.initialize() container.getRoot().addItems([new Object, new Object]) container.getRoot().splitRight(items: [new Object]) [pane1, pane2] = container.getPanes() @@ -199,6 +211,7 @@ describe "PaneContainer", -> getURI: -> 'test' container = new PaneContainer(params) + container.initialize() container.getRoot().splitRight() [pane1, pane2] = container.getPanes() pane1.addItem(new TestItem) @@ -219,6 +232,7 @@ describe "PaneContainer", -> describe "::onDidAddPane(callback)", -> it "invokes the given callback when panes are added", -> container = new PaneContainer(params) + container.initialize() events = [] container.onDidAddPane (event) -> expect(event.pane in container.getPanes()).toBe true @@ -238,6 +252,7 @@ describe "PaneContainer", -> isDestroyed: -> @_isDestroyed container = new PaneContainer(params) + container.initialize() events = [] container.onWillDestroyPane (event) -> itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems()) @@ -254,6 +269,7 @@ describe "PaneContainer", -> describe "::onDidDestroyPane(callback)", -> it "invokes the given callback when panes are destroyed", -> container = new PaneContainer(params) + container.initialize() events = [] container.onDidDestroyPane (event) -> expect(event.pane in container.getPanes()).toBe false @@ -270,6 +286,7 @@ describe "PaneContainer", -> it "invokes the given callback when the container is destroyed", -> container = new PaneContainer(params) + container.initialize() events = [] container.onDidDestroyPane (event) -> expect(event.pane in container.getPanes()).toBe false @@ -286,6 +303,7 @@ describe "PaneContainer", -> describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", -> it "invokes the given callbacks when an item will be destroyed on any pane", -> container = new PaneContainer(params) + container.initialize() pane1 = container.getRoot() item1 = new Object item2 = new Object @@ -313,6 +331,7 @@ describe "PaneContainer", -> describe "::saveAll()", -> it "saves all modified pane items", -> container = new PaneContainer(params) + container.initialize() pane1 = container.getRoot() pane2 = pane1.splitRight() @@ -354,6 +373,7 @@ describe "PaneContainer", -> copy: -> new TestItem(@id) container = new PaneContainer(params) + container.initialize() pane1 = container.getRoot() item1 = new TestItem('1') pane2 = pane1.splitRight(items: [item1]) diff --git a/spec/panel-container-element-spec.coffee b/spec/panel-container-element-spec.coffee deleted file mode 100644 index 55e6f7133..000000000 --- a/spec/panel-container-element-spec.coffee +++ /dev/null @@ -1,137 +0,0 @@ -Panel = require '../src/panel' -PanelContainer = require '../src/panel-container' - -describe "PanelContainerElement", -> - [jasmineContent, element, container] = [] - - class TestPanelContainerItem - constructior: -> - - class TestPanelContainerItemElement extends HTMLElement - createdCallback: -> - @classList.add('test-root') - initialize: (@model) -> - this - - TestPanelContainerItemElement = document.registerElement 'atom-test-container-item-element', prototype: TestPanelContainerItemElement.prototype - - beforeEach -> - jasmineContent = document.body.querySelector('#jasmine-content') - - atom.views.addViewProvider TestPanelContainerItem, (model) -> - new TestPanelContainerItemElement().initialize(model) - - container = new PanelContainer({location: 'left'}) - element = atom.views.getView(container) - jasmineContent.appendChild(element) - - it 'has a location class with value from the model', -> - expect(element).toHaveClass 'left' - - it 'removes the element when the container is destroyed', -> - expect(element.parentNode).toBe jasmineContent - container.destroy() - expect(element.parentNode).not.toBe jasmineContent - - describe "adding and removing panels", -> - it "allows panels to be inserted at any position", -> - panel1 = new Panel({item: new TestPanelContainerItem(), priority: 10}) - panel2 = new Panel({item: new TestPanelContainerItem(), priority: 5}) - panel3 = new Panel({item: new TestPanelContainerItem(), priority: 8}) - - container.addPanel(panel1) - container.addPanel(panel2) - container.addPanel(panel3) - - expect(element.childNodes[2].getModel()).toBe(panel1) - expect(element.childNodes[1].getModel()).toBe(panel3) - expect(element.childNodes[0].getModel()).toBe(panel2) - - describe "when the container is at the left location", -> - it "adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed", -> - expect(element.childNodes.length).toBe 0 - - panel1 = new Panel({item: new TestPanelContainerItem()}) - container.addPanel(panel1) - expect(element.childNodes.length).toBe 1 - expect(element.childNodes[0]).toHaveClass 'left' - expect(element.childNodes[0]).toHaveClass 'tool-panel' # legacy selector support - expect(element.childNodes[0]).toHaveClass 'panel-left' # legacy selector support - - expect(element.childNodes[0].tagName).toBe 'ATOM-PANEL' - - panel2 = new Panel({item: new TestPanelContainerItem()}) - container.addPanel(panel2) - expect(element.childNodes.length).toBe 2 - - expect(atom.views.getView(panel1).style.display).not.toBe 'none' - expect(atom.views.getView(panel2).style.display).not.toBe 'none' - - panel1.destroy() - expect(element.childNodes.length).toBe 1 - - panel2.destroy() - expect(element.childNodes.length).toBe 0 - - describe "when the container is at the bottom location", -> - beforeEach -> - container = new PanelContainer({location: 'bottom'}) - element = atom.views.getView(container) - jasmineContent.appendChild(element) - - it "adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed", -> - expect(element.childNodes.length).toBe 0 - - panel1 = new Panel({item: new TestPanelContainerItem(), className: 'one'}) - container.addPanel(panel1) - expect(element.childNodes.length).toBe 1 - expect(element.childNodes[0]).toHaveClass 'bottom' - expect(element.childNodes[0]).toHaveClass 'tool-panel' # legacy selector support - expect(element.childNodes[0]).toHaveClass 'panel-bottom' # legacy selector support - expect(element.childNodes[0].tagName).toBe 'ATOM-PANEL' - expect(atom.views.getView(panel1)).toHaveClass 'one' - - panel2 = new Panel({item: new TestPanelContainerItem(), className: 'two'}) - container.addPanel(panel2) - expect(element.childNodes.length).toBe 2 - expect(atom.views.getView(panel2)).toHaveClass 'two' - - panel1.destroy() - expect(element.childNodes.length).toBe 1 - - panel2.destroy() - expect(element.childNodes.length).toBe 0 - - describe "when the container is modal", -> - beforeEach -> - container = new PanelContainer({location: 'modal'}) - element = atom.views.getView(container) - jasmineContent.appendChild(element) - - it "allows only one panel to be visible at a time", -> - panel1 = new Panel({item: new TestPanelContainerItem()}) - container.addPanel(panel1) - - expect(atom.views.getView(panel1).style.display).not.toBe 'none' - - panel2 = new Panel({item: new TestPanelContainerItem()}) - container.addPanel(panel2) - - expect(atom.views.getView(panel1).style.display).toBe 'none' - expect(atom.views.getView(panel2).style.display).not.toBe 'none' - - panel1.show() - - expect(atom.views.getView(panel1).style.display).not.toBe 'none' - expect(atom.views.getView(panel2).style.display).toBe 'none' - - it "adds the 'modal' class to panels", -> - panel1 = new Panel({item: new TestPanelContainerItem()}) - container.addPanel(panel1) - - expect(atom.views.getView(panel1)).toHaveClass 'modal' - - # legacy selector support - expect(atom.views.getView(panel1)).not.toHaveClass 'tool-panel' - expect(atom.views.getView(panel1)).toHaveClass 'overlay' - expect(atom.views.getView(panel1)).toHaveClass 'from-top' diff --git a/spec/panel-container-element-spec.js b/spec/panel-container-element-spec.js new file mode 100644 index 000000000..23da47663 --- /dev/null +++ b/spec/panel-container-element-spec.js @@ -0,0 +1,165 @@ +'use strict' + +/* global HTMLElement */ + +const Panel = require('../src/panel') +const PanelContainer = require('../src/panel-container') + +describe('PanelContainerElement', () => { + let jasmineContent, element, container + + class TestPanelContainerItem { + } + + class TestPanelContainerItemElement_ extends HTMLElement { + createdCallback () { + this.classList.add('test-root') + } + initialize (model) { + this.model = model + return this + } + } + + const TestPanelContainerItemElement = document.registerElement( + 'atom-test-container-item-element', + {prototype: TestPanelContainerItemElement_.prototype} + ) + + beforeEach(() => { + jasmineContent = document.body.querySelector('#jasmine-content') + + atom.views.addViewProvider( + TestPanelContainerItem, + model => new TestPanelContainerItemElement().initialize(model) + ) + + container = new PanelContainer({location: 'left'}) + element = atom.views.getView(container) + jasmineContent.appendChild(element) + }) + + it('has a location class with value from the model', () => { + expect(element).toHaveClass('left') + }) + + it('removes the element when the container is destroyed', () => { + expect(element.parentNode).toBe(jasmineContent) + container.destroy() + expect(element.parentNode).not.toBe(jasmineContent) + }) + + describe('adding and removing panels', () => { + it('allows panels to be inserted at any position', () => { + const panel1 = new Panel({item: new TestPanelContainerItem(), priority: 10}) + const panel2 = new Panel({item: new TestPanelContainerItem(), priority: 5}) + const panel3 = new Panel({item: new TestPanelContainerItem(), priority: 8}) + + container.addPanel(panel1) + container.addPanel(panel2) + container.addPanel(panel3) + + expect(element.childNodes[2].getModel()).toBe(panel1) + expect(element.childNodes[1].getModel()).toBe(panel3) + expect(element.childNodes[0].getModel()).toBe(panel2) + }) + + describe('when the container is at the left location', () => + it('adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed', () => { + expect(element.childNodes.length).toBe(0) + + const panel1 = new Panel({item: new TestPanelContainerItem()}) + container.addPanel(panel1) + expect(element.childNodes.length).toBe(1) + expect(element.childNodes[0]).toHaveClass('left') + expect(element.childNodes[0]).toHaveClass('tool-panel') // legacy selector support + expect(element.childNodes[0]).toHaveClass('panel-left') // legacy selector support + + expect(element.childNodes[0].tagName).toBe('ATOM-PANEL') + + const panel2 = new Panel({item: new TestPanelContainerItem()}) + container.addPanel(panel2) + expect(element.childNodes.length).toBe(2) + + expect(atom.views.getView(panel1).style.display).not.toBe('none') + expect(atom.views.getView(panel2).style.display).not.toBe('none') + + panel1.destroy() + expect(element.childNodes.length).toBe(1) + + panel2.destroy() + expect(element.childNodes.length).toBe(0) + }) + ) + + describe('when the container is at the bottom location', () => { + beforeEach(() => { + container = new PanelContainer({location: 'bottom'}) + element = atom.views.getView(container) + jasmineContent.appendChild(element) + }) + + it('adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed', () => { + expect(element.childNodes.length).toBe(0) + + const panel1 = new Panel({item: new TestPanelContainerItem(), className: 'one'}) + container.addPanel(panel1) + expect(element.childNodes.length).toBe(1) + expect(element.childNodes[0]).toHaveClass('bottom') + expect(element.childNodes[0]).toHaveClass('tool-panel') // legacy selector support + expect(element.childNodes[0]).toHaveClass('panel-bottom') // legacy selector support + expect(element.childNodes[0].tagName).toBe('ATOM-PANEL') + expect(atom.views.getView(panel1)).toHaveClass('one') + + const panel2 = new Panel({item: new TestPanelContainerItem(), className: 'two'}) + container.addPanel(panel2) + expect(element.childNodes.length).toBe(2) + expect(atom.views.getView(panel2)).toHaveClass('two') + + panel1.destroy() + expect(element.childNodes.length).toBe(1) + + panel2.destroy() + expect(element.childNodes.length).toBe(0) + }) + }) + }) + + describe('when the container is modal', () => { + beforeEach(() => { + container = new PanelContainer({location: 'modal'}) + element = atom.views.getView(container) + jasmineContent.appendChild(element) + }) + + it('allows only one panel to be visible at a time', () => { + const panel1 = new Panel({item: new TestPanelContainerItem()}) + container.addPanel(panel1) + + expect(atom.views.getView(panel1).style.display).not.toBe('none') + + const panel2 = new Panel({item: new TestPanelContainerItem()}) + container.addPanel(panel2) + + expect(atom.views.getView(panel1).style.display).toBe('none') + expect(atom.views.getView(panel2).style.display).not.toBe('none') + + panel1.show() + + expect(atom.views.getView(panel1).style.display).not.toBe('none') + expect(atom.views.getView(panel2).style.display).toBe('none') + }) + + it("adds the 'modal' class to panels", () => { + const panel1 = new Panel({item: new TestPanelContainerItem()}) + container.addPanel(panel1) + + expect(atom.views.getView(panel1)).toHaveClass('modal') + + // legacy selector support + expect(atom.views.getView(panel1)).not.toHaveClass('tool-panel') + expect(atom.views.getView(panel1)).toHaveClass('overlay') + expect(atom.views.getView(panel1)).toHaveClass('from-top') + }) + }) +}) diff --git a/spec/panel-container-spec.coffee b/spec/panel-container-spec.coffee deleted file mode 100644 index 08eaea92b..000000000 --- a/spec/panel-container-spec.coffee +++ /dev/null @@ -1,96 +0,0 @@ -Panel = require '../src/panel' -PanelContainer = require '../src/panel-container' - -describe "PanelContainer", -> - [container] = [] - - class TestPanelItem - constructior: -> - - beforeEach -> - container = new PanelContainer - - describe "::addPanel(panel)", -> - it 'emits an onDidAddPanel event with the index the panel was inserted at', -> - container.onDidAddPanel addPanelSpy = jasmine.createSpy() - - panel1 = new Panel(item: new TestPanelItem()) - container.addPanel(panel1) - expect(addPanelSpy).toHaveBeenCalledWith({panel: panel1, index: 0}) - - panel2 = new Panel(item: new TestPanelItem()) - container.addPanel(panel2) - expect(addPanelSpy).toHaveBeenCalledWith({panel: panel2, index: 1}) - - describe "when a panel is destroyed", -> - it 'emits an onDidRemovePanel event with the index of the removed item', -> - container.onDidRemovePanel removePanelSpy = jasmine.createSpy() - - panel1 = new Panel(item: new TestPanelItem()) - container.addPanel(panel1) - panel2 = new Panel(item: new TestPanelItem()) - container.addPanel(panel2) - - expect(removePanelSpy).not.toHaveBeenCalled() - - panel2.destroy() - expect(removePanelSpy).toHaveBeenCalledWith({panel: panel2, index: 1}) - - panel1.destroy() - expect(removePanelSpy).toHaveBeenCalledWith({panel: panel1, index: 0}) - - describe "panel priority", -> - describe 'left / top panel container', -> - [initialPanel] = [] - beforeEach -> - # 'left' logic is the same as 'top' - container = new PanelContainer({location: 'left'}) - initialPanel = new Panel(item: new TestPanelItem()) - container.addPanel(initialPanel) - - describe 'when a panel with low priority is added', -> - it 'is inserted at the beginning of the list', -> - container.onDidAddPanel addPanelSpy = jasmine.createSpy() - panel = new Panel(item: new TestPanelItem(), priority: 0) - container.addPanel(panel) - - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - expect(container.getPanels()[0]).toBe panel - - describe 'when a panel with priority between two other panels is added', -> - it 'is inserted at the between the two panels', -> - panel = new Panel(item: new TestPanelItem(), priority: 1000) - container.addPanel(panel) - - container.onDidAddPanel addPanelSpy = jasmine.createSpy() - panel = new Panel(item: new TestPanelItem(), priority: 101) - container.addPanel(panel) - - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 1}) - expect(container.getPanels()[1]).toBe panel - - describe 'right / bottom panel container', -> - [initialPanel] = [] - beforeEach -> - # 'bottom' logic is the same as 'right' - container = new PanelContainer({location: 'right'}) - initialPanel = new Panel(item: new TestPanelItem()) - container.addPanel(initialPanel) - - describe 'when a panel with high priority is added', -> - it 'is inserted at the beginning of the list', -> - container.onDidAddPanel addPanelSpy = jasmine.createSpy() - panel = new Panel(item: new TestPanelItem(), priority: 1000) - container.addPanel(panel) - - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - expect(container.getPanels()[0]).toBe panel - - describe 'when a panel with low priority is added', -> - it 'is inserted at the end of the list', -> - container.onDidAddPanel addPanelSpy = jasmine.createSpy() - panel = new Panel(item: new TestPanelItem(), priority: 0) - container.addPanel(panel) - - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 1}) - expect(container.getPanels()[1]).toBe panel diff --git a/spec/panel-container-spec.js b/spec/panel-container-spec.js new file mode 100644 index 000000000..628904256 --- /dev/null +++ b/spec/panel-container-spec.js @@ -0,0 +1,142 @@ +'use strict' + +const Panel = require('../src/panel') +const PanelContainer = require('../src/panel-container') + +describe('PanelContainer', () => { + let container + + class TestPanelItem { + } + + beforeEach(() => { + container = new PanelContainer() + }) + + describe('::addPanel(panel)', () => { + it('emits an onDidAddPanel event with the index the panel was inserted at', () => { + const addPanelSpy = jasmine.createSpy() + container.onDidAddPanel(addPanelSpy) + + const panel1 = new Panel({item: new TestPanelItem()}) + container.addPanel(panel1) + expect(addPanelSpy).toHaveBeenCalledWith({panel: panel1, index: 0}) + + const panel2 = new Panel({item: new TestPanelItem()}) + container.addPanel(panel2) + expect(addPanelSpy).toHaveBeenCalledWith({panel: panel2, index: 1}) + }) + }) + + describe('when a panel is destroyed', () => { + it('emits an onDidRemovePanel event with the index of the removed item', () => { + const removePanelSpy = jasmine.createSpy() + container.onDidRemovePanel(removePanelSpy) + + const panel1 = new Panel({item: new TestPanelItem()}) + container.addPanel(panel1) + const panel2 = new Panel({item: new TestPanelItem()}) + container.addPanel(panel2) + + expect(removePanelSpy).not.toHaveBeenCalled() + + panel2.destroy() + expect(removePanelSpy).toHaveBeenCalledWith({panel: panel2, index: 1}) + + panel1.destroy() + expect(removePanelSpy).toHaveBeenCalledWith({panel: panel1, index: 0}) + }) + }) + + describe('::destroy()', () => { + it('destroys the container and all of its panels', () => { + const destroyedPanels = [] + + const panel1 = new Panel({item: new TestPanelItem()}) + panel1.onDidDestroy(() => { destroyedPanels.push(panel1) }) + container.addPanel(panel1) + + const panel2 = new Panel({item: new TestPanelItem()}) + panel2.onDidDestroy(() => { destroyedPanels.push(panel2) }) + container.addPanel(panel2) + + container.destroy() + + expect(container.getPanels().length).toBe(0) + expect(destroyedPanels).toEqual([panel1, panel2]) + }) + }) + + describe('panel priority', () => { + describe('left / top panel container', () => { + let initialPanel + beforeEach(() => { + // 'left' logic is the same as 'top' + container = new PanelContainer({location: 'left'}) + initialPanel = new Panel({item: new TestPanelItem()}) + container.addPanel(initialPanel) + }) + + describe('when a panel with low priority is added', () => { + it('is inserted at the beginning of the list', () => { + const addPanelSpy = jasmine.createSpy() + container.onDidAddPanel(addPanelSpy) + const panel = new Panel({item: new TestPanelItem(), priority: 0}) + container.addPanel(panel) + + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + expect(container.getPanels()[0]).toBe(panel) + }) + }) + + describe('when a panel with priority between two other panels is added', () => { + it('is inserted at the between the two panels', () => { + const addPanelSpy = jasmine.createSpy() + let panel = new Panel({item: new TestPanelItem(), priority: 1000}) + container.addPanel(panel) + + container.onDidAddPanel(addPanelSpy) + panel = new Panel({item: new TestPanelItem(), priority: 101}) + container.addPanel(panel) + + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 1}) + expect(container.getPanels()[1]).toBe(panel) + }) + }) + }) + + describe('right / bottom panel container', () => { + let initialPanel + beforeEach(() => { + // 'bottom' logic is the same as 'right' + container = new PanelContainer({location: 'right'}) + initialPanel = new Panel({item: new TestPanelItem()}) + container.addPanel(initialPanel) + }) + + describe('when a panel with high priority is added', () => { + it('is inserted at the beginning of the list', () => { + const addPanelSpy = jasmine.createSpy() + container.onDidAddPanel(addPanelSpy) + const panel = new Panel({item: new TestPanelItem(), priority: 1000}) + container.addPanel(panel) + + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + expect(container.getPanels()[0]).toBe(panel) + }) + }) + + describe('when a panel with low priority is added', () => { + it('is inserted at the end of the list', () => { + const addPanelSpy = jasmine.createSpy() + container.onDidAddPanel(addPanelSpy) + const panel = new Panel({item: new TestPanelItem(), priority: 0}) + container.addPanel(panel) + + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 1}) + expect(container.getPanels()[1]).toBe(panel) + }) + }) + }) + }) +}) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 911270d16..3cb771ec7 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -28,7 +28,13 @@ describe "TextEditor", -> editor.foldBufferRow(4) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - editor2 = TextEditor.deserialize(editor.serialize(), atom) + editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: { + bufferForIdSync: (id) -> TextBuffer.deserialize(editor.buffer.serialize()) + } + }) expect(editor2.id).toBe editor.id expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() @@ -4862,8 +4868,8 @@ describe "TextEditor", -> editor.replaceSelectedText {}, -> '123' expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' editor.setCursorBufferPosition([0]) + editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' editor.setCursorBufferPosition([10]) @@ -4876,6 +4882,12 @@ describe "TextEditor", -> editor.replaceSelectedText {}, -> 'ia' expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' + it "replaces the selected text and selects the replacement text", -> + editor.setSelectedBufferRange([[0, 4], [0, 9]]) + editor.replaceSelectedText {}, -> 'whatnot' + expect(buffer.lineForRow(0)).toBe 'var whatnotsort = function () {' + expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [0, 11]] + describe ".transpose()", -> it "swaps two characters", -> editor.buffer.setText("abc") @@ -4896,7 +4908,7 @@ describe "TextEditor", -> editor.setCursorScreenPosition([0, 1]) editor.upperCase() expect(editor.lineTextForBufferRow(0)).toBe 'ABC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] describe "when there is a selection", -> it "upper cases the current selection", -> @@ -4913,7 +4925,7 @@ describe "TextEditor", -> editor.setCursorScreenPosition([0, 1]) editor.lowerCase() expect(editor.lineTextForBufferRow(0)).toBe 'abc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 1]] + expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] describe "when there is a selection", -> it "lower cases the current selection", -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 795f1b43e..7c43ac2f3 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -194,10 +194,10 @@ describe "atom.themes", -> expect(element.getAttribute('source-path')).toEqualPath lessPath expect(element.textContent).toBe """ #header { - color: #4d926f; + color: #4D926F; } h2 { - color: #4d926f; + color: #4D926F; } """ diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index 68a482b48..d805d17cb 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -5,6 +5,7 @@ describe "ViewRegistry", -> beforeEach -> registry = new ViewRegistry + registry.initialize() afterEach -> registry.clearDocumentRequests() diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index e9a7894c3..d4387f23a 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -15,7 +15,8 @@ describe "WindowEventHandler", -> loadSettings.initialPath = initialPath loadSettings atom.project.destroy() - windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate, window, document}) + windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate}) + windowEventHandler.initialize(window, document) afterEach -> windowEventHandler.unsubscribe() diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee deleted file mode 100644 index a741dbbd4..000000000 --- a/spec/workspace-element-spec.coffee +++ /dev/null @@ -1,210 +0,0 @@ -{ipcRenderer} = require 'electron' -path = require 'path' -temp = require('temp').track() -{Disposable} = require 'event-kit' - -describe "WorkspaceElement", -> - afterEach -> - temp.cleanupSync() - - describe "when the workspace element is focused", -> - it "transfers focus to the active pane", -> - workspaceElement = atom.views.getView(atom.workspace) - jasmine.attachToDOM(workspaceElement) - activePaneElement = atom.views.getView(atom.workspace.getActivePane()) - document.body.focus() - expect(document.activeElement).not.toBe(activePaneElement) - workspaceElement.focus() - expect(document.activeElement).toBe(activePaneElement) - - describe "the scrollbar visibility class", -> - it "has a class based on the style of the scrollbar", -> - observeCallback = null - scrollbarStyle = require 'scrollbar-style' - spyOn(scrollbarStyle, 'observePreferredScrollbarStyle').andCallFake (cb) -> - observeCallback = cb - new Disposable(->) - - workspaceElement = atom.views.getView(atom.workspace) - observeCallback('legacy') - expect(workspaceElement.className).toMatch 'scrollbars-visible-always' - - observeCallback('overlay') - expect(workspaceElement).toHaveClass 'scrollbars-visible-when-scrolling' - - describe "editor font styling", -> - [editor, editorElement, workspaceElement] = [] - - beforeEach -> - waitsForPromise -> atom.workspace.open('sample.js') - - runs -> - workspaceElement = atom.views.getView(atom.workspace) - jasmine.attachToDOM(workspaceElement) - editor = atom.workspace.getActiveTextEditor() - editorElement = atom.views.getView(editor) - - it "updates the font-size based on the 'editor.fontSize' config value", -> - initialCharWidth = editor.getDefaultCharWidth() - expect(getComputedStyle(editorElement).fontSize).toBe atom.config.get('editor.fontSize') + 'px' - atom.config.set('editor.fontSize', atom.config.get('editor.fontSize') + 5) - expect(getComputedStyle(editorElement).fontSize).toBe atom.config.get('editor.fontSize') + 'px' - expect(editor.getDefaultCharWidth()).toBeGreaterThan initialCharWidth - - it "updates the font-family based on the 'editor.fontFamily' config value", -> - initialCharWidth = editor.getDefaultCharWidth() - fontFamily = atom.config.get('editor.fontFamily') - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' - expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily - - atom.config.set('editor.fontFamily', 'sans-serif') - fontFamily = atom.config.get('editor.fontFamily') - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' - expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily - expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth - - it "updates the line-height based on the 'editor.lineHeight' config value", -> - initialLineHeight = editor.getLineHeightInPixels() - atom.config.set('editor.lineHeight', '30px') - expect(getComputedStyle(editorElement).lineHeight).toBe atom.config.get('editor.lineHeight') - expect(editor.getLineHeightInPixels()).not.toBe initialLineHeight - - it "increases or decreases the font size when a ctrl-mousewheel event occurs", -> - atom.config.set('editor.zoomFontWhenCtrlScrolling', true) - atom.config.set('editor.fontSize', 12) - - # Zoom out - editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaY: -10, - ctrlKey: true - })) - expect(atom.config.get('editor.fontSize')).toBe(11) - - # Zoom in - editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaY: 10, - ctrlKey: true - })) - expect(atom.config.get('editor.fontSize')).toBe(12) - - # Not on an atom-text-editor - workspaceElement.dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaY: 10, - ctrlKey: true - })) - expect(atom.config.get('editor.fontSize')).toBe(12) - - # No ctrl key - editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaY: 10, - })) - expect(atom.config.get('editor.fontSize')).toBe(12) - - atom.config.set('editor.zoomFontWhenCtrlScrolling', false) - editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { - wheelDeltaY: 10, - ctrlKey: true - })) - expect(atom.config.get('editor.fontSize')).toBe(12) - - describe 'panel containers', -> - it 'inserts panel container elements in the correct places in the DOM', -> - workspaceElement = atom.views.getView(atom.workspace) - - leftContainer = workspaceElement.querySelector('atom-panel-container.left') - rightContainer = workspaceElement.querySelector('atom-panel-container.right') - expect(leftContainer.nextSibling).toBe workspaceElement.verticalAxis - expect(rightContainer.previousSibling).toBe workspaceElement.verticalAxis - - topContainer = workspaceElement.querySelector('atom-panel-container.top') - bottomContainer = workspaceElement.querySelector('atom-panel-container.bottom') - expect(topContainer.nextSibling).toBe workspaceElement.paneContainer - expect(bottomContainer.previousSibling).toBe workspaceElement.paneContainer - - headerContainer = workspaceElement.querySelector('atom-panel-container.header') - footerContainer = workspaceElement.querySelector('atom-panel-container.footer') - expect(headerContainer.nextSibling).toBe workspaceElement.horizontalAxis - expect(footerContainer.previousSibling).toBe workspaceElement.horizontalAxis - - modalContainer = workspaceElement.querySelector('atom-panel-container.modal') - expect(modalContainer.parentNode).toBe workspaceElement - - it 'stretches header/footer panels to the workspace width', -> - workspaceElement = atom.views.getView(atom.workspace) - jasmine.attachToDOM(workspaceElement) - expect(workspaceElement.offsetWidth).toBeGreaterThan(0) - - headerItem = document.createElement('div') - atom.workspace.addHeaderPanel({item: headerItem}) - expect(headerItem.offsetWidth).toEqual(workspaceElement.offsetWidth) - - footerItem = document.createElement('div') - atom.workspace.addFooterPanel({item: footerItem}) - expect(footerItem.offsetWidth).toEqual(workspaceElement.offsetWidth) - - it 'shrinks horizontal axis according to header/footer panels height', -> - workspaceElement = atom.views.getView(atom.workspace) - workspaceElement.style.height = '100px' - horizontalAxisElement = workspaceElement.querySelector('atom-workspace-axis.horizontal') - jasmine.attachToDOM(workspaceElement) - - originalHorizontalAxisHeight = horizontalAxisElement.offsetHeight - expect(workspaceElement.offsetHeight).toBeGreaterThan(0) - expect(originalHorizontalAxisHeight).toBeGreaterThan(0) - - headerItem = document.createElement('div') - headerItem.style.height = '10px' - atom.workspace.addHeaderPanel({item: headerItem}) - expect(headerItem.offsetHeight).toBeGreaterThan(0) - - footerItem = document.createElement('div') - footerItem.style.height = '15px' - atom.workspace.addFooterPanel({item: footerItem}) - expect(footerItem.offsetHeight).toBeGreaterThan(0) - - expect(horizontalAxisElement.offsetHeight).toEqual(originalHorizontalAxisHeight - headerItem.offsetHeight - footerItem.offsetHeight) - - describe "the 'window:toggle-invisibles' command", -> - it "shows/hides invisibles in all open and future editors", -> - workspaceElement = atom.views.getView(atom.workspace) - expect(atom.config.get('editor.showInvisibles')).toBe false - atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles') - expect(atom.config.get('editor.showInvisibles')).toBe true - atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles') - expect(atom.config.get('editor.showInvisibles')).toBe false - - describe "the 'window:run-package-specs' command", -> - it "runs the package specs for the active item's project path, or the first project path", -> - workspaceElement = atom.views.getView(atom.workspace) - spyOn(ipcRenderer, 'send') - - # No project paths. Don't try to run specs. - atom.commands.dispatch(workspaceElement, "window:run-package-specs") - expect(ipcRenderer.send).not.toHaveBeenCalledWith("run-package-specs") - - projectPaths = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")] - atom.project.setPaths(projectPaths) - - # No active item. Use first project directory. - atom.commands.dispatch(workspaceElement, "window:run-package-specs") - expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) - ipcRenderer.send.reset() - - # Active item doesn't implement ::getPath(). Use first project directory. - item = document.createElement("div") - atom.workspace.getActivePane().activateItem(item) - atom.commands.dispatch(workspaceElement, "window:run-package-specs") - expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) - ipcRenderer.send.reset() - - # Active item has no path. Use first project directory. - item.getPath = -> null - atom.commands.dispatch(workspaceElement, "window:run-package-specs") - expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec")) - ipcRenderer.send.reset() - - # Active item has path. Use project path for item path. - item.getPath = -> path.join(projectPaths[1], "a-file.txt") - atom.commands.dispatch(workspaceElement, "window:run-package-specs") - expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec")) - ipcRenderer.send.reset() diff --git a/spec/workspace-element-spec.js b/spec/workspace-element-spec.js new file mode 100644 index 000000000..a7f6957a0 --- /dev/null +++ b/spec/workspace-element-spec.js @@ -0,0 +1,232 @@ +'use strict' + +/* global getComputedStyle, WheelEvent */ + +const {ipcRenderer} = require('electron') +const path = require('path') +const temp = require('temp').track() +const {Disposable} = require('event-kit') + +describe('WorkspaceElement', () => { + afterEach(() => { temp.cleanupSync() }) + + describe('when the workspace element is focused', () => { + it('transfers focus to the active pane', () => { + const workspaceElement = atom.views.getView(atom.workspace) + jasmine.attachToDOM(workspaceElement) + const activePaneElement = atom.views.getView(atom.workspace.getActivePane()) + document.body.focus() + expect(document.activeElement).not.toBe(activePaneElement) + workspaceElement.focus() + expect(document.activeElement).toBe(activePaneElement) + }) + }) + + describe('the scrollbar visibility class', () => { + it('has a class based on the style of the scrollbar', () => { + let observeCallback + const scrollbarStyle = require('scrollbar-style') + spyOn(scrollbarStyle, 'observePreferredScrollbarStyle').andCallFake(cb => { + observeCallback = cb + return new Disposable(() => {}) + }) + + const workspaceElement = atom.views.getView(atom.workspace) + observeCallback('legacy') + expect(workspaceElement.className).toMatch('scrollbars-visible-always') + + observeCallback('overlay') + expect(workspaceElement).toHaveClass('scrollbars-visible-when-scrolling') + }) + }) + + describe('editor font styling', () => { + let editor, editorElement, workspaceElement + + beforeEach(() => { + waitsForPromise(() => atom.workspace.open('sample.js')) + + runs(() => { + workspaceElement = atom.views.getView(atom.workspace) + jasmine.attachToDOM(workspaceElement) + editor = atom.workspace.getActiveTextEditor() + editorElement = atom.views.getView(editor) + }) + }) + + it("updates the font-size based on the 'editor.fontSize' config value", () => { + const initialCharWidth = editor.getDefaultCharWidth() + expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px') + atom.config.set('editor.fontSize', atom.config.get('editor.fontSize') + 5) + expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px') + expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialCharWidth) + }) + + it("updates the font-family based on the 'editor.fontFamily' config value", () => { + const initialCharWidth = editor.getDefaultCharWidth() + let fontFamily = atom.config.get('editor.fontFamily') + expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily) + + atom.config.set('editor.fontFamily', 'sans-serif') + fontFamily = atom.config.get('editor.fontFamily') + expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily) + expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) + }) + + it("updates the line-height based on the 'editor.lineHeight' config value", () => { + const initialLineHeight = editor.getLineHeightInPixels() + atom.config.set('editor.lineHeight', '30px') + expect(getComputedStyle(editorElement).lineHeight).toBe(atom.config.get('editor.lineHeight')) + expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeight) + }) + + it('increases or decreases the font size when a ctrl-mousewheel event occurs', () => { + atom.config.set('editor.zoomFontWhenCtrlScrolling', true) + atom.config.set('editor.fontSize', 12) + + // Zoom out + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaY: -10, + ctrlKey: true + })) + expect(atom.config.get('editor.fontSize')).toBe(11) + + // Zoom in + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaY: 10, + ctrlKey: true + })) + expect(atom.config.get('editor.fontSize')).toBe(12) + + // Not on an atom-text-editor + workspaceElement.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaY: 10, + ctrlKey: true + })) + expect(atom.config.get('editor.fontSize')).toBe(12) + + // No ctrl key + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaY: 10 + })) + expect(atom.config.get('editor.fontSize')).toBe(12) + + atom.config.set('editor.zoomFontWhenCtrlScrolling', false) + editorElement.querySelector('span').dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaY: 10, + ctrlKey: true + })) + expect(atom.config.get('editor.fontSize')).toBe(12) + }) + }) + + describe('panel containers', () => { + it('inserts panel container elements in the correct places in the DOM', () => { + const workspaceElement = atom.views.getView(atom.workspace) + + const leftContainer = workspaceElement.querySelector('atom-panel-container.left') + const rightContainer = workspaceElement.querySelector('atom-panel-container.right') + expect(leftContainer.nextSibling).toBe(workspaceElement.verticalAxis) + expect(rightContainer.previousSibling).toBe(workspaceElement.verticalAxis) + + const topContainer = workspaceElement.querySelector('atom-panel-container.top') + const bottomContainer = workspaceElement.querySelector('atom-panel-container.bottom') + expect(topContainer.nextSibling).toBe(workspaceElement.paneContainer) + expect(bottomContainer.previousSibling).toBe(workspaceElement.paneContainer) + + const headerContainer = workspaceElement.querySelector('atom-panel-container.header') + const footerContainer = workspaceElement.querySelector('atom-panel-container.footer') + expect(headerContainer.nextSibling).toBe(workspaceElement.horizontalAxis) + expect(footerContainer.previousSibling).toBe(workspaceElement.horizontalAxis) + + const modalContainer = workspaceElement.querySelector('atom-panel-container.modal') + expect(modalContainer.parentNode).toBe(workspaceElement) + }) + + it('stretches header/footer panels to the workspace width', () => { + const workspaceElement = atom.views.getView(atom.workspace) + jasmine.attachToDOM(workspaceElement) + expect(workspaceElement.offsetWidth).toBeGreaterThan(0) + + const headerItem = document.createElement('div') + atom.workspace.addHeaderPanel({item: headerItem}) + expect(headerItem.offsetWidth).toEqual(workspaceElement.offsetWidth) + + const footerItem = document.createElement('div') + atom.workspace.addFooterPanel({item: footerItem}) + expect(footerItem.offsetWidth).toEqual(workspaceElement.offsetWidth) + }) + + it('shrinks horizontal axis according to header/footer panels height', () => { + const workspaceElement = atom.views.getView(atom.workspace) + workspaceElement.style.height = '100px' + const horizontalAxisElement = workspaceElement.querySelector('atom-workspace-axis.horizontal') + jasmine.attachToDOM(workspaceElement) + + const originalHorizontalAxisHeight = horizontalAxisElement.offsetHeight + expect(workspaceElement.offsetHeight).toBeGreaterThan(0) + expect(originalHorizontalAxisHeight).toBeGreaterThan(0) + + const headerItem = document.createElement('div') + headerItem.style.height = '10px' + atom.workspace.addHeaderPanel({item: headerItem}) + expect(headerItem.offsetHeight).toBeGreaterThan(0) + + const footerItem = document.createElement('div') + footerItem.style.height = '15px' + atom.workspace.addFooterPanel({item: footerItem}) + expect(footerItem.offsetHeight).toBeGreaterThan(0) + + expect(horizontalAxisElement.offsetHeight).toEqual(originalHorizontalAxisHeight - headerItem.offsetHeight - footerItem.offsetHeight) + }) + }) + + describe("the 'window:toggle-invisibles' command", () => { + it('shows/hides invisibles in all open and future editors', () => { + const workspaceElement = atom.views.getView(atom.workspace) + expect(atom.config.get('editor.showInvisibles')).toBe(false) + atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles') + expect(atom.config.get('editor.showInvisibles')).toBe(true) + atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles') + expect(atom.config.get('editor.showInvisibles')).toBe(false) + }) + }) + + describe("the 'window:run-package-specs' command", () => { + it("runs the package specs for the active item's project path, or the first project path", () => { + const workspaceElement = atom.views.getView(atom.workspace) + spyOn(ipcRenderer, 'send') + + // No project paths. Don't try to run specs. + atom.commands.dispatch(workspaceElement, 'window:run-package-specs') + expect(ipcRenderer.send).not.toHaveBeenCalledWith('run-package-specs') + + const projectPaths = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')] + atom.project.setPaths(projectPaths) + + // No active item. Use first project directory. + atom.commands.dispatch(workspaceElement, 'window:run-package-specs') + expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec')) + ipcRenderer.send.reset() + + // Active item doesn't implement ::getPath(). Use first project directory. + const item = document.createElement('div') + atom.workspace.getActivePane().activateItem(item) + atom.commands.dispatch(workspaceElement, 'window:run-package-specs') + expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec')) + ipcRenderer.send.reset() + + // Active item has no path. Use first project directory. + item.getPath = () => null + atom.commands.dispatch(workspaceElement, 'window:run-package-specs') + expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[0], 'spec')) + ipcRenderer.send.reset() + + // Active item has path. Use project path for item path. + item.getPath = () => path.join(projectPaths[1], 'a-file.txt') + atom.commands.dispatch(workspaceElement, 'window:run-package-specs') + expect(ipcRenderer.send).toHaveBeenCalledWith('run-package-specs', path.join(projectPaths[1], 'spec')) + ipcRenderer.send.reset() + }) + }) +}) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee deleted file mode 100644 index 153cc5dc3..000000000 --- a/spec/workspace-spec.coffee +++ /dev/null @@ -1,1778 +0,0 @@ -path = require 'path' -temp = require('temp').track() -TextEditor = require '../src/text-editor' -Workspace = require '../src/workspace' -Project = require '../src/project' -platform = require './spec-helper-platform' -_ = require 'underscore-plus' -fstream = require 'fstream' -fs = require 'fs-plus' - -describe "Workspace", -> - [workspace, setDocumentEdited] = [] - - beforeEach -> - workspace = atom.workspace - workspace.resetFontSize() - spyOn(atom.applicationDelegate, "confirm") - setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) - waits(1) - - afterEach -> - temp.cleanupSync() - - describe "serialization", -> - simulateReload = -> - workspaceState = atom.workspace.serialize() - projectState = atom.project.serialize({isUnloading: true}) - atom.workspace.destroy() - atom.project.destroy() - atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}) - atom.project.deserialize(projectState) - atom.workspace = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - grammarRegistry: atom.grammars, deserializerManager: atom.deserializers, - notificationManager: atom.notifications, - applicationDelegate: atom.applicationDelegate, - viewRegistry: atom.views, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors - }) - atom.workspace.deserialize(workspaceState, atom.deserializers) - - describe "when the workspace contains text editors", -> - it "constructs the view with the same panes", -> - pane1 = atom.workspace.getActivePane() - pane2 = pane1.splitRight(copyActiveItem: true) - pane3 = pane2.splitRight(copyActiveItem: true) - pane4 = null - - waitsForPromise -> - atom.workspace.open(null).then (editor) -> editor.setText("An untitled editor.") - - waitsForPromise -> - atom.workspace.open('b').then (editor) -> - pane2.activateItem(editor.copy()) - - waitsForPromise -> - atom.workspace.open('../sample.js').then (editor) -> - pane3.activateItem(editor) - - runs -> - pane3.activeItem.setCursorScreenPosition([2, 4]) - pane4 = pane2.splitDown() - - waitsForPromise -> - atom.workspace.open('../sample.txt').then (editor) -> - pane4.activateItem(editor) - - runs -> - pane4.getActiveItem().setCursorScreenPosition([0, 2]) - pane2.activate() - - simulateReload() - - expect(atom.workspace.getTextEditors().length).toBe 5 - [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() - expect(editor1.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') - expect(editor2.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.txt') - expect(editor2.getCursorScreenPosition()).toEqual [0, 2] - expect(editor3.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') - expect(editor4.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.js') - expect(editor4.getCursorScreenPosition()).toEqual [2, 4] - expect(untitledEditor.getPath()).toBeUndefined() - expect(untitledEditor.getText()).toBe("An untitled editor.") - - expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}/// - - describe "where there are no open panes or editors", -> - it "constructs the view with no open editors", -> - atom.workspace.getActivePane().destroy() - expect(atom.workspace.getTextEditors().length).toBe 0 - simulateReload() - expect(atom.workspace.getTextEditors().length).toBe 0 - - describe "::open(uri, options)", -> - openEvents = null - - beforeEach -> - openEvents = [] - workspace.onDidOpen (event) -> openEvents.push(event) - spyOn(workspace.getActivePane(), 'activate').andCallThrough() - - describe "when the 'searchAllPanes' option is false (default)", -> - describe "when called without a uri", -> - it "adds and activates an empty editor on the active pane", -> - [editor1, editor2] = [] - - waitsForPromise -> - workspace.open().then (editor) -> editor1 = editor - - runs -> - expect(editor1.getPath()).toBeUndefined() - expect(workspace.getActivePane().items).toEqual [editor1] - expect(workspace.getActivePaneItem()).toBe editor1 - expect(workspace.getActivePane().activate).toHaveBeenCalled() - expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}] - openEvents = [] - - waitsForPromise -> - workspace.open().then (editor) -> editor2 = editor - - runs -> - expect(editor2.getPath()).toBeUndefined() - expect(workspace.getActivePane().items).toEqual [editor1, editor2] - expect(workspace.getActivePaneItem()).toBe editor2 - expect(workspace.getActivePane().activate).toHaveBeenCalled() - expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}] - - describe "when called with a uri", -> - describe "when the active pane already has an editor for the given uri", -> - it "activates the existing editor on the active pane", -> - editor = null - editor1 = null - editor2 = null - - waitsForPromise -> - workspace.open('a').then (o) -> - editor1 = o - workspace.open('b').then (o) -> - editor2 = o - workspace.open('a').then (o) -> - editor = o - - runs -> - expect(editor).toBe editor1 - expect(workspace.getActivePaneItem()).toBe editor - expect(workspace.getActivePane().activate).toHaveBeenCalled() - - expect(openEvents).toEqual [ - { - uri: atom.project.getDirectories()[0]?.resolve('a') - item: editor1 - pane: atom.workspace.getActivePane() - index: 0 - } - { - uri: atom.project.getDirectories()[0]?.resolve('b') - item: editor2 - pane: atom.workspace.getActivePane() - index: 1 - } - { - uri: atom.project.getDirectories()[0]?.resolve('a') - item: editor1 - pane: atom.workspace.getActivePane() - index: 0 - } - ] - - describe "when the active pane does not have an editor for the given uri", -> - it "adds and activates a new editor for the given path on the active pane", -> - editor = null - waitsForPromise -> - workspace.open('a').then (o) -> editor = o - - runs -> - expect(editor.getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(workspace.getActivePaneItem()).toBe editor - expect(workspace.getActivePane().items).toEqual [editor] - expect(workspace.getActivePane().activate).toHaveBeenCalled() - - describe "when the 'searchAllPanes' option is true", -> - describe "when an editor for the given uri is already open on an inactive pane", -> - it "activates the existing editor on the inactive pane, then activates that pane", -> - editor1 = null - editor2 = null - pane1 = workspace.getActivePane() - pane2 = workspace.getActivePane().splitRight() - - waitsForPromise -> - pane1.activate() - workspace.open('a').then (o) -> editor1 = o - - waitsForPromise -> - pane2.activate() - workspace.open('b').then (o) -> editor2 = o - - runs -> - expect(workspace.getActivePaneItem()).toBe editor2 - - waitsForPromise -> - workspace.open('a', searchAllPanes: true) - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(workspace.getActivePaneItem()).toBe editor1 - - describe "when no editor for the given uri is open in any pane", -> - it "opens an editor for the given uri in the active pane", -> - editor = null - waitsForPromise -> - workspace.open('a', searchAllPanes: true).then (o) -> editor = o - - runs -> - expect(workspace.getActivePaneItem()).toBe editor - - describe "when the 'split' option is set", -> - describe "when the 'split' option is 'left'", -> - it "opens the editor in the leftmost pane of the current pane axis", -> - pane1 = workspace.getActivePane() - pane2 = pane1.splitRight() - expect(workspace.getActivePane()).toBe pane2 - - editor = null - waitsForPromise -> - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - # Focus right pane and reopen the file on the left - waitsForPromise -> - pane2.focus() - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - describe "when a pane axis is the leftmost sibling of the current pane", -> - it "opens the new item in the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitLeft() - pane3 = pane2.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - - waitsForPromise -> - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - - describe "when the 'split' option is 'right'", -> - it "opens the editor in the rightmost pane of the current pane axis", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = null - waitsForPromise -> - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - # Focus right pane and reopen the file on the right - waitsForPromise -> - pane1.focus() - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - describe "when a pane axis is the rightmost sibling of the current pane", -> - it "opens the new item in a new pane split to the right of the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitRight() - pane3 = pane2.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - pane4 = null - - waitsForPromise -> - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane4 - expect(pane4.items).toEqual [editor] - expect(workspace.paneContainer.root.children[0]).toBe pane1 - expect(workspace.paneContainer.root.children[1]).toBe pane4 - - describe "when the 'split' option is 'up'", -> - it "opens the editor in the topmost pane of the current pane axis", -> - pane1 = workspace.getActivePane() - pane2 = pane1.splitDown() - expect(workspace.getActivePane()).toBe pane2 - - editor = null - waitsForPromise -> - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - # Focus bottom pane and reopen the file on the top - waitsForPromise -> - pane2.focus() - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - describe "when a pane axis is the topmost sibling of the current pane", -> - it "opens the new item in the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitUp() - pane3 = pane2.splitRight() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - - waitsForPromise -> - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - - describe "when the 'split' option is 'down'", -> - it "opens the editor in the bottommost pane of the current pane axis", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = null - waitsForPromise -> - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - # Focus bottom pane and reopen the file on the right - waitsForPromise -> - pane1.focus() - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - describe "when a pane axis is the bottommost sibling of the current pane", -> - it "opens the new item in a new pane split to the bottom of the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - pane4 = null - - waitsForPromise -> - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane4 - expect(pane4.items).toEqual [editor] - expect(workspace.paneContainer.root.children[0]).toBe pane1 - expect(workspace.paneContainer.root.children[1]).toBe pane2 - - describe "when an initialLine and initialColumn are specified", -> - it "moves the cursor to the indicated location", -> - waitsForPromise -> - workspace.open('a', initialLine: 1, initialColumn: 5) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [1, 5] - - waitsForPromise -> - workspace.open('a', initialLine: 2, initialColumn: 4) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 4] - - waitsForPromise -> - workspace.open('a', initialLine: 0, initialColumn: 0) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [0, 0] - - waitsForPromise -> - workspace.open('a', initialLine: NaN, initialColumn: 4) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [0, 4] - - waitsForPromise -> - workspace.open('a', initialLine: 2, initialColumn: NaN) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 0] - - waitsForPromise -> - workspace.open('a', initialLine: Infinity, initialColumn: Infinity) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 11] - - describe "when the file is over 2MB", -> - it "opens the editor with largeFileMode: true", -> - spyOn(fs, 'getSizeSync').andReturn 2 * 1048577 # 2MB - - editor = null - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - - runs -> - expect(editor.largeFileMode).toBe true - - describe "when the file is over user-defined limit", -> - shouldPromptForFileOfSize = (size, shouldPrompt) -> - spyOn(fs, 'getSizeSync').andReturn size * 1048577 - atom.applicationDelegate.confirm.andCallFake -> selectedButtonIndex - atom.applicationDelegate.confirm() - selectedButtonIndex = 1 # cancel - - editor = null - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - if shouldPrompt - runs -> - expect(editor).toBeUndefined() - expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - - atom.applicationDelegate.confirm.reset() - selectedButtonIndex = 0 # open the file - - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - - runs -> - expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - expect(editor.largeFileMode).toBe true - else - runs -> - expect(editor).not.toBeUndefined() - - it "prompts the user to make sure they want to open a file this big", -> - atom.config.set "core.warnOnLargeFileLimit", 20 - shouldPromptForFileOfSize 20, true - - it "doesn't prompt on files below the limit", -> - atom.config.set "core.warnOnLargeFileLimit", 30 - shouldPromptForFileOfSize 20, false - - it "prompts for smaller files with a lower limit", -> - atom.config.set "core.warnOnLargeFileLimit", 5 - shouldPromptForFileOfSize 10, true - - describe "when passed a path that matches a custom opener", -> - it "returns the resource returned by the custom opener", -> - fooOpener = (pathToOpen, options) -> {foo: pathToOpen, options} if pathToOpen?.match(/\.foo/) - barOpener = (pathToOpen) -> {bar: pathToOpen} if pathToOpen?.match(/^bar:\/\//) - workspace.addOpener(fooOpener) - workspace.addOpener(barOpener) - - waitsForPromise -> - pathToOpen = atom.project.getDirectories()[0]?.resolve('a.foo') - workspace.open(pathToOpen, hey: "there").then (item) -> - expect(item).toEqual {foo: pathToOpen, options: {hey: "there"}} - - waitsForPromise -> - workspace.open("bar://baz").then (item) -> - expect(item).toEqual {bar: "bar://baz"} - - it "adds the file to the application's recent documents list", -> - return unless process.platform is 'darwin' # Feature only supported on macOS - spyOn(atom.applicationDelegate, 'addRecentDocument') - - waitsForPromise -> - workspace.open() - - runs -> - expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled() - - waitsForPromise -> - workspace.open('something://a/url') - - runs -> - expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled() - - waitsForPromise -> - workspace.open(__filename) - - runs -> - expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename) - - it "notifies ::onDidAddTextEditor observers", -> - absolutePath = require.resolve('./fixtures/dir/a') - newEditorHandler = jasmine.createSpy('newEditorHandler') - workspace.onDidAddTextEditor newEditorHandler - - editor = null - waitsForPromise -> - workspace.open(absolutePath).then (e) -> editor = e - - runs -> - expect(newEditorHandler.argsForCall[0][0].textEditor).toBe editor - - describe "when there is an error opening the file", -> - notificationSpy = null - beforeEach -> - atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() - - describe "when a file does not exist", -> - it "creates an empty buffer for the specified path", -> - waitsForPromise -> - workspace.open('not-a-file.md') - - runs -> - editor = workspace.getActiveTextEditor() - expect(notificationSpy).not.toHaveBeenCalled() - expect(editor.getPath()).toContain 'not-a-file.md' - - describe "when the user does not have access to the file", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EACCES, permission denied '#{path}'") - error.path = path - error.code = 'EACCES' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain 'file1' - - describe "when the the operation is not permitted", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EPERM, operation not permitted '#{path}'") - error.path = path - error.code = 'EPERM' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to open' - expect(notification.getMessage()).toContain 'file1' - - describe "when the the file is already open in windows", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EBUSY, resource busy or locked '#{path}'") - error.path = path - error.code = 'EBUSY' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to open' - expect(notification.getMessage()).toContain 'file1' - - describe "when there is an unhandled error", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - throw new Error("I dont even know what is happening right now!!") - - it "creates a notification", -> - open = -> workspace.open('file1', workspace.getActivePane()) - expect(open).toThrow() - - describe "when the file is already open in pending state", -> - it "should terminate the pending state", -> - editor = null - pane = null - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true).then (o) -> - editor = o - pane = atom.workspace.getActivePane() - - runs -> - expect(pane.getPendingItem()).toEqual editor - - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - expect(pane.getPendingItem()).toBeNull() - - describe "when opening will switch from a pending tab to a permanent tab", -> - it "keeps the pending tab open", -> - editor1 = null - editor2 = null - - waitsForPromise -> - atom.workspace.open('sample.txt').then (o) -> - editor1 = o - - waitsForPromise -> - atom.workspace.open('sample2.txt', pending: true).then (o) -> - editor2 = o - - runs -> - pane = atom.workspace.getActivePane() - pane.activateItem(editor1) - expect(pane.getItems().length).toBe 2 - expect(pane.getItems()).toEqual [editor1, editor2] - - describe "when replacing a pending item which is the last item in a second pane", -> - it "does not destroy the pane even if core.destroyEmptyPanes is on", -> - atom.config.set('core.destroyEmptyPanes', true) - editor1 = null - editor2 = null - leftPane = atom.workspace.getActivePane() - rightPane = null - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true, split: 'right').then (o) -> - editor1 = o - rightPane = atom.workspace.getActivePane() - spyOn rightPane, "destroyed" - - runs -> - expect(leftPane).not.toBe rightPane - expect(atom.workspace.getActivePane()).toBe rightPane - expect(atom.workspace.getActivePane().getItems().length).toBe 1 - expect(rightPane.getPendingItem()).toBe editor1 - - waitsForPromise -> - atom.workspace.open('sample.txt', pending: true).then (o) -> - editor2 = o - - runs -> - expect(rightPane.getPendingItem()).toBe editor2 - expect(rightPane.destroyed.callCount).toBe 0 - - describe 'the grammar-used hook', -> - it 'fires when opening a file or changing the grammar of an open file', -> - editor = null - javascriptGrammarUsed = false - coffeescriptGrammarUsed = false - - atom.packages.triggerDeferredActivationHooks() - - runs -> - atom.packages.onDidTriggerActivationHook 'language-javascript:grammar-used', -> javascriptGrammarUsed = true - atom.packages.onDidTriggerActivationHook 'language-coffee-script:grammar-used', -> coffeescriptGrammarUsed = true - - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsFor -> javascriptGrammarUsed - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - editor.setGrammar(atom.grammars.selectGrammar('.coffee')) - - waitsFor -> coffeescriptGrammarUsed - - describe "::reopenItem()", -> - it "opens the uri associated with the last closed pane that isn't currently open", -> - pane = workspace.getActivePane() - waitsForPromise -> - workspace.open('a').then -> - workspace.open('b').then -> - workspace.open('file1').then -> - workspace.open() - - runs -> - # does not reopen items with no uri - expect(workspace.getActivePaneItem().getURI()).toBeUndefined() - pane.destroyActiveItem() - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() - - # destroy all items - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('file1') - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('b') - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - pane.destroyActiveItem() - - # reopens items with uris - expect(workspace.getActivePaneItem()).toBeUndefined() - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - - # does not reopen items that are already open - waitsForPromise -> - workspace.open('b') - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('b') - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('file1') - - describe "::increase/decreaseFontSize()", -> - it "increases/decreases the font size without going below 1", -> - atom.config.set('editor.fontSize', 1) - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 2 - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 3 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 2 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 1 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 1 - - describe "::resetFontSize()", -> - it "resets the font size to the window's starting font size", -> - originalFontSize = atom.config.get('editor.fontSize') - - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize + 1 - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - 1 - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - it "does nothing if the font size has not been changed", -> - originalFontSize = atom.config.get('editor.fontSize') - - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - it "resets the font size when the editor's font size changes", -> - originalFontSize = atom.config.get('editor.fontSize') - - atom.config.set('editor.fontSize', originalFontSize + 1) - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - atom.config.set('editor.fontSize', originalFontSize - 1) - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - describe "::openLicense()", -> - it "opens the license as plain-text in a buffer", -> - waitsForPromise -> workspace.openLicense() - runs -> expect(workspace.getActivePaneItem().getText()).toMatch /Copyright/ - - describe "::isTextEditor(obj)", -> - it "returns true when the passed object is an instance of `TextEditor`", -> - expect(workspace.isTextEditor(new TextEditor)).toBe(true) - expect(workspace.isTextEditor({getText: -> null})).toBe(false) - expect(workspace.isTextEditor(null)).toBe(false) - expect(workspace.isTextEditor(undefined)).toBe(false) - - describe "::observeTextEditors()", -> - it "invokes the observer with current and future text editors", -> - observed = [] - - waitsForPromise -> workspace.open() - waitsForPromise -> workspace.open() - waitsForPromise -> workspace.openLicense() - - runs -> - workspace.observeTextEditors (editor) -> observed.push(editor) - - waitsForPromise -> workspace.open() - - expect(observed).toEqual workspace.getTextEditors() - - describe "when an editor is destroyed", -> - it "removes the editor", -> - editor = null - - waitsForPromise -> - workspace.open("a").then (e) -> editor = e - - runs -> - expect(workspace.getTextEditors()).toHaveLength 1 - editor.destroy() - expect(workspace.getTextEditors()).toHaveLength 0 - - describe "when an editor is copied because its pane is split", -> - it "sets up the new editor to be configured by the text editor registry", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - workspace.open('a').then (editor) -> - atom.textEditors.setGrammarOverride(editor, 'source.js') - expect(editor.getGrammar().name).toBe('JavaScript') - - workspace.getActivePane().splitRight(copyActiveItem: true) - newEditor = workspace.getActiveTextEditor() - expect(newEditor).not.toBe(editor) - expect(newEditor.getGrammar().name).toBe('JavaScript') - - it "stores the active grammars used by all the open editors", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - waitsForPromise -> - atom.packages.activatePackage('language-todo') - - waitsForPromise -> - atom.workspace.open('sample.coffee') - - runs -> - atom.workspace.getActiveTextEditor().setText """ - i = /test/; #FIXME - """ - - state = atom.workspace.serialize() - expect(state.packagesWithActiveGrammars).toEqual ['language-coffee-script', 'language-javascript', 'language-todo'] - - jsPackage = atom.packages.getLoadedPackage('language-javascript') - coffeePackage = atom.packages.getLoadedPackage('language-coffee-script') - spyOn(jsPackage, 'loadGrammarsSync') - spyOn(coffeePackage, 'loadGrammarsSync') - - workspace2 = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - notificationManager: atom.notifications, deserializerManager: atom.deserializers, - viewRegistry: atom.views, grammarRegistry: atom.grammars, - applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors - }) - workspace2.deserialize(state, atom.deserializers) - expect(jsPackage.loadGrammarsSync.callCount).toBe 1 - expect(coffeePackage.loadGrammarsSync.callCount).toBe 1 - - describe "document.title", -> - describe "when there is no item open", -> - 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 -> - waitsForPromise -> - atom.workspace.open('b').then -> - atom.project.setPaths([]) - - it "sets the title to the pane item's title plus the item's path", -> - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the title of the active pane item changes", -> - it "updates the window title based on the item's new title", -> - editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(temp.dir, 'hi')) - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the active pane's item changes", -> - it "updates the title to the new item's title plus the project path", -> - atom.workspace.getActivePane().activateNextItem() - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when an inactive pane's item changes", -> - it "does not update the title", -> - pane = atom.workspace.getActivePane() - pane.splitRight() - initialTitle = document.title - pane.activateNextItem() - expect(document.title).toBe initialTitle - - describe "when the active pane item is inside a project path", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('b') - - describe "when there is an active pane item", -> - it "sets the title to the pane item's title plus the project path", -> - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the title of the active pane item changes", -> - it "updates the window title based on the item's new title", -> - editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the active pane's item changes", -> - it "updates the title to the new item's title plus the project path", -> - atom.workspace.getActivePane().activateNextItem() - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the last pane item is removed", -> - it "updates the title to the project's first path", -> - atom.workspace.getActivePane().destroy() - expect(atom.workspace.getActivePaneItem()).toBeUndefined() - 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", -> - pane = atom.workspace.getActivePane() - pane.splitRight() - initialTitle = document.title - pane.activateNextItem() - expect(document.title).toBe initialTitle - - describe "when the workspace is deserialized", -> - beforeEach -> - waitsForPromise -> atom.workspace.open('a') - - it "updates the title to contain the project's path", -> - document.title = null - workspace2 = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - notificationManager: atom.notifications, deserializerManager: atom.deserializers, - viewRegistry: atom.views, grammarRegistry: atom.grammars, - applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors - }) - workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) - item = workspace2.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}/// - workspace2.destroy() - - describe "document edited status", -> - [item1, item2] = [] - - beforeEach -> - waitsForPromise -> atom.workspace.open('a') - waitsForPromise -> atom.workspace.open('b') - runs -> - [item1, item2] = atom.workspace.getPaneItems() - - it "calls setDocumentEdited when the active item changes", -> - expect(atom.workspace.getActivePaneItem()).toBe item2 - item1.insertText('a') - expect(item1.isModified()).toBe true - atom.workspace.getActivePane().activateNextItem() - - expect(setDocumentEdited).toHaveBeenCalledWith(true) - - it "calls atom.setDocumentEdited when the active item's modified status changes", -> - expect(atom.workspace.getActivePaneItem()).toBe item2 - item2.insertText('a') - advanceClock(item2.getBuffer().getStoppedChangingDelay()) - - expect(item2.isModified()).toBe true - expect(setDocumentEdited).toHaveBeenCalledWith(true) - - item2.undo() - advanceClock(item2.getBuffer().getStoppedChangingDelay()) - - expect(item2.isModified()).toBe false - expect(setDocumentEdited).toHaveBeenCalledWith(false) - - describe "adding panels", -> - class TestItem - - class TestItemElement extends HTMLElement - constructor: -> - initialize: (@model) -> this - getModel: -> @model - - beforeEach -> - atom.views.addViewProvider TestItem, (model) -> - new TestItemElement().initialize(model) - - describe '::addLeftPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getLeftPanels().length).toBe(0) - atom.workspace.panelContainers.left.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addLeftPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addRightPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getRightPanels().length).toBe(0) - atom.workspace.panelContainers.right.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addRightPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addTopPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getTopPanels().length).toBe(0) - atom.workspace.panelContainers.top.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addTopPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addBottomPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getBottomPanels().length).toBe(0) - atom.workspace.panelContainers.bottom.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addBottomPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addHeaderPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getHeaderPanels().length).toBe(0) - atom.workspace.panelContainers.header.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addHeaderPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addFooterPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getFooterPanels().length).toBe(0) - atom.workspace.panelContainers.footer.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addFooterPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addModalPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getModalPanels().length).toBe(0) - atom.workspace.panelContainers.modal.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addModalPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe "::panelForItem(item)", -> - it "returns the panel associated with the item", -> - item = new TestItem - panel = atom.workspace.addLeftPanel(item: item) - - itemWithNoPanel = new TestItem - - expect(atom.workspace.panelForItem(item)).toBe panel - expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe null - - describe "::scan(regex, options, callback)", -> - describe "when called with a regex", -> - it "calls the callback with all regex results in all files in the project", -> - results = [] - waitsForPromise -> - atom.workspace.scan /(a)+/, (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength(3) - expect(results[0].filePath).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(results[0].matches).toHaveLength(3) - expect(results[0].matches[0]).toEqual - matchText: 'aaa' - lineText: 'aaa bbb' - lineTextOffset: 0 - range: [[0, 0], [0, 3]] - - it "works with with escaped literals (like $ and ^)", -> - results = [] - waitsForPromise -> - atom.workspace.scan /\$\w+/, (result) -> results.push(result) - - runs -> - expect(results.length).toBe 1 - - {filePath, matches} = results[0] - expect(filePath).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(matches).toHaveLength 1 - expect(matches[0]).toEqual - matchText: '$bill' - lineText: 'dollar$bill' - lineTextOffset: 0 - range: [[2, 6], [2, 11]] - - it "works on evil filenames", -> - atom.config.set('core.excludeVcsIgnoredPaths', false) - platform.generateEvilFiles() - atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /evil/, (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - _.each(matches, (m) -> expect(m.matchText).toEqual 'evil') - - if platform.isWindows() - expect(paths.length).toBe 3 - expect(paths[0]).toMatch /a_file_with_utf8.txt$/ - expect(paths[1]).toMatch /file with spaces.txt$/ - expect(path.basename(paths[2])).toBe "utfa\u0306.md" - else - expect(paths.length).toBe 5 - expect(paths[0]).toMatch /a_file_with_utf8.txt$/ - expect(paths[1]).toMatch /file with spaces.txt$/ - expect(paths[2]).toMatch /goddam\nnewlines$/m - expect(paths[3]).toMatch /quote".txt$/m - expect(path.basename(paths[4])).toBe "utfa\u0306.md" - - it "ignores case if the regex includes the `i` flag", -> - results = [] - waitsForPromise -> - atom.workspace.scan /DOLLAR/i, (result) -> results.push(result) - - runs -> - expect(results).toHaveLength 1 - - describe "when the core.excludeVcsIgnoredPaths config is truthy", -> - [projectPath, ignoredPath] = [] - - beforeEach -> - sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') - projectPath = path.join(temp.mkdirSync("atom")) - - writerStream = fstream.Writer(projectPath) - fstream.Reader(sourceProjectPath).pipe(writerStream) - - waitsFor (done) -> - writerStream.on 'close', done - writerStream.on 'error', done - - runs -> - fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) - ignoredPath = path.join(projectPath, 'ignored.txt') - fs.writeFileSync(ignoredPath, 'this match should not be included') - - afterEach -> - fs.removeSync(projectPath) if fs.existsSync(projectPath) - - it "excludes ignored files", -> - atom.project.setPaths([projectPath]) - atom.config.set('core.excludeVcsIgnoredPaths', true) - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.workspace.scan /match/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "includes only files when a directory filter is specified", -> - projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) - atom.project.setPaths([projectPath]) - - filePath = path.join(projectPath, 'a-dir', 'oh-git') - - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /aaa/, paths: ["a-dir#{path.sep}"], (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - expect(paths.length).toBe 1 - expect(paths[0]).toBe filePath - expect(matches.length).toBe 1 - - it "includes files and folders that begin with a '.'", -> - projectPath = temp.mkdirSync('atom-spec-workspace') - filePath = path.join(projectPath, '.text') - fs.writeFileSync(filePath, 'match this') - atom.project.setPaths([projectPath]) - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /match this/, (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - expect(paths.length).toBe 1 - expect(paths[0]).toBe filePath - expect(matches.length).toBe 1 - - it "excludes values in core.ignoredNames", -> - ignoredNames = atom.config.get("core.ignoredNames") - ignoredNames.push("a") - atom.config.set("core.ignoredNames", ignoredNames) - - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.workspace.scan /dollar/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "scans buffer contents if the buffer is modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('a').then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.workspace.scan /a|Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 3 - resultForA = _.find results, ({filePath}) -> path.basename(filePath) is 'a' - expect(resultForA.matches).toHaveLength 1 - expect(resultForA.matches[0].matchText).toBe 'Elephant' - - it "ignores buffers outside the project", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open(temp.openSync().path).then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.workspace.scan /Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 0 - - describe "when the project has multiple root directories", -> - [dir1, dir2, file1, file2] = [] - - beforeEach -> - [dir1] = atom.project.getPaths() - file1 = path.join(dir1, "a-dir", "oh-git") - - dir2 = temp.mkdirSync("a-second-dir") - aDir2 = path.join(dir2, "a-dir") - file2 = path.join(aDir2, "a-file") - fs.mkdirSync(aDir2) - fs.writeFileSync(file2, "ccc aaaa") - - atom.project.addPath(dir2) - - it "searches matching files in all of the project's root directories", -> - resultPaths = [] - waitsForPromise -> - atom.workspace.scan /aaaa/, ({filePath}) -> - resultPaths.push(filePath) - - runs -> - expect(resultPaths.sort()).toEqual([file1, file2].sort()) - - describe "when an inclusion path starts with the basename of a root directory", -> - it "interprets the inclusion path as starting from that directory", -> - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: ["dir"], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file1]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.join("dir", "a-dir")], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file1]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.basename(dir2)], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file2]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.join(path.basename(dir2), "a-dir")], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file2]) - - describe "when a custom directory searcher is registered", -> - fakeSearch = null - # Function that is invoked once all of the fields on fakeSearch are set. - onFakeSearchCreated = null - - class FakeSearch - constructor: (@options) -> - # Note that hoisting resolve and reject in this way is generally frowned upon. - @promise = new Promise (resolve, reject) => - @hoistedResolve = resolve - @hoistedReject = reject - onFakeSearchCreated?(this) - then: (args...) -> - @promise.then.apply(@promise, args) - cancel: -> - @cancelled = true - # According to the spec for a DirectorySearcher, invoking `cancel()` should - # resolve the thenable rather than reject it. - @hoistedResolve() - - beforeEach -> - fakeSearch = null - onFakeSearchCreated = null - atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory: (directory) -> directory.getPath() is dir1 - search: (directory, regex, options) -> fakeSearch = new FakeSearch(options) - }) - - waitsFor -> - atom.workspace.directorySearchers.length > 0 - - it "can override the DefaultDirectorySearcher on a per-directory basis", -> - foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' - numPathsSearchedInDir2 = 1 - numPathsToPretendToSearchInCustomDirectorySearcher = 10 - searchResult = - filePath: foreignFilePath, - matches: [ - { - lineText: 'Hello world', - lineTextOffset: 0, - matchText: 'Hello', - range: [[0, 0], [0, 5]], - }, - ] - onFakeSearchCreated = (fakeSearch) -> - fakeSearch.options.didMatch(searchResult) - fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) - fakeSearch.hoistedResolve() - - resultPaths = [] - onPathsSearched = jasmine.createSpy('onPathsSearched') - waitsForPromise -> - atom.workspace.scan /aaaa/, {onPathsSearched}, ({filePath}) -> - resultPaths.push(filePath) - - runs -> - expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) - # onPathsSearched should be called once by each DirectorySearcher. The order is not - # guaranteed, so we can only verify the total number of paths searched is correct - # after the second call. - expect(onPathsSearched.callCount).toBe(2) - expect(onPathsSearched.mostRecentCall.args[0]).toBe( - numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) - - it "can be cancelled when the object returned by scan() has its cancel() method invoked", -> - thenable = atom.workspace.scan /aaaa/, -> - resultOfPromiseSearch = null - - waitsFor 'fakeSearch to be defined', -> fakeSearch? - - runs -> - expect(fakeSearch.cancelled).toBe(undefined) - thenable.cancel() - expect(fakeSearch.cancelled).toBe(true) - - - waitsForPromise -> - thenable.then (promiseResult) -> resultOfPromiseSearch = promiseResult - - runs -> - expect(resultOfPromiseSearch).toBe('cancelled') - - it "will have the side-effect of failing the overall search if it fails", -> - # This provider's search should be cancelled when the first provider fails - fakeSearch2 = null - atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory: (directory) -> directory.getPath() is dir2 - search: (directory, regex, options) -> fakeSearch2 = new FakeSearch(options) - }) - - didReject = false - promise = cancelableSearch = atom.workspace.scan /aaaa/, -> - waitsFor 'fakeSearch to be defined', -> fakeSearch? - - runs -> - fakeSearch.hoistedReject() - - waitsForPromise -> - cancelableSearch.catch -> didReject = true - - waitsFor (done) -> promise.then(null, done) - - runs -> - expect(didReject).toBe(true) - expect(fakeSearch2.cancelled).toBe true # Cancels other ongoing searches - - describe "::replace(regex, replacementText, paths, iterator)", -> - [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] - - beforeEach -> - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('../')]) - - filePath = atom.project.getDirectories()[0]?.resolve('sample.js') - commentFilePath = atom.project.getDirectories()[0]?.resolve('sample-with-comments.js') - sampleContent = fs.readFileSync(filePath).toString() - sampleCommentContent = fs.readFileSync(commentFilePath).toString() - - afterEach -> - fs.writeFileSync(filePath, sampleContent) - fs.writeFileSync(commentFilePath, sampleCommentContent) - - describe "when a file doesn't exist", -> - it "calls back with an error", -> - errors = [] - missingPath = path.resolve('/not-a-file.js') - expect(fs.existsSync(missingPath)).toBeFalsy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [missingPath], (result, error) -> - errors.push(error) - - runs -> - expect(errors).toHaveLength 1 - expect(errors[0].path).toBe missingPath - - describe "when called with unopened files", -> - it "replaces properly", -> - results = [] - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - describe "when a buffer is already open", -> - it "replaces properly and saves when not modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - expect(editor.isModified()).toBeFalsy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeFalsy() - - it "does not replace when the path is not specified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample-with-comments.js').then (o) -> editor = o - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [commentFilePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe commentFilePath - - it "does NOT save when modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') - expect(editor.isModified()).toBeTruthy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'okthen', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeTruthy() - - describe "::saveActivePaneItem()", -> - editor = null - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - describe "when there is an error", -> - it "emits a warning notification when the file cannot be saved", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("'/some/file' is a directory") - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the directory cannot be written to", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the user does not have permission", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") - error.code = 'EACCES' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the operation is not permitted", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") - error.code = 'EPERM' - error.path = '/Some/dir/and-a-file.js' - throw error - - it "emits a warning notification when the file is already open by another app", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") - error.code = 'EBUSY' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - - notificaiton = addedSpy.mostRecentCall.args[0] - expect(notificaiton.getType()).toBe 'warning' - expect(notificaiton.getMessage()).toContain 'Unable to save' - - it "emits a warning notification when the file system is read-only", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") - error.code = 'EROFS' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - - notification = addedSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to save' - - it "emits a warning notification when the file cannot be saved", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("no one knows") - - save = -> atom.workspace.saveActivePaneItem() - expect(save).toThrow() - - describe "::closeActivePaneItemOrEmptyPaneOrWindow", -> - beforeEach -> - spyOn(atom, 'close') - waitsForPromise -> atom.workspace.open() - - it "closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane", -> - atom.config.set('core.destroyEmptyPanes', false) - - pane1 = atom.workspace.getActivePane() - pane2 = pane1.splitRight(copyActiveItem: true) - - expect(atom.workspace.getPanes().length).toBe 2 - expect(pane2.getItems().length).toBe 1 - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - - expect(atom.workspace.getPanes().length).toBe 2 - expect(pane2.getItems().length).toBe 0 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - - expect(atom.workspace.getPanes().length).toBe 1 - expect(pane1.getItems().length).toBe 1 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe 1 - expect(pane1.getItems().length).toBe 0 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe 1 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.close).toHaveBeenCalled() - - describe "when the core.allowPendingPaneItems option is falsey", -> - it "does not open item with `pending: true` option as pending", -> - pane = null - atom.config.set('core.allowPendingPaneItems', false) - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true).then -> - pane = atom.workspace.getActivePane() - - runs -> - expect(pane.getPendingItem()).toBeFalsy() - - describe "grammar activation", -> - it "notifies the workspace of which grammar is used", -> - editor = null - atom.packages.triggerDeferredActivationHooks() - - javascriptGrammarUsed = jasmine.createSpy('js grammar used') - rubyGrammarUsed = jasmine.createSpy('ruby grammar used') - cGrammarUsed = jasmine.createSpy('c grammar used') - - atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed) - atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed) - atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed) - - waitsForPromise -> atom.packages.activatePackage('language-ruby') - waitsForPromise -> atom.packages.activatePackage('language-javascript') - waitsForPromise -> atom.packages.activatePackage('language-c') - waitsForPromise -> atom.workspace.open('sample-with-comments.js') - - runs -> - # Hooks are triggered when opening new editors - expect(javascriptGrammarUsed).toHaveBeenCalled() - - # Hooks are triggered when changing existing editors grammars - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')) - expect(cGrammarUsed).toHaveBeenCalled() - - # Hooks are triggered when editors are added in other ways. - atom.workspace.getActivePane().splitRight(copyActiveItem: true) - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) - expect(rubyGrammarUsed).toHaveBeenCalled() - - describe ".checkoutHeadRevision()", -> - editor = null - beforeEach -> - atom.config.set("editor.confirmCheckoutHeadRevision", false) - - waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o - - it "reverts to the version of its file checked into the project repository", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText("---\n") - expect(editor.lineTextForBufferRow(0)).toBe "---" - - waitsForPromise -> - atom.workspace.checkoutHeadRevision(editor) - - runs -> - expect(editor.lineTextForBufferRow(0)).toBe "" - - describe "when there's no repository for the editor's file", -> - it "doesn't do anything", -> - editor = new TextEditor - editor.setText("stuff") - atom.workspace.checkoutHeadRevision(editor) - - waitsForPromise -> atom.workspace.checkoutHeadRevision(editor) - - escapeStringRegex = (str) -> - str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js new file mode 100644 index 000000000..d2f922b57 --- /dev/null +++ b/spec/workspace-spec.js @@ -0,0 +1,2091 @@ +'use strict' + +/* global advanceClock, HTMLElement, waits */ + +const path = require('path') +const temp = require('temp').track() +const TextEditor = require('../src/text-editor') +const Workspace = require('../src/workspace') +const Project = require('../src/project') +const platform = require('./spec-helper-platform') +const _ = require('underscore-plus') +const fstream = require('fstream') +const fs = require('fs-plus') +const AtomEnvironment = require('../src/atom-environment') + +describe('Workspace', () => { + let workspace + let setDocumentEdited + + beforeEach(() => { + workspace = atom.workspace + workspace.resetFontSize() + spyOn(atom.applicationDelegate, 'confirm') + setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') + atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) + waits(1) + }) + + afterEach(() => temp.cleanupSync()) + + describe('serialization', () => { + const simulateReload = () => { + const workspaceState = atom.workspace.serialize() + const projectState = atom.project.serialize({isUnloading: true}) + atom.workspace.destroy() + atom.project.destroy() + atom.project = new Project({ + notificationManager: atom.notifications, + packageManager: atom.packages, + confirm: atom.confirm.bind(atom), + applicationDelegate: atom.applicationDelegate + }) + atom.project.deserialize(projectState) + atom.workspace = new Workspace({ + config: atom.config, + project: atom.project, + packageManager: atom.packages, + grammarRegistry: atom.grammars, + deserializerManager: atom.deserializers, + notificationManager: atom.notifications, + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views, + assert: atom.assert.bind(atom), + textEditorRegistry: atom.textEditors + }) + atom.workspace.initialize() + return atom.workspace.deserialize(workspaceState, atom.deserializers) + } + + describe('when the workspace contains text editors', () => { + it('constructs the view with the same panes', () => { + const pane1 = atom.workspace.getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + const pane3 = pane2.splitRight({copyActiveItem: true}) + let pane4 = null + + waitsForPromise(() => atom.workspace.open(null).then(editor => editor.setText('An untitled editor.'))) + + waitsForPromise(() => + atom.workspace.open('b').then(editor => pane2.activateItem(editor.copy())) + ) + + waitsForPromise(() => + atom.workspace.open('../sample.js').then(editor => pane3.activateItem(editor)) + ) + + runs(() => { + pane3.activeItem.setCursorScreenPosition([2, 4]) + pane4 = pane2.splitDown() + }) + + waitsForPromise(() => + atom.workspace.open('../sample.txt').then(editor => pane4.activateItem(editor)) + ) + + runs(() => { + pane4.getActiveItem().setCursorScreenPosition([0, 2]) + pane2.activate() + + simulateReload() + + expect(atom.workspace.getTextEditors().length).toBe(5) + const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) + expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) + expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) + expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) + expect(untitledEditor.getPath()).toBeUndefined() + expect(untitledEditor.getText()).toBe('An untitled editor.') + + expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()) + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)) + }) + }) + }) + + describe('where there are no open panes or editors', () => { + it('constructs the view with no open editors', () => { + atom.workspace.getActivePane().destroy() + expect(atom.workspace.getTextEditors().length).toBe(0) + simulateReload() + expect(atom.workspace.getTextEditors().length).toBe(0) + }) + }) + }) + + describe('::open(uri, options)', () => { + let openEvents = null + + beforeEach(() => { + openEvents = [] + workspace.onDidOpen(event => openEvents.push(event)) + spyOn(workspace.getActivePane(), 'activate').andCallThrough() + }) + + describe("when the 'searchAllPanes' option is false (default)", () => { + describe('when called without a uri', () => { + it('adds and activates an empty editor on the active pane', () => { + let editor1 + let editor2 + + waitsForPromise(() => workspace.open().then(editor => { editor1 = editor })) + + runs(() => { + expect(editor1.getPath()).toBeUndefined() + expect(workspace.getActivePane().items).toEqual([editor1]) + expect(workspace.getActivePaneItem()).toBe(editor1) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]) + openEvents = [] + }) + + waitsForPromise(() => workspace.open().then(editor => { editor2 = editor })) + + runs(() => { + expect(editor2.getPath()).toBeUndefined() + expect(workspace.getActivePane().items).toEqual([editor1, editor2]) + expect(workspace.getActivePaneItem()).toBe(editor2) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]) + }) + }) + }) + + describe('when called with a uri', () => { + describe('when the active pane already has an editor for the given uri', () => { + it('activates the existing editor on the active pane', () => { + let editor = null + let editor1 = null + let editor2 = null + + waitsForPromise(() => + workspace.open('a').then(o => { + editor1 = o + return workspace.open('b').then(o => { + editor2 = o + return workspace.open('a').then(o => { editor = o }) + }) + }) + ) + + runs(() => { + expect(editor).toBe(editor1) + expect(workspace.getActivePaneItem()).toBe(editor) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(openEvents).toEqual([ + { + uri: firstDirectory.resolve('a'), + item: editor1, + pane: atom.workspace.getActivePane(), + index: 0 + }, + { + uri: firstDirectory.resolve('b'), + item: editor2, + pane: atom.workspace.getActivePane(), + index: 1 + }, + { + uri: firstDirectory.resolve('a'), + item: editor1, + pane: atom.workspace.getActivePane(), + index: 0 + } + ]) + }) + }) + }) + + describe('when the active pane does not have an editor for the given uri', () => { + it('adds and activates a new editor for the given path on the active pane', () => { + let editor = null + waitsForPromise(() => workspace.open('a').then(o => { editor = o })) + + runs(() => { + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor.getURI()).toBe(firstDirectory.resolve('a')) + expect(workspace.getActivePaneItem()).toBe(editor) + expect(workspace.getActivePane().items).toEqual([editor]) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + }) + }) + }) + }) + }) + + describe("when the 'searchAllPanes' option is true", () => { + describe('when an editor for the given uri is already open on an inactive pane', () => { + it('activates the existing editor on the inactive pane, then activates that pane', () => { + let editor1 = null + let editor2 = null + const pane1 = workspace.getActivePane() + const pane2 = workspace.getActivePane().splitRight() + + waitsForPromise(() => { + pane1.activate() + return workspace.open('a').then(o => { editor1 = o }) + }) + + waitsForPromise(() => { + pane2.activate() + return workspace.open('b').then(o => { editor2 = o }) + }) + + runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)) + + waitsForPromise(() => workspace.open('a', {searchAllPanes: true})) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(workspace.getActivePaneItem()).toBe(editor1) + }) + }) + }) + + describe('when no editor for the given uri is open in any pane', () => { + it('opens an editor for the given uri in the active pane', () => { + let editor = null + waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => { editor = o })) + + runs(() => expect(workspace.getActivePaneItem()).toBe(editor)) + }) + }) + }) + + describe("when the 'split' option is set", () => { + describe("when the 'split' option is 'left'", () => { + it('opens the editor in the leftmost pane of the current pane axis', () => { + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitRight() + expect(workspace.getActivePane()).toBe(pane2) + + let editor = null + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + expect(pane2.items).toEqual([]) + }) + + // Focus right pane and reopen the file on the left + waitsForPromise(() => { + pane2.focus() + return workspace.open('a', {split: 'left'}).then(o => { editor = o }) + }) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + expect(pane2.items).toEqual([]) + }) + }) + }) + + describe('when a pane axis is the leftmost sibling of the current pane', () => { + it('opens the new item in the current pane', () => { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitLeft() + pane2.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + }) + }) + }) + + describe("when the 'split' option is 'right'", () => { + it('opens the editor in the rightmost pane of the current pane axis', () => { + let editor = null + const pane1 = workspace.getActivePane() + let pane2 = null + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) + + runs(() => { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + expect(pane2.items).toEqual([editor]) + }) + + // Focus right pane and reopen the file on the right + waitsForPromise(() => { + pane1.focus() + return workspace.open('a', {split: 'right'}).then(o => { editor = o }) + }) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + expect(pane2.items).toEqual([editor]) + }) + }) + + describe('when a pane axis is the rightmost sibling of the current pane', () => { + it('opens the new item in a new pane split to the right of the current pane', () => { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitRight() + pane2.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + let pane4 = null + + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) + + runs(() => { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane4) + expect(pane4.items).toEqual([editor]) + expect(workspace.paneContainer.root.children[0]).toBe(pane1) + expect(workspace.paneContainer.root.children[1]).toBe(pane4) + }) + }) + }) + }) + + describe("when the 'split' option is 'up'", () => { + it('opens the editor in the topmost pane of the current pane axis', () => { + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitDown() + expect(workspace.getActivePane()).toBe(pane2) + + let editor = null + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + expect(pane2.items).toEqual([]) + }) + + // Focus bottom pane and reopen the file on the top + waitsForPromise(() => { + pane2.focus() + return workspace.open('a', {split: 'up'}).then(o => { editor = o }) + }) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + expect(pane2.items).toEqual([]) + }) + }) + }) + + describe('when a pane axis is the topmost sibling of the current pane', () => { + it('opens the new item in the current pane', () => { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitUp() + pane2.splitRight() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + }) + }) + }) + + describe("when the 'split' option is 'down'", () => { + it('opens the editor in the bottommost pane of the current pane axis', () => { + let editor = null + const pane1 = workspace.getActivePane() + let pane2 = null + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) + + runs(() => { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + expect(pane2.items).toEqual([editor]) + }) + + // Focus bottom pane and reopen the file on the right + waitsForPromise(() => { + pane1.focus() + return workspace.open('a', {split: 'down'}).then(o => { editor = o }) + }) + + runs(() => { + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + expect(pane2.items).toEqual([editor]) + }) + }) + + describe('when a pane axis is the bottommost sibling of the current pane', () => { + it('opens the new item in a new pane split to the bottom of the current pane', () => { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + let pane4 = null + + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) + + runs(() => { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane4) + expect(pane4.items).toEqual([editor]) + expect(workspace.paneContainer.root.children[0]).toBe(pane1) + expect(workspace.paneContainer.root.children[1]).toBe(pane2) + }) + }) + }) + }) + }) + + describe('when an initialLine and initialColumn are specified', () => { + it('moves the cursor to the indicated location', () => { + waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])) + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: 4})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 4])) + + waitsForPromise(() => workspace.open('a', {initialLine: 0, initialColumn: 0})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 0])) + + waitsForPromise(() => workspace.open('a', {initialLine: NaN, initialColumn: 4})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 4])) + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: NaN})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 0])) + + waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11])) + }) + }) + + describe('when the file is over 2MB', () => { + it('opens the editor with largeFileMode: true', () => { + spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB + + let editor = null + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) + + runs(() => expect(editor.largeFileMode).toBe(true)) + }) + }) + + describe('when the file is over user-defined limit', () => { + const shouldPromptForFileOfSize = (size, shouldPrompt) => { + spyOn(fs, 'getSizeSync').andReturn(size * 1048577) + atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex) + atom.applicationDelegate.confirm() + var selectedButtonIndex = 1 // cancel + + let editor = null + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) + if (shouldPrompt) { + runs(() => { + expect(editor).toBeUndefined() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + + atom.applicationDelegate.confirm.reset() + selectedButtonIndex = 0 + }) // open the file + + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) + + runs(() => { + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(editor.largeFileMode).toBe(true) + }) + } else { + runs(() => expect(editor).not.toBeUndefined()) + } + } + + it('prompts the user to make sure they want to open a file this big', () => { + atom.config.set('core.warnOnLargeFileLimit', 20) + shouldPromptForFileOfSize(20, true) + }) + + it("doesn't prompt on files below the limit", () => { + atom.config.set('core.warnOnLargeFileLimit', 30) + shouldPromptForFileOfSize(20, false) + }) + + it('prompts for smaller files with a lower limit', () => { + atom.config.set('core.warnOnLargeFileLimit', 5) + shouldPromptForFileOfSize(10, true) + }) + }) + + describe('when passed a path that matches a custom opener', () => { + it('returns the resource returned by the custom opener', () => { + const fooOpener = (pathToOpen, options) => { + if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { + return {foo: pathToOpen, options} + } + } + const barOpener = (pathToOpen) => { + if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { + return {bar: pathToOpen} + } + } + workspace.addOpener(fooOpener) + workspace.addOpener(barOpener) + + waitsForPromise(() => { + const pathToOpen = atom.project.getDirectories()[0].resolve('a.foo') + return workspace.open(pathToOpen, {hey: 'there'}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: 'there'}})) + }) + + waitsForPromise(() => + workspace.open('bar://baz').then(item => expect(item).toEqual({bar: 'bar://baz'}))) + }) + }) + + it("adds the file to the application's recent documents list", () => { + if (process.platform !== 'darwin') { return } // Feature only supported on macOS + spyOn(atom.applicationDelegate, 'addRecentDocument') + + waitsForPromise(() => workspace.open()) + + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) + + waitsForPromise(() => workspace.open('something://a/url')) + + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) + + waitsForPromise(() => workspace.open(__filename)) + + runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)) + }) + + it('notifies ::onDidAddTextEditor observers', () => { + const absolutePath = require.resolve('./fixtures/dir/a') + const newEditorHandler = jasmine.createSpy('newEditorHandler') + workspace.onDidAddTextEditor(newEditorHandler) + + let editor = null + waitsForPromise(() => workspace.open(absolutePath).then(e => { editor = e })) + + runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)) + }) + + describe('when there is an error opening the file', () => { + let notificationSpy = null + beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())) + + describe('when a file does not exist', () => { + it('creates an empty buffer for the specified path', () => { + waitsForPromise(() => workspace.open('not-a-file.md')) + + runs(() => { + const editor = workspace.getActiveTextEditor() + expect(notificationSpy).not.toHaveBeenCalled() + expect(editor.getPath()).toContain('not-a-file.md') + }) + }) + }) + + describe('when the user does not have access to the file', () => { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(path => { + const error = new Error(`EACCES, permission denied '${path}'`) + error.path = path + error.code = 'EACCES' + throw error + }) + ) + + it('creates a notification', () => { + waitsForPromise(() => workspace.open('file1')) + + runs(() => { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Permission denied') + expect(notification.getMessage()).toContain('file1') + }) + }) + }) + + describe('when the the operation is not permitted', () => { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(path => { + const error = new Error(`EPERM, operation not permitted '${path}'`) + error.path = path + error.code = 'EPERM' + throw error + }) + ) + + it('creates a notification', () => { + waitsForPromise(() => workspace.open('file1')) + + runs(() => { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Unable to open') + expect(notification.getMessage()).toContain('file1') + }) + }) + }) + + describe('when the the file is already open in windows', () => { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(path => { + const error = new Error(`EBUSY, resource busy or locked '${path}'`) + error.path = path + error.code = 'EBUSY' + throw error + }) + ) + + it('creates a notification', () => { + waitsForPromise(() => workspace.open('file1')) + + runs(() => { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Unable to open') + expect(notification.getMessage()).toContain('file1') + }) + }) + }) + + describe('when there is an unhandled error', () => { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(path => { + throw new Error('I dont even know what is happening right now!!') + }) + ) + + it('creates a notification', () => { + const open = () => workspace.open('file1', workspace.getActivePane()) + expect(open).toThrow() + }) + }) + }) + + describe('when the file is already open in pending state', () => { + it('should terminate the pending state', () => { + let editor = null + let pane = null + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true}).then(o => { + editor = o + pane = atom.workspace.getActivePane() + }) + ) + + runs(() => expect(pane.getPendingItem()).toEqual(editor)) + + waitsForPromise(() => atom.workspace.open('sample.js')) + + runs(() => expect(pane.getPendingItem()).toBeNull()) + }) + }) + + describe('when opening will switch from a pending tab to a permanent tab', () => { + it('keeps the pending tab open', () => { + let editor1 = null + let editor2 = null + + waitsForPromise(() => + atom.workspace.open('sample.txt').then(o => { editor1 = o }) + ) + + waitsForPromise(() => + atom.workspace.open('sample2.txt', {pending: true}).then(o => { editor2 = o }) + ) + + runs(() => { + const pane = atom.workspace.getActivePane() + pane.activateItem(editor1) + expect(pane.getItems().length).toBe(2) + expect(pane.getItems()).toEqual([editor1, editor2]) + }) + }) + }) + + describe('when replacing a pending item which is the last item in a second pane', () => { + it('does not destroy the pane even if core.destroyEmptyPanes is on', () => { + atom.config.set('core.destroyEmptyPanes', true) + let editor1 = null + let editor2 = null + const leftPane = atom.workspace.getActivePane() + let rightPane = null + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(o => { + editor1 = o + rightPane = atom.workspace.getActivePane() + spyOn(rightPane, 'destroyed') + }) + ) + + runs(() => { + expect(leftPane).not.toBe(rightPane) + expect(atom.workspace.getActivePane()).toBe(rightPane) + expect(atom.workspace.getActivePane().getItems().length).toBe(1) + expect(rightPane.getPendingItem()).toBe(editor1) + }) + + waitsForPromise(() => + atom.workspace.open('sample.txt', {pending: true}).then(o => { editor2 = o }) + ) + + runs(() => { + expect(rightPane.getPendingItem()).toBe(editor2) + expect(rightPane.destroyed.callCount).toBe(0) + }) + }) + }) + }) + + describe('the grammar-used hook', () => { + it('fires when opening a file or changing the grammar of an open file', () => { + let editor = null + let javascriptGrammarUsed = false + let coffeescriptGrammarUsed = false + + atom.packages.triggerDeferredActivationHooks() + + runs(() => { + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => { javascriptGrammarUsed = true }) + atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => { coffeescriptGrammarUsed = true }) + }) + + waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => { editor = o })) + + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + + waitsFor(() => javascriptGrammarUsed) + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))) + + waitsFor(() => coffeescriptGrammarUsed) + }) + }) + + describe('::reopenItem()', () => { + it("opens the uri associated with the last closed pane that isn't currently open", () => { + const pane = workspace.getActivePane() + waitsForPromise(() => + workspace.open('a').then(() => + workspace.open('b').then(() => + workspace.open('file1').then(() => workspace.open()) + ) + ) + ) + + runs(() => { + // does not reopen items with no uri + expect(workspace.getActivePaneItem().getURI()).toBeUndefined() + pane.destroyActiveItem() + }) + + waitsForPromise(() => workspace.reopenItem()) + + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + + runs(() => { + expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() + + // destroy all items + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1')) + pane.destroyActiveItem() + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b')) + pane.destroyActiveItem() + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a')) + pane.destroyActiveItem() + + // reopens items with uris + expect(workspace.getActivePaneItem()).toBeUndefined() + }) + + waitsForPromise(() => workspace.reopenItem()) + + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a'))) + + // does not reopen items that are already open + waitsForPromise(() => workspace.open('b')) + + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b'))) + + waitsForPromise(() => workspace.reopenItem()) + + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1'))) + }) + }) + + describe('::increase/decreaseFontSize()', () => { + it('increases/decreases the font size without going below 1', () => { + atom.config.set('editor.fontSize', 1) + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(2) + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(3) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(2) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(1) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(1) + }) + }) + + describe('::resetFontSize()', () => { + it("resets the font size to the window's starting font size", () => { + const originalFontSize = atom.config.get('editor.fontSize') + + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize + 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + }) + + it('does nothing if the font size has not been changed', () => { + const originalFontSize = atom.config.get('editor.fontSize') + + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + }) + + it("resets the font size when the editor's font size changes", () => { + const originalFontSize = atom.config.get('editor.fontSize') + + atom.config.set('editor.fontSize', originalFontSize + 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + atom.config.set('editor.fontSize', originalFontSize - 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + }) + }) + + describe('::openLicense()', () => { + it('opens the license as plain-text in a buffer', () => { + waitsForPromise(() => workspace.openLicense()) + runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)) + }) + }) + + describe('::isTextEditor(obj)', () => { + it('returns true when the passed object is an instance of `TextEditor`', () => { + expect(workspace.isTextEditor(new TextEditor())).toBe(true) + expect(workspace.isTextEditor({getText: () => null})).toBe(false) + expect(workspace.isTextEditor(null)).toBe(false) + expect(workspace.isTextEditor(undefined)).toBe(false) + }) + }) + + describe('::observeTextEditors()', () => { + it('invokes the observer with current and future text editors', () => { + const observed = [] + + waitsForPromise(() => workspace.open()) + waitsForPromise(() => workspace.open()) + waitsForPromise(() => workspace.openLicense()) + + runs(() => workspace.observeTextEditors(editor => observed.push(editor))) + + waitsForPromise(() => workspace.open()) + + expect(observed).toEqual(workspace.getTextEditors()) + }) + }) + + describe('when an editor is destroyed', () => { + it('removes the editor', () => { + let editor = null + + waitsForPromise(() => workspace.open('a').then(e => { editor = e })) + + runs(() => { + expect(workspace.getTextEditors()).toHaveLength(1) + editor.destroy() + expect(workspace.getTextEditors()).toHaveLength(0) + }) + }) + }) + + describe('when an editor is copied because its pane is split', () => { + it('sets up the new editor to be configured by the text editor registry', () => { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + + waitsForPromise(() => + workspace.open('a').then(editor => { + atom.textEditors.setGrammarOverride(editor, 'source.js') + expect(editor.getGrammar().name).toBe('JavaScript') + + workspace.getActivePane().splitRight({copyActiveItem: true}) + const newEditor = workspace.getActiveTextEditor() + expect(newEditor).not.toBe(editor) + expect(newEditor.getGrammar().name).toBe('JavaScript') + }) + ) + }) + }) + + it('stores the active grammars used by all the open editors', () => { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + waitsForPromise(() => atom.packages.activatePackage('language-todo')) + + waitsForPromise(() => atom.workspace.open('sample.coffee')) + + runs(function () { + atom.workspace.getActiveTextEditor().setText(`\ +i = /test/; #FIXME\ +` + ) + + const atom2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) + atom2.initialize({ + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div') + } + ) + }) + + atom2.packages.loadPackage('language-javascript') + atom2.packages.loadPackage('language-coffee-script') + atom2.packages.loadPackage('language-todo') + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) + + expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ + 'CoffeeScript', + 'CoffeeScript (Literate)', + 'JavaScript', + 'Null Grammar', + 'Regular Expression Replacement (JavaScript)', + 'Regular Expressions (JavaScript)', + 'TODO' + ]) + + atom2.destroy() + }) + }) + + describe('document.title', () => { + describe('when there is no item open', () => { + 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(() => + waitsForPromise(() => + atom.workspace.open('b').then(() => atom.project.setPaths([])) + ) + ) + + it("sets the title to the pane item's title plus the item's path", () => { + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + }) + + describe('when the title of the active pane item changes', () => { + it("updates the window title based on the item's new title", () => { + const editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(temp.dir, 'hi')) + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) + expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) + }) + }) + + describe("when the active pane's item changes", () => { + it("updates the title to the new item's title plus the project path", () => { + atom.workspace.getActivePane().activateNextItem() + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + }) + }) + + describe("when an inactive pane's item changes", () => { + it('does not update the title', () => { + const pane = atom.workspace.getActivePane() + pane.splitRight() + const initialTitle = document.title + pane.activateNextItem() + expect(document.title).toBe(initialTitle) + }) + }) + }) + + describe('when the active pane item is inside a project path', () => { + beforeEach(() => + waitsForPromise(() => atom.workspace.open('b')) + ) + + describe('when there is an active pane item', () => { + it("sets the title to the pane item's title plus the project path", () => { + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + }) + }) + + describe('when the title of the active pane item changes', () => { + it("updates the window title based on the item's new title", () => { + const editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) + }) + }) + + describe("when the active pane's item changes", () => { + it("updates the title to the new item's title plus the project path", () => { + atom.workspace.getActivePane().activateNextItem() + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + }) + }) + + describe('when the last pane item is removed', () => { + it("updates the title to the project's first path", () => { + atom.workspace.getActivePane().destroy() + expect(atom.workspace.getActivePaneItem()).toBeUndefined() + 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', () => { + const pane = atom.workspace.getActivePane() + pane.splitRight() + const initialTitle = document.title + pane.activateNextItem() + expect(document.title).toBe(initialTitle) + }) + }) + }) + + describe('when the workspace is deserialized', () => { + beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))) + + it("updates the title to contain the project's path", () => { + document.title = null + + const atom2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) + atom2.initialize({ + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div') + } + ) + }) + + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) + const item = atom2.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)) + + atom2.destroy() + }) + }) + }) + + describe('document edited status', () => { + let item1 + let item2 + + beforeEach(() => { + waitsForPromise(() => atom.workspace.open('a')) + waitsForPromise(() => atom.workspace.open('b')) + runs(() => { + [item1, item2] = atom.workspace.getPaneItems() + }) + }) + + it('calls setDocumentEdited when the active item changes', () => { + expect(atom.workspace.getActivePaneItem()).toBe(item2) + item1.insertText('a') + expect(item1.isModified()).toBe(true) + atom.workspace.getActivePane().activateNextItem() + + expect(setDocumentEdited).toHaveBeenCalledWith(true) + }) + + it("calls atom.setDocumentEdited when the active item's modified status changes", () => { + expect(atom.workspace.getActivePaneItem()).toBe(item2) + item2.insertText('a') + advanceClock(item2.getBuffer().getStoppedChangingDelay()) + + expect(item2.isModified()).toBe(true) + expect(setDocumentEdited).toHaveBeenCalledWith(true) + + item2.undo() + advanceClock(item2.getBuffer().getStoppedChangingDelay()) + + expect(item2.isModified()).toBe(false) + expect(setDocumentEdited).toHaveBeenCalledWith(false) + }) + }) + + describe('adding panels', () => { + class TestItem {} + + // Don't use ES6 classes because then we'll have to call `super()` which we can't do with + // HTMLElement + function TestItemElement () { this.constructor = TestItemElement } + function Ctor () { this.constructor = TestItemElement } + Ctor.prototype = HTMLElement.prototype + TestItemElement.prototype = new Ctor() + TestItemElement.__super__ = HTMLElement.prototype + TestItemElement.prototype.initialize = function (model) { this.model = model; return this } + TestItemElement.prototype.getModel = function () { return this.model } + + beforeEach(() => + atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model)) + ) + + describe('::addLeftPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getLeftPanels().length).toBe(0) + atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addLeftPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addRightPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getRightPanels().length).toBe(0) + atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addRightPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addTopPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getTopPanels().length).toBe(0) + atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addTopPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addBottomPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getBottomPanels().length).toBe(0) + atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addBottomPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addHeaderPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getHeaderPanels().length).toBe(0) + atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addHeaderPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addFooterPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getFooterPanels().length).toBe(0) + atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addFooterPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::addModalPanel(model)', () => { + it('adds a panel to the correct panel container', () => { + let addPanelSpy + expect(atom.workspace.getModalPanels().length).toBe(0) + atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()) + + const model = new TestItem() + const panel = atom.workspace.addModalPanel({item: model}) + + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) + + const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + expect(itemView.getModel()).toBe(model) + }) + }) + + describe('::panelForItem(item)', () => { + it('returns the panel associated with the item', () => { + const item = new TestItem() + const panel = atom.workspace.addLeftPanel({item}) + + const itemWithNoPanel = new TestItem() + + expect(atom.workspace.panelForItem(item)).toBe(panel) + expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null) + }) + }) + }) + + describe('::scan(regex, options, callback)', () => { + describe('when called with a regex', () => { + it('calls the callback with all regex results in all files in the project', () => { + const results = [] + waitsForPromise(() => + atom.workspace.scan( + /(a)+/, {leadingContextLineCount: 1, trailingContextLineCount: 1}, + result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(3) + expect(results[0].filePath).toBe(atom.project.getDirectories()[0].resolve('a')) + expect(results[0].matches).toHaveLength(3) + expect(results[0].matches[0]).toEqual({ + matchText: 'aaa', + lineText: 'aaa bbb', + lineTextOffset: 0, + range: [[0, 0], [0, 3]], + leadingContextLines: [], + trailingContextLines: ['cc aa cc'] + }) + }) + }) + + it('works with with escaped literals (like $ and ^)', () => { + const results = [] + waitsForPromise(() => atom.workspace.scan( + /\$\w+/, {leadingContextLineCount: 1, trailingContextLineCount: 1}, + result => results.push(result))) + + runs(() => { + expect(results.length).toBe(1) + const {filePath, matches} = results[0] + expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a')) + expect(matches).toHaveLength(1) + expect(matches[0]).toEqual({ + matchText: '$bill', + lineText: 'dollar$bill', + lineTextOffset: 0, + range: [[2, 6], [2, 11]], + leadingContextLines: ['cc aa cc'], + trailingContextLines: [] + }) + }) + }) + + it('works on evil filenames', () => { + atom.config.set('core.excludeVcsIgnoredPaths', false) + platform.generateEvilFiles() + atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) + const paths = [] + let matches = [] + waitsForPromise(() => + atom.workspace.scan(/evil/, result => { + paths.push(result.filePath) + matches = matches.concat(result.matches) + }) + ) + + runs(() => { + _.each(matches, m => expect(m.matchText).toEqual('evil')) + + if (platform.isWindows()) { + expect(paths.length).toBe(3) + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) + expect(paths[1]).toMatch(/file with spaces.txt$/) + expect(path.basename(paths[2])).toBe('utfa\u0306.md') + } else { + expect(paths.length).toBe(5) + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) + expect(paths[1]).toMatch(/file with spaces.txt$/) + expect(paths[2]).toMatch(/goddam\nnewlines$/m) + expect(paths[3]).toMatch(/quote".txt$/m) + expect(path.basename(paths[4])).toBe('utfa\u0306.md') + } + }) + }) + + it('ignores case if the regex includes the `i` flag', () => { + const results = [] + waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))) + + runs(() => expect(results).toHaveLength(1)) + }) + + describe('when the core.excludeVcsIgnoredPaths config is truthy', () => { + let projectPath + let ignoredPath + + beforeEach(() => { + const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') + projectPath = path.join(temp.mkdirSync('atom')) + + const writerStream = fstream.Writer(projectPath) + fstream.Reader(sourceProjectPath).pipe(writerStream) + + waitsFor(done => { + writerStream.on('close', done) + writerStream.on('error', done) + }) + + runs(() => { + fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + ignoredPath = path.join(projectPath, 'ignored.txt') + fs.writeFileSync(ignoredPath, 'this match should not be included') + }) + }) + + afterEach(() => { + if (fs.existsSync(projectPath)) { + fs.removeSync(projectPath) + } + }) + + it('excludes ignored files', () => { + atom.project.setPaths([projectPath]) + atom.config.set('core.excludeVcsIgnoredPaths', true) + const resultHandler = jasmine.createSpy('result found') + waitsForPromise(() => + atom.workspace.scan(/match/, results => resultHandler()) + ) + + runs(() => expect(resultHandler).not.toHaveBeenCalled()) + }) + }) + + it('includes only files when a directory filter is specified', () => { + const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) + atom.project.setPaths([projectPath]) + + const filePath = path.join(projectPath, 'a-dir', 'oh-git') + + const paths = [] + let matches = [] + waitsForPromise(() => + atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, result => { + paths.push(result.filePath) + matches = matches.concat(result.matches) + }) + ) + + runs(() => { + expect(paths.length).toBe(1) + expect(paths[0]).toBe(filePath) + expect(matches.length).toBe(1) + }) + }) + + it("includes files and folders that begin with a '.'", () => { + const projectPath = temp.mkdirSync('atom-spec-workspace') + const filePath = path.join(projectPath, '.text') + fs.writeFileSync(filePath, 'match this') + atom.project.setPaths([projectPath]) + const paths = [] + let matches = [] + waitsForPromise(() => + atom.workspace.scan(/match this/, result => { + paths.push(result.filePath) + matches = matches.concat(result.matches) + }) + ) + + runs(() => { + expect(paths.length).toBe(1) + expect(paths[0]).toBe(filePath) + expect(matches.length).toBe(1) + }) + }) + + it('excludes values in core.ignoredNames', () => { + const ignoredNames = atom.config.get('core.ignoredNames') + ignoredNames.push('a') + atom.config.set('core.ignoredNames', ignoredNames) + + const resultHandler = jasmine.createSpy('result found') + waitsForPromise(() => + atom.workspace.scan(/dollar/, results => resultHandler()) + ) + + runs(() => expect(resultHandler).not.toHaveBeenCalled()) + }) + + it('scans buffer contents if the buffer is modified', () => { + let editor = null + const results = [] + + waitsForPromise(() => + atom.workspace.open('a').then(o => { + editor = o + editor.setText('Elephant') + }) + ) + + waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))) + + runs(() => { + expect(results).toHaveLength(3) + const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a') + expect(resultForA.matches).toHaveLength(1) + expect(resultForA.matches[0].matchText).toBe('Elephant') + }) + }) + + it('ignores buffers outside the project', () => { + let editor = null + const results = [] + + waitsForPromise(() => + atom.workspace.open(temp.openSync().path).then(o => { + editor = o + editor.setText('Elephant') + }) + ) + + waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))) + + runs(() => expect(results).toHaveLength(0)) + }) + + describe('when the project has multiple root directories', () => { + let dir1 + let dir2 + let file1 + let file2 + + beforeEach(() => { + dir1 = atom.project.getPaths()[0] + file1 = path.join(dir1, 'a-dir', 'oh-git') + + dir2 = temp.mkdirSync('a-second-dir') + const aDir2 = path.join(dir2, 'a-dir') + file2 = path.join(aDir2, 'a-file') + fs.mkdirSync(aDir2) + fs.writeFileSync(file2, 'ccc aaaa') + + atom.project.addPath(dir2) + }) + + it("searches matching files in all of the project's root directories", () => { + const resultPaths = [] + waitsForPromise(() => + atom.workspace.scan(/aaaa/, ({filePath}) => resultPaths.push(filePath)) + ) + + runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())) + }) + + describe('when an inclusion path starts with the basename of a root directory', () => { + it('interprets the inclusion path as starting from that directory', () => { + waitsForPromise(() => { + const resultPaths = [] + return atom.workspace + .scan(/aaaa/, {paths: ['dir']}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } + }) + .then(() => expect(resultPaths).toEqual([file1])) + }) + + waitsForPromise(() => { + const resultPaths = [] + return atom.workspace + .scan(/aaaa/, {paths: [path.join('dir', 'a-dir')]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } + }) + .then(() => expect(resultPaths).toEqual([file1])) + }) + + waitsForPromise(() => { + const resultPaths = [] + return atom.workspace + .scan(/aaaa/, {paths: [path.basename(dir2)]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } + }) + .then(() => expect(resultPaths).toEqual([file2])) + }) + + waitsForPromise(() => { + const resultPaths = [] + return atom.workspace + .scan(/aaaa/, {paths: [path.join(path.basename(dir2), 'a-dir')]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } + }) + .then(() => expect(resultPaths).toEqual([file2])) + }) + }) + }) + + describe('when a custom directory searcher is registered', () => { + let fakeSearch = null + // Function that is invoked once all of the fields on fakeSearch are set. + let onFakeSearchCreated = null + + class FakeSearch { + constructor (options) { + // Note that hoisting resolve and reject in this way is generally frowned upon. + this.options = options + this.promise = new Promise((resolve, reject) => { + this.hoistedResolve = resolve + this.hoistedReject = reject + if (typeof onFakeSearchCreated === 'function') { + onFakeSearchCreated(this) + } + }) + } + then (...args) { + return this.promise.then.apply(this.promise, args) + } + cancel () { + this.cancelled = true + // According to the spec for a DirectorySearcher, invoking `cancel()` should + // resolve the thenable rather than reject it. + this.hoistedResolve() + } + } + + beforeEach(() => { + fakeSearch = null + onFakeSearchCreated = null + atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { + canSearchDirectory (directory) { return directory.getPath() === dir1 }, + search (directory, regex, options) { + fakeSearch = new FakeSearch(options) + return fakeSearch + } + }) + + waitsFor(() => atom.workspace.directorySearchers.length > 0) + }) + + it('can override the DefaultDirectorySearcher on a per-directory basis', () => { + const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' + const numPathsSearchedInDir2 = 1 + const numPathsToPretendToSearchInCustomDirectorySearcher = 10 + const searchResult = { + filePath: foreignFilePath, + matches: [ + { + lineText: 'Hello world', + lineTextOffset: 0, + matchText: 'Hello', + range: [[0, 0], [0, 5]] + } + ] + } + onFakeSearchCreated = fakeSearch => { + fakeSearch.options.didMatch(searchResult) + fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) + fakeSearch.hoistedResolve() + } + + const resultPaths = [] + const onPathsSearched = jasmine.createSpy('onPathsSearched') + waitsForPromise(() => + atom.workspace.scan(/aaaa/, {onPathsSearched}, ({filePath}) => resultPaths.push(filePath)) + ) + + runs(() => { + expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) + // onPathsSearched should be called once by each DirectorySearcher. The order is not + // guaranteed, so we can only verify the total number of paths searched is correct + // after the second call. + expect(onPathsSearched.callCount).toBe(2) + expect(onPathsSearched.mostRecentCall.args[0]).toBe( + numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) + }) + }) + + it('can be cancelled when the object returned by scan() has its cancel() method invoked', () => { + const thenable = atom.workspace.scan(/aaaa/, () => {}) + let resultOfPromiseSearch = null + + waitsFor('fakeSearch to be defined', () => fakeSearch != null) + + runs(() => { + expect(fakeSearch.cancelled).toBe(undefined) + thenable.cancel() + expect(fakeSearch.cancelled).toBe(true) + }) + + waitsForPromise(() => thenable.then(promiseResult => { resultOfPromiseSearch = promiseResult })) + + runs(() => expect(resultOfPromiseSearch).toBe('cancelled')) + }) + + it('will have the side-effect of failing the overall search if it fails', () => { + // This provider's search should be cancelled when the first provider fails + let cancelableSearch + let fakeSearch2 = null + atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { + canSearchDirectory (directory) { return directory.getPath() === dir2 }, + search (directory, regex, options) { + fakeSearch2 = new FakeSearch(options) + return fakeSearch2 + } + }) + + let didReject = false + const promise = cancelableSearch = atom.workspace.scan(/aaaa/, () => {}) + waitsFor('fakeSearch to be defined', () => fakeSearch != null) + + runs(() => fakeSearch.hoistedReject()) + + waitsForPromise(() => cancelableSearch.catch(() => { didReject = true })) + + waitsFor(done => promise.then(null, done)) + + runs(() => { + expect(didReject).toBe(true) + expect(fakeSearch2.cancelled).toBe(true) + }) + }) + }) + }) + }) + }) // Cancels other ongoing searches + + describe('::replace(regex, replacementText, paths, iterator)', () => { + let filePath + let commentFilePath + let sampleContent + let sampleCommentContent + + beforeEach(() => { + atom.project.setPaths([atom.project.getDirectories()[0].resolve('../')]) + + filePath = atom.project.getDirectories()[0].resolve('sample.js') + commentFilePath = atom.project.getDirectories()[0].resolve('sample-with-comments.js') + sampleContent = fs.readFileSync(filePath).toString() + sampleCommentContent = fs.readFileSync(commentFilePath).toString() + }) + + afterEach(() => { + fs.writeFileSync(filePath, sampleContent) + fs.writeFileSync(commentFilePath, sampleCommentContent) + }) + + describe("when a file doesn't exist", () => { + it('calls back with an error', () => { + const errors = [] + const missingPath = path.resolve('/not-a-file.js') + expect(fs.existsSync(missingPath)).toBeFalsy() + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [missingPath], (result, error) => errors.push(error)) + ) + + runs(() => { + expect(errors).toHaveLength(1) + expect(errors[0].path).toBe(missingPath) + }) + }) + }) + + describe('when called with unopened files', () => { + it('replaces properly', () => { + const results = [] + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(6) + }) + }) + }) + + describe('when a buffer is already open', () => { + it('replaces properly and saves when not modified', () => { + let editor = null + const results = [] + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) + + runs(() => expect(editor.isModified()).toBeFalsy()) + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(6) + + expect(editor.isModified()).toBeFalsy() + }) + }) + + it('does not replace when the path is not specified', () => { + const results = [] + + waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [commentFilePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(commentFilePath) + }) + }) + + it('does NOT save when modified', () => { + let editor = null + const results = [] + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) + + runs(() => { + editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') + expect(editor.isModified()).toBeTruthy() + }) + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(6) + + expect(editor.isModified()).toBeTruthy() + }) + }) + }) + }) + + describe('::saveActivePaneItem()', () => { + let editor = null + beforeEach(() => + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) + ) + + describe('when there is an error', () => { + it('emits a warning notification when the file cannot be saved', () => { + let addedSpy + spyOn(editor, 'save').andCallFake(() => { + throw new Error("'/some/file' is a directory") + }) + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) + + it('emits a warning notification when the directory cannot be written to', () => { + let addedSpy + spyOn(editor, 'save').andCallFake(() => { + throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") + }) + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) + + it('emits a warning notification when the user does not have permission', () => { + let addedSpy + spyOn(editor, 'save').andCallFake(() => { + const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") + error.code = 'EACCES' + error.path = '/Some/dir/and-a-file.js' + throw error + }) + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) + + it('emits a warning notification when the operation is not permitted', () => { + spyOn(editor, 'save').andCallFake(() => { + const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") + error.code = 'EPERM' + error.path = '/Some/dir/and-a-file.js' + throw error + }) + }) + + it('emits a warning notification when the file is already open by another app', () => { + let addedSpy + spyOn(editor, 'save').andCallFake(() => { + const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") + error.code = 'EBUSY' + error.path = '/Some/dir/and-a-file.js' + throw error + }) + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + + const notificaiton = addedSpy.mostRecentCall.args[0] + expect(notificaiton.getType()).toBe('warning') + expect(notificaiton.getMessage()).toContain('Unable to save') + }) + + it('emits a warning notification when the file system is read-only', () => { + let addedSpy + spyOn(editor, 'save').andCallFake(() => { + const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") + error.code = 'EROFS' + error.path = '/Some/dir/and-a-file.js' + throw error + }) + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + + const notification = addedSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Unable to save') + }) + + it('emits a warning notification when the file cannot be saved', () => { + spyOn(editor, 'save').andCallFake(() => { + throw new Error('no one knows') + }) + + const save = () => atom.workspace.saveActivePaneItem() + expect(save).toThrow() + }) + }) + }) + + describe('::closeActivePaneItemOrEmptyPaneOrWindow', () => { + beforeEach(() => { + spyOn(atom, 'close') + waitsForPromise(() => atom.workspace.open()) + }) + + it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', () => { + atom.config.set('core.destroyEmptyPanes', false) + + const pane1 = atom.workspace.getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + + expect(atom.workspace.getPanes().length).toBe(2) + expect(pane2.getItems().length).toBe(1) + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + + expect(atom.workspace.getPanes().length).toBe(2) + expect(pane2.getItems().length).toBe(0) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + + expect(atom.workspace.getPanes().length).toBe(1) + expect(pane1.getItems().length).toBe(1) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.workspace.getPanes().length).toBe(1) + expect(pane1.getItems().length).toBe(0) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.workspace.getPanes().length).toBe(1) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.close).toHaveBeenCalled() + }) + }) + + describe('when the core.allowPendingPaneItems option is falsey', () => { + it('does not open item with `pending: true` option as pending', () => { + let pane = null + atom.config.set('core.allowPendingPaneItems', false) + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true}).then(() => { + pane = atom.workspace.getActivePane() + }) + ) + + runs(() => expect(pane.getPendingItem()).toBeFalsy()) + }) + }) + + describe('grammar activation', () => { + it('notifies the workspace of which grammar is used', () => { + atom.packages.triggerDeferredActivationHooks() + + const javascriptGrammarUsed = jasmine.createSpy('js grammar used') + const rubyGrammarUsed = jasmine.createSpy('ruby grammar used') + const cGrammarUsed = jasmine.createSpy('c grammar used') + + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed) + atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed) + atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed) + + waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + waitsForPromise(() => atom.packages.activatePackage('language-c')) + waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) + + runs(() => { + // Hooks are triggered when opening new editors + expect(javascriptGrammarUsed).toHaveBeenCalled() + + // Hooks are triggered when changing existing editors grammars + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')) + expect(cGrammarUsed).toHaveBeenCalled() + + // Hooks are triggered when editors are added in other ways. + atom.workspace.getActivePane().splitRight({copyActiveItem: true}) + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) + expect(rubyGrammarUsed).toHaveBeenCalled() + }) + }) + }) + + describe('.checkoutHeadRevision()', () => { + let editor = null + beforeEach(() => { + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => { editor = o })) + }) + + it('reverts to the version of its file checked into the project repository', () => { + editor.setCursorBufferPosition([0, 0]) + editor.insertText('---\n') + expect(editor.lineTextForBufferRow(0)).toBe('---') + + waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) + + runs(() => expect(editor.lineTextForBufferRow(0)).toBe('')) + }) + + describe("when there's no repository for the editor's file", () => { + it("doesn't do anything", () => { + editor = new TextEditor() + editor.setText('stuff') + atom.workspace.checkoutHeadRevision(editor) + + waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) + }) + }) + }) +}) + +const escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 766ba7aa8..e69efa458 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{screen, ipcRenderer, remote, shell, webFrame} = require 'electron' +{ipcRenderer, remote, shell} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' getWindowLoadSettings = require './get-window-load-settings' @@ -80,6 +80,12 @@ class ApplicationDelegate setWindowFullScreen: (fullScreen=false) -> ipcHelpers.call('window-method', 'setFullScreen', fullScreen) + onDidEnterFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) + + onDidLeaveFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) + openWindowDevTools: -> # Defer DevTools interaction to the next tick, because using them during # event handling causes some wrong input events to be triggered on @@ -254,20 +260,6 @@ class ApplicationDelegate openExternal: (url) -> shell.openExternal(url) - disableZoom: -> - outerCallback = -> - webFrame.setZoomLevelLimits(1, 1) - - outerCallback() - # Set the limits every time a display is added or removed, otherwise the - # configuration gets reset to the default, which allows zooming the - # webframe. - screen.on('display-added', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - checkForUpdate: -> ipcRenderer.send('command', 'application:check-for-update') @@ -285,3 +277,14 @@ class ApplicationDelegate emitDidSavePath: (path) -> ipcRenderer.sendSync('did-save-path', path) + + resolveProxy: (requestId, url) -> + ipcRenderer.send('resolve-proxy', requestId, url) + + onDidResolveProxy: (callback) -> + outerCallback = (event, requestId, proxy) -> + callback(requestId, proxy) + + ipcRenderer.on('did-resolve-proxy', outerCallback) + new Disposable -> + ipcRenderer.removeListener('did-resolve-proxy', outerCallback) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 3133b5af8..a34cdf9b1 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -13,6 +13,7 @@ StateStore = require './state-store' StorageFolder = require './storage-folder' registerDefaultCommands = require './register-default-commands' {updateProcessEnv} = require './update-process-env' +ConfigSchema = require './config-schema' DeserializerManager = require './deserializer-manager' ViewRegistry = require './view-registry' @@ -131,64 +132,45 @@ class AtomEnvironment extends Model # Call .loadOrCreate instead constructor: (params={}) -> - {@blobStore, @applicationDelegate, @window, @document, @clipboard, @configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params + {@applicationDelegate, @clipboard, @enablePersistence, onlyLoadBaseStyleSheets} = params + @nextProxyRequestId = 0 @unloaded = false @loadTime = null - {devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings() - @emitter = new Emitter @disposables = new CompositeDisposable + @deserializers = new DeserializerManager(this) + @deserializeTimings = {} + @views = new ViewRegistry(this) + @notifications = new NotificationManager @stateStore = new StateStore('AtomEnvironments', 1) - if clearWindowState - @getStorageFolder().clear() - @stateStore.clear() - - @deserializers = new DeserializerManager(this) - @deserializeTimings = {} - - @views = new ViewRegistry(this) - - @notifications = new NotificationManager - - @config = new Config({@configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence}) - @setConfigSchema() - - @keymaps = new KeymapManager({@configDirPath, resourcePath, notificationManager: @notifications}) + @config = new Config({notificationManager: @notifications, @enablePersistence}) + @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} + @keymaps = new KeymapManager({notificationManager: @notifications}) @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) - @commands = new CommandRegistry - @commands.attach(@window) - @grammars = new GrammarRegistry({@config}) - - @styles = new StyleManager({@configDirPath}) - + @styles = new StyleManager() @packages = new PackageManager({ - devMode, @configDirPath, resourcePath, safeMode, @config, styleManager: @styles, + @config, styleManager: @styles, commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views }) - @themes = new ThemeManager({ - packageManager: @packages, @configDirPath, resourcePath, safeMode, @config, - styleManager: @styles, notificationManager: @notifications, viewRegistry: @views + packageManager: @packages, @config, styleManager: @styles, + notificationManager: @notifications, viewRegistry: @views }) - - @menu = new MenuManager({resourcePath, keymapManager: @keymaps, packageManager: @packages}) - - @contextMenu = new ContextMenuManager({resourcePath, devMode, keymapManager: @keymaps}) - + @menu = new MenuManager({keymapManager: @keymaps, packageManager: @packages}) + @contextMenu = new ContextMenuManager({keymapManager: @keymaps}) @packages.setMenuManager(@menu) @packages.setContextMenuManager(@contextMenu) @packages.setThemeManager(@themes) @project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate}) - - @commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate) + @commandInstaller = new CommandInstaller(@applicationDelegate) @textEditors = new TextEditorRegistry({ @config, grammarRegistry: @grammars, assert: @assert.bind(this), @@ -205,6 +187,57 @@ class AtomEnvironment extends Model @autoUpdater = new AutoUpdateManager({@applicationDelegate}) + if @keymaps.canLoadBundledKeymapsFromMemory() + @keymaps.loadBundledKeymaps() + + @registerDefaultCommands() + @registerDefaultOpeners() + @registerDefaultDeserializers() + @registerDefaultViewProviders() + + @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) + + @history = new HistoryManager({@project, @commands, @stateStore}) + # Keep instances of HistoryManager in sync + @disposables.add @history.onDidChangeProjects (e) => + @applicationDelegate.didChangeHistoryManager() unless e.reloaded + + initialize: (params={}) -> + {@window, @document, @blobStore, @configDirPath, onlyLoadBaseStyleSheets} = params + {devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings() + + if clearWindowState + @getStorageFolder().clear() + @stateStore.clear() + + @views.initialize() + + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + @config.initialize({@configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome}) + + @menu.initialize({resourcePath}) + @contextMenu.initialize({resourcePath, devMode}) + + @keymaps.configDirPath = @configDirPath + @keymaps.resourcePath = resourcePath + @keymaps.devMode = devMode + unless @keymaps.canLoadBundledKeymapsFromMemory() + @keymaps.loadBundledKeymaps() + + @commands.attach(@window) + + @styles.initialize({@configDirPath}) + @packages.initialize({devMode, @configDirPath, resourcePath, safeMode}) + @themes.initialize({@configDirPath, resourcePath, safeMode}) + + @commandInstaller.initialize(@getVersion()) + @workspace.initialize() + @autoUpdater.initialize() + @config.load() @themes.loadBaseStylesheets() @@ -215,30 +248,17 @@ class AtomEnvironment extends Model @stylesElement = @styles.buildStylesElement() @document.head.appendChild(@stylesElement) - @disposables.add(@applicationDelegate.disableZoom()) - @keymaps.subscribeToFileReadFailure() - @keymaps.loadBundledKeymaps() - - @registerDefaultCommands() - @registerDefaultOpeners() - @registerDefaultDeserializers() - @registerDefaultViewProviders() @installUncaughtErrorHandler() @attachSaveStateListeners() - @installWindowEventHandler() + @windowEventHandler.initialize(@window, @document) @observeAutoHideMenuBar() - @history = new HistoryManager({@project, @commands, localStorage}) - # Keep instances of HistoryManager in sync - @history.onDidChangeProjects (e) => - @applicationDelegate.didChangeHistoryManager() unless e.reloaded + @history.initialize(@window.localStorage) @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)}) - attachSaveStateListeners: -> saveState = _.debounce((=> window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded @@ -249,9 +269,6 @@ class AtomEnvironment extends Model @document.removeEventListener('mousedown', saveState, true) @document.removeEventListener('keydown', saveState, true) - setConfigSchema: -> - @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} - registerDefaultDeserializers: -> @deserializers.add(Workspace) @deserializers.add(PaneContainer) @@ -304,7 +321,7 @@ class AtomEnvironment extends Model @registerDefaultDeserializers() @config.clear() - @setConfigSchema() + @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} @keymaps.clear() @keymaps.loadBundledKeymaps() @@ -716,7 +733,14 @@ class AtomEnvironment extends Model @openInitialEmptyEditorIfNecessary() - Promise.all([loadStatePromise, updateProcessEnvPromise]) + loadHistoryPromise = @history.loadState().then => + @reopenProjectMenuManager = new ReopenProjectMenuManager({ + @menu, @commands, @history, @config, + open: (paths) => @open(pathsToOpen: paths) + }) + @reopenProjectMenuManager.update() + + Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) serialize: (options) -> version: @constructor.version @@ -736,6 +760,10 @@ class AtomEnvironment extends Model @saveBlobStoreSync() @unloaded = true + saveBlobStoreSync: -> + if @enablePersistence + @blobStore.save() + openInitialEmptyEditorIfNecessary: -> return unless @config.get('core.openEmptyEditorOnStart') if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0 @@ -747,7 +775,10 @@ class AtomEnvironment extends Model @lastUncaughtError = Array::slice.call(arguments) [message, url, line, column, originalError] = @lastUncaughtError - {line, column} = mapSourcePosition({source: url, line, column}) + {line, column, source} = mapSourcePosition({source: url, line, column}) + + if url is '' + url = source eventObject = {message, url, line, column, originalError} @@ -765,10 +796,12 @@ class AtomEnvironment extends Model @window.onerror = @previousWindowErrorHandler installWindowEventHandler: -> - @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate, @window, @document}) + @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) + @windowEventHandler.initialize(@window, @document) uninstallWindowEventHandler: -> @windowEventHandler?.unsubscribe() + @windowEventHandler = null ### Section: Messaging the User @@ -840,6 +873,8 @@ class AtomEnvironment extends Model error.metadata = callbackOrMetadata @emitter.emit 'did-fail-assertion', error + unless @isReleasedVersion() + throw error false @@ -867,11 +902,6 @@ class AtomEnvironment extends Model showSaveDialogSync: (options={}) -> @applicationDelegate.showSaveDialog(options) - saveBlobStoreSync: -> - return unless @enablePersistence - - @blobStore.save() - saveState: (options) -> new Promise (resolve, reject) => if @enablePersistence and @project @@ -988,6 +1018,16 @@ class AtomEnvironment extends Model return + resolveProxy: (url) -> + return new Promise (resolve, reject) => + requestId = @nextProxyRequestId++ + disposable = @applicationDelegate.onDidResolveProxy (id, proxy) -> + if id is requestId + disposable.dispose() + resolve(proxy) + + @applicationDelegate.resolveProxy(requestId, url) + # Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = (callback) -> deprecate("Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done") diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js index fb6325a26..111147f32 100644 --- a/src/auto-update-manager.js +++ b/src/auto-update-manager.js @@ -7,21 +7,23 @@ export default class AutoUpdateManager { this.applicationDelegate = applicationDelegate this.subscriptions = new CompositeDisposable() this.emitter = new Emitter() + } + initialize () { this.subscriptions.add( - applicationDelegate.onDidBeginCheckingForUpdate(() => { + this.applicationDelegate.onDidBeginCheckingForUpdate(() => { this.emitter.emit('did-begin-checking-for-update') }), - applicationDelegate.onDidBeginDownloadingUpdate(() => { + this.applicationDelegate.onDidBeginDownloadingUpdate(() => { this.emitter.emit('did-begin-downloading-update') }), - applicationDelegate.onDidCompleteDownloadingUpdate((details) => { + this.applicationDelegate.onDidCompleteDownloadingUpdate((details) => { this.emitter.emit('did-complete-downloading-update', details) }), - applicationDelegate.onUpdateNotAvailable(() => { + this.applicationDelegate.onUpdateNotAvailable(() => { this.emitter.emit('update-not-available') }), - applicationDelegate.onUpdateError(() => { + this.applicationDelegate.onUpdateError(() => { this.emitter.emit('update-error') }) ) diff --git a/src/babel.js b/src/babel.js index d72b29ffd..a944f2e8c 100644 --- a/src/babel.js +++ b/src/babel.js @@ -6,7 +6,6 @@ var defaultOptions = require('../static/babelrc.json') var babel = null var babelVersionDirectory = null -var options = null var PREFIXES = [ '/** @babel */', @@ -48,27 +47,16 @@ 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, '/') } - options.filename = filePath - + var options = {filename: filePath} + for (var key in defaultOptions) { + options[key] = defaultOptions[key] + } return babel.transform(sourceCode, options).code } diff --git a/src/command-installer.coffee b/src/command-installer.coffee index e37e6a0e6..7873014fa 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -26,7 +26,9 @@ symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) -> module.exports = class CommandInstaller - constructor: (@appVersion, @applicationDelegate) -> + constructor: (@applicationDelegate) -> + + initialize: (@appVersion) -> getInstallDirectory: -> "/usr/local/bin" diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 056446203..a419ac08c 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -91,19 +91,20 @@ class CommandRegistry # # Returns a {Disposable} on which `.dispose()` can be called to remove the # added command handler(s). - add: (target, commandName, callback) -> + add: (target, commandName, callback, throwOnInvalidSelector = true) -> if typeof commandName is 'object' commands = commandName + throwOnInvalidSelector = callback disposable = new CompositeDisposable for commandName, callback of commands - disposable.add @add(target, commandName, callback) + disposable.add @add(target, commandName, callback, throwOnInvalidSelector) return disposable if typeof callback isnt 'function' throw new Error("Can't register a command with non-function callback.") if typeof target is 'string' - validateSelector(target) + validateSelector(target) if throwOnInvalidSelector @addSelectorBasedListener(target, commandName, callback) else @addInlineListener(target, commandName, callback) diff --git a/src/compile-cache.js b/src/compile-cache.js index 8a4451d90..3483f8509 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -7,6 +7,7 @@ var path = require('path') var fs = require('fs-plus') +var sourceMapSupport = require('source-map-support') var PackageTranspilationRegistry = require('./package-transpilation-registry') var CSON = null @@ -113,109 +114,119 @@ function writeCachedJavascript (relativeCachePath, code) { var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg -require('source-map-support').install({ - handleUncaughtExceptions: false, +exports.install = function (resourcesPath, nodeRequire) { + sourceMapSupport.install({ + handleUncaughtExceptions: false, - // Most of this logic is the same as the default implementation in the - // source-map-support module, but we've overridden it to read the javascript - // code from our cache directory. - retrieveSourceMap: function (filePath) { - if (!cacheDirectory || !fs.isFileSync(filePath)) { - return null - } + // Most of this logic is the same as the default implementation in the + // source-map-support module, but we've overridden it to read the javascript + // code from our cache directory. + retrieveSourceMap: function (filePath) { + if (filePath === '') { + return { + map: snapshotResult.sourceMap, // eslint-disable-line no-undef + url: path.join(resourcesPath, 'app', 'static', 'index.js') + } + } - try { - var sourceCode = fs.readFileSync(filePath, 'utf8') - } catch (error) { - console.warn('Error reading source file', error.stack) - return null - } + if (!cacheDirectory || !fs.isFileSync(filePath)) { + return null + } - var compiler = COMPILERS[path.extname(filePath)] - if (!compiler) compiler = COMPILERS['.js'] + try { + var sourceCode = fs.readFileSync(filePath, 'utf8') + } catch (error) { + console.warn('Error reading source file', error.stack) + return null + } - try { - var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath)) - } catch (error) { - console.warn('Error reading compiled file', error.stack) - return null - } + var compiler = COMPILERS[path.extname(filePath)] + if (!compiler) compiler = COMPILERS['.js'] - if (fileData == null) { - return null - } + try { + var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath)) + } catch (error) { + console.warn('Error reading compiled file', error.stack) + return null + } - var match, lastMatch - INLINE_SOURCE_MAP_REGEXP.lastIndex = 0 - while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) { - lastMatch = match - } - if (lastMatch == null) { - return null - } + if (fileData == null) { + return null + } - var sourceMappingURL = lastMatch[1] - var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1) + var match, lastMatch + INLINE_SOURCE_MAP_REGEXP.lastIndex = 0 + while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) { + lastMatch = match + } + if (lastMatch == null) { + return null + } - try { - var sourceMap = JSON.parse(new Buffer(rawData, 'base64')) - } catch (error) { - console.warn('Error parsing source map', error.stack) - return null - } + var sourceMappingURL = lastMatch[1] + var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1) - return { - map: sourceMap, - url: null - } - } -}) + try { + var sourceMap = JSON.parse(new Buffer(rawData, 'base64')) + } catch (error) { + console.warn('Error parsing source map', error.stack) + return null + } -var prepareStackTraceWithSourceMapping = Error.prepareStackTrace -var prepareStackTrace = prepareStackTraceWithSourceMapping - -function prepareStackTraceWithRawStackAssignment (error, frames) { - if (error.rawStack) { // avoid infinite recursion - return prepareStackTraceWithSourceMapping(error, frames) - } else { - error.rawStack = frames - return prepareStackTrace(error, frames) - } -} - -Error.stackTraceLimit = 30 - -Object.defineProperty(Error, 'prepareStackTrace', { - get: function () { - return prepareStackTraceWithRawStackAssignment - }, - - set: function (newValue) { - prepareStackTrace = newValue - process.nextTick(function () { - prepareStackTrace = prepareStackTraceWithSourceMapping - }) - } -}) - -Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native - // Access this.stack to ensure prepareStackTrace has been run on this error - // because it assigns this.rawStack as a side-effect - this.stack - return this.rawStack -} - -Object.keys(COMPILERS).forEach(function (extension) { - var compiler = COMPILERS[extension] - - Object.defineProperty(require.extensions, extension, { - enumerable: true, - writable: false, - value: function (module, filePath) { - var code = compileFileAtPath(compiler, filePath, extension) - return module._compile(code, filePath) + return { + map: sourceMap, + url: null + } } }) -}) + var prepareStackTraceWithSourceMapping = Error.prepareStackTrace + var prepareStackTrace = prepareStackTraceWithSourceMapping + + function prepareStackTraceWithRawStackAssignment (error, frames) { + if (error.rawStack) { // avoid infinite recursion + return prepareStackTraceWithSourceMapping(error, frames) + } else { + error.rawStack = frames + return prepareStackTrace(error, frames) + } + } + + Error.stackTraceLimit = 30 + + Object.defineProperty(Error, 'prepareStackTrace', { + get: function () { + return prepareStackTraceWithRawStackAssignment + }, + + set: function (newValue) { + prepareStackTrace = newValue + process.nextTick(function () { + prepareStackTrace = prepareStackTraceWithSourceMapping + }) + } + }) + + Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native + // Access this.stack to ensure prepareStackTrace has been run on this error + // because it assigns this.rawStack as a side-effect + this.stack + return this.rawStack + } + + Object.keys(COMPILERS).forEach(function (extension) { + var compiler = COMPILERS[extension] + + Object.defineProperty(nodeRequire.extensions, extension, { + enumerable: true, + writable: false, + value: function (module, filePath) { + var code = compileFileAtPath(compiler, filePath, extension) + return module._compile(code, filePath) + } + }) + }) +} + +exports.supportedExtensions = Object.keys(COMPILERS) exports.resetCacheStats() diff --git a/src/config-schema.js b/src/config-schema.js index 41b5ecbb6..b19624a38 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -1,8 +1,3 @@ -/** @babel */ - -import path from 'path' -import fs from 'fs-plus' - // This is loaded by atom-environment.coffee. See // https://atom.io/docs/api/latest/Config for more information about config // schemas. @@ -58,11 +53,6 @@ const configSchema = { }, description: 'Names of UI and syntax themes which will be used when Atom starts.' }, - projectHome: { - type: 'string', - default: path.join(fs.getHomeDirectory(), 'github'), - description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' - }, audioBeep: { type: 'boolean', default: true, @@ -506,4 +496,4 @@ if (process.platform === 'darwin') { } } -export default configSchema +module.exports = configSchema diff --git a/src/config.coffee b/src/config.coffee index e873a1348..f0628ffee 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -398,11 +398,16 @@ class Config value # Created during initialization, available as `atom.config` - constructor: ({@configDirPath, @resourcePath, @notificationManager, @enablePersistence}={}) -> + constructor: ({@notificationManager, @enablePersistence}={}) -> + @clear() + + initialize: ({@configDirPath, @resourcePath, projectHomeSchema}) -> if @enablePersistence? @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') - @clear() + + @schema.properties.core.properties.projectHome = projectHomeSchema + @defaultSettings.core.projectHome = projectHomeSchema.default clear: -> @emitter = new Emitter @@ -839,7 +844,7 @@ class Config relativePath = sourcePath.substring(templateConfigDirPath.length + 1) destinationPath = path.join(@configDirPath, relativePath) queue.push({sourcePath, destinationPath}) - fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) + fs.traverseTree(templateConfigDirPath, onConfigDirFile, ((path) -> true), (->)) loadUserConfig: -> return if @shouldNotAccessFileSystem() diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 4dc54cede..8a25373b0 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -40,15 +40,17 @@ platformContextMenu = require('../package.json')?._atomMenu?['context-menu'] # {::add} for more information. module.exports = class ContextMenuManager - constructor: ({@resourcePath, @devMode, @keymapManager}) -> + constructor: ({@keymapManager}) -> @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data @clear() @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() + initialize: ({@resourcePath, @devMode}) -> + loadPlatformItems: -> if platformContextMenu? - @add(platformContextMenu) + @add(platformContextMenu, @devMode ? false) else menusDirPath = path.join(@resourcePath, 'menus') platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) @@ -107,11 +109,11 @@ class ContextMenuManager # # Returns a {Disposable} on which `.dispose()` can be called to remove the # added menu items. - add: (itemsBySelector) -> + add: (itemsBySelector, throwOnInvalidSelector = true) -> addedItemSets = [] for selector, items of itemsBySelector - validateSelector(selector) + validateSelector(selector) if throwOnInvalidSelector itemSet = new ContextMenuItemSet(selector, items) addedItemSets.push(itemSet) @itemSets.push(itemSet) @@ -206,14 +208,17 @@ class ContextMenuManager clear: -> @activeElement = null @itemSets = [] - @add 'atom-workspace': [{ - label: 'Inspect Element' - command: 'application:inspect' - devMode: true - created: (event) -> - {pageX, pageY} = event - @commandDetail = {x: pageX, y: pageY} - }] + inspectElement = { + 'atom-workspace': [{ + label: 'Inspect Element' + command: 'application:inspect' + devMode: true + created: (event) -> + {pageX, pageY} = event + @commandDetail = {x: pageX, y: pageY} + }] + } + @add(inspectElement, false) class ContextMenuItemSet constructor: (@selector, @items) -> diff --git a/src/default-directory-searcher.coffee b/src/default-directory-searcher.coffee index 6b8ffe3e3..3955d38e3 100644 --- a/src/default-directory-searcher.coffee +++ b/src/default-directory-searcher.coffee @@ -11,13 +11,16 @@ class DirectorySearch excludeVcsIgnores: options.excludeVcsIgnores globalExclusions: options.exclusions follow: options.follow + searchOptions = + leadingContextLineCount: options.leadingContextLineCount + trailingContextLineCount: options.trailingContextLineCount @task = new Task(require.resolve('./scan-handler')) @task.on 'scan:result-found', options.didMatch @task.on 'scan:file-error', options.didError @task.on 'scan:paths-searched', options.didSearchPaths @promise = new Promise (resolve, reject) => @task.on('task:cancelled', reject) - @task.start rootPaths, regex.source, scanHandlerOptions, => + @task.start rootPaths, regex.source, scanHandlerOptions, searchOptions, => @task.terminate() resolve() diff --git a/src/file-system-blob-store.js b/src/file-system-blob-store.js index 7bbbdcb14..67a959735 100644 --- a/src/file-system-blob-store.js +++ b/src/file-system-blob-store.js @@ -14,16 +14,15 @@ class FileSystemBlobStore { constructor (directory) { this.blobFilename = path.join(directory, 'BLOB') this.blobMapFilename = path.join(directory, 'MAP') - this.invalidationKeysFilename = path.join(directory, 'INVKEYS') this.lockFilename = path.join(directory, 'LOCK') this.reset() } reset () { this.inMemoryBlobs = new Map() - this.invalidationKeys = {} this.storedBlob = new Buffer(0) this.storedBlobMap = {} + this.usedKeys = new Set() } load () { @@ -33,14 +32,10 @@ class FileSystemBlobStore { if (!fs.existsSync(this.blobFilename)) { return } - if (!fs.existsSync(this.invalidationKeysFilename)) { - return - } try { this.storedBlob = fs.readFileSync(this.blobFilename) this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename)) - this.invalidationKeys = JSON.parse(fs.readFileSync(this.invalidationKeysFilename)) } catch (e) { this.reset() } @@ -50,7 +45,6 @@ class FileSystemBlobStore { let dump = this.getDump() let blobToStore = Buffer.concat(dump[0]) let mapToStore = JSON.stringify(dump[1]) - let invalidationKeysToStore = JSON.stringify(this.invalidationKeys) let acquiredLock = false try { @@ -59,7 +53,6 @@ class FileSystemBlobStore { fs.writeFileSync(this.blobFilename, blobToStore) fs.writeFileSync(this.blobMapFilename, mapToStore) - fs.writeFileSync(this.invalidationKeysFilename, invalidationKeysToStore) } catch (error) { // Swallow the exception silently only if we fail to acquire the lock. if (error.code !== 'EEXIST') { @@ -72,20 +65,19 @@ class FileSystemBlobStore { } } - has (key, invalidationKey) { - let containsKey = this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key) - let isValid = this.invalidationKeys[key] === invalidationKey - return containsKey && isValid + has (key) { + return this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key) } - get (key, invalidationKey) { - if (this.has(key, invalidationKey)) { + get (key) { + if (this.has(key)) { + this.usedKeys.add(key) return this.getFromMemory(key) || this.getFromStorage(key) } } - set (key, invalidationKey, buffer) { - this.invalidationKeys[key] = invalidationKey + set (key, buffer) { + this.usedKeys.add(key) return this.inMemoryBlobs.set(key, buffer) } @@ -119,11 +111,13 @@ class FileSystemBlobStore { } for (let key of this.inMemoryBlobs.keys()) { - dump(key, this.getFromMemory.bind(this)) + if (this.usedKeys.has(key)) { + dump(key, this.getFromMemory.bind(this)) + } } for (let key of Object.keys(this.storedBlobMap)) { - if (!blobMap[key]) { + if (!blobMap[key] && this.usedKeys.has(key)) { dump(key, this.getFromStorage.bind(this)) } } diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js index 7ee465141..d35b24213 100644 --- a/src/get-window-load-settings.js +++ b/src/get-window-load-settings.js @@ -4,7 +4,7 @@ let windowLoadSettings = null module.exports = () => { if (!windowLoadSettings) { - windowLoadSettings = remote.getCurrentWindow().loadSettings + windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON) } return windowLoadSettings } diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 899bb4cff..a2341c967 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -15,7 +15,7 @@ PathSplitRegex = new RegExp("[/.]") module.exports = class GrammarRegistry extends FirstMate.GrammarRegistry constructor: ({@config}={}) -> - super(maxTokensPerLine: 100) + super(maxTokensPerLine: 100, maxLineLength: 1000) createToken: (value, scopes) -> new Token({value, scopes}) diff --git a/src/history-manager.js b/src/history-manager.js index f013957b9..cd151e660 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -1,6 +1,6 @@ /** @babel */ -import {Emitter} from 'event-kit' +import {Emitter, CompositeDisposable} from 'event-kit' // Extended: History manager for remembering which projects have been opened. // @@ -8,12 +8,21 @@ import {Emitter} from 'event-kit' // // The project history is used to enable the 'Reopen Project' menu. export class HistoryManager { - constructor ({project, commands, localStorage}) { - this.localStorage = localStorage - commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}) + constructor ({project, commands, stateStore}) { + this.stateStore = stateStore this.emitter = new Emitter() - this.loadState() - project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)) + this.projects = [] + this.disposables = new CompositeDisposable() + this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}, false)) + this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))) + } + + initialize (localStorage) { + this.localStorage = localStorage + } + + destroy () { + this.disposables.dispose() } // Public: Obtain a list of previously opened projects. @@ -27,9 +36,12 @@ export class HistoryManager { // // Note: This is not a privacy function - other traces will still exist, // e.g. window state. - clearProjects () { + // + // Return a {Promise} that resolves when the history has been successfully + // cleared. + async clearProjects () { this.projects = [] - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -46,7 +58,7 @@ export class HistoryManager { this.emitter.emit('did-change-projects', args || { reloaded: false }) } - addProject (paths, lastOpened) { + async addProject (paths, lastOpened) { if (paths.length === 0) return let project = this.getProject(paths) @@ -57,11 +69,11 @@ export class HistoryManager { project.lastOpened = lastOpened || new Date() this.projects.sort((a, b) => b.lastOpened - a.lastOpened) - this.saveState() + await this.saveState() this.didChangeProjects() } - removeProject (paths) { + async removeProject (paths) { if (paths.length === 0) return let project = this.getProject(paths) @@ -70,7 +82,7 @@ export class HistoryManager { let index = this.projects.indexOf(project) this.projects.splice(index, 1) - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -84,31 +96,23 @@ export class HistoryManager { return null } - loadState () { - const state = JSON.parse(this.localStorage.getItem('history')) - if (state && state.projects) { - this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) - this.didChangeProjects({ reloaded: true }) + async loadState () { + let history = await this.stateStore.load('history-manager') + if (!history) { + history = JSON.parse(this.localStorage.getItem('history')) + } + + if (history && history.projects) { + this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) + this.didChangeProjects({reloaded: true}) } else { this.projects = [] } } - saveState () { - const state = JSON.stringify({ - projects: this.projects.map(p => ({ - paths: p.paths, lastOpened: p.lastOpened - })) - }) - this.localStorage.setItem('history', state) - } - - async importProjectHistory () { - for (let project of await HistoryImporter.getAllProjects()) { - this.addProject(project.paths, project.lastOpened) - } - this.saveState() - this.didChangeProjects() + async saveState () { + const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened})) + await this.stateStore.save('history-manager', {projects}) } } @@ -132,32 +136,3 @@ export class HistoryProject { set lastOpened (lastOpened) { this._lastOpened = lastOpened } get lastOpened () { return this._lastOpened } } - -class HistoryImporter { - static async getStateStoreCursor () { - const db = await atom.stateStore.dbPromise - const store = db.transaction(['states']).objectStore('states') - return store.openCursor() - } - - static async getAllProjects (stateStore) { - const request = await HistoryImporter.getStateStoreCursor() - return new Promise((resolve, reject) => { - const rows = [] - request.onerror = reject - request.onsuccess = event => { - const cursor = event.target.result - if (cursor) { - let project = cursor.value.value.project - let storedAt = cursor.value.storedAt - if (project && project.paths && storedAt) { - rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt)))) - } - cursor.continue() - } else { - resolve(rows) - } - } - }) - } -} diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index be13ce6c6..076b1bf32 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -1,3 +1,69 @@ +AtomEnvironment = require './atom-environment' +ApplicationDelegate = require './application-delegate' +Clipboard = require './clipboard' +TextEditor = require './text-editor' +TextEditorComponent = require './text-editor-component' +FileSystemBlobStore = require './file-system-blob-store' +NativeCompileCache = require './native-compile-cache' +CompileCache = require './compile-cache' +ModuleCache = require './module-cache' + +require('about') +require('archive-view') +require('autocomplete-atom-api') +require('autocomplete-css') +require('autocomplete-html') +require('autocomplete-plus') +require('autocomplete-snippets') +require('autoflow') +require('autosave') +require('background-tips') +require('bookmarks') +require('bracket-matcher') +require('command-palette') +require('deprecation-cop') +require('dev-live-reload') +require('encoding-selector') +require('exception-reporting') +require('dalek') +require('find-and-replace') +require('fuzzy-finder') +require('git-diff') +require('go-to-line') +require('grammar-selector') +require('image-view') +require('incompatible-packages') +require('keybinding-resolver') +require('line-ending-selector') +require('link') +require('markdown-preview') +require('metrics') +require('notifications') +require('open-on-github') +require('package-generator') +require('settings-view') +require('snippets') +require('spell-check') +require('status-bar') +require('styleguide') +require('symbols-view') +require('tabs') +require('timecop') +require('tree-view') +require('update-package-dependencies') +require('welcome') +require('whitespace') +require('wrap-guide') + +clipboard = new Clipboard +TextEditor.setClipboard(clipboard) + +window.atom = new AtomEnvironment({ + clipboard, + applicationDelegate: new ApplicationDelegate, + enablePersistence: true +}) + # Like sands through the hourglass, so are the days of our lives. module.exports = ({blobStore}) -> {updateProcessEnv} = require('./update-process-env') @@ -16,23 +82,13 @@ module.exports = ({blobStore}) -> # Make React faster process.env.NODE_ENV ?= 'production' unless devMode - AtomEnvironment = require './atom-environment' - ApplicationDelegate = require './application-delegate' - Clipboard = require './clipboard' - TextEditor = require './text-editor' - - clipboard = new Clipboard - TextEditor.setClipboard(clipboard) - - window.atom = new AtomEnvironment({ - window, document, clipboard, blobStore, - applicationDelegate: new ApplicationDelegate, + window.atom.initialize({ + window, document, blobStore, configDirPath: process.env.ATOM_HOME, - enablePersistence: true, env: process.env }) - atom.startEditorWindow().then -> + window.atom.startEditorWindow().then -> # Workaround for focus getting cleared upon window creation windowFocused = -> window.removeEventListener('focus', windowFocused) diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index a223e0b03..a8f1aafe6 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -56,14 +56,16 @@ export default async function () { TextEditor.setClipboard(clipboard) const applicationDelegate = new ApplicationDelegate() - global.atom = new AtomEnvironment({ + const environmentParams = { applicationDelegate, window, document, clipboard, configDirPath: process.env.ATOM_HOME, enablePersistence: false - }) + } + global.atom = new AtomEnvironment(environmentParams) + global.atom.initialize(environmentParams) // Prevent benchmarks from modifying application menus global.atom.menu.sendToBrowserProcess = function () { } diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index 794db3174..e87586374 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -79,7 +79,9 @@ module.exports = ({blobStore}) -> params.clipboard = clipboard unless params.hasOwnProperty("clipboard") params.blobStore = blobStore unless params.hasOwnProperty("blobStore") params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets") - new AtomEnvironment(params) + atomEnvironment = new AtomEnvironment(params) + atomEnvironment.initialize(params) + atomEnvironment promise = testRunner({ logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index bf8302f4c..02b1a8a97 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -11,13 +11,16 @@ KeymapManager::onDidLoadBundledKeymaps = (callback) -> KeymapManager::onDidLoadUserKeymap = (callback) -> @emitter.on 'did-load-user-keymap', callback +KeymapManager::canLoadBundledKeymapsFromMemory = -> + bundledKeymaps? + KeymapManager::loadBundledKeymaps = -> - keymapsPath = path.join(@resourcePath, 'keymaps') if bundledKeymaps? for keymapName, keymap of bundledKeymaps - keymapPath = path.join(keymapsPath, keymapName) - @add(keymapPath, keymap) + keymapPath = "core/#{keymapName}" + @add(keymapPath, keymap, 0, @devMode ? false) else + keymapsPath = path.join(@resourcePath, 'keymaps') @loadKeymap(keymapsPath) @emitter.emit 'did-load-bundled-keymaps' diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 6c9271179..02d396021 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -2,24 +2,24 @@ CursorsComponent = require './cursors-component' LinesTileComponent = require './lines-tile-component' TiledComponent = require './tiled-component' -DummyLineNode = document.createElement('div') -DummyLineNode.className = 'line' -DummyLineNode.style.position = 'absolute' -DummyLineNode.style.visibility = 'hidden' -DummyLineNode.appendChild(document.createElement('span')) -DummyLineNode.appendChild(document.createElement('span')) -DummyLineNode.appendChild(document.createElement('span')) -DummyLineNode.appendChild(document.createElement('span')) -DummyLineNode.children[0].textContent = 'x' -DummyLineNode.children[1].textContent = '我' -DummyLineNode.children[2].textContent = 'ハ' -DummyLineNode.children[3].textContent = '세' - module.exports = class LinesComponent extends TiledComponent placeholderTextDiv: null constructor: ({@views, @presenter, @domElementPool, @assert}) -> + @DummyLineNode = document.createElement('div') + @DummyLineNode.className = 'line' + @DummyLineNode.style.position = 'absolute' + @DummyLineNode.style.visibility = 'hidden' + @DummyLineNode.appendChild(document.createElement('span')) + @DummyLineNode.appendChild(document.createElement('span')) + @DummyLineNode.appendChild(document.createElement('span')) + @DummyLineNode.appendChild(document.createElement('span')) + @DummyLineNode.children[0].textContent = 'x' + @DummyLineNode.children[1].textContent = '我' + @DummyLineNode.children[2].textContent = 'ハ' + @DummyLineNode.children[3].textContent = '세' + @domNode = document.createElement('div') @domNode.classList.add('lines') @tilesNode = document.createElement("div") @@ -78,15 +78,15 @@ class LinesComponent extends TiledComponent getTilesNode: -> @tilesNode measureLineHeightAndDefaultCharWidth: -> - @domNode.appendChild(DummyLineNode) + @domNode.appendChild(@DummyLineNode) - lineHeightInPixels = DummyLineNode.getBoundingClientRect().height - defaultCharWidth = DummyLineNode.children[0].getBoundingClientRect().width - doubleWidthCharWidth = DummyLineNode.children[1].getBoundingClientRect().width - halfWidthCharWidth = DummyLineNode.children[2].getBoundingClientRect().width - koreanCharWidth = DummyLineNode.children[3].getBoundingClientRect().width + lineHeightInPixels = @DummyLineNode.getBoundingClientRect().height + defaultCharWidth = @DummyLineNode.children[0].getBoundingClientRect().width + doubleWidthCharWidth = @DummyLineNode.children[1].getBoundingClientRect().width + halfWidthCharWidth = @DummyLineNode.children[2].getBoundingClientRect().width + koreanCharWidth = @DummyLineNode.children[3].getBoundingClientRect().width - @domNode.removeChild(DummyLineNode) + @domNode.removeChild(@DummyLineNode) @presenter.setLineHeight(lineHeightInPixels) @presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 93e9e3395..749f09efc 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -6,8 +6,8 @@ StorageFolder = require '../storage-folder' Config = require '../config' FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron' -{CompositeDisposable} = require 'event-kit' +{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' +{CompositeDisposable, Disposable} = require 'event-kit' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -17,6 +17,7 @@ url = require 'url' _ = require 'underscore-plus' FindParentDir = null Resolve = null +ConfigSchema = require '../config-schema' LocationSuffixRegExp = /(:\d+)(:\d+)?$/ @@ -68,11 +69,22 @@ class AtomApplication @pidsToOpenWindows = {} @windows = [] - @config = new Config({configDirPath: process.env.ATOM_HOME, @resourcePath, enablePersistence: true}) - @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))} + @config = new Config({enablePersistence: true}) + @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + @config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome}) @config.load() @fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery")) @storageFolder = new StorageFolder(process.env.ATOM_HOME) + @autoUpdateManager = new AutoUpdateManager( + @version, + options.test or options.benchmark or options.benchmarkTest, + @config + ) @disposable = new CompositeDisposable @handleEvents() @@ -89,12 +101,10 @@ class AtomApplication if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') @config.unset('core.useCustomTitleBar') @config.set('core.titleBar', 'custom') - + @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) - @autoUpdateManager = new AutoUpdateManager( - @version, options.test or options.benchmark or options.benchmarkTest, @resourcePath, @config - ) + process.nextTick => @autoUpdateManager.initialize() @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @@ -280,6 +290,11 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'restart-application', => @restart() + @disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) -> + event.sender.session.resolveProxy url, (proxy) -> + unless event.sender.isDestroyed() + event.sender.send('did-resolve-proxy', requestId, proxy) + @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => for atomWindow in @windows webContents = atomWindow.browserWindow.webContents @@ -394,6 +409,8 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => @saveState(false) + @disposable.add(@disableZoomOnDisplayChange()) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ @@ -812,3 +829,17 @@ class AtomApplication args.push("--resource-path=#{@resourcePath}") app.relaunch({args}) app.quit() + + disableZoomOnDisplayChange: -> + outerCallback = => + for window in @windows + window.disableZoom() + + # Set the limits every time a display is added or removed, otherwise the + # configuration gets reset to the default, which allows zooming the + # webframe. + screen.on('display-added', outerCallback) + screen.on('display-removed', outerCallback) + new Disposable -> + screen.removeListener('display-added', outerCallback) + screen.removeListener('display-removed', outerCallback) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 03386d31a..bbc235bc5 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -83,12 +83,18 @@ class AtomWindow @representedDirectoryPaths = loadSettings.initialPaths @env = loadSettings.env if loadSettings.env? - @browserWindow.loadSettings = loadSettings + @browserWindow.loadSettingsJSON = JSON.stringify(loadSettings) @browserWindow.on 'window:loaded', => @emit 'window:loaded' @resolveLoadedPromise() + @browserWindow.on 'enter-full-screen', => + @browserWindow.webContents.send('did-enter-full-screen') + + @browserWindow.on 'leave-full-screen', => + @browserWindow.webContents.send('did-leave-full-screen') + @browserWindow.loadURL url.format protocol: 'file' pathname: "#{@resourcePath}/static/index.html" @@ -101,6 +107,7 @@ class AtomWindow hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() + @disableZoom() @atomApplication.addWindow(this) @@ -303,3 +310,6 @@ class AtomWindow @atomApplication.saveState() copy: -> @browserWindow.copy() + + disableZoom: -> + @browserWindow.webContents.setZoomLevelLimits(1, 1) diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index ff29dd3d6..2ff2852cb 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -14,12 +14,11 @@ module.exports = class AutoUpdateManager Object.assign @prototype, EventEmitter.prototype - constructor: (@version, @testMode, resourcePath, @config) -> + constructor: (@version, @testMode, @config) -> @state = IdleState @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') - process.nextTick => @setupAutoUpdater() - setupAutoUpdater: -> + initialize: -> if process.platform is 'win32' archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch @feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}" diff --git a/src/main-process/main.js b/src/main-process/main.js index 7ccd1a6c3..d63de0677 100644 --- a/src/main-process/main.js +++ b/src/main-process/main.js @@ -1,3 +1,7 @@ +if (typeof snapshotResult !== 'undefined') { + snapshotResult.setGlobals(global, process, global, {}, require) // eslint-disable-line no-undef +} + const startTime = Date.now() const electron = require('electron') diff --git a/src/main-process/start.js b/src/main-process/start.js index f54d263e0..29658d805 100644 --- a/src/main-process/start.js +++ b/src/main-process/start.js @@ -83,4 +83,5 @@ function handleStartupEventWithSquirrel () { function setupCompileCache () { const CompileCache = require('../compile-cache') CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) + CompileCache.install(process.resourcesPath, require) } diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index b6ed7fd1a..18dd49c5a 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -60,12 +60,17 @@ platformMenu = require('../package.json')?._atomMenu?.menu module.exports = class MenuManager constructor: ({@resourcePath, @keymapManager, @packageManager}) -> + @initialized = false @pendingUpdateOperation = null @template = [] @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() - @keymapManager.onDidReloadKeymap => @update() @packageManager.onDidActivateInitialPackages => @sortPackagesMenu() + initialize: ({@resourcePath}) -> + @keymapManager.onDidReloadKeymap => @update() + @update() + @initialized = true + # Public: Adds the given items to the application menu. # # ## Examples @@ -89,7 +94,7 @@ class MenuManager add: (items) -> items = _.deepClone(items) @merge(@template, item) for item in items - @update() + @update() if @initialized new Disposable => @remove(items) remove: (items) -> diff --git a/src/module-cache.coffee b/src/module-cache.coffee index 8c6a7c312..5bc162ab1 100644 --- a/src/module-cache.coffee +++ b/src/module-cache.coffee @@ -20,7 +20,7 @@ class Range extends semver.Range @unmatchedVersions.add(version) matches -nativeModules = process.binding('natives') +nativeModules = null cache = builtins: {} @@ -171,6 +171,7 @@ resolveModulePath = (relativePath, parentModule) -> return unless relativePath return unless parentModule?.filename + nativeModules ?= process.binding('natives') return if nativeModules.hasOwnProperty(relativePath) return if relativePath[0] is '.' return if isAbsolute(relativePath) @@ -212,35 +213,6 @@ registerBuiltins = (devMode) -> for builtin in rendererBuiltins cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js") -if cache.debug - cache.findPathCount = 0 - cache.findPathTime = 0 - cache.loadCount = 0 - cache.requireTime = 0 - global.moduleCache = cache - - originalLoad = Module::load - Module::load = -> - cache.loadCount++ - originalLoad.apply(this, arguments) - - originalRequire = Module::require - Module::require = -> - startTime = Date.now() - exports = originalRequire.apply(this, arguments) - cache.requireTime += Date.now() - startTime - exports - - originalFindPath = Module._findPath - Module._findPath = (request, paths) -> - cacheKey = JSON.stringify({request, paths}) - cache.findPathCount++ unless Module._pathCache[cacheKey] - - startTime = Date.now() - foundPath = originalFindPath.apply(global, arguments) - cache.findPathTime += Date.now() - startTime - foundPath - exports.create = (modulePath) -> fs = require 'fs-plus' diff --git a/src/native-compile-cache.js b/src/native-compile-cache.js index e4e7fc146..09a62b186 100644 --- a/src/native-compile-cache.js +++ b/src/native-compile-cache.js @@ -1,5 +1,3 @@ -'use strict' - const Module = require('module') const path = require('path') const cachedVm = require('cached-run-in-this-context') @@ -38,7 +36,6 @@ class NativeCompileCache { overrideModuleCompile () { let self = this - let resolvedArgv = null // Here we override Node's module.js // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing // only the bits that affect compilation in order to use the cached one. @@ -63,11 +60,10 @@ class NativeCompileCache { // create wrapper function let wrapper = Module.wrap(content) - let cacheKey = filename - let invalidationKey = computeHash(wrapper + self.v8Version) + let cacheKey = computeHash(wrapper + self.v8Version) let compiledWrapper = null - if (self.cacheStore.has(cacheKey, invalidationKey)) { - let buffer = self.cacheStore.get(cacheKey, invalidationKey) + if (self.cacheStore.has(cacheKey)) { + let buffer = self.cacheStore.get(cacheKey) let compilationResult = cachedVm.runInThisContextCached(wrapper, filename, buffer) compiledWrapper = compilationResult.result if (compilationResult.wasRejected) { @@ -82,29 +78,11 @@ class NativeCompileCache { throw err } if (compilationResult.cacheBuffer) { - self.cacheStore.set(cacheKey, invalidationKey, compilationResult.cacheBuffer) + self.cacheStore.set(cacheKey, compilationResult.cacheBuffer) } compiledWrapper = compilationResult.result } - if (global.v8debug) { - if (!resolvedArgv) { - // we enter the repl if we're not given a filename argument. - if (process.argv[1]) { - resolvedArgv = Module._resolveFilename(process.argv[1], null) - } else { - resolvedArgv = 'repl' - } - } - // Set breakpoint on module start - if (filename === resolvedArgv) { - // Installing this dummy debug event listener tells V8 to start - // the debugger. Without it, the setBreakPoint() fails with an - // 'illegal access' error. - global.v8debug.Debug.setListener(function () {}) - global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0) - } - } let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global] return compiledWrapper.apply(moduleSelf.exports, args) } diff --git a/src/package-manager.coffee b/src/package-manager.coffee index fb4f7a658..a7b068bbf 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -30,9 +30,8 @@ module.exports = class PackageManager constructor: (params) -> { - configDirPath, @devMode, safeMode, @resourcePath, @config, @styleManager, - @notificationManager, @keymapManager, @commandRegistry, @grammarRegistry, - @deserializerManager, @viewRegistry + @config, @styleManager, @notificationManager, @keymapManager, + @commandRegistry, @grammarRegistry, @deserializerManager, @viewRegistry } = params @emitter = new Emitter @@ -40,11 +39,6 @@ class PackageManager @packageDirPaths = [] @deferredActivationHooks = [] @triggeredActivationHooks = new Set() - if configDirPath? and not safeMode - if @devMode - @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) - @packageDirPaths.push(path.join(configDirPath, "packages")) - @packagesCache = require('../package.json')?._atomPackages ? {} @initialPackagesLoaded = false @initialPackagesActivated = false @@ -57,6 +51,13 @@ class PackageManager @packageActivators = [] @registerPackageActivator(this, ['atom', 'textmate']) + initialize: (params) -> + {configDirPath, @devMode, safeMode, @resourcePath} = params + if configDirPath? and not safeMode + if @devMode + @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) + @packageDirPaths.push(path.join(configDirPath, "packages")) + setContextMenuManager: (@contextMenuManager) -> setMenuManager: (@menuManager) -> diff --git a/src/package.coffee b/src/package.coffee index 63efbf02c..ed0f7aa87 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -42,7 +42,8 @@ class Package @metadata ?= @packageManager.loadPackageMetadata(@path) @bundledPackage = @packageManager.isBundledPackagePath(@path) @name = @metadata?.name ? path.basename(@path) - ModuleCache.add(@path, @metadata) + unless @bundledPackage + ModuleCache.add(@path, @metadata) @reset() ### @@ -502,7 +503,7 @@ class Package path.join(@path, @metadata.main) else path.join(@path, 'index') - @mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...]) + @mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...]) activationShouldBeDeferred: -> @hasActivationCommands() or @hasActivationHooks() diff --git a/src/pane-container.coffee b/src/pane-container.coffee index fc092122e..20d14389d 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -21,9 +21,11 @@ class PaneContainer extends Model @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager})) @setActivePane(@getRoot()) - @monitorActivePaneItem() @monitorPaneItems() + initialize: -> + @monitorActivePaneItem() + serialize: (params) -> deserializer: 'PaneContainer' version: @serializationVersion diff --git a/src/panel-container-element.coffee b/src/panel-container-element.coffee deleted file mode 100644 index b66a0cd90..000000000 --- a/src/panel-container-element.coffee +++ /dev/null @@ -1,45 +0,0 @@ -{CompositeDisposable} = require 'event-kit' - -class PanelContainerElement extends HTMLElement - createdCallback: -> - @subscriptions = new CompositeDisposable - - initialize: (@model, {@views}) -> - throw new Error("Must pass a views parameter when initializing PanelContainerElements") unless @views? - - @subscriptions.add @model.onDidAddPanel(@panelAdded.bind(this)) - @subscriptions.add @model.onDidDestroy(@destroyed.bind(this)) - @classList.add(@model.getLocation()) - this - - getModel: -> @model - - panelAdded: ({panel, index}) -> - panelElement = @views.getView(panel) - panelElement.classList.add(@model.getLocation()) - if @model.isModal() - panelElement.classList.add("overlay", "from-top") - else - panelElement.classList.add("tool-panel", "panel-#{@model.getLocation()}") - - if index >= @childNodes.length - @appendChild(panelElement) - else - referenceItem = @childNodes[index] - @insertBefore(panelElement, referenceItem) - - if @model.isModal() - @hideAllPanelsExcept(panel) - @subscriptions.add panel.onDidChangeVisible (visible) => - @hideAllPanelsExcept(panel) if visible - - destroyed: -> - @subscriptions.dispose() - @parentNode?.removeChild(this) - - hideAllPanelsExcept: (excludedPanel) -> - for panel in @model.getPanels() - panel.hide() unless panel is excludedPanel - return - -module.exports = PanelContainerElement = document.registerElement 'atom-panel-container', prototype: PanelContainerElement.prototype diff --git a/src/panel-container-element.js b/src/panel-container-element.js new file mode 100644 index 000000000..dbc595186 --- /dev/null +++ b/src/panel-container-element.js @@ -0,0 +1,65 @@ +'use strict' + +/* global HTMLElement */ + +const {CompositeDisposable} = require('event-kit') + +class PanelContainerElement extends HTMLElement { + createdCallback () { + this.subscriptions = new CompositeDisposable() + } + + initialize (model, {views}) { + this.model = model + this.views = views + if (this.views == null) { + throw new Error('Must pass a views parameter when initializing PanelContainerElements') + } + + this.subscriptions.add(this.model.onDidAddPanel(this.panelAdded.bind(this))) + this.subscriptions.add(this.model.onDidDestroy(this.destroyed.bind(this))) + this.classList.add(this.model.getLocation()) + return this + } + + getModel () { return this.model } + + panelAdded ({panel, index}) { + const panelElement = this.views.getView(panel) + panelElement.classList.add(this.model.getLocation()) + if (this.model.isModal()) { + panelElement.classList.add('overlay', 'from-top') + } else { + panelElement.classList.add('tool-panel', `panel-${this.model.getLocation()}`) + } + + if (index >= this.childNodes.length) { + this.appendChild(panelElement) + } else { + const referenceItem = this.childNodes[index] + this.insertBefore(panelElement, referenceItem) + } + + if (this.model.isModal()) { + this.hideAllPanelsExcept(panel) + this.subscriptions.add(panel.onDidChangeVisible(visible => { + if (visible) { this.hideAllPanelsExcept(panel) } + })) + } + } + + destroyed () { + this.subscriptions.dispose() + if (this.parentNode != null) { + this.parentNode.removeChild(this) + } + } + + hideAllPanelsExcept (excludedPanel) { + for (let panel of this.model.getPanels()) { + if (panel !== excludedPanel) { panel.hide() } + } + } +} + +module.exports = document.registerElement('atom-panel-container', {prototype: PanelContainerElement.prototype}) diff --git a/src/panel-container.coffee b/src/panel-container.coffee deleted file mode 100644 index 322773f69..000000000 --- a/src/panel-container.coffee +++ /dev/null @@ -1,71 +0,0 @@ -{Emitter, CompositeDisposable} = require 'event-kit' - -module.exports = -class PanelContainer - constructor: ({@location}={}) -> - @emitter = new Emitter - @subscriptions = new CompositeDisposable - @panels = [] - - destroy: -> - panel.destroy() for panel in @getPanels() - @subscriptions.dispose() - @emitter.emit 'did-destroy', this - @emitter.dispose() - - ### - Section: Event Subscription - ### - - onDidAddPanel: (callback) -> - @emitter.on 'did-add-panel', callback - - onDidRemovePanel: (callback) -> - @emitter.on 'did-remove-panel', callback - - onDidDestroy: (callback) -> - @emitter.on 'did-destroy', callback - - ### - Section: Panels - ### - - getLocation: -> @location - - isModal: -> @location is 'modal' - - getPanels: -> @panels - - addPanel: (panel) -> - @subscriptions.add panel.onDidDestroy(@panelDestroyed.bind(this)) - - index = @getPanelIndex(panel) - if index is @panels.length - @panels.push(panel) - else - @panels.splice(index, 0, panel) - - @emitter.emit 'did-add-panel', {panel, index} - panel - - panelForItem: (item) -> - for panel in @panels - return panel if panel.getItem() is item - null - - panelDestroyed: (panel) -> - index = @panels.indexOf(panel) - if index > -1 - @panels.splice(index, 1) - @emitter.emit 'did-remove-panel', {panel, index} - - getPanelIndex: (panel) -> - priority = panel.getPriority() - if @location in ['bottom', 'right'] - for p, i in @panels by -1 - return i + 1 if priority < p.getPriority() - 0 - else - for p, i in @panels - return i if priority < p.getPriority() - @panels.length diff --git a/src/panel-container.js b/src/panel-container.js new file mode 100644 index 000000000..377b4cd97 --- /dev/null +++ b/src/panel-container.js @@ -0,0 +1,91 @@ +'use strict' + +const {Emitter, CompositeDisposable} = require('event-kit') + +module.exports = class PanelContainer { + constructor ({location} = {}) { + this.location = location + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + this.panels = [] + } + + destroy () { + for (let panel of this.getPanels()) { panel.destroy() } + this.subscriptions.dispose() + this.emitter.emit('did-destroy', this) + this.emitter.dispose() + } + + /* + Section: Event Subscription + */ + + onDidAddPanel (callback) { + return this.emitter.on('did-add-panel', callback) + } + + onDidRemovePanel (callback) { + return this.emitter.on('did-remove-panel', callback) + } + + onDidDestroy (callback) { + return this.emitter.on('did-destroy', callback) + } + + /* + Section: Panels + */ + + getLocation () { return this.location } + + isModal () { return this.location === 'modal' } + + getPanels () { return this.panels.slice() } + + addPanel (panel) { + this.subscriptions.add(panel.onDidDestroy(this.panelDestroyed.bind(this))) + + const index = this.getPanelIndex(panel) + if (index === this.panels.length) { + this.panels.push(panel) + } else { + this.panels.splice(index, 0, panel) + } + + this.emitter.emit('did-add-panel', {panel, index}) + return panel + } + + panelForItem (item) { + for (let panel of this.panels) { + if (panel.getItem() === item) { return panel } + } + return null + } + + panelDestroyed (panel) { + const index = this.panels.indexOf(panel) + if (index > -1) { + this.panels.splice(index, 1) + this.emitter.emit('did-remove-panel', {panel, index}) + } + } + + getPanelIndex (panel) { + const priority = panel.getPriority() + if (['bottom', 'right'].includes(this.location)) { + for (let i = this.panels.length - 1; i >= 0; i--) { + const p = this.panels[i] + if (priority < p.getPriority()) { return i + 1 } + } + return 0 + } else { + for (let i = 0; i < this.panels.length; i++) { + const p = this.panels[i] + if (priority < p.getPriority()) { return i } + } + return this.panels.length + } + } +} diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 8196d9237..3b3849f12 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -2,223 +2,260 @@ Grim = require 'grim' module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) -> - commandRegistry.add 'atom-workspace', - 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() - 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() - 'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack() - 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() - 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() - 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) - 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1) - 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2) - 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3) - 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4) - 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5) - 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6) - 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7) - 'pane:show-item-9': -> @getModel().getActivePane().activateLastItem() - 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight() - 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() - 'window:increase-font-size': -> @getModel().increaseFontSize() - 'window:decrease-font-size': -> @getModel().decreaseFontSize() - 'window:reset-font-size': -> @getModel().resetFontSize() - 'application:about': -> ipcRenderer.send('command', 'application:about') - 'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings') - 'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings') - 'application:quit': -> ipcRenderer.send('command', 'application:quit') - 'application:hide': -> ipcRenderer.send('command', 'application:hide') - 'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications') - 'application:install-update': -> ipcRenderer.send('command', 'application:install-update') - 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') - 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') - 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') - 'application:open': -> - defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] - ipcRenderer.send('open-command', 'application:open', defaultPath) - 'application:open-file': -> - defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] - ipcRenderer.send('open-command', 'application:open-file', defaultPath) - 'application:open-folder': -> - defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] - ipcRenderer.send('open-command', 'application:open-folder', defaultPath) - 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') - 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') - 'application:add-project-folder': -> atom.addProjectFolder() - 'application:minimize': -> ipcRenderer.send('command', 'application:minimize') - 'application:zoom': -> ipcRenderer.send('command', 'application:zoom') - 'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front') - 'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config') - 'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script') - 'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap') - 'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets') - 'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet') - 'application:open-license': -> @getModel().openLicense() - 'window:run-package-specs': -> @runPackageSpecs() - 'window:run-benchmarks': -> @runBenchmarks() - 'window:focus-next-pane': -> @getModel().activateNextPane() - 'window:focus-previous-pane': -> @getModel().activatePreviousPane() - 'window:focus-pane-above': -> @focusPaneViewAbove() - 'window:focus-pane-below': -> @focusPaneViewBelow() - 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() - 'window:focus-pane-on-right': -> @focusPaneViewOnRight() - 'window:move-active-item-to-pane-above': -> @moveActiveItemToPaneAbove() - 'window:move-active-item-to-pane-below': -> @moveActiveItemToPaneBelow() - 'window:move-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft() - 'window:move-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight() - 'window:copy-active-item-to-pane-above': -> @moveActiveItemToPaneAbove(keepOriginal: true) - 'window:copy-active-item-to-pane-below': -> @moveActiveItemToPaneBelow(keepOriginal: true) - 'window:copy-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft(keepOriginal: true) - 'window:copy-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight(keepOriginal: true) - 'window:save-all': -> @getModel().saveAll() - 'window:toggle-invisibles': -> config.set("editor.showInvisibles", not config.get("editor.showInvisibles")) - 'window:log-deprecation-warnings': -> Grim.logDeprecations() - 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent")) - 'pane:reopen-closed-item': -> @getModel().reopenItem() - 'core:close': -> @getModel().closeActivePaneItemOrEmptyPaneOrWindow() - 'core:save': -> @getModel().saveActivePaneItem() - 'core:save-as': -> @getModel().saveActivePaneItemAs() + commandRegistry.add( + 'atom-workspace', + { + 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() + 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() + 'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack() + 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() + 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() + 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) + 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1) + 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2) + 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3) + 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4) + 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5) + 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6) + 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7) + 'pane:show-item-9': -> @getModel().getActivePane().activateLastItem() + 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight() + 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() + 'window:increase-font-size': -> @getModel().increaseFontSize() + 'window:decrease-font-size': -> @getModel().decreaseFontSize() + 'window:reset-font-size': -> @getModel().resetFontSize() + 'application:about': -> ipcRenderer.send('command', 'application:about') + 'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings') + 'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings') + 'application:quit': -> ipcRenderer.send('command', 'application:quit') + 'application:hide': -> ipcRenderer.send('command', 'application:hide') + 'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications') + 'application:install-update': -> ipcRenderer.send('command', 'application:install-update') + 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') + 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') + 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') + 'application:open': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open', defaultPath) + 'application:open-file': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-file', defaultPath) + 'application:open-folder': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-folder', defaultPath) + 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') + 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') + 'application:add-project-folder': -> atom.addProjectFolder() + 'application:minimize': -> ipcRenderer.send('command', 'application:minimize') + 'application:zoom': -> ipcRenderer.send('command', 'application:zoom') + 'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front') + 'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config') + 'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script') + 'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap') + 'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets') + 'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet') + 'application:open-license': -> @getModel().openLicense() + 'window:run-package-specs': -> @runPackageSpecs() + 'window:run-benchmarks': -> @runBenchmarks() + 'window:focus-next-pane': -> @getModel().activateNextPane() + 'window:focus-previous-pane': -> @getModel().activatePreviousPane() + 'window:focus-pane-above': -> @focusPaneViewAbove() + 'window:focus-pane-below': -> @focusPaneViewBelow() + 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() + 'window:focus-pane-on-right': -> @focusPaneViewOnRight() + 'window:move-active-item-to-pane-above': -> @moveActiveItemToPaneAbove() + 'window:move-active-item-to-pane-below': -> @moveActiveItemToPaneBelow() + 'window:move-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft() + 'window:move-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight() + 'window:copy-active-item-to-pane-above': -> @moveActiveItemToPaneAbove(keepOriginal: true) + 'window:copy-active-item-to-pane-below': -> @moveActiveItemToPaneBelow(keepOriginal: true) + 'window:copy-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft(keepOriginal: true) + 'window:copy-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight(keepOriginal: true) + 'window:save-all': -> @getModel().saveAll() + 'window:toggle-invisibles': -> config.set("editor.showInvisibles", not config.get("editor.showInvisibles")) + 'window:log-deprecation-warnings': -> Grim.logDeprecations() + 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent")) + 'pane:reopen-closed-item': -> @getModel().reopenItem() + 'core:close': -> @getModel().closeActivePaneItemOrEmptyPaneOrWindow() + 'core:save': -> @getModel().saveActivePaneItem() + 'core:save-as': -> @getModel().saveActivePaneItemAs() + }, + false + ) + if process.platform is 'darwin' - commandRegistry.add 'atom-workspace', 'window:install-shell-commands', -> - commandInstaller.installShellCommandsInteractively() + commandRegistry.add( + 'atom-workspace', + 'window:install-shell-commands', + (-> commandInstaller.installShellCommandsInteractively()), + false + ) - commandRegistry.add 'atom-pane', - 'pane:save-items': -> @getModel().saveItems() - 'pane:split-left': -> @getModel().splitLeft() - 'pane:split-right': -> @getModel().splitRight() - 'pane:split-up': -> @getModel().splitUp() - 'pane:split-down': -> @getModel().splitDown() - 'pane:split-left-and-copy-active-item': -> @getModel().splitLeft(copyActiveItem: true) - 'pane:split-right-and-copy-active-item': -> @getModel().splitRight(copyActiveItem: true) - 'pane:split-up-and-copy-active-item': -> @getModel().splitUp(copyActiveItem: true) - 'pane:split-down-and-copy-active-item': -> @getModel().splitDown(copyActiveItem: true) - 'pane:split-left-and-move-active-item': -> @getModel().splitLeft(moveActiveItem: true) - 'pane:split-right-and-move-active-item': -> @getModel().splitRight(moveActiveItem: true) - 'pane:split-up-and-move-active-item': -> @getModel().splitUp(moveActiveItem: true) - 'pane:split-down-and-move-active-item': -> @getModel().splitDown(moveActiveItem: true) - 'pane:close': -> @getModel().close() - 'pane:close-other-items': -> @getModel().destroyInactiveItems() - 'pane:increase-size': -> @getModel().increaseSize() - 'pane:decrease-size': -> @getModel().decreaseSize() - - commandRegistry.add 'atom-text-editor', stopEventPropagation( - 'core:undo': -> @undo() - 'core:redo': -> @redo() - 'core:move-left': -> @moveLeft() - 'core:move-right': -> @moveRight() - 'core:select-left': -> @selectLeft() - 'core:select-right': -> @selectRight() - 'core:select-up': -> @selectUp() - 'core:select-down': -> @selectDown() - 'core:select-all': -> @selectAll() - 'editor:select-word': -> @selectWordsContainingCursors() - 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() - 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() - 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() - 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() - 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() - 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() - 'editor:move-to-end-of-line': -> @moveToEndOfLine() - 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() - 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() - 'editor:move-to-end-of-word': -> @moveToEndOfWord() - 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() - 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() - 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() - 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() - 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() - 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() - 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() - 'editor:select-to-end-of-line': -> @selectToEndOfLine() - 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() - 'editor:select-to-end-of-word': -> @selectToEndOfWord() - 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() - 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() - 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() - 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() - 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary() - 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() - 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() - 'editor:select-line': -> @selectLinesContainingCursors() + commandRegistry.add( + 'atom-pane', + { + 'pane:save-items': -> @getModel().saveItems() + 'pane:split-left': -> @getModel().splitLeft() + 'pane:split-right': -> @getModel().splitRight() + 'pane:split-up': -> @getModel().splitUp() + 'pane:split-down': -> @getModel().splitDown() + 'pane:split-left-and-copy-active-item': -> @getModel().splitLeft(copyActiveItem: true) + 'pane:split-right-and-copy-active-item': -> @getModel().splitRight(copyActiveItem: true) + 'pane:split-up-and-copy-active-item': -> @getModel().splitUp(copyActiveItem: true) + 'pane:split-down-and-copy-active-item': -> @getModel().splitDown(copyActiveItem: true) + 'pane:split-left-and-move-active-item': -> @getModel().splitLeft(moveActiveItem: true) + 'pane:split-right-and-move-active-item': -> @getModel().splitRight(moveActiveItem: true) + 'pane:split-up-and-move-active-item': -> @getModel().splitUp(moveActiveItem: true) + 'pane:split-down-and-move-active-item': -> @getModel().splitDown(moveActiveItem: true) + 'pane:close': -> @getModel().close() + 'pane:close-other-items': -> @getModel().destroyInactiveItems() + 'pane:increase-size': -> @getModel().increaseSize() + 'pane:decrease-size': -> @getModel().decreaseSize() + }, + false ) - commandRegistry.add 'atom-text-editor', stopEventPropagationAndGroupUndo(config, - 'core:backspace': -> @backspace() - 'core:delete': -> @delete() - 'core:cut': -> @cutSelectedText() - 'core:copy': -> @copySelectedText() - 'core:paste': -> @pasteText() - 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() - 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() - 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() - 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() - 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() - 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() - 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() - 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() - 'editor:delete-line': -> @deleteLine() - 'editor:cut-to-end-of-line': -> @cutToEndOfLine() - 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine() - 'editor:transpose': -> @transpose() - 'editor:upper-case': -> @upperCase() - 'editor:lower-case': -> @lowerCase() - 'editor:copy-selection': -> @copyOnlySelectedText() + commandRegistry.add( + 'atom-text-editor', + stopEventPropagation({ + 'core:undo': -> @undo() + 'core:redo': -> @redo() + 'core:move-left': -> @moveLeft() + 'core:move-right': -> @moveRight() + 'core:select-left': -> @selectLeft() + 'core:select-right': -> @selectRight() + 'core:select-up': -> @selectUp() + 'core:select-down': -> @selectDown() + 'core:select-all': -> @selectAll() + 'editor:select-word': -> @selectWordsContainingCursors() + 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() + 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() + 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() + 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() + 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() + 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() + 'editor:move-to-end-of-line': -> @moveToEndOfLine() + 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() + 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() + 'editor:move-to-end-of-word': -> @moveToEndOfWord() + 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() + 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() + 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() + 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() + 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() + 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() + 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() + 'editor:select-to-end-of-line': -> @selectToEndOfLine() + 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() + 'editor:select-to-end-of-word': -> @selectToEndOfWord() + 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() + 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() + 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() + 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() + 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary() + 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() + 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() + 'editor:select-line': -> @selectLinesContainingCursors() + }), + false ) - commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagation( - 'core:move-up': -> @moveUp() - 'core:move-down': -> @moveDown() - 'core:move-to-top': -> @moveToTop() - 'core:move-to-bottom': -> @moveToBottom() - 'core:page-up': -> @pageUp() - 'core:page-down': -> @pageDown() - 'core:select-to-top': -> @selectToTop() - 'core:select-to-bottom': -> @selectToBottom() - 'core:select-page-up': -> @selectPageUp() - 'core:select-page-down': -> @selectPageDown() - 'editor:add-selection-below': -> @addSelectionBelow() - 'editor:add-selection-above': -> @addSelectionAbove() - 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() - 'editor:toggle-soft-tabs': -> @toggleSoftTabs() - 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() - 'editor:fold-all': -> @foldAll() - 'editor:unfold-all': -> @unfoldAll() - 'editor:fold-current-row': -> @foldCurrentRow() - 'editor:unfold-current-row': -> @unfoldCurrentRow() - 'editor:fold-selection': -> @foldSelectedLines() - 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) - 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) - 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) - 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) - 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) - 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) - 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) - 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) - 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) - 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) - 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false) - 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true) - 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) - 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) - 'editor:scroll-to-cursor': -> @scrollToCursorPosition() + commandRegistry.add( + 'atom-text-editor', + stopEventPropagationAndGroupUndo( + config, + { + 'core:backspace': -> @backspace() + 'core:delete': -> @delete() + 'core:cut': -> @cutSelectedText() + 'core:copy': -> @copySelectedText() + 'core:paste': -> @pasteText() + 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() + 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() + 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() + 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() + 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() + 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() + 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() + 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() + 'editor:delete-line': -> @deleteLine() + 'editor:cut-to-end-of-line': -> @cutToEndOfLine() + 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine() + 'editor:transpose': -> @transpose() + 'editor:upper-case': -> @upperCase() + 'editor:lower-case': -> @lowerCase() + 'editor:copy-selection': -> @copyOnlySelectedText() + } + ), + false ) - commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(config, - 'editor:indent': -> @indent() - 'editor:auto-indent': -> @autoIndentSelectedRows() - 'editor:indent-selected-rows': -> @indentSelectedRows() - 'editor:outdent-selected-rows': -> @outdentSelectedRows() - 'editor:newline': -> @insertNewline() - 'editor:newline-below': -> @insertNewlineBelow() - 'editor:newline-above': -> @insertNewlineAbove() - 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() - 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this) - 'editor:move-line-up': -> @moveLineUp() - 'editor:move-line-down': -> @moveLineDown() - 'editor:move-selection-left': -> @moveSelectionLeft() - 'editor:move-selection-right': -> @moveSelectionRight() - 'editor:duplicate-lines': -> @duplicateLines() - 'editor:join-lines': -> @joinLines() + commandRegistry.add( + 'atom-text-editor:not([mini])', + stopEventPropagation({ + 'core:move-up': -> @moveUp() + 'core:move-down': -> @moveDown() + 'core:move-to-top': -> @moveToTop() + 'core:move-to-bottom': -> @moveToBottom() + 'core:page-up': -> @pageUp() + 'core:page-down': -> @pageDown() + 'core:select-to-top': -> @selectToTop() + 'core:select-to-bottom': -> @selectToBottom() + 'core:select-page-up': -> @selectPageUp() + 'core:select-page-down': -> @selectPageDown() + 'editor:add-selection-below': -> @addSelectionBelow() + 'editor:add-selection-above': -> @addSelectionAbove() + 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() + 'editor:toggle-soft-tabs': -> @toggleSoftTabs() + 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() + 'editor:fold-all': -> @foldAll() + 'editor:unfold-all': -> @unfoldAll() + 'editor:fold-current-row': -> @foldCurrentRow() + 'editor:unfold-current-row': -> @unfoldCurrentRow() + 'editor:fold-selection': -> @foldSelectedLines() + 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) + 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) + 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) + 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) + 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) + 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) + 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) + 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) + 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) + 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) + 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false) + 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true) + 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) + 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) + 'editor:scroll-to-cursor': -> @scrollToCursorPosition() + }), + false + ) + + commandRegistry.add( + 'atom-text-editor:not([mini])', + stopEventPropagationAndGroupUndo( + config, + { + 'editor:indent': -> @indent() + 'editor:auto-indent': -> @autoIndentSelectedRows() + 'editor:indent-selected-rows': -> @indentSelectedRows() + 'editor:outdent-selected-rows': -> @outdentSelectedRows() + 'editor:newline': -> @insertNewline() + 'editor:newline-below': -> @insertNewlineBelow() + 'editor:newline-above': -> @insertNewlineAbove() + 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() + 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this) + 'editor:move-line-up': -> @moveLineUp() + 'editor:move-line-down': -> @moveLineDown() + 'editor:move-selection-left': -> @moveSelectionLeft() + 'editor:move-selection-right': -> @moveSelectionRight() + 'editor:duplicate-lines': -> @duplicateLines() + 'editor:join-lines': -> @joinLines() + } + ), + false ) stopEventPropagation = (commandListeners) -> diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 79acbba66..3f88e41f0 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -58,7 +58,7 @@ export default class ReopenProjectMenuManager { // 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 () { + async applyWindowsJumpListRemovals () { if (process.platform !== 'win32') return if (this.app === undefined) { this.app = require('remote').app @@ -68,7 +68,7 @@ export default class ReopenProjectMenuManager { if (removed.length === 0) return for (let project of this.historyManager.getProjects()) { if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) { - this.historyManager.removeProject(project.paths) + await this.historyManager.removeProject(project.paths) } } } diff --git a/src/scan-handler.coffee b/src/scan-handler.coffee index 8ee8f715e..db2e8299b 100644 --- a/src/scan-handler.coffee +++ b/src/scan-handler.coffee @@ -2,13 +2,13 @@ path = require "path" async = require "async" {PathSearcher, PathScanner, search} = require 'scandal' -module.exports = (rootPaths, regexSource, options) -> +module.exports = (rootPaths, regexSource, options, searchOptions={}) -> callback = @async() PATHS_COUNTER_SEARCHED_CHUNK = 50 pathsSearched = 0 - searcher = new PathSearcher() + searcher = new PathSearcher(searchOptions) searcher.on 'file-error', ({code, path, message}) -> emit('scan:file-error', {code, path, message}) diff --git a/src/state-store.js b/src/state-store.js index b192d8b04..e16857580 100644 --- a/src/state-store.js +++ b/src/state-store.js @@ -4,22 +4,31 @@ module.exports = class StateStore { constructor (databaseName, version) { this.connected = false - this.dbPromise = new Promise((resolve) => { - let dbOpenRequest = indexedDB.open(databaseName, version) - dbOpenRequest.onupgradeneeded = (event) => { - let db = event.target.result - 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) - } - }) + this.databaseName = databaseName + this.version = version + } + + get dbPromise () { + if (!this._dbPromise) { + this._dbPromise = new Promise((resolve) => { + const dbOpenRequest = indexedDB.open(this.databaseName, this.version) + dbOpenRequest.onupgradeneeded = (event) => { + let db = event.target.result + 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) + } + }) + } + + return this._dbPromise } isConnected () { diff --git a/src/style-manager.js b/src/style-manager.js index 0a0b401d3..718c1ee74 100644 --- a/src/style-manager.js +++ b/src/style-manager.js @@ -13,17 +13,20 @@ const DEPRECATED_SYNTAX_SELECTORS = require('./deprecated-syntax-selectors') // own, but is instead subscribed to by individual `` elements, // which clone and attach style elements in different contexts. module.exports = class StyleManager { - constructor ({configDirPath}) { - this.configDirPath = configDirPath - if (this.configDirPath != null) { - this.cacheDirPath = path.join(this.configDirPath, 'compile-cache', 'style-manager') - } + constructor () { this.emitter = new Emitter() this.styleElements = [] this.styleElementsBySourcePath = {} this.deprecationsBySourcePath = {} } + initialize ({configDirPath}) { + this.configDirPath = configDirPath + if (this.configDirPath != null) { + this.cacheDirPath = path.join(this.configDirPath, 'compile-cache', 'style-manager') + } + } + /* Section: Event Subscription */ diff --git a/src/task.coffee b/src/task.coffee index fc8c5bd6b..7a03310b5 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -70,7 +70,13 @@ class Task compileCachePath = require('./compile-cache').getCacheDirectory() taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');" bootstrap = """ - #{compileCacheRequire}.setCacheDirectory('#{compileCachePath}'); + if (typeof snapshotResult !== 'undefined') { + snapshotResult.setGlobals(global, process, global, {}, require) + } + + CompileCache = #{compileCacheRequire} + CompileCache.setCacheDirectory('#{compileCachePath}'); + CompileCache.install("#{process.resourcesPath}", require) #{taskBootstrapRequire} """ bootstrap = bootstrap.replace(/\\/g, "\\\\") diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 30600ff08..3343ce89c 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -1,7 +1,7 @@ /** @babel */ import {Emitter, Disposable, CompositeDisposable} from 'event-kit' -import {Point, Range} from 'atom' +import {Point, Range} from 'text-buffer' import TextEditor from './text-editor' import ScopeDescriptor from './scope-descriptor' diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 13b8a0bfa..fc3d9e08e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -192,6 +192,9 @@ class TextEditor extends Model @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @defaultMarkerLayer = @displayLayer.addMarkerLayer() + @disposables.add(@defaultMarkerLayer.onDidDestroy => + @assert(false, "defaultMarkerLayer destroyed at an unexpected time") + ) @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true @@ -353,7 +356,8 @@ class TextEditor extends Model cursor.setShowCursorOnSelection(value) for cursor in @getCursors() else - throw new TypeError("Invalid TextEditor parameter: '#{param}'") + if param isnt 'ref' and param isnt 'key' + throw new TypeError("Invalid TextEditor parameter: '#{param}'") @displayLayer.reset(displayLayerParams) @@ -383,7 +387,7 @@ class TextEditor extends Model softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, + @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth } @@ -1324,12 +1328,12 @@ class TextEditor extends Model replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @mutateSelectedText (selection) -> - range = selection.getBufferRange() + selection.getBufferRange() if selectWordIfEmpty and selection.isEmpty() selection.selectWord() text = selection.getText() selection.deleteSelectedText() - selection.insertText(fn(text)) + range = selection.insertText(fn(text)) selection.setBufferRange(range) # Split multi-line selections into one selection per line. @@ -2822,6 +2826,11 @@ class TextEditor extends Model # {::backwardsScanInBufferRange} to avoid tripping over your own changes. # # * `regex` A {RegExp} to search for. + # * `options` (optional) {Object} + # * `leadingContextLineCount` {Number} default `0`; The number of lines + # before the matched line to include in the results object. + # * `trailingContextLineCount` {Number} default `0`; The number of lines + # after the matched line to include in the results object. # * `iterator` A {Function} that's called on each match # * `object` {Object} # * `match` The current regular expression match. @@ -2829,7 +2838,12 @@ class TextEditor extends Model # * `range` The {Range} of the match. # * `stop` Call this {Function} to terminate the scan. # * `replace` Call this {Function} with a {String} to replace the match. - scan: (regex, iterator) -> @buffer.scan(regex, iterator) + scan: (regex, options={}, iterator) -> + if _.isFunction(options) + iterator = options + options = {} + + @buffer.scan(regex, options, iterator) # Essential: Scan regular expression matches in a given range, calling the given # iterator function on each match. diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 58297b2db..f035889a2 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -9,7 +9,7 @@ fs = require 'fs-plus' # An instance of this class is always available as the `atom.themes` global. module.exports = class ThemeManager - constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode, @config, @styleManager, @notificationManager, @viewRegistry}) -> + constructor: ({@packageManager, @config, @styleManager, @notificationManager, @viewRegistry}) -> @emitter = new Emitter @styleSheetDisposablesBySourcePath = {} @lessCache = null @@ -18,6 +18,8 @@ class ThemeManager @packageManager.onDidActivateInitialPackages => @onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets() + initialize: ({@resourcePath, @configDirPath, @safeMode}) -> + ### Section: Event Subscription ### diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 77221f52e..234f82be9 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -8,8 +8,6 @@ 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 @@ -253,8 +251,6 @@ 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/view-registry.coffee b/src/view-registry.coffee index e2cd986dc..5a8e72a4c 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -54,9 +54,12 @@ class ViewRegistry minimumPollInterval: 200 constructor: (@atomEnvironment) -> - @observer = new MutationObserver(@requestDocumentPoll) + @polling = false @clear() + initialize: -> + @observer = new MutationObserver(@requestDocumentPoll) + clear: -> @views = new WeakMap @providers = [] @@ -267,10 +270,13 @@ class ViewRegistry startPollingDocument: -> window.addEventListener('resize', @requestDocumentPoll) @observer.observe(document, {subtree: true, childList: true, attributes: true}) + @polling = true stopPollingDocument: -> - window.removeEventListener('resize', @requestDocumentPoll) - @observer.disconnect() + if @polling + window.removeEventListener('resize', @requestDocumentPoll) + @observer.disconnect() + @polling = false requestDocumentPoll: => if @animationFrameRequest? diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 62ce4527a..aac0f0ba5 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -4,31 +4,13 @@ listen = require './delegated-listener' # Handles low-level events related to the @window. module.exports = class WindowEventHandler - constructor: ({@atomEnvironment, @applicationDelegate, @window, @document}) -> + constructor: ({@atomEnvironment, @applicationDelegate}) -> @reloadRequested = false @subscriptions = new CompositeDisposable - @addEventListener(@window, 'beforeunload', @handleWindowBeforeunload) - @addEventListener(@window, 'focus', @handleWindowFocus) - @addEventListener(@window, 'blur', @handleWindowBlur) - - @addEventListener(@document, 'keyup', @handleDocumentKeyEvent) - @addEventListener(@document, 'keydown', @handleDocumentKeyEvent) - @addEventListener(@document, 'drop', @handleDocumentDrop) - @addEventListener(@document, 'dragover', @handleDocumentDragover) - @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu) - @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) - @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - - browserWindow = @applicationDelegate.getCurrentWindow() - browserWindow.on 'enter-full-screen', @handleEnterFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('enter-full-screen', @handleEnterFullScreen) - - browserWindow.on 'leave-full-screen', @handleLeaveFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('leave-full-screen', @handleLeaveFullScreen) + @handleNativeKeybindings() + initialize: (@window, @document) -> @subscriptions.add @atomEnvironment.commands.add @window, 'window:toggle-full-screen': @handleWindowToggleFullScreen 'window:close': @handleWindowClose @@ -43,14 +25,31 @@ class WindowEventHandler 'core:focus-next': @handleFocusNext 'core:focus-previous': @handleFocusPrevious - @handleNativeKeybindings() + @addEventListener(@window, 'beforeunload', @handleWindowBeforeunload) + @addEventListener(@window, 'focus', @handleWindowFocus) + @addEventListener(@window, 'blur', @handleWindowBlur) + + @addEventListener(@document, 'keyup', @handleDocumentKeyEvent) + @addEventListener(@document, 'keydown', @handleDocumentKeyEvent) + @addEventListener(@document, 'drop', @handleDocumentDrop) + @addEventListener(@document, 'dragover', @handleDocumentDragover) + @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu) + @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) + @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) + + @subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen)) + @subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen)) # Wire commands that should be handled by Chromium for elements with the # `.native-key-bindings` class. handleNativeKeybindings: -> bindCommandToAction = (command, action) => - @subscriptions.add @atomEnvironment.commands.add '.native-key-bindings', command, (event) => - @applicationDelegate.getCurrentWindow().webContents[action]() + @subscriptions.add @atomEnvironment.commands.add( + '.native-key-bindings', + command, + ((event) => @applicationDelegate.getCurrentWindow().webContents[action]()), + false + ) bindCommandToAction('core:copy', 'copy') bindCommandToAction('core:paste', 'paste') diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee deleted file mode 100644 index f598bef0b..000000000 --- a/src/workspace-element.coffee +++ /dev/null @@ -1,154 +0,0 @@ -{ipcRenderer} = require 'electron' -path = require 'path' -fs = require 'fs-plus' -{CompositeDisposable} = require 'event-kit' -scrollbarStyle = require 'scrollbar-style' - -module.exports = -class WorkspaceElement extends HTMLElement - globalTextEditorStyleSheet: null - - attachedCallback: -> - @focus() - - detachedCallback: -> - @subscriptions.dispose() - - initializeContent: -> - @classList.add 'workspace' - @setAttribute 'tabindex', -1 - - @verticalAxis = document.createElement('atom-workspace-axis') - @verticalAxis.classList.add('vertical') - - @horizontalAxis = document.createElement('atom-workspace-axis') - @horizontalAxis.classList.add('horizontal') - @horizontalAxis.appendChild(@verticalAxis) - - @appendChild(@horizontalAxis) - - observeScrollbarStyle: -> - @subscriptions.add scrollbarStyle.observePreferredScrollbarStyle (style) => - switch style - when 'legacy' - @classList.remove('scrollbars-visible-when-scrolling') - @classList.add("scrollbars-visible-always") - when 'overlay' - @classList.remove('scrollbars-visible-always') - @classList.add("scrollbars-visible-when-scrolling") - - observeTextEditorFontConfig: -> - @updateGlobalTextEditorStyleSheet() - @subscriptions.add @config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this) - @subscriptions.add @config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this) - @subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) - - updateGlobalTextEditorStyleSheet: -> - fontFamily = @config.get('editor.fontFamily') - # TODO: There is a bug in how some emojis (e.g. ❤️) are rendered on macOS. - # This workaround should be removed once we update to Chromium 51, where the - # problem was fixed. - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' - styleSheetSource = """ - atom-text-editor { - font-size: #{@config.get('editor.fontSize')}px; - font-family: #{fontFamily}; - line-height: #{@config.get('editor.lineHeight')}; - } - """ - @styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles') - @views.performDocumentPoll() - - initialize: (@model, {@views, @workspace, @project, @config, @styles}) -> - throw new Error("Must pass a views parameter when initializing WorskpaceElements") unless @views? - throw new Error("Must pass a workspace parameter when initializing WorskpaceElements") unless @workspace? - throw new Error("Must pass a project parameter when initializing WorskpaceElements") unless @project? - throw new Error("Must pass a config parameter when initializing WorskpaceElements") unless @config? - throw new Error("Must pass a styles parameter when initializing WorskpaceElements") unless @styles? - - @subscriptions = new CompositeDisposable - @initializeContent() - @observeScrollbarStyle() - @observeTextEditorFontConfig() - - @paneContainer = @views.getView(@model.paneContainer) - @verticalAxis.appendChild(@paneContainer) - @addEventListener 'focus', @handleFocus.bind(this) - - @addEventListener 'mousewheel', @handleMousewheel.bind(this), true - - @panelContainers = - top: @views.getView(@model.panelContainers.top) - left: @views.getView(@model.panelContainers.left) - right: @views.getView(@model.panelContainers.right) - bottom: @views.getView(@model.panelContainers.bottom) - header: @views.getView(@model.panelContainers.header) - footer: @views.getView(@model.panelContainers.footer) - modal: @views.getView(@model.panelContainers.modal) - - @horizontalAxis.insertBefore(@panelContainers.left, @verticalAxis) - @horizontalAxis.appendChild(@panelContainers.right) - - @verticalAxis.insertBefore(@panelContainers.top, @paneContainer) - @verticalAxis.appendChild(@panelContainers.bottom) - - @insertBefore(@panelContainers.header, @horizontalAxis) - @appendChild(@panelContainers.footer) - - @appendChild(@panelContainers.modal) - - this - - getModel: -> @model - - handleMousewheel: (event) -> - 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 - @model.decreaseFontSize() - event.preventDefault() - event.stopPropagation() - - handleFocus: (event) -> - @model.getActivePane().activate() - - focusPaneViewAbove: -> @paneContainer.focusPaneViewAbove() - - focusPaneViewBelow: -> @paneContainer.focusPaneViewBelow() - - focusPaneViewOnLeft: -> @paneContainer.focusPaneViewOnLeft() - - focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight() - - moveActiveItemToPaneAbove: (params) -> @paneContainer.moveActiveItemToPaneAbove(params) - - moveActiveItemToPaneBelow: (params) -> @paneContainer.moveActiveItemToPaneBelow(params) - - moveActiveItemToPaneOnLeft: (params) -> @paneContainer.moveActiveItemToPaneOnLeft(params) - - moveActiveItemToPaneOnRight: (params) -> @paneContainer.moveActiveItemToPaneOnRight(params) - - runPackageSpecs: -> - if activePath = @workspace.getActivePaneItem()?.getPath?() - [projectPath] = @project.relativizePath(activePath) - else - [projectPath] = @project.getPaths() - if projectPath - specPath = path.join(projectPath, 'spec') - testPath = path.join(projectPath, 'test') - if not fs.existsSync(specPath) and fs.existsSync(testPath) - specPath = testPath - - ipcRenderer.send('run-package-specs', specPath) - - runBenchmarks: -> - if activePath = @workspace.getActivePaneItem()?.getPath?() - [projectPath] = @project.relativizePath(activePath) - else - [projectPath] = @project.getPaths() - - if projectPath - ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks')) - -module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype diff --git a/src/workspace-element.js b/src/workspace-element.js new file mode 100644 index 000000000..65333e7d3 --- /dev/null +++ b/src/workspace-element.js @@ -0,0 +1,184 @@ +'use strict' + +/* global HTMLElement */ + +const {ipcRenderer} = require('electron') +const path = require('path') +const fs = require('fs-plus') +const {CompositeDisposable} = require('event-kit') +const scrollbarStyle = require('scrollbar-style') + +class WorkspaceElement extends HTMLElement { + attachedCallback () { + this.focus() + } + + detachedCallback () { + this.subscriptions.dispose() + } + + initializeContent () { + this.classList.add('workspace') + this.setAttribute('tabindex', -1) + + this.verticalAxis = document.createElement('atom-workspace-axis') + this.verticalAxis.classList.add('vertical') + + this.horizontalAxis = document.createElement('atom-workspace-axis') + this.horizontalAxis.classList.add('horizontal') + this.horizontalAxis.appendChild(this.verticalAxis) + + this.appendChild(this.horizontalAxis) + } + + observeScrollbarStyle () { + this.subscriptions.add(scrollbarStyle.observePreferredScrollbarStyle(style => { + switch (style) { + case 'legacy': + this.classList.remove('scrollbars-visible-when-scrolling') + this.classList.add('scrollbars-visible-always') + break + case 'overlay': + this.classList.remove('scrollbars-visible-always') + this.classList.add('scrollbars-visible-when-scrolling') + break + } + })) + } + + observeTextEditorFontConfig () { + this.updateGlobalTextEditorStyleSheet() + this.subscriptions.add(this.config.onDidChange('editor.fontSize', this.updateGlobalTextEditorStyleSheet.bind(this))) + this.subscriptions.add(this.config.onDidChange('editor.fontFamily', this.updateGlobalTextEditorStyleSheet.bind(this))) + this.subscriptions.add(this.config.onDidChange('editor.lineHeight', this.updateGlobalTextEditorStyleSheet.bind(this))) + } + + updateGlobalTextEditorStyleSheet () { + const styleSheetSource = `atom-text-editor { + font-size: ${this.config.get('editor.fontSize')}px; + font-family: ${this.config.get('editor.fontFamily')}; + line-height: ${this.config.get('editor.lineHeight')}; +}` + this.styles.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles'}) + this.views.performDocumentPoll() + } + + initialize (model, {views, workspace, project, config, styles}) { + this.model = model + this.views = views + this.workspace = workspace + this.project = project + this.config = config + this.styles = styles + if (this.views == null) { throw new Error('Must pass a views parameter when initializing WorskpaceElements') } + if (this.workspace == null) { throw new Error('Must pass a workspace parameter when initializing WorskpaceElements') } + if (this.project == null) { throw new Error('Must pass a project parameter when initializing WorskpaceElements') } + if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorskpaceElements') } + if (this.styles == null) { throw new Error('Must pass a styles parameter when initializing WorskpaceElements') } + + this.subscriptions = new CompositeDisposable() + this.initializeContent() + this.observeScrollbarStyle() + this.observeTextEditorFontConfig() + + this.paneContainer = this.views.getView(this.model.paneContainer) + this.verticalAxis.appendChild(this.paneContainer) + this.addEventListener('focus', this.handleFocus.bind(this)) + + this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true) + + this.panelContainers = { + top: this.views.getView(this.model.panelContainers.top), + left: this.views.getView(this.model.panelContainers.left), + right: this.views.getView(this.model.panelContainers.right), + bottom: this.views.getView(this.model.panelContainers.bottom), + header: this.views.getView(this.model.panelContainers.header), + footer: this.views.getView(this.model.panelContainers.footer), + modal: this.views.getView(this.model.panelContainers.modal) + } + + this.horizontalAxis.insertBefore(this.panelContainers.left, this.verticalAxis) + this.horizontalAxis.appendChild(this.panelContainers.right) + + this.verticalAxis.insertBefore(this.panelContainers.top, this.paneContainer) + this.verticalAxis.appendChild(this.panelContainers.bottom) + + this.insertBefore(this.panelContainers.header, this.horizontalAxis) + this.appendChild(this.panelContainers.footer) + + this.appendChild(this.panelContainers.modal) + + return this + } + + getModel () { return this.model } + + handleMousewheel (event) { + if (event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && (event.target.closest('atom-text-editor') != null)) { + if (event.wheelDeltaY > 0) { + this.model.increaseFontSize() + } else if (event.wheelDeltaY < 0) { + this.model.decreaseFontSize() + } + event.preventDefault() + event.stopPropagation() + } + } + + handleFocus (event) { + this.model.getActivePane().activate() + } + + focusPaneViewAbove () { this.paneContainer.focusPaneViewAbove() } + + focusPaneViewBelow () { this.paneContainer.focusPaneViewBelow() } + + focusPaneViewOnLeft () { this.paneContainer.focusPaneViewOnLeft() } + + focusPaneViewOnRight () { this.paneContainer.focusPaneViewOnRight() } + + moveActiveItemToPaneAbove (params) { this.paneContainer.moveActiveItemToPaneAbove(params) } + + moveActiveItemToPaneBelow (params) { this.paneContainer.moveActiveItemToPaneBelow(params) } + + moveActiveItemToPaneOnLeft (params) { this.paneContainer.moveActiveItemToPaneOnLeft(params) } + + moveActiveItemToPaneOnRight (params) { this.paneContainer.moveActiveItemToPaneOnRight(params) } + + runPackageSpecs () { + const activePaneItem = this.workspace.getActivePaneItem() + const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null + let projectPath + if (activePath != null) { + [projectPath] = this.project.relativizePath(activePath) + } else { + [projectPath] = this.project.getPaths() + } + if (projectPath) { + let specPath = path.join(projectPath, 'spec') + const testPath = path.join(projectPath, 'test') + if (!fs.existsSync(specPath) && fs.existsSync(testPath)) { + specPath = testPath + } + + ipcRenderer.send('run-package-specs', specPath) + } + } + + runBenchmarks () { + const activePaneItem = this.workspace.getActivePaneItem() + const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null + let projectPath + if (activePath) { + [projectPath] = this.project.relativizePath(activePath) + } else { + [projectPath] = this.project.getPaths() + } + + if (projectPath) { + ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks')) + } + } +} + +module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype}) diff --git a/src/workspace.coffee b/src/workspace.coffee deleted file mode 100644 index 2a46ce57a..000000000 --- a/src/workspace.coffee +++ /dev/null @@ -1,1121 +0,0 @@ -_ = require 'underscore-plus' -url = require 'url' -path = require 'path' -{Emitter, Disposable, CompositeDisposable} = require 'event-kit' -fs = require 'fs-plus' -{Directory} = require 'pathwatcher' -DefaultDirectorySearcher = require './default-directory-searcher' -Model = require './model' -TextEditor = require './text-editor' -PaneContainer = require './pane-container' -Panel = require './panel' -PanelContainer = require './panel-container' -Task = require './task' - -# Essential: Represents the state of the user interface for the entire window. -# An instance of this class is available via the `atom.workspace` global. -# -# Interact with this object to open files, be notified of current and future -# editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} -# and friends. -# -# * `editor` {TextEditor} the new editor -# -module.exports = -class Workspace extends Model - constructor: (params) -> - super - - { - @packageManager, @config, @project, @grammarRegistry, @notificationManager, - @viewRegistry, @grammarRegistry, @applicationDelegate, @assert, - @deserializerManager, @textEditorRegistry - } = params - - @emitter = new Emitter - @openers = [] - @destroyedItemURIs = [] - - @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) - @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - - @defaultDirectorySearcher = new DefaultDirectorySearcher() - @consumeServices(@packageManager) - - # One cannot simply .bind here since it could be used as a component with - # Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always - # the newly created object. - realThis = this - @buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments) - - @panelContainers = - top: new PanelContainer({location: 'top'}) - left: new PanelContainer({location: 'left'}) - right: new PanelContainer({location: 'right'}) - bottom: new PanelContainer({location: 'bottom'}) - header: new PanelContainer({location: 'header'}) - footer: new PanelContainer({location: 'footer'}) - modal: new PanelContainer({location: 'modal'}) - - @subscribeToEvents() - - reset: (@packageManager) -> - @emitter.dispose() - @emitter = new Emitter - - @paneContainer.destroy() - panelContainer.destroy() for panelContainer in @panelContainers - - @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) - @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - - @panelContainers = - top: new PanelContainer({location: 'top'}) - left: new PanelContainer({location: 'left'}) - right: new PanelContainer({location: 'right'}) - bottom: new PanelContainer({location: 'bottom'}) - header: new PanelContainer({location: 'header'}) - footer: new PanelContainer({location: 'footer'}) - modal: new PanelContainer({location: 'modal'}) - - @originalFontSize = null - @openers = [] - @destroyedItemURIs = [] - @consumeServices(@packageManager) - - subscribeToEvents: -> - @subscribeToActiveItem() - @subscribeToFontSize() - @subscribeToAddedItems() - - consumeServices: ({serviceHub}) -> - @directorySearchers = [] - serviceHub.consume( - 'atom.directory-searcher', - '^0.1.0', - (provider) => @directorySearchers.unshift(provider)) - - # Called by the Serializable mixin during serialization. - serialize: -> - deserializer: 'Workspace' - paneContainer: @paneContainer.serialize() - packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars() - destroyedItemURIs: @destroyedItemURIs.slice() - - deserialize: (state, deserializerManager) -> - for packageName in state.packagesWithActiveGrammars ? [] - @packageManager.getLoadedPackage(packageName)?.loadGrammarsSync() - if state.destroyedItemURIs? - @destroyedItemURIs = state.destroyedItemURIs - @paneContainer.deserialize(state.paneContainer, deserializerManager) - - getPackageNamesWithActiveGrammars: -> - packageNames = [] - addGrammar = ({includedGrammarScopes, packageName}={}) => - return unless packageName - # Prevent cycles - return if packageNames.indexOf(packageName) isnt -1 - - packageNames.push(packageName) - for scopeName in includedGrammarScopes ? [] - addGrammar(@grammarRegistry.grammarForScopeName(scopeName)) - return - - editors = @getTextEditors() - addGrammar(editor.getGrammar()) for editor in editors - - if editors.length > 0 - for grammar in @grammarRegistry.getGrammars() when grammar.injectionSelector - addGrammar(grammar) - - _.uniq(packageNames) - - subscribeToActiveItem: -> - @updateWindowTitle() - @updateDocumentEdited() - @project.onDidChangePaths @updateWindowTitle - - @observeActivePaneItem (item) => - @updateWindowTitle() - @updateDocumentEdited() - - @activeItemSubscriptions?.dispose() - @activeItemSubscriptions = new CompositeDisposable - - if typeof item?.onDidChangeTitle is 'function' - titleSubscription = item.onDidChangeTitle(@updateWindowTitle) - else if typeof item?.on is 'function' - titleSubscription = item.on('title-changed', @updateWindowTitle) - unless typeof titleSubscription?.dispose is 'function' - titleSubscription = new Disposable => item.off('title-changed', @updateWindowTitle) - - if typeof item?.onDidChangeModified is 'function' - modifiedSubscription = item.onDidChangeModified(@updateDocumentEdited) - else if typeof item?.on? is 'function' - modifiedSubscription = item.on('modified-status-changed', @updateDocumentEdited) - unless typeof modifiedSubscription?.dispose is 'function' - modifiedSubscription = new Disposable => item.off('modified-status-changed', @updateDocumentEdited) - - @activeItemSubscriptions.add(titleSubscription) if titleSubscription? - @activeItemSubscriptions.add(modifiedSubscription) if modifiedSubscription? - - subscribeToAddedItems: -> - @onDidAddPaneItem ({item, pane, index}) => - if item instanceof TextEditor - subscriptions = new CompositeDisposable( - @textEditorRegistry.add(item) - @textEditorRegistry.maintainGrammar(item) - @textEditorRegistry.maintainConfig(item) - item.observeGrammar(@handleGrammarUsed.bind(this)) - ) - item.onDidDestroy -> subscriptions.dispose() - @emitter.emit 'did-add-text-editor', {textEditor: item, pane, index} - - # Updates the application's title and proxy icon based on whichever file is - # open. - updateWindowTitle: => - appName = 'Atom' - projectPaths = @project.getPaths() ? [] - if item = @getActivePaneItem() - itemPath = item.getPath?() - itemTitle = item.getLongTitle?() ? item.getTitle?() - projectPath = _.find projectPaths, (projectPath) -> - itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) - itemTitle ?= "untitled" - projectPath ?= if itemPath then path.dirname(itemPath) else projectPaths[0] - if projectPath? - projectPath = fs.tildify(projectPath) - - titleParts = [] - if item? and projectPath? - titleParts.push itemTitle, projectPath - representedPath = itemPath ? projectPath - else if projectPath? - titleParts.push projectPath - representedPath = projectPath - else - titleParts.push itemTitle - representedPath = "" - - unless process.platform is 'darwin' - titleParts.push appName - - document.title = titleParts.join(" \u2014 ") - @applicationDelegate.setRepresentedFilename(representedPath) - - # On macOS, fades the application window's proxy icon when the current file - # has been modified. - updateDocumentEdited: => - modified = @getActivePaneItem()?.isModified?() ? false - @applicationDelegate.setWindowDocumentEdited(modified) - - ### - Section: Event Subscription - ### - - # Essential: Invoke the given callback with all current and future text - # editors in the workspace. - # - # * `callback` {Function} to be called with current and future text editors. - # * `editor` An {TextEditor} that is present in {::getTextEditors} at the time - # of subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeTextEditors: (callback) -> - callback(textEditor) for textEditor in @getTextEditors() - @onDidAddTextEditor ({textEditor}) -> callback(textEditor) - - # Essential: Invoke the given callback with all current and future panes items - # in the workspace. - # - # * `callback` {Function} to be called with current and future pane items. - # * `item` An item that is present in {::getPaneItems} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems: (callback) -> @paneContainer.observePaneItems(callback) - - # Essential: Invoke the given callback when the active pane item changes. - # - # Because observers are invoked synchronously, it's important not to perform - # any expensive operations via this method. Consider - # {::onDidStopChangingActivePaneItem} to delay operations until after changes - # stop occurring. - # - # * `callback` {Function} to be called when the active pane item changes. - # * `item` The active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePaneItem: (callback) -> - @paneContainer.onDidChangeActivePaneItem(callback) - - # Essential: Invoke the given callback when the active pane item stops - # changing. - # - # Observers are called asynchronously 100ms after the last active pane item - # change. Handling changes here rather than in the synchronous - # {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly - # changing or closing tabs and ensures critical UI feedback, like changing the - # highlighted tab, gets priority over work that can be done asynchronously. - # - # * `callback` {Function} to be called when the active pane item stopts - # changing. - # * `item` The active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChangingActivePaneItem: (callback) -> - @paneContainer.onDidStopChangingActivePaneItem(callback) - - # Essential: Invoke the given callback with the current active pane item and - # with all future active pane items in the workspace. - # - # * `callback` {Function} to be called when the active pane item changes. - # * `item` The current active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePaneItem: (callback) -> @paneContainer.observeActivePaneItem(callback) - - # Essential: Invoke the given callback whenever an item is opened. Unlike - # {::onDidAddPaneItem}, observers will be notified for items that are already - # present in the workspace when they are reopened. - # - # * `callback` {Function} to be called whenever an item is opened. - # * `event` {Object} with the following keys: - # * `uri` {String} representing the opened URI. Could be `undefined`. - # * `item` The opened item. - # * `pane` The pane in which the item was opened. - # * `index` The index of the opened item on its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidOpen: (callback) -> - @emitter.on 'did-open', callback - - # Extended: Invoke the given callback when a pane is added to the workspace. - # - # * `callback` {Function} to be called panes are added. - # * `event` {Object} with the following keys: - # * `pane` The added pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPane: (callback) -> @paneContainer.onDidAddPane(callback) - - # Extended: Invoke the given callback before a pane is destroyed in the - # workspace. - # - # * `callback` {Function} to be called before panes are destroyed. - # * `event` {Object} with the following keys: - # * `pane` The pane to be destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroyPane: (callback) -> @paneContainer.onWillDestroyPane(callback) - - # Extended: Invoke the given callback when a pane is destroyed in the - # workspace. - # - # * `callback` {Function} to be called panes are destroyed. - # * `event` {Object} with the following keys: - # * `pane` The destroyed pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroyPane: (callback) -> @paneContainer.onDidDestroyPane(callback) - - # Extended: Invoke the given callback with all current and future panes in the - # workspace. - # - # * `callback` {Function} to be called with current and future panes. - # * `pane` A {Pane} that is present in {::getPanes} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePanes: (callback) -> @paneContainer.observePanes(callback) - - # Extended: Invoke the given callback when the active pane changes. - # - # * `callback` {Function} to be called when the active pane changes. - # * `pane` A {Pane} that is the current return value of {::getActivePane}. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePane: (callback) -> @paneContainer.onDidChangeActivePane(callback) - - # Extended: Invoke the given callback with the current active pane and when - # the active pane changes. - # - # * `callback` {Function} to be called with the current and future active# - # panes. - # * `pane` A {Pane} that is the current return value of {::getActivePane}. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePane: (callback) -> @paneContainer.observeActivePane(callback) - - # Extended: Invoke the given callback when a pane item is added to the - # workspace. - # - # * `callback` {Function} to be called when pane items are added. - # * `event` {Object} with the following keys: - # * `item` The added pane item. - # * `pane` {Pane} containing the added item. - # * `index` {Number} indicating the index of the added item in its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPaneItem: (callback) -> @paneContainer.onDidAddPaneItem(callback) - - # Extended: Invoke the given callback when a pane item is about to be - # destroyed, before the user is prompted to save it. - # - # * `callback` {Function} to be called before pane items are destroyed. - # * `event` {Object} with the following keys: - # * `item` The item to be destroyed. - # * `pane` {Pane} containing the item to be destroyed. - # * `index` {Number} indicating the index of the item to be destroyed in - # its pane. - # - # Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onWillDestroyPaneItem: (callback) -> @paneContainer.onWillDestroyPaneItem(callback) - - # Extended: Invoke the given callback when a pane item is destroyed. - # - # * `callback` {Function} to be called when pane items are destroyed. - # * `event` {Object} with the following keys: - # * `item` The destroyed item. - # * `pane` {Pane} containing the destroyed item. - # * `index` {Number} indicating the index of the destroyed item in its - # pane. - # - # Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onDidDestroyPaneItem: (callback) -> @paneContainer.onDidDestroyPaneItem(callback) - - # Extended: Invoke the given callback when a text editor is added to the - # workspace. - # - # * `callback` {Function} to be called panes are added. - # * `event` {Object} with the following keys: - # * `textEditor` {TextEditor} that was added. - # * `pane` {Pane} containing the added text editor. - # * `index` {Number} indicating the index of the added text editor in its - # pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddTextEditor: (callback) -> - @emitter.on 'did-add-text-editor', callback - - ### - Section: Opening - ### - - # Essential: Opens the given URI in Atom asynchronously. - # If the URI is already open, the existing item for that URI will be - # activated. If no URI is given, or no registered opener can open - # the URI, a new empty {TextEditor} will be created. - # - # * `uri` (optional) A {String} containing a URI. - # * `options` (optional) {Object} - # * `initialLine` A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # * `initialColumn` A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # * `split` Either 'left', 'right', 'up' or 'down'. - # If 'left', the item will be opened in leftmost pane of the current active pane's row. - # If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. - # If 'up', the item will be opened in topmost pane of the current active pane's column. - # If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. - # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on - # containing pane. Defaults to `true`. - # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} - # on containing pane. Defaults to `true`. - # * `pending` A {Boolean} indicating whether or not the item should be opened - # in a pending state. Existing pending items in a pane are replaced with - # new pending items when they are opened. - # * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to - # activate an existing item for the given URI on any pane. - # If `false`, only the active pane will be searched for - # an existing item for the same URI. Defaults to `false`. - # - # Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open: (uri, options={}) -> - searchAllPanes = options.searchAllPanes - split = options.split - uri = @project.resolvePath(uri) - - if not atom.config.get('core.allowPendingPaneItems') - options.pending = false - - # Avoid adding URLs as recent documents to work-around this Spotlight crash: - # https://github.com/atom/atom/issues/10071 - if uri? and (not url.parse(uri).protocol? or process.platform is 'win32') - @applicationDelegate.addRecentDocument(uri) - - pane = @paneContainer.paneForURI(uri) if searchAllPanes - pane ?= switch split - when 'left' - @getActivePane().findLeftmostSibling() - when 'right' - @getActivePane().findOrCreateRightmostSibling() - when 'up' - @getActivePane().findTopmostSibling() - when 'down' - @getActivePane().findOrCreateBottommostSibling() - else - @getActivePane() - - @openURIInPane(uri, pane, options) - - # Open Atom's license in the active pane. - openLicense: -> - @open(path.join(process.resourcesPath, 'LICENSE.md')) - - # Synchronously open the given URI in the active pane. **Only use this method - # in specs. Calling this in production code will block the UI thread and - # everyone will be mad at you.** - # - # * `uri` A {String} containing a URI. - # * `options` An optional options {Object} - # * `initialLine` A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # * `initialColumn` A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on - # the containing pane. Defaults to `true`. - # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} - # on containing pane. Defaults to `true`. - openSync: (uri='', options={}) -> - {initialLine, initialColumn} = options - activatePane = options.activatePane ? true - activateItem = options.activateItem ? true - - uri = @project.resolvePath(uri) - item = @getActivePane().itemForURI(uri) - if uri - item ?= opener(uri, options) for opener in @getOpeners() when not item - item ?= @project.openSync(uri, {initialLine, initialColumn}) - - @getActivePane().activateItem(item) if activateItem - @itemOpened(item) - @getActivePane().activate() if activatePane - item - - openURIInPane: (uri, pane, options={}) -> - activatePane = options.activatePane ? true - activateItem = options.activateItem ? true - - if uri? - if item = pane.itemForURI(uri) - pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item - item ?= opener(uri, options) for opener in @getOpeners() when not item - - try - item ?= @openTextFile(uri, options) - catch error - switch error.code - when 'CANCELLED' - return Promise.resolve() - when 'EACCES' - @notificationManager.addWarning("Permission denied '#{error.path}'") - return Promise.resolve() - when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE', 'ENOTDIR', 'EAGAIN' - @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) - return Promise.resolve() - else - throw error - - Promise.resolve(item) - .then (item) => - return item if pane.isDestroyed() - - @itemOpened(item) - pane.activateItem(item, {pending: options.pending}) if activateItem - pane.activate() if activatePane - - initialLine = initialColumn = 0 - unless Number.isNaN(options.initialLine) - initialLine = options.initialLine - unless Number.isNaN(options.initialColumn) - initialColumn = options.initialColumn - if initialLine >= 0 or initialColumn >= 0 - item.setCursorBufferPosition?([initialLine, initialColumn]) - - index = pane.getActiveItemIndex() - @emitter.emit 'did-open', {uri, pane, item, index} - item - - openTextFile: (uri, options) -> - filePath = @project.resolvePath(uri) - - if filePath? - try - fs.closeSync(fs.openSync(filePath, 'r')) - catch error - # allow ENOENT errors to create an editor for paths that dont exist - throw error unless error.code is 'ENOENT' - - fileSize = fs.getSizeSync(filePath) - - largeFileMode = fileSize >= 2 * 1048576 # 2MB - if fileSize >= @config.get('core.warnOnLargeFileLimit') * 1048576 # 20MB by default - choice = @applicationDelegate.confirm - message: 'Atom will be unresponsive during the loading of very large files.' - detailedMessage: "Do you still want to load this file?" - buttons: ["Proceed", "Cancel"] - if choice is 1 - error = new Error - error.code = 'CANCELLED' - throw error - - @project.bufferForPath(filePath, options).then (buffer) => - @textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) - - handleGrammarUsed: (grammar) -> - return unless grammar? - - @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") - - # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. - # - # * `object` An {Object} you want to perform the check against. - isTextEditor: (object) -> - object instanceof TextEditor - - # Extended: Create a new text editor. - # - # Returns a {TextEditor}. - buildTextEditor: (params) -> - editor = @textEditorRegistry.build(params) - subscriptions = new CompositeDisposable( - @textEditorRegistry.maintainGrammar(editor) - @textEditorRegistry.maintainConfig(editor), - ) - editor.onDidDestroy -> subscriptions.dispose() - editor - - # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been - # reopened. - # - # Returns a {Promise} that is resolved when the item is opened - reopenItem: -> - if uri = @destroyedItemURIs.pop() - @open(uri) - else - Promise.resolve() - - # Public: Register an opener for a uri. - # - # When a URI is opened via {Workspace::open}, Atom loops through its registered - # opener functions until one returns a value for the given uri. - # Openers are expected to return an object that inherits from HTMLElement or - # a model which has an associated view in the {ViewRegistry}. - # A {TextEditor} will be used if no opener returns a value. - # - # ## Examples - # - # ```coffee - # atom.workspace.addOpener (uri) -> - # if path.extname(uri) is '.toml' - # return new TomlEditor(uri) - # ``` - # - # * `opener` A {Function} to be called when a path is being opened. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # opener. - # - # Note that the opener will be called if and only if the URI is not already open - # in the current pane. The searchAllPanes flag expands the search from the - # current pane to all panes. If you wish to open a view of a different type for - # a file that is already open, consider changing the protocol of the URI. For - # example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` - # that is already open in a text editor view. You could signal this by calling - # {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener - # can check the protocol for quux-preview and only handle those URIs that match. - addOpener: (opener) -> - @openers.push(opener) - new Disposable => _.remove(@openers, opener) - - getOpeners: -> - @openers - - ### - Section: Pane Items - ### - - # Essential: Get all pane items in the workspace. - # - # Returns an {Array} of items. - getPaneItems: -> - @paneContainer.getPaneItems() - - # Essential: Get the active {Pane}'s active item. - # - # Returns an pane item {Object}. - getActivePaneItem: -> - @paneContainer.getActivePaneItem() - - # Essential: Get all text editors in the workspace. - # - # Returns an {Array} of {TextEditor}s. - getTextEditors: -> - @getPaneItems().filter (item) -> item instanceof TextEditor - - # Essential: Get the active item if it is an {TextEditor}. - # - # Returns an {TextEditor} or `undefined` if the current active item is not an - # {TextEditor}. - getActiveTextEditor: -> - activeItem = @getActivePaneItem() - activeItem if activeItem instanceof TextEditor - - # Save all pane items. - saveAll: -> - @paneContainer.saveAll() - - confirmClose: (options) -> - @paneContainer.confirmClose(options) - - # Save the active pane item. - # - # If the active pane item currently has a URI according to the item's - # `.getURI` method, calls `.save` on the item. Otherwise - # {::saveActivePaneItemAs} # will be called instead. This method does nothing - # if the active item does not implement a `.save` method. - saveActivePaneItem: -> - @getActivePane().saveActiveItem() - - # Prompt the user for a path and save the active pane item to it. - # - # Opens a native dialog where the user selects a path on disk, then calls - # `.saveAs` on the item with the selected path. This method does nothing if - # the active item does not implement a `.saveAs` method. - saveActivePaneItemAs: -> - @getActivePane().saveActiveItemAs() - - # Destroy (close) the active pane item. - # - # Removes the active pane item and calls the `.destroy` method on it if one is - # defined. - destroyActivePaneItem: -> - @getActivePane().destroyActiveItem() - - ### - Section: Panes - ### - - # Extended: Get all panes in the workspace. - # - # Returns an {Array} of {Pane}s. - getPanes: -> - @paneContainer.getPanes() - - # Extended: Get the active {Pane}. - # - # Returns a {Pane}. - getActivePane: -> - @paneContainer.getActivePane() - - # Extended: Make the next pane active. - activateNextPane: -> - @paneContainer.activateNextPane() - - # Extended: Make the previous pane active. - activatePreviousPane: -> - @paneContainer.activatePreviousPane() - - # Extended: Get the first {Pane} with an item for the given URI. - # - # * `uri` {String} uri - # - # Returns a {Pane} or `undefined` if no pane exists for the given URI. - paneForURI: (uri) -> - @paneContainer.paneForURI(uri) - - # Extended: Get the {Pane} containing the given item. - # - # * `item` Item the returned pane contains. - # - # Returns a {Pane} or `undefined` if no pane exists for the given item. - paneForItem: (item) -> - @paneContainer.paneForItem(item) - - # Destroy (close) the active pane. - destroyActivePane: -> - @getActivePane()?.destroy() - - # Close the active pane item, or the active pane if it is empty, - # or the current window if there is only the empty root pane. - closeActivePaneItemOrEmptyPaneOrWindow: -> - if @getActivePaneItem()? - @destroyActivePaneItem() - else if @getPanes().length > 1 - @destroyActivePane() - else if @config.get('core.closeEmptyWindows') - atom.close() - - # Increase the editor font size by 1px. - increaseFontSize: -> - @config.set("editor.fontSize", @config.get("editor.fontSize") + 1) - - # Decrease the editor font size by 1px. - decreaseFontSize: -> - fontSize = @config.get("editor.fontSize") - @config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - - # Restore to the window's original editor font size. - resetFontSize: -> - if @originalFontSize - @config.set("editor.fontSize", @originalFontSize) - - subscribeToFontSize: -> - @config.onDidChange 'editor.fontSize', ({oldValue}) => - @originalFontSize ?= oldValue - - # Removes the item's uri from the list of potential items to reopen. - itemOpened: (item) -> - if typeof item.getURI is 'function' - uri = item.getURI() - else if typeof item.getUri is 'function' - uri = item.getUri() - - if uri? - _.remove(@destroyedItemURIs, uri) - - # Adds the destroyed item's uri to the list of items to reopen. - didDestroyPaneItem: ({item}) => - if typeof item.getURI is 'function' - uri = item.getURI() - else if typeof item.getUri is 'function' - uri = item.getUri() - - if uri? - @destroyedItemURIs.push(uri) - - # Called by Model superclass when destroyed - destroyed: -> - @paneContainer.destroy() - @activeItemSubscriptions?.dispose() - - - ### - Section: Panels - - Panels are used to display UI related to an editor window. They are placed at one of the four - edges of the window: left, right, top or bottom. If there are multiple panels on the same window - edge they are stacked in order of priority: higher priority is closer to the center, lower - priority towards the edge. - - *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher - priority, allowing fixed size panels to be closer to the edge. This allows control targets to - remain more static for easier targeting by users that employ mice or trackpads. (See - [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) - ### - - # Essential: Get an {Array} of all the panel items at the bottom of the editor window. - getBottomPanels: -> - @getPanels('bottom') - - # Essential: Adds a panel item to the bottom of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addBottomPanel: (options) -> - @addPanel('bottom', options) - - # Essential: Get an {Array} of all the panel items to the left of the editor window. - getLeftPanels: -> - @getPanels('left') - - # Essential: Adds a panel item to the left of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addLeftPanel: (options) -> - @addPanel('left', options) - - # Essential: Get an {Array} of all the panel items to the right of the editor window. - getRightPanels: -> - @getPanels('right') - - # Essential: Adds a panel item to the right of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addRightPanel: (options) -> - @addPanel('right', options) - - # Essential: Get an {Array} of all the panel items at the top of the editor window. - getTopPanels: -> - @getPanels('top') - - # Essential: Adds a panel item to the top of the editor window above the tabs. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addTopPanel: (options) -> - @addPanel('top', options) - - # Essential: Get an {Array} of all the panel items in the header. - getHeaderPanels: -> - @getPanels('header') - - # Essential: Adds a panel item to the header. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addHeaderPanel: (options) -> - @addPanel('header', options) - - # Essential: Get an {Array} of all the panel items in the footer. - getFooterPanels: -> - @getPanels('footer') - - # Essential: Adds a panel item to the footer. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addFooterPanel: (options) -> - @addPanel('footer', options) - - # Essential: Get an {Array} of all the modal panel items - getModalPanels: -> - @getPanels('modal') - - # Essential: Adds a panel item as a modal dialog. - # - # * `options` {Object} - # * `item` Your panel content. It can be a DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # model option. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addModalPanel: (options={}) -> - @addPanel('modal', options) - - # Essential: Returns the {Panel} associated with the given item. Returns - # `null` when the item has no panel. - # - # * `item` Item the panel contains - panelForItem: (item) -> - for location, container of @panelContainers - panel = container.panelForItem(item) - return panel if panel? - null - - getPanels: (location) -> - @panelContainers[location].getPanels() - - addPanel: (location, options) -> - options ?= {} - @panelContainers[location].addPanel(new Panel(options)) - - ### - Section: Searching and Replacing - ### - - # Public: Performs a search across all files in the workspace. - # - # * `regex` {RegExp} to search with. - # * `options` (optional) {Object} - # * `paths` An {Array} of glob patterns to search within. - # * `onPathsSearched` (optional) {Function} to be periodically called - # with number of paths searched. - # * `iterator` {Function} callback on each file found. - # - # Returns a {Promise} with a `cancel()` method that will cancel all - # of the underlying searches that were started as part of this scan. - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - # Find a searcher for every Directory in the project. Each searcher that is matched - # will be associated with an Array of Directory objects in the Map. - directoriesForSearcher = new Map() - for directory in @project.getDirectories() - searcher = @defaultDirectorySearcher - for directorySearcher in @directorySearchers - if directorySearcher.canSearchDirectory(directory) - searcher = directorySearcher - break - directories = directoriesForSearcher.get(searcher) - unless directories - directories = [] - directoriesForSearcher.set(searcher, directories) - directories.push(directory) - - # Define the onPathsSearched callback. - if _.isFunction(options.onPathsSearched) - # Maintain a map of directories to the number of search results. When notified of a new count, - # replace the entry in the map and update the total. - onPathsSearchedOption = options.onPathsSearched - totalNumberOfPathsSearched = 0 - numberOfPathsSearchedForSearcher = new Map() - onPathsSearched = (searcher, numberOfPathsSearched) -> - oldValue = numberOfPathsSearchedForSearcher.get(searcher) - if oldValue - totalNumberOfPathsSearched -= oldValue - numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched) - totalNumberOfPathsSearched += numberOfPathsSearched - onPathsSearchedOption(totalNumberOfPathsSearched) - else - onPathsSearched = -> - - # Kick off all of the searches and unify them into one Promise. - allSearches = [] - directoriesForSearcher.forEach (directories, searcher) => - searchOptions = - inclusions: options.paths or [] - includeHidden: true - excludeVcsIgnores: @config.get('core.excludeVcsIgnoredPaths') - exclusions: @config.get('core.ignoredNames') - follow: @config.get('core.followSymlinks') - didMatch: (result) => - iterator(result) unless @project.isPathModified(result.filePath) - didError: (error) -> - iterator(null, error) - didSearchPaths: (count) -> onPathsSearched(searcher, count) - directorySearcher = searcher.search(directories, regex, searchOptions) - allSearches.push(directorySearcher) - searchPromise = Promise.all(allSearches) - - for buffer in @project.getBuffers() when buffer.isModified() - filePath = buffer.getPath() - continue unless @project.contains(filePath) - matches = [] - buffer.scan regex, (match) -> matches.push match - iterator {filePath, matches} if matches.length > 0 - - # Make sure the Promise that is returned to the client is cancelable. To be consistent - # with the existing behavior, instead of cancel() rejecting the promise, it should - # resolve it with the special value 'cancelled'. At least the built-in find-and-replace - # package relies on this behavior. - isCancelled = false - cancellablePromise = new Promise (resolve, reject) -> - onSuccess = -> - if isCancelled - resolve('cancelled') - else - resolve(null) - - onFailure = -> - promise.cancel() for promise in allSearches - reject() - - searchPromise.then(onSuccess, onFailure) - cancellablePromise.cancel = -> - isCancelled = true - # Note that cancelling all of the members of allSearches will cause all of the searches - # to resolve, which causes searchPromise to resolve, which is ultimately what causes - # cancellablePromise to resolve. - promise.cancel() for promise in allSearches - - # Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` - # method in the find-and-replace package expects the object returned by this method to have a - # `done()` method. Include a done() method until find-and-replace can be updated. - cancellablePromise.done = (onSuccessOrFailure) -> - cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) - cancellablePromise - - # Public: Performs a replace across all the specified files in the project. - # - # * `regex` A {RegExp} to search with. - # * `replacementText` {String} to replace all matches of regex with. - # * `filePaths` An {Array} of file path strings to run the replace on. - # * `iterator` A {Function} callback on each file with replacements: - # * `options` {Object} with keys `filePath` and `replacements`. - # - # Returns a {Promise}. - replace: (regex, replacementText, filePaths, iterator) -> - new Promise (resolve, reject) => - openPaths = (buffer.getPath() for buffer in @project.getBuffers()) - outOfProcessPaths = _.difference(filePaths, openPaths) - - inProcessFinished = not openPaths.length - outOfProcessFinished = not outOfProcessPaths.length - checkFinished = -> - resolve() if outOfProcessFinished and inProcessFinished - - unless outOfProcessFinished.length - flags = 'g' - flags += 'i' if regex.ignoreCase - - task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> - outOfProcessFinished = true - checkFinished() - - task.on 'replace:path-replaced', iterator - task.on 'replace:file-error', (error) -> iterator(null, error) - - for buffer in @project.getBuffers() - continue unless buffer.getPath() in filePaths - replacements = buffer.replace(regex, replacementText, iterator) - iterator({filePath: buffer.getPath(), replacements}) if replacements - - inProcessFinished = true - checkFinished() - - checkoutHeadRevision: (editor) -> - if editor.getPath() - checkoutHead = => - @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then (repository) -> - repository?.checkoutHeadForEditor(editor) - - if @config.get('editor.confirmCheckoutHeadRevision') - @applicationDelegate.confirm - message: 'Confirm Checkout HEAD Revision' - detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?" - buttons: - OK: checkoutHead - Cancel: null - else - checkoutHead() - else - Promise.resolve(false) diff --git a/src/workspace.js b/src/workspace.js new file mode 100644 index 000000000..1916c24c6 --- /dev/null +++ b/src/workspace.js @@ -0,0 +1,1423 @@ +'use strict' + +const _ = require('underscore-plus') +const url = require('url') +const path = require('path') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const fs = require('fs-plus') +const {Directory} = require('pathwatcher') +const DefaultDirectorySearcher = require('./default-directory-searcher') +const Model = require('./model') +const TextEditor = require('./text-editor') +const PaneContainer = require('./pane-container') +const Panel = require('./panel') +const PanelContainer = require('./panel-container') +const Task = require('./task') + +// Essential: Represents the state of the user interface for the entire window. +// An instance of this class is available via the `atom.workspace` global. +// +// Interact with this object to open files, be notified of current and future +// editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} +// and friends. +// +// * `editor` {TextEditor} the new editor +// +module.exports = class Workspace extends Model { + constructor (params) { + super(...arguments) + + this.updateWindowTitle = this.updateWindowTitle.bind(this) + this.updateDocumentEdited = this.updateDocumentEdited.bind(this) + this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this) + this.didChangeActivePaneItem = this.didChangeActivePaneItem.bind(this) + + this.packageManager = params.packageManager + this.config = params.config + this.project = params.project + this.notificationManager = params.notificationManager + this.viewRegistry = params.viewRegistry + this.grammarRegistry = params.grammarRegistry + this.applicationDelegate = params.applicationDelegate + this.assert = params.assert + this.deserializerManager = params.deserializerManager + this.textEditorRegistry = params.textEditorRegistry + + this.emitter = new Emitter() + this.openers = [] + this.destroyedItemURIs = [] + + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + notificationManager: this.notificationManager, + deserializerManager: this.deserializerManager + }) + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) + + this.defaultDirectorySearcher = new DefaultDirectorySearcher() + this.consumeServices(this.packageManager) + + this.panelContainers = { + top: new PanelContainer({location: 'top'}), + left: new PanelContainer({location: 'left'}), + right: new PanelContainer({location: 'right'}), + bottom: new PanelContainer({location: 'bottom'}), + header: new PanelContainer({location: 'header'}), + footer: new PanelContainer({location: 'footer'}), + modal: new PanelContainer({location: 'modal'}) + } + + this.subscribeToEvents() + } + + initialize () { + this.paneContainer.initialize() + this.didChangeActivePaneItem() + } + + reset (packageManager) { + this.packageManager = packageManager + this.emitter.dispose() + this.emitter = new Emitter() + + this.paneContainer.destroy() + _.values(this.panelContainers).forEach(panelContainer => { panelContainer.destroy() }) + + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + notificationManager: this.notificationManager, + deserializerManager: this.deserializerManager + }) + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) + + this.panelContainers = { + top: new PanelContainer({location: 'top'}), + left: new PanelContainer({location: 'left'}), + right: new PanelContainer({location: 'right'}), + bottom: new PanelContainer({location: 'bottom'}), + header: new PanelContainer({location: 'header'}), + footer: new PanelContainer({location: 'footer'}), + modal: new PanelContainer({location: 'modal'}) + } + + this.originalFontSize = null + this.openers = [] + this.destroyedItemURIs = [] + this.consumeServices(this.packageManager) + this.initialize() + } + + subscribeToEvents () { + this.subscribeToActiveItem() + this.subscribeToFontSize() + this.subscribeToAddedItems() + } + + consumeServices ({serviceHub}) { + this.directorySearchers = [] + serviceHub.consume( + 'atom.directory-searcher', + '^0.1.0', + provider => this.directorySearchers.unshift(provider) + ) + } + + // Called by the Serializable mixin during serialization. + serialize () { + return { + deserializer: 'Workspace', + paneContainer: this.paneContainer.serialize(), + packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), + destroyedItemURIs: this.destroyedItemURIs.slice() + } + } + + deserialize (state, deserializerManager) { + const packagesWithActiveGrammars = + state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : [] + for (let packageName of packagesWithActiveGrammars) { + const pkg = this.packageManager.getLoadedPackage(packageName) + if (pkg != null) { + pkg.loadGrammarsSync() + } + } + if (state.destroyedItemURIs != null) { + this.destroyedItemURIs = state.destroyedItemURIs + } + return this.paneContainer.deserialize(state.paneContainer, deserializerManager) + } + + getPackageNamesWithActiveGrammars () { + const packageNames = [] + const addGrammar = ({includedGrammarScopes, packageName} = {}) => { + if (!packageName) { return } + // Prevent cycles + if (packageNames.indexOf(packageName) !== -1) { return } + + packageNames.push(packageName) + for (let scopeName of includedGrammarScopes != null ? includedGrammarScopes : []) { + addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)) + } + } + + const editors = this.getTextEditors() + for (let editor of editors) { addGrammar(editor.getGrammar()) } + + if (editors.length > 0) { + for (let grammar of this.grammarRegistry.getGrammars()) { + if (grammar.injectionSelector) { + addGrammar(grammar) + } + } + } + + return _.uniq(packageNames) + } + + subscribeToActiveItem () { + this.project.onDidChangePaths(this.updateWindowTitle) + this.onDidChangeActivePaneItem(this.didChangeActivePaneItem) + } + + didChangeActivePaneItem (item) { + this.updateWindowTitle() + this.updateDocumentEdited() + if (this.activeItemSubscriptions != null) { + this.activeItemSubscriptions.dispose() + } + this.activeItemSubscriptions = new CompositeDisposable() + + let modifiedSubscription, titleSubscription + + if (item != null && typeof item.onDidChangeTitle === 'function') { + titleSubscription = item.onDidChangeTitle(this.updateWindowTitle) + } else if (item != null && typeof item.on === 'function') { + titleSubscription = item.on('title-changed', this.updateWindowTitle) + if (titleSubscription == null || typeof titleSubscription.dispose !== 'function') { + titleSubscription = new Disposable(() => { + item.off('title-changed', this.updateWindowTitle) + }) + } + } + + if (item != null && typeof item.onDidChangeModified === 'function') { + modifiedSubscription = item.onDidChangeModified(this.updateDocumentEdited) + } else if (item != null && typeof item.on === 'function') { + modifiedSubscription = item.on('modified-status-changed', this.updateDocumentEdited) + if (modifiedSubscription == null || typeof modifiedSubscription.dispose !== 'function') { + modifiedSubscription = new Disposable(() => { + item.off('modified-status-changed', this.updateDocumentEdited) + }) + } + } + + if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription) } + if (modifiedSubscription != null) { this.activeItemSubscriptions.add(modifiedSubscription) } + } + + subscribeToAddedItems () { + this.onDidAddPaneItem(({item, pane, index}) => { + if (item instanceof TextEditor) { + const subscriptions = new CompositeDisposable( + this.textEditorRegistry.add(item), + this.textEditorRegistry.maintainGrammar(item), + this.textEditorRegistry.maintainConfig(item), + item.observeGrammar(this.handleGrammarUsed.bind(this)) + ) + item.onDidDestroy(() => { subscriptions.dispose() }) + this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}) + } + }) + } + + // Updates the application's title and proxy icon based on whichever file is + // open. + updateWindowTitle () { + let itemPath, itemTitle, projectPath, representedPath + const appName = 'Atom' + const left = this.project.getPaths() + const projectPaths = left != null ? left : [] + const item = this.getActivePaneItem() + if (item) { + itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined + const longTitle = typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined + itemTitle = longTitle == null + ? (typeof item.getTitle === 'function' ? item.getTitle() : undefined) + : longTitle + projectPath = _.find( + projectPaths, + projectPath => + (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined) + ) + } + if (itemTitle == null) { itemTitle = 'untitled' } + if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0] } + if (projectPath != null) { + projectPath = fs.tildify(projectPath) + } + + const titleParts = [] + if ((item != null) && (projectPath != null)) { + titleParts.push(itemTitle, projectPath) + representedPath = itemPath != null ? itemPath : projectPath + } else if (projectPath != null) { + titleParts.push(projectPath) + representedPath = projectPath + } else { + titleParts.push(itemTitle) + representedPath = '' + } + + if (process.platform !== 'darwin') { + titleParts.push(appName) + } + + document.title = titleParts.join(' \u2014 ') + this.applicationDelegate.setRepresentedFilename(representedPath) + } + + // On macOS, fades the application window's proxy icon when the current file + // has been modified. + updateDocumentEdited () { + const activePaneItem = this.getActivePaneItem() + const modified = activePaneItem != null && typeof activePaneItem.isModified === 'function' + ? activePaneItem.isModified() || false + : false + this.applicationDelegate.setWindowDocumentEdited(modified) + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the workspace. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors (callback) { + for (let textEditor of this.getTextEditors()) { callback(textEditor) } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) + } + + // Essential: Invoke the given callback with all current and future panes items + // in the workspace. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the workspace. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem (callback) { return this.paneContainer.observeActivePaneItem(callback) } + + // Essential: Invoke the given callback whenever an item is opened. Unlike + // {::onDidAddPaneItem}, observers will be notified for items that are already + // present in the workspace when they are reopened. + // + // * `callback` {Function} to be called whenever an item is opened. + // * `event` {Object} with the following keys: + // * `uri` {String} representing the opened URI. Could be `undefined`. + // * `item` The opened item. + // * `pane` The pane in which the item was opened. + // * `index` The index of the opened item on its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidOpen (callback) { + return this.emitter.on('did-open', callback) + } + + // Extended: Invoke the given callback when a pane is added to the workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane (callback) { return this.paneContainer.onDidAddPane(callback) } + + // Extended: Invoke the given callback before a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane (callback) { return this.paneContainer.onWillDestroyPane(callback) } + + // Extended: Invoke the given callback when a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane (callback) { return this.paneContainer.onDidDestroyPane(callback) } + + // Extended: Invoke the given callback with all current and future panes in the + // workspace. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes (callback) { return this.paneContainer.observePanes(callback) } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane (callback) { return this.paneContainer.onDidChangeActivePane(callback) } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane (callback) { return this.paneContainer.observeActivePane(callback) } + + // Extended: Invoke the given callback when a pane item is added to the + // workspace. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem (callback) { return this.paneContainer.onDidAddPaneItem(callback) } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem (callback) { return this.paneContainer.onWillDestroyPaneItem(callback) } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem (callback) { return this.paneContainer.onDidDestroyPaneItem(callback) } + + // Extended: Invoke the given callback when a text editor is added to the + // workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `textEditor` {TextEditor} that was added. + // * `pane` {Pane} containing the added text editor. + // * `index` {Number} indicating the index of the added text editor in its + // pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddTextEditor (callback) { + return this.emitter.on('did-add-text-editor', callback) + } + + /* + Section: Opening + */ + + // Essential: Opens the given URI in Atom asynchronously. + // If the URI is already open, the existing item for that URI will be + // activated. If no URI is given, or no registered opener can open + // the URI, a new empty {TextEditor} will be created. + // + // * `uri` (optional) A {String} containing a URI. + // * `options` (optional) {Object} + // * `initialLine` A {Number} indicating which row to move the cursor to + // initially. Defaults to `0`. + // * `initialColumn` A {Number} indicating which column to move the cursor to + // initially. Defaults to `0`. + // * `split` Either 'left', 'right', 'up' or 'down'. + // If 'left', the item will be opened in leftmost pane of the current active pane's row. + // If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. + // If 'up', the item will be opened in topmost pane of the current active pane's column. + // If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. + // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + // containing pane. Defaults to `true`. + // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + // on containing pane. Defaults to `true`. + // * `pending` A {Boolean} indicating whether or not the item should be opened + // in a pending state. Existing pending items in a pane are replaced with + // new pending items when they are opened. + // * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to + // activate an existing item for the given URI on any pane. + // If `false`, only the active pane will be searched for + // an existing item for the same URI. Defaults to `false`. + // + // Returns a {Promise} that resolves to the {TextEditor} for the file URI. + open (uri_, options = {}) { + const uri = this.project.resolvePath(uri_) + const {searchAllPanes, split} = options + + if (!atom.config.get('core.allowPendingPaneItems')) { + options.pending = false + } + + // Avoid adding URLs as recent documents to work-around this Spotlight crash: + // https://github.com/atom/atom/issues/10071 + if ((uri != null) && ((url.parse(uri).protocol == null) || (process.platform === 'win32'))) { + this.applicationDelegate.addRecentDocument(uri) + } + + let pane + if (searchAllPanes) { pane = this.paneForURI(uri) } + if (pane == null) { + switch (split) { + case 'left': + pane = this.getActivePane().findLeftmostSibling() + break + case 'right': + pane = this.getActivePane().findOrCreateRightmostSibling() + break + case 'up': + pane = this.getActivePane().findTopmostSibling() + break + case 'down': + pane = this.getActivePane().findOrCreateBottommostSibling() + break + default: + pane = this.getActivePane() + break + } + } + + let item + if (uri != null) { + item = pane.itemForURI(uri) + } + if (item == null) { + item = this.createItemForURI(uri, options) + } + + return Promise.resolve(item) + .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + } + + // Open Atom's license in the active pane. + openLicense () { + return this.open(path.join(process.resourcesPath, 'LICENSE.md')) + } + + // Synchronously open the given URI in the active pane. **Only use this method + // in specs. Calling this in production code will block the UI thread and + // everyone will be mad at you.** + // + // * `uri` A {String} containing a URI. + // * `options` An optional options {Object} + // * `initialLine` A {Number} indicating which row to move the cursor to + // initially. Defaults to `0`. + // * `initialColumn` A {Number} indicating which column to move the cursor to + // initially. Defaults to `0`. + // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + // the containing pane. Defaults to `true`. + // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + // on containing pane. Defaults to `true`. + openSync (uri_ = '', options = {}) { + const {initialLine, initialColumn} = options + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + + const uri = this.project.resolvePath(uri) + let item = this.getActivePane().itemForURI(uri) + if (uri && (item == null)) { + for (const opener of this.getOpeners()) { + item = opener(uri, options) + if (item) break + } + } + if (item == null) { + item = this.project.openSync(uri, {initialLine, initialColumn}) + } + + if (activateItem) { + this.getActivePane().activateItem(item) + } + this.itemOpened(item) + if (activatePane) { + this.getActivePane().activate() + } + return item + } + + openURIInPane (uri, pane, options = {}) { + let item + if (uri != null) { + item = pane.itemForURI(uri) + } + if (item == null) { + item = this.createItemForURI(uri, options) + } + return Promise.resolve(item) + .then(item => this.openItem(item, Object.assign({pane, uri}, options))) + } + + // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI. + createItemForURI (uri, options) { + if (uri != null) { + for (let opener of this.getOpeners()) { + const item = opener(uri, options) + if (item != null) return Promise.resolve(item) + } + } + + try { + return this.openTextFile(uri, options) + } catch (error) { + switch (error.code) { + case 'CANCELLED': + return Promise.resolve() + case 'EACCES': + this.notificationManager.addWarning(`Permission denied '${error.path}'`) + return Promise.resolve() + case 'EPERM': + case 'EBUSY': + case 'ENXIO': + case 'EIO': + case 'ENOTCONN': + case 'UNKNOWN': + case 'ECONNRESET': + case 'EINVAL': + case 'EMFILE': + case 'ENOTDIR': + case 'EAGAIN': + this.notificationManager.addWarning( + `Unable to open '${error.path != null ? error.path : uri}'`, + {detail: error.message} + ) + return Promise.resolve() + default: + throw error + } + } + } + + openItem (item, options = {}) { + const {pane} = options + + if (item == null) return undefined + if (pane.isDestroyed()) return item + + if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() + } + + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true + this.itemOpened(item) + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } + + let initialColumn = 0 + let initialLine = 0 + if (!Number.isNaN(options.initialLine)) { + initialLine = options.initialLine + } + if (!Number.isNaN(options.initialColumn)) { + initialColumn = options.initialColumn + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]) + } + } + + const index = pane.getActiveItemIndex() + const uri = options.uri == null && typeof item.getURI === 'function' ? item.getURI() : options.uri + this.emitter.emit('did-open', {uri, pane, item, index}) + return item + } + + openTextFile (uri, options) { + const filePath = this.project.resolvePath(uri) + + if (filePath != null) { + try { + fs.closeSync(fs.openSync(filePath, 'r')) + } catch (error) { + // allow ENOENT errors to create an editor for paths that dont exist + if (error.code !== 'ENOENT') { + throw error + } + } + } + + const fileSize = fs.getSizeSync(filePath) + + const largeFileMode = fileSize >= (2 * 1048576) // 2MB + if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 20MB by default + const choice = this.applicationDelegate.confirm({ + message: 'Atom will be unresponsive during the loading of very large files.', + detailedMessage: 'Do you still want to load this file?', + buttons: ['Proceed', 'Cancel'] + }) + if (choice === 1) { + const error = new Error() + error.code = 'CANCELLED' + throw error + } + } + + return this.project.bufferForPath(filePath, options) + .then(buffer => { + return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) + }) + } + + handleGrammarUsed (grammar) { + if (grammar == null) { return } + return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`) + } + + // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. + // + // * `object` An {Object} you want to perform the check against. + isTextEditor (object) { + return object instanceof TextEditor + } + + // Extended: Create a new text editor. + // + // Returns a {TextEditor}. + buildTextEditor (params) { + const editor = this.textEditorRegistry.build(params) + const subscriptions = new CompositeDisposable( + this.textEditorRegistry.maintainGrammar(editor), + this.textEditorRegistry.maintainConfig(editor) + ) + editor.onDidDestroy(() => { subscriptions.dispose() }) + return editor + } + + // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been + // reopened. + // + // Returns a {Promise} that is resolved when the item is opened + reopenItem () { + const uri = this.destroyedItemURIs.pop() + if (uri) { + return this.open(uri) + } else { + return Promise.resolve() + } + } + + // Public: Register an opener for a uri. + // + // When a URI is opened via {Workspace::open}, Atom loops through its registered + // opener functions until one returns a value for the given uri. + // Openers are expected to return an object that inherits from HTMLElement or + // a model which has an associated view in the {ViewRegistry}. + // A {TextEditor} will be used if no opener returns a value. + // + // ## Examples + // + // ```coffee + // atom.workspace.addOpener (uri) -> + // if path.extname(uri) is '.toml' + // return new TomlEditor(uri) + // ``` + // + // * `opener` A {Function} to be called when a path is being opened. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // opener. + // + // Note that the opener will be called if and only if the URI is not already open + // in the current pane. The searchAllPanes flag expands the search from the + // current pane to all panes. If you wish to open a view of a different type for + // a file that is already open, consider changing the protocol of the URI. For + // example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` + // that is already open in a text editor view. You could signal this by calling + // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener + // can check the protocol for quux-preview and only handle those URIs that match. + addOpener (opener) { + this.openers.push(opener) + return new Disposable(() => { _.remove(this.openers, opener) }) + } + + getOpeners () { + return this.openers + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the workspace. + // + // Returns an {Array} of items. + getPaneItems () { + return this.paneContainer.getPaneItems() + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() + } + + // Essential: Get all text editors in the workspace. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors () { + return this.getPaneItems().filter(item => item instanceof TextEditor) + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } + } + + // Save all pane items. + saveAll () { + return this.paneContainer.saveAll() + } + + confirmClose (options) { + return this.paneContainer.confirmClose(options) + } + + // Save the active pane item. + // + // If the active pane item currently has a URI according to the item's + // `.getURI` method, calls `.save` on the item. Otherwise + // {::saveActivePaneItemAs} # will be called instead. This method does nothing + // if the active item does not implement a `.save` method. + saveActivePaneItem () { + return this.getActivePane().saveActiveItem() + } + + // Prompt the user for a path and save the active pane item to it. + // + // Opens a native dialog where the user selects a path on disk, then calls + // `.saveAs` on the item with the selected path. This method does nothing if + // the active item does not implement a `.saveAs` method. + saveActivePaneItemAs () { + return this.getActivePane().saveActiveItemAs() + } + + // Destroy (close) the active pane item. + // + // Removes the active pane item and calls the `.destroy` method on it if one is + // defined. + destroyActivePaneItem () { + return this.getActivePane().destroyActiveItem() + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the workspace. + // + // Returns an {Array} of {Pane}s. + getPanes () { + return this.paneContainer.getPanes() + } + + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane () { + return this.paneContainer.getActivePane() + } + + // Extended: Make the next pane active. + activateNextPane () { + return this.paneContainer.activateNextPane() + } + + // Extended: Make the previous pane active. + activatePreviousPane () { + return this.paneContainer.activatePreviousPane() + } + + // Extended: Get the first {Pane} with an item for the given URI. + // + // * `uri` {String} uri + // + // Returns a {Pane} or `undefined` if no pane exists for the given URI. + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) + } + + // Extended: Get the {Pane} containing the given item. + // + // * `item` Item the returned pane contains. + // + // Returns a {Pane} or `undefined` if no pane exists for the given item. + paneForItem (item) { + return this.paneContainer.paneForItem(item) + } + + // Destroy (close) the active pane. + destroyActivePane () { + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } + } + + // Close the active pane item, or the active pane if it is empty, + // or the current window if there is only the empty root pane. + closeActivePaneItemOrEmptyPaneOrWindow () { + if (this.getActivePaneItem() != null) { + this.destroyActivePaneItem() + } else if (this.getPanes().length > 1) { + this.destroyActivePane() + } else if (this.config.get('core.closeEmptyWindows')) { + atom.close() + } + } + + // Increase the editor font size by 1px. + increaseFontSize () { + this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1) + } + + // Decrease the editor font size by 1px. + decreaseFontSize () { + const fontSize = this.config.get('editor.fontSize') + if (fontSize > 1) { + this.config.set('editor.fontSize', fontSize - 1) + } + } + + // Restore to the window's original editor font size. + resetFontSize () { + if (this.originalFontSize) { + this.config.set('editor.fontSize', this.originalFontSize) + } + } + + subscribeToFontSize () { + return this.config.onDidChange('editor.fontSize', ({oldValue}) => { + if (this.originalFontSize == null) { + this.originalFontSize = oldValue + } + }) + } + + // Removes the item's uri from the list of potential items to reopen. + itemOpened (item) { + let uri + if (typeof item.getURI === 'function') { + uri = item.getURI() + } else if (typeof item.getUri === 'function') { + uri = item.getUri() + } + + if (uri != null) { + _.remove(this.destroyedItemURIs, uri) + } + } + + // Adds the destroyed item's uri to the list of items to reopen. + didDestroyPaneItem ({item}) { + let uri + if (typeof item.getURI === 'function') { + uri = item.getURI() + } else if (typeof item.getUri === 'function') { + uri = item.getUri() + } + + if (uri != null) { + this.destroyedItemURIs.push(uri) + } + } + + // Called by Model superclass when destroyed + destroyed () { + this.paneContainer.destroy() + if (this.activeItemSubscriptions != null) { + this.activeItemSubscriptions.dispose() + } + } + + /* + Section: Panels + + Panels are used to display UI related to an editor window. They are placed at one of the four + edges of the window: left, right, top or bottom. If there are multiple panels on the same window + edge they are stacked in order of priority: higher priority is closer to the center, lower + priority towards the edge. + + *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher + priority, allowing fixed size panels to be closer to the edge. This allows control targets to + remain more static for easier targeting by users that employ mice or trackpads. (See + [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) + */ + + // Essential: Get an {Array} of all the panel items at the bottom of the editor window. + getBottomPanels () { + return this.getPanels('bottom') + } + + // Essential: Adds a panel item to the bottom of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addBottomPanel (options) { + return this.addPanel('bottom', options) + } + + // Essential: Get an {Array} of all the panel items to the left of the editor window. + getLeftPanels () { + return this.getPanels('left') + } + + // Essential: Adds a panel item to the left of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addLeftPanel (options) { + return this.addPanel('left', options) + } + + // Essential: Get an {Array} of all the panel items to the right of the editor window. + getRightPanels () { + return this.getPanels('right') + } + + // Essential: Adds a panel item to the right of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addRightPanel (options) { + return this.addPanel('right', options) + } + + // Essential: Get an {Array} of all the panel items at the top of the editor window. + getTopPanels () { + return this.getPanels('top') + } + + // Essential: Adds a panel item to the top of the editor window above the tabs. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addTopPanel (options) { + return this.addPanel('top', options) + } + + // Essential: Get an {Array} of all the panel items in the header. + getHeaderPanels () { + return this.getPanels('header') + } + + // Essential: Adds a panel item to the header. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addHeaderPanel (options) { + return this.addPanel('header', options) + } + + // Essential: Get an {Array} of all the panel items in the footer. + getFooterPanels () { + return this.getPanels('footer') + } + + // Essential: Adds a panel item to the footer. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addFooterPanel (options) { + return this.addPanel('footer', options) + } + + // Essential: Get an {Array} of all the modal panel items + getModalPanels () { + return this.getPanels('modal') + } + + // Essential: Adds a panel item as a modal dialog. + // + // * `options` {Object} + // * `item` Your panel content. It can be a DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // model option. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addModalPanel (options = {}) { + return this.addPanel('modal', options) + } + + // Essential: Returns the {Panel} associated with the given item. Returns + // `null` when the item has no panel. + // + // * `item` Item the panel contains + panelForItem (item) { + for (let location in this.panelContainers) { + const container = this.panelContainers[location] + const panel = container.panelForItem(item) + if (panel != null) { return panel } + } + return null + } + + getPanels (location) { + return this.panelContainers[location].getPanels() + } + + addPanel (location, options) { + if (options == null) { options = {} } + return this.panelContainers[location].addPanel(new Panel(options)) + } + + /* + Section: Searching and Replacing + */ + + // Public: Performs a search across all files in the workspace. + // + // * `regex` {RegExp} to search with. + // * `options` (optional) {Object} + // * `paths` An {Array} of glob patterns to search within. + // * `onPathsSearched` (optional) {Function} to be periodically called + // with number of paths searched. + // * `leadingContextLineCount` {Number} default `0`; The number of lines + // before the matched line to include in the results object. + // * `trailingContextLineCount` {Number} default `0`; The number of lines + // after the matched line to include in the results object. + // * `iterator` {Function} callback on each file found. + // + // Returns a {Promise} with a `cancel()` method that will cancel all + // of the underlying searches that were started as part of this scan. + scan (regex, options = {}, iterator) { + if (_.isFunction(options)) { + iterator = options + options = {} + } + + // Find a searcher for every Directory in the project. Each searcher that is matched + // will be associated with an Array of Directory objects in the Map. + const directoriesForSearcher = new Map() + for (const directory of this.project.getDirectories()) { + let searcher = this.defaultDirectorySearcher + for (const directorySearcher of this.directorySearchers) { + if (directorySearcher.canSearchDirectory(directory)) { + searcher = directorySearcher + break + } + } + let directories = directoriesForSearcher.get(searcher) + if (!directories) { + directories = [] + directoriesForSearcher.set(searcher, directories) + } + directories.push(directory) + } + + // Define the onPathsSearched callback. + let onPathsSearched + if (_.isFunction(options.onPathsSearched)) { + // Maintain a map of directories to the number of search results. When notified of a new count, + // replace the entry in the map and update the total. + const onPathsSearchedOption = options.onPathsSearched + let totalNumberOfPathsSearched = 0 + const numberOfPathsSearchedForSearcher = new Map() + onPathsSearched = function (searcher, numberOfPathsSearched) { + const oldValue = numberOfPathsSearchedForSearcher.get(searcher) + if (oldValue) { + totalNumberOfPathsSearched -= oldValue + } + numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched) + totalNumberOfPathsSearched += numberOfPathsSearched + return onPathsSearchedOption(totalNumberOfPathsSearched) + } + } else { + onPathsSearched = function () {} + } + + // Kick off all of the searches and unify them into one Promise. + const allSearches = [] + directoriesForSearcher.forEach((directories, searcher) => { + const searchOptions = { + inclusions: options.paths || [], + includeHidden: true, + excludeVcsIgnores: this.config.get('core.excludeVcsIgnoredPaths'), + exclusions: this.config.get('core.ignoredNames'), + follow: this.config.get('core.followSymlinks'), + leadingContextLineCount: options.leadingContextLineCount || 0, + trailingContextLineCount: options.trailingContextLineCount || 0, + didMatch: result => { + if (!this.project.isPathModified(result.filePath)) { + return iterator(result) + } + }, + didError (error) { + return iterator(null, error) + }, + didSearchPaths (count) { + return onPathsSearched(searcher, count) + } + } + const directorySearcher = searcher.search(directories, regex, searchOptions) + allSearches.push(directorySearcher) + }) + const searchPromise = Promise.all(allSearches) + + for (let buffer of this.project.getBuffers()) { + if (buffer.isModified()) { + const filePath = buffer.getPath() + if (!this.project.contains(filePath)) { + continue + } + var matches = [] + buffer.scan(regex, match => matches.push(match)) + if (matches.length > 0) { + iterator({filePath, matches}) + } + } + } + + // Make sure the Promise that is returned to the client is cancelable. To be consistent + // with the existing behavior, instead of cancel() rejecting the promise, it should + // resolve it with the special value 'cancelled'. At least the built-in find-and-replace + // package relies on this behavior. + let isCancelled = false + const cancellablePromise = new Promise((resolve, reject) => { + const onSuccess = function () { + if (isCancelled) { + resolve('cancelled') + } else { + resolve(null) + } + } + + const onFailure = function () { + for (let promise of allSearches) { promise.cancel() } + reject() + } + + searchPromise.then(onSuccess, onFailure) + }) + cancellablePromise.cancel = () => { + isCancelled = true + // Note that cancelling all of the members of allSearches will cause all of the searches + // to resolve, which causes searchPromise to resolve, which is ultimately what causes + // cancellablePromise to resolve. + allSearches.map((promise) => promise.cancel()) + } + + // Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` + // method in the find-and-replace package expects the object returned by this method to have a + // `done()` method. Include a done() method until find-and-replace can be updated. + cancellablePromise.done = onSuccessOrFailure => { + cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) + } + return cancellablePromise + } + + // Public: Performs a replace across all the specified files in the project. + // + // * `regex` A {RegExp} to search with. + // * `replacementText` {String} to replace all matches of regex with. + // * `filePaths` An {Array} of file path strings to run the replace on. + // * `iterator` A {Function} callback on each file with replacements: + // * `options` {Object} with keys `filePath` and `replacements`. + // + // Returns a {Promise}. + replace (regex, replacementText, filePaths, iterator) { + return new Promise((resolve, reject) => { + let buffer + const openPaths = this.project.getBuffers().map(buffer => buffer.getPath()) + const outOfProcessPaths = _.difference(filePaths, openPaths) + + let inProcessFinished = !openPaths.length + let outOfProcessFinished = !outOfProcessPaths.length + const checkFinished = () => { + if (outOfProcessFinished && inProcessFinished) { + resolve() + } + } + + if (!outOfProcessFinished.length) { + let flags = 'g' + if (regex.ignoreCase) { flags += 'i' } + + const task = Task.once( + require.resolve('./replace-handler'), + outOfProcessPaths, + regex.source, + flags, + replacementText, + () => { + outOfProcessFinished = true + checkFinished() + } + ) + + task.on('replace:path-replaced', iterator) + task.on('replace:file-error', error => { iterator(null, error) }) + } + + for (buffer of this.project.getBuffers()) { + if (!filePaths.includes(buffer.getPath())) { continue } + const replacements = buffer.replace(regex, replacementText, iterator) + if (replacements) { + iterator({filePath: buffer.getPath(), replacements}) + } + } + + inProcessFinished = true + checkFinished() + }) + } + + checkoutHeadRevision (editor) { + if (editor.getPath()) { + const checkoutHead = () => { + return this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) + .then(repository => repository != null ? repository.checkoutHeadForEditor(editor) : undefined) + } + + if (this.config.get('editor.confirmCheckoutHeadRevision')) { + this.applicationDelegate.confirm({ + message: 'Confirm Checkout HEAD Revision', + detailedMessage: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`, + buttons: { + OK: checkoutHead, + Cancel: null + } + }) + } else { + return checkoutHead() + } + } else { + return Promise.resolve(false) + } + } +} diff --git a/static/babelrc.json b/static/babelrc.json index 11474dd8d..26b70dc41 100644 --- a/static/babelrc.json +++ b/static/babelrc.json @@ -1,16 +1,7 @@ { + "breakConfig": true, "sourceMap": "inline", - "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", {}] - ] + "blacklist": ["es6.forOf", "useStrict"], + "optional": ["asyncToGenerator"], + "stage": 0 } diff --git a/static/index.js b/static/index.js index aa57a594a..ad7d096e1 100644 --- a/static/index.js +++ b/static/index.js @@ -1,35 +1,66 @@ (function () { - 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') + // Eagerly require cached-run-in-this-context to prevent a circular require + // when using `NativeCompileCache` for the first time. + require('cached-run-in-this-context') - var blobStore = null + const electron = require('electron') + const path = require('path') + const Module = require('module') + const getWindowLoadSettings = require('../src/get-window-load-settings') + const entryPointDirPath = __dirname + let blobStore = null + let useSnapshot = false window.onload = function () { try { - var startTime = Date.now() + const startTime = Date.now() process.on('unhandledRejection', function (error, promise) { console.error('Unhandled promise rejection %o with error: %o', promise, error) }) - blobStore = FileSystemBlobStore.load( - path.join(process.env.ATOM_HOME, 'blob-store/') - ) - NativeCompileCache.setCacheStore(blobStore) - NativeCompileCache.setV8Version(process.versions.v8) - NativeCompileCache.install() - // Normalize to make sure drive letter case is consistent on Windows process.resourcesPath = path.normalize(process.resourcesPath) - var devMode = getWindowLoadSettings().devMode || !getWindowLoadSettings().resourcePath.startsWith(process.resourcesPath + path.sep) + setupAtomHome() + const devMode = getWindowLoadSettings().devMode || !getWindowLoadSettings().resourcePath.startsWith(process.resourcesPath + path.sep) + useSnapshot = !devMode && typeof snapshotResult !== 'undefined' if (devMode) { - setupDeprecatedPackages() + const metadata = require('../package.json') + if (!metadata._deprecatedPackages) { + try { + metadata._deprecatedPackages = require('../script/deprecated-packages.json') + } catch (requireError) { + console.error('Failed to setup deprecated packages list', requireError.stack) + } + } + } else if (useSnapshot) { + Module.prototype.require = function (module) { + const absoluteFilePath = Module._resolveFilename(module, this, false) + let relativeFilePath = path.relative(entryPointDirPath, absoluteFilePath) + if (process.platform === 'win32') { + relativeFilePath = relativeFilePath.replace(/\\/g, '/') + } + let cachedModule = snapshotResult.customRequire.cache[relativeFilePath] // eslint-disable-line no-undef + if (!cachedModule) { + cachedModule = {exports: Module._load(module, this, false)} + snapshotResult.customRequire.cache[relativeFilePath] = cachedModule // eslint-disable-line no-undef + } + return cachedModule.exports + } + + snapshotResult.setGlobals(global, process, window, document, require) // eslint-disable-line no-undef } + const FileSystemBlobStore = useSnapshot ? snapshotResult.customRequire('../src/file-system-blob-store.js') : require('../src/file-system-blob-store') // eslint-disable-line no-undef + blobStore = FileSystemBlobStore.load(path.join(process.env.ATOM_HOME, 'blob-store')) + + const NativeCompileCache = useSnapshot ? snapshotResult.customRequire('../src/native-compile-cache.js') : require('../src/native-compile-cache') // eslint-disable-line no-undef + NativeCompileCache.setCacheStore(blobStore) + NativeCompileCache.setV8Version(process.versions.v8) + NativeCompileCache.install() + if (getWindowLoadSettings().profileStartup) { profileStartup(Date.now() - startTime) } else { @@ -48,7 +79,7 @@ } function handleSetupError (error) { - var currentWindow = require('electron').remote.getCurrentWindow() + const currentWindow = electron.remote.getCurrentWindow() currentWindow.setSize(800, 600) currentWindow.center() currentWindow.show() @@ -57,53 +88,30 @@ } function setupWindow () { - var CompileCache = require('../src/compile-cache') + const CompileCache = useSnapshot ? snapshotResult.customRequire('../src/compile-cache.js') : require('../src/compile-cache') // eslint-disable-line no-undef CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) + CompileCache.install(process.resourcesPath, require) - var ModuleCache = require('../src/module-cache') + const ModuleCache = useSnapshot ? snapshotResult.customRequire('../src/module-cache.js') : require('../src/module-cache') // eslint-disable-line no-undef 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') + const startCrashReporter = useSnapshot ? snapshotResult.customRequire('../src/crash-reporter-start.js') : require('../src/crash-reporter-start') // eslint-disable-line no-undef startCrashReporter({_version: getWindowLoadSettings().appVersion}) - setupVmCompatibility() - setupCsonCache(CompileCache.getCacheDirectory()) + const CSON = useSnapshot ? snapshotResult.customRequire('../node_modules/season/lib/cson.js') : require('season') // eslint-disable-line no-undef + CSON.setCacheDir(path.join(CompileCache.getCacheDirectory(), 'cson')) - var initialize = require(getWindowLoadSettings().windowInitializationScript) + const initScriptPath = path.relative(entryPointDirPath, getWindowLoadSettings().windowInitializationScript) + const initialize = useSnapshot ? snapshotResult.customRequire(initScriptPath) : require(initScriptPath) // eslint-disable-line no-undef return initialize({blobStore: blobStore}).then(function () { - require('electron').ipcRenderer.send('window-command', 'window:loaded') + electron.ipcRenderer.send('window-command', 'window:loaded') }) } - function setupCsonCache (cacheDir) { - require('season').setCacheDir(path.join(cacheDir, 'cson')) - } - - function setupVmCompatibility () { - var vm = require('vm') - if (!vm.Script.createContext) { - vm.Script.createContext = vm.createContext - } - } - - function setupDeprecatedPackages () { - var metadata = require('../package.json') - if (!metadata._deprecatedPackages) { - try { - metadata._deprecatedPackages = require('../script/deprecated-packages.json') - } catch (requireError) { - console.error('Failed to setup deprecated packages list', requireError.stack) - } - } - } - function profileStartup (initialTime) { function profile () { console.profile('startup') - var startTime = Date.now() + const startTime = Date.now() setupWindow().then(function () { setLoadTime(Date.now() - startTime + initialTime) console.profileEnd('startup') @@ -111,7 +119,7 @@ }) } - const webContents = require('electron').remote.getCurrentWindow().webContents + const webContents = electron.remote.getCurrentWindow().webContents if (webContents.devToolsWebContents) { profile() } else { @@ -120,7 +128,7 @@ } } - var setupAtomHome = function () { + function setupAtomHome () { if (process.env.ATOM_HOME) { return } @@ -132,6 +140,4 @@ process.env.ATOM_HOME = getWindowLoadSettings().atomHome } } - - setupAtomHome() })() diff --git a/static/variables/octicon-mixins.less b/static/variables/octicon-mixins.less index c58ce3a52..672a2cef6 100644 --- a/static/variables/octicon-mixins.less +++ b/static/variables/octicon-mixins.less @@ -1,3 +1,5 @@ +@import "octicon-utf-codes.less"; + .icon-size(@size) { font-size: @size; width: @size; @@ -17,7 +19,6 @@ } .octicon(@name, @size: 16px) { - @import "octicon-utf-codes.less"; &::before { .icon(@size); content: @@name @@ -25,7 +26,6 @@ } .mega-octicon(@name, @size: 32px) { - @import "octicon-utf-codes.less"; &::before { .icon(@size); content: @@name