diff --git a/.travis.yml b/.travis.yml index 7b6113aa0..9ac8cc72a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,8 @@ compiler: clang matrix: include: - - os: linux - env: NODE_VERSION=0.12 - os: linux env: NODE_VERSION=4 - - os: osx - env: ATOM_SPECS_TASK=core NODE_VERSION=0.12 - - os: osx - env: ATOM_SPECS_TASK=packages NODE_VERSION=0.12 sudo: false @@ -28,16 +22,15 @@ install: - git clone https://github.com/creationix/nvm.git /tmp/.nvm - source /tmp/.nvm/nvm.sh - nvm install $NODE_VERSION - - nvm use $NODE_VERSION + - nvm use --delete-prefix $NODE_VERSION script: script/cibuild cache: directories: - node_modules - - build/node_modules - apm/node_modules - - $HOME/.atom/compile-cache + - build/node_modules notifications: email: @@ -46,8 +39,11 @@ notifications: addons: apt: + sources: + - ubuntu-toolchain-r-test packages: - build-essential - git - libgnome-keyring-dev - fakeroot + - gcc-multilib diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f7d539ca..837961e6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ These are just guidelines, not rules, use your best judgment and feel free to pr [Styleguides](#styleguides) * [Git Commit Messages](#git-commit-messages) + * [JavaScript Styleguide](#javascript-styleguide) * [CoffeeScript Styleguide](#coffeescript-styleguide) * [Specs Styleguide](#specs-styleguide) * [Documentation Styleguide](#documentation-styleguide) @@ -280,6 +281,25 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F * :arrow_down: `:arrow_down:` when downgrading dependencies * :shirt: `:shirt:` when removing linter warnings +### JavaScript Styleguide + +All JavaScript must adhere to [JavaScript Standard Style](http://standardjs.com/). + +* Prefer `Object.assign()` to the object spread operator (`{...anotherObj}`) +* Inline `export`s with expressions + ```js + // Use this: + export default class ClassName { + + } + + // Instead of: + class ClassName { + + } + export default ClassName + ``` + ### CoffeeScript Styleguide * Set parameter defaults without spaces around the equal sign diff --git a/Dockerfile b/Dockerfile index d792c30c5..22a101743 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # DESCRIPTION: Image to build Atom and create a .rpm file # Base docker image -FROM fedora:21 +FROM nodesource/fedora21:4.2.6 # Install dependencies RUN yum install -y \ @@ -12,11 +12,9 @@ RUN yum install -y \ glibc-devel \ git-core \ libgnome-keyring-devel \ - rpmdevtools \ - nodejs \ - npm + rpmdevtools -RUN npm install -g npm@1.4.28 --loglevel error +RUN npm install -g npm --loglevel error ADD . /atom WORKDIR /atom diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 5b3ea42f4..d2ac45f05 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ### Prerequisites -* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode)? +* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode)? * [ ] Are you running the [latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version)? * [ ] Did you check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/)? * [ ] Did you check the [FAQs on Discuss](https://discuss.atom.io/c/faq)? diff --git a/README.md b/README.md index 9f052ccff..a41ddeb64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) -[![macOS Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) +[![macOS Build Status](https://circleci.com/gh/atom/atom.svg?style=svg)](https://circleci.com/gh/atom/atom) [![Linux Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) [![Join the Atom Community on Slack](http://atom-slack.herokuapp.com/badge.svg)](http://atom-slack.herokuapp.com/) diff --git a/apm/package.json b/apm/package.json index d4fcc851a..31162dcaf 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.10.0" + "atom-package-manager": "1.12.5" } } diff --git a/appveyor.yml b/appveyor.yml index 51e074a4c..316701aae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,5 @@ version: "{build}" -branches: - only: - - master - skip_tags: true clone_folder: c:\projects\atom clone_depth: 10 @@ -16,9 +12,10 @@ environment: ATOM_DEV_RESOURCE_PATH: c:\projects\atom ATOM_ACCESS_TOKEN: secure: Q7vxmSq0bVCLTTRPzXw5ZhPTe7XYhWxX0tQV6neEkddTH6pZkOYNmSCG6VnMX2f+ + ATOM_NOISY_BUILD: 1 matrix: - - NODE_VERSION: 0.10.35 + - NODE_VERSION: 4.4.5 install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% @@ -37,6 +34,9 @@ artifacts: name: AtomSetup.msi cache: + - '%APPVEYOR_BUILD_FOLDER%\build\node_modules' + - '%APPVEYOR_BUILD_FOLDER%\apm\node_modules' + - '%APPVEYOR_BUILD_FOLDER%\node_modules' - '%USERPROFILE%\.atom\.apm' - '%USERPROFILE%\.atom\.node-gyp\.atom' - '%USERPROFILE%\.atom\.npm' diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index b248eda2e..451752829 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -14,6 +14,8 @@ _ = require 'underscore-plus' packageJson = require '../package.json' module.exports = (grunt) -> + process.env.ATOM_RESOURCE_PATH ?= path.resolve(__dirname, '..') + require('time-grunt')(grunt) grunt.loadNpmTasks('grunt-babel') @@ -296,7 +298,9 @@ module.exports = (grunt) -> ciTasks.push('create-windows-installer:installer') ciTasks.push('codesign:installer') if process.env.JANKY_SIGNTOOL ciTasks.push('codesign:cleanup') - ciTasks.push('publish-build') unless process.env.CI + + if process.env.ATOM_PUBLISH_REPO or not process.env.CI + ciTasks.push('publish-build') grunt.registerTask('ci', ciTasks) @@ -316,7 +320,7 @@ getDefaultChannelAndReleaseBranch = (version) -> else channel = 'stable' - minorVersion = version.match(/^\d\.\d/)[0] + minorVersion = version.match(/^\d+\.\d+/)[0] releaseBranch = "#{minorVersion}-releases" [channel, releaseBranch] diff --git a/build/package.json b/build/package.json index 2f8d88d8a..98a806134 100644 --- a/build/package.json +++ b/build/package.json @@ -30,9 +30,10 @@ "grunt-peg": "~1.1.0", "grunt-shell": "~0.3.1", "grunt-standard": "^2.0.0", + "joanna": "0.0.6", "legal-eagle": "~0.13.0", "minidump": "~0.9", - "npm": "2.13.3", + "npm": "3.10.5", "rcedit": "~0.3.0", "request": "~2.27.0", "rimraf": "~2.2.2", diff --git a/build/tasks/compile-packages-slug-task.coffee b/build/tasks/compile-packages-slug-task.coffee index 7317f158a..f6f297bec 100644 --- a/build/tasks/compile-packages-slug-task.coffee +++ b/build/tasks/compile-packages-slug-task.coffee @@ -40,6 +40,8 @@ module.exports = (grunt) -> continue if path.basename(moduleDirectory) is '.bin' metadataPath = path.join(moduleDirectory, 'package.json') + continue unless fs.existsSync(metadataPath) + metadata = grunt.file.readJSON(metadataPath) continue unless metadata?.engines?.atom? diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee index 75e21de8a..1e80eb2ce 100644 --- a/build/tasks/docs-task.coffee +++ b/build/tasks/docs-task.coffee @@ -4,7 +4,9 @@ fs = require 'fs-plus' _ = require 'underscore-plus' donna = require 'donna' +joanna = require 'joanna' tello = require 'tello' +glob = require 'glob' module.exports = (grunt) -> getClassesToInclude = -> @@ -30,8 +32,16 @@ module.exports = (grunt) -> grunt.registerTask 'build-docs', 'Builds the API docs in src', -> docsOutputDir = grunt.config.get('docsOutputDir') - metadata = donna.generateMetadata(['.']) - api = tello.digest(metadata) + [coffeeMetadata] = donna.generateMetadata(['.']) + jsMetadata = joanna(glob.sync('src/*.js')) + + metadata = { + repository: coffeeMetadata.repository, + version: coffeeMetadata.version, + files: Object.assign(coffeeMetadata.files, jsMetadata.files) + } + + api = tello.digest([metadata]) _.extend(api.classes, getClassesToInclude()) api.classes = sortClasses(api.classes) diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 0a18c9c23..ad31c0d8e 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -11,6 +11,7 @@ AWS = require 'aws-sdk' grunt = null token = process.env.ATOM_ACCESS_TOKEN +repo = process.env.ATOM_PUBLISH_REPO ? 'atom/atom' defaultHeaders = Authorization: "token #{token}" 'User-Agent': 'Atom' @@ -33,7 +34,12 @@ module.exports = (gruntObject) -> grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', -> releaseBranch = grunt.config.get('atom.releaseBranch') isPrerelease = grunt.config.get('atom.channel') is 'beta' - return unless releaseBranch? + + unless releaseBranch? + grunt.log.ok("Skipping upload-assets to #{repo} repo because this is not a release branch") + return + + grunt.log.ok("Starting upload-assets to #{repo} repo") doneCallback = @async() startTime = Date.now() @@ -118,8 +124,10 @@ logError = (message, error, details) -> zipAssets = (buildDir, assets, callback) -> zip = (directory, sourcePath, assetName, callback) -> + grunt.log.ok("Zipping #{sourcePath} into #{assetName}") if process.platform is 'win32' - zipCommand = "C:/psmodules/7z.exe a -r #{assetName} \"#{sourcePath}\"" + sevenZipPath = if process.env.JANKY_SHA1? then "C:/psmodules/" else "" + zipCommand = "#{sevenZipPath}7z.exe a -r \"#{assetName}\" \"#{sourcePath}\"" else zipCommand = "zip -r --symlinks '#{assetName}' '#{sourcePath}'" options = {cwd: directory, maxBuffer: Infinity} @@ -134,10 +142,11 @@ zipAssets = (buildDir, assets, callback) -> async.parallel(tasks, callback) getAtomDraftRelease = (isPrerelease, branchName, callback) -> - atomRepo = new GitHub({repo: 'atom/atom', token}) + grunt.log.ok("Obtaining GitHub draft release for #{branchName}") + atomRepo = new GitHub({repo: repo, token}) atomRepo.getReleases {prerelease: isPrerelease}, (error, releases=[]) -> if error? - logError('Fetching atom/atom releases failed', error, releases) + logError("Fetching #{repo} #{if isPrerelease then "pre" else "" }releases failed", error, releases) callback(error) else [firstDraft] = releases.filter ({draft}) -> draft @@ -152,15 +161,17 @@ getAtomDraftRelease = (isPrerelease, branchName, callback) -> logError('Fetching draft release assets failed', error, assets) callback(error ? new Error(response.statusCode)) else + grunt.log.ok("Using GitHub draft release #{firstDraft.name}") firstDraft.assets = assets callback(null, firstDraft) else createAtomDraftRelease(isPrerelease, branchName, callback) createAtomDraftRelease = (isPrerelease, branchName, callback) -> + grunt.log.ok("Creating GitHub draft release #{branchName}") {version} = require('../../package.json') options = - uri: 'https://api.github.com/repos/atom/atom/releases' + uri: "https://api.github.com/repos/#{repo}/releases" method: 'POST' headers: defaultHeaders json: @@ -177,12 +188,13 @@ createAtomDraftRelease = (isPrerelease, branchName, callback) -> request options, (error, response, body='') -> if error? or response.statusCode isnt 201 - logError("Creating atom/atom draft release failed", error, body) + logError("Creating #{repo} draft release failed", error, body) callback(error ? new Error(response.statusCode)) else callback(null, body) deleteRelease = (release) -> + grunt.log.ok("Deleting GitHub release #{release.tag_name}") options = uri: release.url method: 'DELETE' @@ -193,6 +205,7 @@ deleteRelease = (release) -> logError('Deleting release failed', error, body) deleteExistingAssets = (release, assetNames, callback) -> + grunt.log.ok("Deleting #{assetNames.join(',')} from GitHub release #{release.tag_name}") [callback, assetNames] = [assetNames, callback] if not callback? deleteAsset = (url, callback) -> @@ -214,6 +227,7 @@ deleteExistingAssets = (release, assetNames, callback) -> uploadAssets = (release, buildDir, assets, callback) -> uploadToReleases = (release, assetName, assetPath, callback) -> + grunt.log.ok("Uploading #{assetName} to GitHub release #{release.tag_name}") options = uri: release.upload_url.replace(/\{.*$/, "?name=#{assetName}") method: 'POST' @@ -246,6 +260,7 @@ uploadAssets = (release, buildDir, assets, callback) -> s3 = new AWS.S3 s3Info key = "releases/#{release.tag_name}/#{assetName}" + grunt.log.ok("Uploading to S3 #{key}") uploadParams = Bucket: s3Bucket ACL: 'public-read' diff --git a/circle.yml b/circle.yml index a55900cca..95ebb024c 100644 --- a/circle.yml +++ b/circle.yml @@ -1,4 +1,38 @@ +machine: + environment: + XCODE_SCHEME: test + XCODE_WORKSPACE: test + XCODE_PROJECT: test + + xcode: + version: 7.3 + general: - branches: - only: - - io-circle-ci + artifacts: + - out/Atom.zip + +dependencies: + pre: + - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash + - nvm install 4.4.7 + - nvm use 4.4.7 + - npm install -g npm + - script/fingerprint-clean + + override: + - script/bootstrap + + post: + - script/fingerprint-write + + cache_directories: + - apm/node_modules + - build/node_modules + - node_modules + +test: + override: + - script/grunt ci + + post: + - zip -r out/Atom.zip out/Atom.app diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 126604c49..f737a6cda 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -25,7 +25,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. ### Fedora / CentOS / RHEL * `sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` -* Instructions for [Node.js](https://github.com/nodejs/node-v0.x-archive/wiki/Installing-Node.js-via-package-manager#enterprise-linux-and-fedora). +* Instructions for [Node.js](https://nodejs.org/en/download/package-manager/#enterprise-linux-and-fedora). ### Arch diff --git a/exports/atom.coffee b/exports/atom.coffee index 4953d3756..bd8a1b62c 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -8,7 +8,6 @@ module.exports = BufferedNodeProcess: require '../src/buffered-node-process' BufferedProcess: require '../src/buffered-process' GitRepository: require '../src/git-repository' - GitRepositoryAsync: require '../src/git-repository-async' Notification: require '../src/notification' TextBuffer: TextBuffer Point: Point @@ -19,6 +18,10 @@ module.exports = Disposable: Disposable CompositeDisposable: CompositeDisposable +# Shell integration is required by both Squirrel and Settings-View +if process.platform is 'win32' + module.exports.WinShell = require '../src/main-process/win-shell' + # The following classes can't be used from a Task handler and should therefore # only be exported when not running as a child node process unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE diff --git a/package.json b/package.json index 7aa699a51..1704f02f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.10.0-dev", + "version": "1.11.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -16,13 +16,14 @@ "dependencies": { "async": "0.2.6", "atom-keymap": "6.3.2", - "atom-ui": "0.3.3", + "atom-ui": "0.4.1", "babel-core": "^5.8.21", "cached-run-in-this-context": "0.4.1", "chai": "3.5.0", "clear-cut": "^2.0.1", "coffee-script": "1.8.0", "color": "^0.7.3", + "dedent": "0.6.0", "devtron": "1.1.0", "event-kit": "^1.5.0", "find-parent-dir": "^0.3.0", @@ -42,7 +43,6 @@ "mocha": "2.5.1", "normalize-package-data": "^2.0.0", "nslog": "^3", - "ohnogit": "0.0.13", "oniguruma": "^5", "pathwatcher": "~6.5", "property-accessors": "^1.1.3", @@ -58,9 +58,10 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.2.2", + "text-buffer": "9.2.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", + "winreg": "^1.2.1", "yargs": "^3.23.0" }, "packageDependencies": { @@ -70,13 +71,13 @@ "atom-light-ui": "0.44.0", "base16-tomorrow-dark-theme": "1.1.0", "base16-tomorrow-light-theme": "1.1.1", - "one-dark-ui": "1.4.0", - "one-light-ui": "1.4.0", + "one-dark-ui": "1.5.0", + "one-light-ui": "1.5.0", "one-dark-syntax": "1.3.0", "one-light-syntax": "1.3.0", "solarized-dark-syntax": "1.0.2", "solarized-light-syntax": "1.0.2", - "about": "1.5.3", + "about": "1.6.0", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.2", @@ -93,12 +94,12 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.22.0", "exception-reporting": "0.38.1", - "find-and-replace": "0.200.0", - "fuzzy-finder": "1.2.0", - "git-diff": "1.0.1", + "find-and-replace": "0.201.0", + "fuzzy-finder": "1.3.0", + "git-diff": "1.1.0", "go-to-line": "0.31.0", "grammar-selector": "0.48.1", - "image-view": "0.58.1", + "image-view": "0.58.2", "incompatible-packages": "0.26.1", "keybinding-resolver": "0.35.0", "line-ending-selector": "0.5.0", @@ -106,52 +107,52 @@ "markdown-preview": "0.158.0", "metrics": "0.53.1", "notifications": "0.65.0", - "open-on-github": "1.1.0", + "open-on-github": "1.2.0", "package-generator": "1.0.0", - "settings-view": "0.240.0", + "settings-view": "0.241.1", "snippets": "1.0.2", "spell-check": "0.67.1", - "status-bar": "1.3.1", - "styleguide": "0.46.0", + "status-bar": "1.4.0", + "styleguide": "0.47.0", "symbols-view": "0.113.0", - "tabs": "0.99.0", - "timecop": "0.33.1", - "tree-view": "0.208.0", + "tabs": "0.100.0", + "timecop": "0.33.2", + "tree-view": "0.208.2", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", - "whitespace": "0.32.2", + "whitespace": "0.33.0", "wrap-guide": "0.38.1", "language-c": "0.52.1", "language-clojure": "0.21.0", - "language-coffee-script": "0.47.1", + "language-coffee-script": "0.47.2", "language-csharp": "0.12.1", - "language-css": "0.37.0", + "language-css": "0.37.1", "language-gfm": "0.88.0", - "language-git": "0.14.0", - "language-go": "0.42.0", - "language-html": "0.44.1", + "language-git": "0.15.0", + "language-go": "0.42.1", + "language-html": "0.45.1", "language-hyperlink": "0.16.0", "language-java": "0.23.0", "language-javascript": "0.119.0", - "language-json": "0.18.1", - "language-less": "0.29.4", + "language-json": "0.18.2", + "language-less": "0.29.5", "language-make": "0.22.2", "language-mustache": "0.13.0", "language-objective-c": "0.15.1", "language-perl": "0.35.0", - "language-php": "0.37.1", + "language-php": "0.37.2", "language-property-list": "0.8.0", "language-python": "0.45.0", - "language-ruby": "0.68.6", + "language-ruby": "0.69.0", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.55.0", + "language-sass": "0.56.0", "language-shellscript": "0.22.4", "language-source": "0.9.0", - "language-sql": "0.22.0", + "language-sql": "0.23.0", "language-text": "0.7.1", "language-todo": "0.28.0", "language-toml": "0.18.0", - "language-xml": "0.34.8", + "language-xml": "0.34.9", "language-yaml": "0.26.0" }, "private": true, diff --git a/script/bootstrap b/script/bootstrap index 3b9a35735..6fcf8fef0 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -75,7 +75,7 @@ function bootstrap() { var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; - var apmInstallCommand = npmPath + npmFlags + '--target=0.10.40 ' + 'install'; + var apmInstallCommand = npmPath + npmFlags + '--target=4.4.5 --global-style ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; @@ -97,7 +97,7 @@ function bootstrap() { } var moduleInstallOptions = {env: moduleInstallEnv}; - if (process.argv.indexOf('--no-quiet') === -1) { + if (process.argv.indexOf('--no-quiet') === -1 || process.env.ATOM_NOISY_BUILD) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; moduleInstallCommand += ' --loglevel error'; @@ -122,11 +122,6 @@ function bootstrap() { message: 'Installing apm...', options: apmInstallOptions }, - { - command: apmPath + ' clean' + apmFlags, - message: 'Deleting old packages...', - options: moduleInstallOptions - }, { command: moduleInstallCommand, options: moduleInstallOptions diff --git a/script/cibuild b/script/cibuild index 860e0a938..6f16ac5cd 100755 --- a/script/cibuild +++ b/script/cibuild @@ -40,10 +40,6 @@ function setEnvironmentVariables() { process.env.CC = 'clang'; process.env.CXX = 'clang++'; process.env.npm_config_clang = '1'; - } else if (process.platform === 'win32') { - process.env.BUILD_ATOM_RELEASES_S3_KEY = process.env.BUILD_ATOM_WIN_RELEASES_S3_KEY - process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET - process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } } @@ -62,6 +58,7 @@ function removeNodeModules() { try { fsPlus.removeSync(path.resolve(__dirname, '..', 'node_modules')); + fsPlus.removeSync(path.resolve(__dirname, '..', 'apm', 'node_modules')); } catch (error) { console.error(error.message); process.exit(1); diff --git a/script/cibuild-atom-linux b/script/cibuild-atom-linux index 2c3395608..fe79c19fe 100755 --- a/script/cibuild-atom-linux +++ b/script/cibuild-atom-linux @@ -7,10 +7,11 @@ export BUILD_ATOM_RELEASES_S3_KEY=$BUILD_ATOM_LINUX_RELEASES_S3_KEY export BUILD_ATOM_RELEASES_S3_SECRET=$BUILD_ATOM_LINUX_RELEASES_S3_SECRET export BUILD_ATOM_RELEASES_S3_BUCKET=$BUILD_ATOM_LINUX_RELEASES_S3_BUCKET -if [ -d /usr/local/share/nodenv ]; then - export NODENV_ROOT=/usr/local/share/nodenv - export PATH=/usr/local/share/nodenv/bin:/usr/local/share/nodenv/shims:$PATH - export NODENV_VERSION="v0.10.21" -fi +rm -rf /tmp/.atom-nvm +git clone https://github.com/creationix/nvm.git /tmp/.atom-nvm +source /tmp/.atom-nvm/nvm.sh +nvm install 4.4.7 +nvm use 4.4.7 +npm install -g npm script/cibuild diff --git a/script/fingerprint-clean b/script/fingerprint-clean new file mode 100755 index 000000000..659395190 --- /dev/null +++ b/script/fingerprint-clean @@ -0,0 +1,29 @@ +#!/usr/bin/env node +var fingerprint = require('./utils/fingerprint') +var fs = require('fs') +var path = require('path') + +if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules', '.atom-ci-fingerprint'))) { + return +} + +if (fingerprint.fingerprintMatches()) { + console.log('node_modules matches current fingerprint ' + fingerprint.fingerprint() + ' - not removing') + return +} + +var fsPlus +try { + fsPlus = require('fs-plus') +} catch (error) { + console.log(error.message) + return +} + +try { + fsPlus.removeSync(path.resolve(__dirname, '..', 'node_modules')) + fsPlus.removeSync(path.resolve(__dirname, '..', 'apm', 'node_modules')) +} catch (error) { + console.error(error.message) + process.exit(1) +} diff --git a/script/fingerprint-write b/script/fingerprint-write new file mode 100755 index 000000000..e5fad4d72 --- /dev/null +++ b/script/fingerprint-write @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./utils/fingerprint').writeFingerprint() diff --git a/script/utils/fingerprint.js b/script/utils/fingerprint.js index c419ba4fd..e1f6276d2 100644 --- a/script/utils/fingerprint.js +++ b/script/utils/fingerprint.js @@ -6,12 +6,14 @@ var fingerprintPath = path.resolve(__dirname, '..', '..', 'node_modules', '.atom module.exports = { fingerprint: function () { - var packageJson = fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json')) + var atomPackageJson = fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json')) + var apmPackageJson = fs.readFileSync(path.resolve(__dirname, '..', '..', 'apm', 'package.json')) //Include the electron minor version in the fingerprint since that changing requires a re-install - var electronVersion = JSON.parse(packageJson).electronVersion.replace(/\.\d+$/, '') + var electronVersion = JSON.parse(atomPackageJson).electronVersion.replace(/\.\d+$/, '') + var apmVersion = JSON.parse(apmPackageJson).dependencies['atom-package-manager'] - var body = electronVersion + process.platform + process.version + var body = electronVersion + apmVersion + process.platform + process.version return crypto.createHash('sha1').update(body).digest('hex') }, diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js deleted file mode 100644 index 8b824bab3..000000000 --- a/spec/git-repository-async-spec.js +++ /dev/null @@ -1,918 +0,0 @@ -'use babel' - -import fs from 'fs-plus' -import path from 'path' -import temp from 'temp' - -import {it, beforeEach, afterEach} from './async-spec-helpers' - -import GitRepositoryAsync from '../src/git-repository-async' -import Project from '../src/project' - -temp.track() - -function openFixture (fixture) { - return GitRepositoryAsync.open(path.join(__dirname, 'fixtures', 'git', fixture)) -} - -function copyRepository (name = 'working-dir') { - const workingDirPath = temp.mkdirSync('atom-working-dir') - fs.copySync(path.join(__dirname, 'fixtures', 'git', name), workingDirPath) - fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - return fs.realpathSync(workingDirPath) -} - -function copySubmoduleRepository () { - const workingDirectory = copyRepository('repo-with-submodules') - const reGit = (name) => { - fs.renameSync(path.join(workingDirectory, name, 'git.git'), path.join(workingDirectory, name, '.git')) - } - reGit('jstips') - reGit('You-Dont-Need-jQuery') - - return workingDirectory -} - -describe('GitRepositoryAsync', () => { - let repo - - afterEach(() => { - if (repo != null) repo.destroy() - }) - - describe('@open(path)', () => { - it('should throw when no repository is found', async () => { - repo = GitRepositoryAsync.open(path.join(temp.dir, 'nogit.txt')) - - let threw = false - try { - await repo.getRepo() - } catch (e) { - threw = true - } - - expect(threw).toBe(true) - }) - }) - - describe('openedPath', () => { - it('is the path passed to .open', () => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - expect(repo.openedPath).toBe(workingDirPath) - }) - }) - - describe('.getRepo()', () => { - beforeEach(() => { - const workingDirectory = copySubmoduleRepository() - repo = GitRepositoryAsync.open(workingDirectory) - waitsForPromise(() => repo.refreshStatus()) - }) - - it('returns the repository when not given a path', async () => { - const nodeGitRepo1 = await repo.getRepo() - const nodeGitRepo2 = await repo.getRepo() - expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir()) - }) - - it('returns the repository when given a non-submodule path', async () => { - const nodeGitRepo1 = await repo.getRepo() - const nodeGitRepo2 = await repo.getRepo('README') - expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir()) - }) - - it('returns the submodule repository when given a submodule path', async () => { - const nodeGitRepo1 = await repo.getRepo() - const nodeGitRepo2 = await repo.getRepo('jstips') - expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir()) - - const nodeGitRepo3 = await repo.getRepo('jstips/README.md') - expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo3.workdir()) - expect(nodeGitRepo2.workdir()).toBe(nodeGitRepo3.workdir()) - }) - }) - - describe('.openRepository()', () => { - it('returns a new repository instance', async () => { - repo = openFixture('master.git') - - const originalRepo = await repo.getRepo() - expect(originalRepo).not.toBeNull() - - const nodeGitRepo = repo.openRepository() - expect(nodeGitRepo).not.toBeNull() - expect(originalRepo).not.toBe(nodeGitRepo) - }) - }) - - describe('.getPath()', () => { - it('returns the repository path for a repository path', async () => { - repo = openFixture('master.git') - const repoPath = await repo.getPath() - expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git')) - }) - }) - - describe('.isPathIgnored(path)', () => { - beforeEach(() => { - repo = openFixture('ignore.git') - }) - - it('resolves true for an ignored path', async () => { - const ignored = await repo.isPathIgnored('a.txt') - expect(ignored).toBe(true) - }) - - it('resolves false for a non-ignored path', async () => { - const ignored = await repo.isPathIgnored('b.txt') - expect(ignored).toBe(false) - }) - }) - - describe('.isPathModified(path)', () => { - let filePath, newPath, emptyPath - - beforeEach(() => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - emptyPath = path.join(workingDirPath, 'empty-path.txt') - }) - - describe('when the path is unstaged', () => { - it('resolves false if the path has not been modified', async () => { - const modified = await repo.isPathModified(filePath) - expect(modified).toBe(false) - }) - - it('resolves true if the path is modified', async () => { - fs.writeFileSync(filePath, 'change') - const modified = await repo.isPathModified(filePath) - expect(modified).toBe(true) - }) - - it('resolves false if the path is new', async () => { - const modified = await repo.isPathModified(newPath) - expect(modified).toBe(false) - }) - - it('resolves false if the path is invalid', async () => { - const modified = await repo.isPathModified(emptyPath) - expect(modified).toBe(false) - }) - }) - }) - - describe('.isPathNew(path)', () => { - let newPath - - beforeEach(() => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - }) - - describe('when the path is unstaged', () => { - it('returns true if the path is new', async () => { - const isNew = await repo.isPathNew(newPath) - expect(isNew).toBe(true) - }) - - it("returns false if the path isn't new", async () => { - const modified = await repo.isPathModified(newPath) - expect(modified).toBe(false) - }) - }) - }) - - describe('.checkoutHead(path)', () => { - let filePath - - beforeEach(() => { - const workingDirPath = copyRepository() - repo = GitRepositoryAsync.open(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - }) - - it('no longer reports a path as modified after checkout', async () => { - let modified = await repo.isPathModified(filePath) - expect(modified).toBe(false) - - fs.writeFileSync(filePath, 'ch ch changes') - - modified = await repo.isPathModified(filePath) - expect(modified).toBe(true) - - await repo.checkoutHead(filePath) - - modified = await repo.isPathModified(filePath) - expect(modified).toBe(false) - }) - - it('restores the contents of the path to the original text', async () => { - fs.writeFileSync(filePath, 'ch ch changes') - await repo.checkoutHead(filePath) - expect(fs.readFileSync(filePath, 'utf8')).toBe('') - }) - - it('fires a did-change-status event if the checkout completes successfully', async () => { - fs.writeFileSync(filePath, 'ch ch changes') - - await repo.getPathStatus(filePath) - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - - await repo.checkoutHead(filePath) - - expect(statusHandler.callCount).toBe(1) - expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) - - await repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe(1) - }) - }) - - describe('.checkoutHeadForEditor(editor)', () => { - let filePath - let editor - - beforeEach(async () => { - spyOn(atom, 'confirm') - - const workingDirPath = copyRepository() - repo = new GitRepositoryAsync(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - editor = await atom.workspace.open(filePath) - }) - - it('displays a confirmation dialog by default', async () => { - atom.confirm.andCallFake(({buttons}) => buttons.OK()) - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - await repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe('') - }) - - it('does not display a dialog when confirmation is disabled', async () => { - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - await repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe('') - expect(atom.confirm).not.toHaveBeenCalled() - }) - }) - - describe('.destroy()', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('throws an exception when any method is called after it is called', async () => { - repo.destroy() - - let error = null - try { - await repo.getShortHead() - } catch (e) { - error = e - } - - expect(error.name).toBe(GitRepositoryAsync.DestroyedErrorName) - - repo = null - }) - }) - - describe('.getPathStatus(path)', () => { - let filePath - - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - }) - - it('trigger a status-changed event when the new status differs from the last cached one', async () => { - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - fs.writeFileSync(filePath, '') - - await repo.getPathStatus(filePath) - - expect(statusHandler.callCount).toBe(1) - const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED - expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) - fs.writeFileSync(filePath, 'abc') - - await repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe(1) - }) - }) - - describe('.getDirectoryStatus(path)', () => { - let directoryPath, filePath - - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - directoryPath = path.join(workingDirectory, 'dir') - filePath = path.join(directoryPath, 'b.txt') - }) - - it('gets the status based on the files inside the directory', async () => { - await repo.checkoutHead(filePath) - - let result = await repo.getDirectoryStatus(directoryPath) - expect(repo.isStatusModified(result)).toBe(false) - - fs.writeFileSync(filePath, 'abc') - - result = await repo.getDirectoryStatus(directoryPath) - expect(repo.isStatusModified(result)).toBe(true) - }) - }) - - describe('.refreshStatus()', () => { - let newPath, modifiedPath, cleanPath, workingDirectory - - beforeEach(() => { - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - modifiedPath = path.join(workingDirectory, 'file.txt') - newPath = path.join(workingDirectory, 'untracked.txt') - cleanPath = path.join(workingDirectory, 'other.txt') - fs.writeFileSync(cleanPath, 'Full of text') - fs.writeFileSync(newPath, '') - fs.writeFileSync(modifiedPath, 'making this path modified') - newPath = fs.absolute(newPath) // specs could be running under symbol path. - }) - - it('returns status information for all new and modified files', async () => { - await repo.refreshStatus() - - expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBe(true) - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true) - }) - - describe('in a repository with submodules', () => { - beforeEach(() => { - workingDirectory = copySubmoduleRepository() - repo = GitRepositoryAsync.open(workingDirectory) - modifiedPath = path.join(workingDirectory, 'jstips', 'README.md') - newPath = path.join(workingDirectory, 'You-Dont-Need-jQuery', 'untracked.txt') - cleanPath = path.join(workingDirectory, 'jstips', 'CONTRIBUTING.md') - fs.writeFileSync(newPath, '') - fs.writeFileSync(modifiedPath, 'making this path modified') - newPath = fs.absolute(newPath) // specs could be running under symbol path. - }) - - it('returns status information for all new and modified files', async () => { - await repo.refreshStatus() - - expect(await repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(await repo.getCachedPathStatus(newPath))).toBe(true) - expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true) - }) - }) - - it('caches the proper statuses when a subdir is open', async () => { - const subDir = path.join(workingDirectory, 'dir') - fs.mkdirSync(subDir) - - const filePath = path.join(subDir, 'b.txt') - fs.writeFileSync(filePath, '') - - atom.project.setPaths([subDir]) - - await atom.workspace.open('b.txt') - - const repo = atom.project.getRepositories()[0].async - - await repo.refreshStatus() - - const status = await repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe(false) - expect(repo.isStatusNew(status)).toBe(false) - }) - - it('caches the proper statuses when multiple project are open', async () => { - const otherWorkingDirectory = copyRepository() - - atom.project.setPaths([workingDirectory, otherWorkingDirectory]) - - await atom.workspace.open('b.txt') - - const repo = atom.project.getRepositories()[0].async - - await repo.refreshStatus() - - const subDir = path.join(workingDirectory, 'dir') - fs.mkdirSync(subDir) - - const filePath = path.join(subDir, 'b.txt') - fs.writeFileSync(filePath, 'some content!') - - const status = await repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe(true) - expect(repo.isStatusNew(status)).toBe(false) - }) - - it('emits did-change-statuses if the status changes', async () => { - const someNewPath = path.join(workingDirectory, 'MyNewJSFramework.md') - fs.writeFileSync(someNewPath, '') - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - - await repo.refreshStatus() - - waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0) - }) - - it('emits did-change-statuses if the branch changes', async () => { - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - - repo._refreshBranch = jasmine.createSpy('_refreshBranch').andCallFake(() => { - return Promise.resolve(true) - }) - - await repo.refreshStatus() - - waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0) - }) - - it('emits did-change-statuses if the ahead/behind changes', async () => { - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - - repo._refreshAheadBehindCount = jasmine.createSpy('_refreshAheadBehindCount').andCallFake(() => { - return Promise.resolve(true) - }) - - await repo.refreshStatus() - - waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0) - }) - }) - - describe('.isProjectAtRoot()', () => { - it('returns true when the repository is at the root', async () => { - const workingDirectory = copyRepository() - atom.project.setPaths([workingDirectory]) - const repo = atom.project.getRepositories()[0].async - - const atRoot = await repo.isProjectAtRoot() - expect(atRoot).toBe(true) - }) - - it("returns false when the repository wasn't created with a project", async () => { - const workingDirectory = copyRepository() - const repo = GitRepositoryAsync.open(workingDirectory) - - const atRoot = await repo.isProjectAtRoot() - expect(atRoot).toBe(false) - }) - }) - - describe('buffer events', () => { - let repo - - beforeEach(() => { - const workingDirectory = copyRepository() - atom.project.setPaths([workingDirectory]) - - // When the path is added to the project, the repository is refreshed. We - // need to wait for that to complete before the tests continue so that - // we're in a known state. - repo = atom.project.getRepositories()[0].async - waitsForPromise(() => repo.refreshStatus()) - }) - - it('emits a status-changed event when a buffer is saved', async () => { - const editor = await atom.workspace.open('other.txt') - - editor.insertNewline() - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - editor.save() - - waitsFor('the onDidChangeStatus handler to be called', () => statusHandler.callCount > 0) - runs(() => { - expect(statusHandler.callCount).toBeGreaterThan(0) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) - }) - - it('emits a status-changed event when a buffer is reloaded', async () => { - const editor = await atom.workspace.open('other.txt') - - fs.writeFileSync(editor.getPath(), 'changed') - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - editor.getBuffer().reload() - - waitsFor('the onDidChangeStatus handler to be called', () => statusHandler.callCount > 0) - runs(() => { - expect(statusHandler.callCount).toBeGreaterThan(0) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) - }) - - it("emits a status-changed event when a buffer's path changes", async () => { - const editor = await atom.workspace.open('other.txt') - - fs.writeFileSync(editor.getPath(), 'changed') - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - editor.getBuffer().emitter.emit('did-change-path') - - waitsFor('the onDidChangeStatus handler to be called', () => statusHandler.callCount > 0) - runs(() => { - expect(statusHandler.callCount).toBeGreaterThan(0) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - - const pathHandler = jasmine.createSpy('pathHandler') - const buffer = editor.getBuffer() - buffer.onDidChangePath(pathHandler) - buffer.emitter.emit('did-change-path') - - waitsFor('the onDidChangePath handler to be called', () => pathHandler.callCount > 0) - runs(() => expect(pathHandler.callCount).toBeGreaterThan(0)) - }) - }) - - it('stops listening to the buffer when the repository is destroyed (regression)', async () => { - const editor = await atom.workspace.open('other.txt') - const repo = atom.project.getRepositories()[0] - repo.destroy() - expect(() => editor.save()).not.toThrow() - }) - }) - - describe('when a project is deserialized', () => { - let project2 - - beforeEach(() => { - atom.project.setPaths([copyRepository()]) - - // See the comment in the 'buffer events' beforeEach for why we need to do - // this. - const repository = atom.project.getRepositories()[0].async - waitsForPromise(() => repository.refreshStatus()) - }) - - afterEach(() => { - if (project2) project2.destroy() - }) - - it('subscribes to all the serialized buffers in the project', async () => { - await atom.workspace.open('file.txt') - - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) - project2.deserialize(atom.project.serialize({isUnloading: true})) - - const repo = project2.getRepositories()[0].async - waitsForPromise(() => repo.refreshStatus()) - runs(() => { - const buffer = project2.getBuffers()[0] - - waitsFor(() => buffer.loaded) - runs(() => { - buffer.append('changes') - - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus(statusHandler) - buffer.save() - - waitsFor(() => statusHandler.callCount > 0) - runs(() => { - expect(statusHandler.callCount).toBeGreaterThan(0) - expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) - }) - }) - }) - }) - }) - - describe('GitRepositoryAsync::relativize(filePath, workdir)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - // This is a change in implementation from the git-utils version - it('just returns path if workdir is not provided', () => { - const _path = '/foo/bar/baz.txt' - const relPath = repo.relativize(_path) - expect(_path).toEqual(relPath) - }) - - it('relativizes a repo path', () => { - const workdir = '/tmp/foo/bar/baz/' - const relativizedPath = repo.relativize(`${workdir}a/b.txt`, workdir) - expect(relativizedPath).toBe('a/b.txt') - }) - - it("doesn't require workdir to end in a slash", () => { - const workdir = '/tmp/foo/bar/baz' - const relativizedPath = repo.relativize(`${workdir}/a/b.txt`, workdir) - expect(relativizedPath).toBe('a/b.txt') - }) - - it('preserves file case', () => { - repo.isCaseInsensitive = true - - const workdir = '/tmp/foo/bar/baz/' - const relativizedPath = repo.relativize(`${workdir}a/README.txt`, workdir) - expect(relativizedPath).toBe('a/README.txt') - }) - }) - - describe('.getShortHead(path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the human-readable branch name', async () => { - const head = await repo.getShortHead() - expect(head).toBe('master') - }) - - describe('in a submodule', () => { - beforeEach(() => { - const workingDirectory = copySubmoduleRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the human-readable branch name', async () => { - await repo.refreshStatus() - - const head = await repo.getShortHead('jstips') - expect(head).toBe('test') - }) - }) - }) - - describe('.isSubmodule(path)', () => { - beforeEach(() => { - const workingDirectory = copySubmoduleRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it("returns false for a path that isn't a submodule", async () => { - const isSubmodule = await repo.isSubmodule('README') - expect(isSubmodule).toBe(false) - }) - - it('returns true for a path that is a submodule', async () => { - const isSubmodule = await repo.isSubmodule('jstips') - expect(isSubmodule).toBe(true) - }) - }) - - describe('.getAheadBehindCount(reference, path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns 0, 0 for a branch with no upstream', async () => { - const {ahead, behind} = await repo.getAheadBehindCount('master') - expect(ahead).toBe(0) - expect(behind).toBe(0) - }) - }) - - describe('.getCachedUpstreamAheadBehindCount(path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns 0, 0 for a branch with no upstream', async () => { - await repo.refreshStatus() - - const {ahead, behind} = await repo.getCachedUpstreamAheadBehindCount() - expect(ahead).toBe(0) - expect(behind).toBe(0) - }) - - describe('in a submodule', () => { - beforeEach(() => { - const workingDirectory = copySubmoduleRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns 1, 0 for a branch which is ahead by 1', async () => { - await repo.refreshStatus() - - const {ahead, behind} = await repo.getCachedUpstreamAheadBehindCount('You-Dont-Need-jQuery') - expect(ahead).toBe(1) - expect(behind).toBe(0) - }) - }) - }) - - describe('.getDiffStats(path)', () => { - let workingDirectory - beforeEach(() => { - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the diff stat', async () => { - const filePath = path.join(workingDirectory, 'a.txt') - fs.writeFileSync(filePath, 'change') - - const {added, deleted} = await repo.getDiffStats('a.txt') - expect(added).toBe(1) - expect(deleted).toBe(0) - }) - }) - - describe('.hasBranch(branch)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('resolves true when the branch exists', async () => { - const hasBranch = await repo.hasBranch('master') - expect(hasBranch).toBe(true) - }) - - it("resolves false when the branch doesn't exist", async () => { - const hasBranch = await repo.hasBranch('trolleybus') - expect(hasBranch).toBe(false) - }) - }) - - describe('.getReferences(path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the heads, remotes, and tags', async () => { - const {heads, remotes, tags} = await repo.getReferences() - expect(heads.length).toBe(1) - expect(remotes.length).toBe(0) - expect(tags.length).toBe(0) - }) - }) - - describe('.getReferenceTarget(reference, path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the SHA target', async () => { - const SHA = await repo.getReferenceTarget('refs/heads/master') - expect(SHA).toBe('8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e') - }) - }) - - describe('.getConfigValue(key, path)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('looks up the value for the key', async () => { - const bare = await repo.getConfigValue('core.bare') - expect(bare).toBe('false') - }) - - it("resolves to null if there's no value", async () => { - const value = await repo.getConfigValue('my.special.key') - expect(value).toBeNull() - }) - }) - - describe('.checkoutReference(reference, create)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('can create new branches', () => { - let success = false - let threw = false - waitsForPromise(() => repo.checkoutReference('my-b', true) - .then(_ => success = true) - .catch(_ => threw = true)) - runs(() => { - expect(success).toBe(true) - expect(threw).toBe(false) - }) - }) - }) - - describe('.getLineDiffs(path, text)', () => { - beforeEach(() => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the old and new lines of the diff', async () => { - const [{oldStart, newStart, oldLines, newLines}] = await repo.getLineDiffs('a.txt', 'hi there') - expect(oldStart).toBe(0) - expect(oldLines).toBe(0) - expect(newStart).toBe(1) - expect(newLines).toBe(1) - }) - }) - - describe('GitRepositoryAsync::relativizeToWorkingDirectory(_path)', () => { - let workingDirectory - - beforeEach(() => { - workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('relativizes the given path to the working directory of the repository', async () => { - let absolutePath = path.join(workingDirectory, 'a.txt') - expect(await repo.relativizeToWorkingDirectory(absolutePath)).toBe('a.txt') - absolutePath = path.join(workingDirectory, 'a/b/c.txt') - expect(await repo.relativizeToWorkingDirectory(absolutePath)).toBe('a/b/c.txt') - expect(await repo.relativizeToWorkingDirectory('a.txt')).toBe('a.txt') - expect(await repo.relativizeToWorkingDirectory('/not/in/workdir')).toBe('/not/in/workdir') - expect(await repo.relativizeToWorkingDirectory(null)).toBe(null) - expect(await repo.relativizeToWorkingDirectory()).toBe(undefined) - expect(await repo.relativizeToWorkingDirectory('')).toBe('') - expect(await repo.relativizeToWorkingDirectory(workingDirectory)).toBe('') - }) - - describe('when the opened path is a symlink', () => { - it('relativizes against both the linked path and real path', async () => { - // Symlinks require admin privs on windows so we just skip this there, - // done in git-utils as well - if (process.platform === 'win32') { - return - } - - const linkDirectory = path.join(temp.mkdirSync('atom-working-dir-symlink'), 'link') - fs.symlinkSync(workingDirectory, linkDirectory) - const linkedRepo = GitRepositoryAsync.open(linkDirectory) - expect(await linkedRepo.relativizeToWorkingDirectory(path.join(workingDirectory, 'test1'))).toBe('test1') - expect(await linkedRepo.relativizeToWorkingDirectory(path.join(linkDirectory, 'test2'))).toBe('test2') - expect(await linkedRepo.relativizeToWorkingDirectory(path.join(linkDirectory, 'test2/test3'))).toBe('test2/test3') - expect(await linkedRepo.relativizeToWorkingDirectory('test2/test3')).toBe('test2/test3') - }) - - it('handles case insensitive filesystems', async () => { - repo.isCaseInsensitive = true - expect(await repo.relativizeToWorkingDirectory(path.join(workingDirectory.toUpperCase(), 'a.txt'))).toBe('a.txt') - expect(await repo.relativizeToWorkingDirectory(path.join(workingDirectory.toUpperCase(), 'a/b/c.txt'))).toBe('a/b/c.txt') - }) - }) - }) - - describe('.getOriginURL()', () => { - beforeEach(() => { - const workingDirectory = copyRepository('repo-with-submodules') - repo = GitRepositoryAsync.open(workingDirectory) - }) - - it('returns the origin URL', async () => { - const url = await repo.getOriginURL() - expect(url).toBe('git@github.com:atom/some-repo-i-guess.git') - }) - }) - - describe('.getUpstreamBranch()', () => { - it('returns null when there is no upstream branch', async () => { - const workingDirectory = copyRepository() - repo = GitRepositoryAsync.open(workingDirectory) - - const upstream = await repo.getUpstreamBranch() - expect(upstream).toBe(null) - }) - - it('returns the upstream branch', async () => { - const workingDirectory = copyRepository('repo-with-submodules') - repo = GitRepositoryAsync.open(workingDirectory) - - const upstream = await repo.getUpstreamBranch() - expect(upstream).toBe('refs/remotes/origin/master') - }) - }) -}) diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index a9de506a2..ef87d83ee 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -25,16 +25,6 @@ describe "GitRepository", -> it "returns null when no repository is found", -> expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() - describe ".async", -> - it "returns a GitRepositoryAsync for the same repo", -> - repoPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - repo = new GitRepository(repoPath) - onSuccess = jasmine.createSpy('onSuccess') - waitsForPromise -> - repo.async.getPath().then(onSuccess) - runs -> - expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath) - describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() @@ -259,36 +249,6 @@ describe "GitRepository", -> expect(repo.isStatusModified(status)).toBe false expect(repo.isStatusNew(status)).toBe false - it 'caches the proper statuses when multiple project are open', -> - otherWorkingDirectory = copyRepository() - - atom.project.setPaths([workingDirectory, otherWorkingDirectory]) - - waitsForPromise -> - atom.workspace.open('b.txt') - - statusHandler = null - runs -> - repo = atom.project.getRepositories()[0] - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - subDir = path.join(workingDirectory, 'dir') - fs.mkdirSync(subDir) - - filePath = path.join(subDir, 'b.txt') - fs.writeFileSync(filePath, '') - - status = repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe true - expect(repo.isStatusNew(status)).toBe false - it 'caches statuses that were looked up synchronously', -> originalContent = 'undefined' fs.writeFileSync(modifiedPath, 'making this path modified') diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 92218e749..57d7a661f 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -24,14 +24,14 @@ describe "Package", -> mockLocalStorage() it "does not activate it", -> - packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module') pack = buildPackage(packagePath) expect(pack.isCompatible()).toBe false expect(pack.incompatibleModules[0].name).toBe 'native-module' expect(pack.incompatibleModules[0].path).toBe path.join(packagePath, 'node_modules', 'native-module') it "utilizes _atomModuleCache if present to determine the package's native dependencies", -> - packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-ignored-incompatible-native-module') + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-ignored-incompatible-native-module') pack = buildPackage(packagePath) expect(pack.getNativeModuleDependencyPaths().length).toBe(1) # doesn't see the incompatible module expect(pack.isCompatible()).toBe true @@ -41,8 +41,7 @@ describe "Package", -> expect(pack.isCompatible()).toBe false it "caches the incompatible native modules in local storage", -> - packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') - + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module') expect(buildPackage(packagePath).isCompatible()).toBe false expect(global.localStorage.getItem.callCount).toBe 1 expect(global.localStorage.setItem.callCount).toBe 1 @@ -51,6 +50,18 @@ describe "Package", -> expect(global.localStorage.getItem.callCount).toBe 2 expect(global.localStorage.setItem.callCount).toBe 1 + it "logs an error to the console describing the problem", -> + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-incompatible-native-module') + + spyOn(console, 'warn') + spyOn(atom.notifications, 'addFatalError') + + buildPackage(packagePath).activateNow() + + expect(atom.notifications.addFatalError).not.toHaveBeenCalled() + expect(console.warn.callCount).toBe(1) + expect(console.warn.mostRecentCall.args[0]).toContain('it requires one or more incompatible native modules (native-module)') + describe "::rebuild()", -> beforeEach -> mockLocalStorage() diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee deleted file mode 100644 index 699e60b21..000000000 --- a/spec/random-editor-spec.coffee +++ /dev/null @@ -1,91 +0,0 @@ -{times, random} = require 'underscore-plus' -randomWords = require 'random-words' -TextBuffer = require 'text-buffer' -TextEditor = require '../src/text-editor' - -describe "TextEditor", -> - [editor, tokenizedBuffer, buffer, steps] = [] - - softWrapColumn = 80 - - beforeEach -> - atom.config.set('editor.softWrapAtPreferredLineLength', true) - atom.config.set('editor.preferredLineLength', softWrapColumn) - - it "properly renders soft-wrapped lines when randomly mutated", -> - times 10, (i) -> - buffer = new TextBuffer - editor = atom.workspace.buildTextEditor({buffer}) - editor.setEditorWidthInChars(80) - tokenizedBuffer = editor.tokenizedBuffer - steps = [] - - times 30, -> - randomlyMutateEditor() - verifyLines() - - verifyLines = -> - {bufferRows, screenLines} = getReferenceScreenLines() - for referenceBufferRow, screenRow in bufferRows - referenceScreenLine = screenLines[screenRow] - actualBufferRow = editor.bufferRowForScreenRow(screenRow) - unless actualBufferRow is referenceBufferRow - logLines() - throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", ) - - actualScreenLine = editor.lineTextForScreenRow(screenRow) - unless actualScreenLine is referenceScreenLine - logLines() - throw new Error("Invalid line text at screen row #{screenRow}") - - logLines = -> - console.log "==== screen lines ====" - editor.logScreenLines() - console.log "==== reference lines ====" - {bufferRows, screenLines} = getReferenceScreenLines() - for bufferRow, screenRow in bufferRows - console.log screenRow, bufferRow, screenLines[screenRow].text - console.log "==== steps to reproduce this failure: ===" - for step in steps - console.log 'editor.' + step[0] + '('+ step[1..].map((a) -> JSON.stringify(a)).join(', ') + ')' - - randomlyMutateEditor = -> - if Math.random() < .2 - softWrapped = not editor.isSoftWrapped() - steps.push(['setSoftWrapped', softWrapped]) - editor.setSoftWrapped(softWrapped) - else - range = getRandomRange() - text = getRandomText() - steps.push(['setTextInBufferRange', range, text]) - editor.setTextInBufferRange(range, text) - - getRandomRange = -> - startRow = random(0, buffer.getLastRow()) - startColumn = random(0, buffer.lineForRow(startRow).length) - endRow = random(startRow, buffer.getLastRow()) - endColumn = random(0, buffer.lineForRow(endRow).length) - [[startRow, startColumn], [endRow, endColumn]] - - getRandomText = -> - text = [] - max = buffer.getText().split(/\s/).length * 0.75 - - times random(5, max), -> - if Math.random() < .1 - text += '\n' - else - text += " " if /\w$/.test(text) - text += randomWords(exactly: 1) - text - - getReferenceScreenLines = -> - referenceEditor = atom.workspace.buildTextEditor() - referenceEditor.setEditorWidthInChars(80) - referenceEditor.setText(editor.getText()) - referenceEditor.setSoftWrapped(editor.isSoftWrapped()) - - screenLines = [0..referenceEditor.getLastScreenRow()].map (row) -> referenceEditor.lineTextForScreenRow(row) - bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow()) - - {screenLines, bufferRows} diff --git a/spec/row-map-spec.coffee b/spec/row-map-spec.coffee deleted file mode 100644 index e89ac1259..000000000 --- a/spec/row-map-spec.coffee +++ /dev/null @@ -1,104 +0,0 @@ -RowMap = require '../src/row-map' - -describe "RowMap", -> - map = null - - beforeEach -> - map = new RowMap - - describe "::screenRowRangeForBufferRow(bufferRow)", -> - it "returns the range of screen rows corresponding to the given buffer row", -> - map.spliceRegions(0, 0, [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 1, screenRows: 5} - {bufferRows: 5, screenRows: 5} - {bufferRows: 5, screenRows: 1} - ]) - - expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1] - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 10] - expect(map.screenRowRangeForBufferRow(6)).toEqual [10, 11] - expect(map.screenRowRangeForBufferRow(11)).toEqual [15, 16] - expect(map.screenRowRangeForBufferRow(12)).toEqual [15, 16] - expect(map.screenRowRangeForBufferRow(16)).toEqual [16, 17] - - describe "::bufferRowRangeForScreenRow(screenRow)", -> - it "returns the range of buffer rows corresponding to the given screen row", -> - map.spliceRegions(0, 0, [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 1, screenRows: 5} - {bufferRows: 5, screenRows: 5} - {bufferRows: 5, screenRows: 1} - ]) - - expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1] - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 6] - expect(map.bufferRowRangeForScreenRow(6)).toEqual [5, 6] - expect(map.bufferRowRangeForScreenRow(10)).toEqual [6, 7] - expect(map.bufferRowRangeForScreenRow(14)).toEqual [10, 11] - expect(map.bufferRowRangeForScreenRow(15)).toEqual [11, 16] - expect(map.bufferRowRangeForScreenRow(16)).toEqual [16, 17] - - describe "::spliceRegions(startBufferRow, bufferRowCount, regions)", -> - it "can insert regions when empty", -> - regions = [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 1, screenRows: 5} - {bufferRows: 5, screenRows: 5} - {bufferRows: 5, screenRows: 1} - ] - map.spliceRegions(0, 0, regions) - expect(map.getRegions()).toEqual regions - - it "can insert wrapped lines into rectangular regions", -> - map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) - map.spliceRegions(5, 0, [{bufferRows: 1, screenRows: 3}]) - expect(map.getRegions()).toEqual [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 1, screenRows: 3} - {bufferRows: 5, screenRows: 5} - ] - - it "can splice wrapped lines into rectangular regions", -> - map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) - map.spliceRegions(5, 1, [{bufferRows: 1, screenRows: 3}]) - expect(map.getRegions()).toEqual [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 1, screenRows: 3} - {bufferRows: 4, screenRows: 4} - ] - - it "can splice folded lines into rectangular regions", -> - map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) - map.spliceRegions(5, 3, [{bufferRows: 3, screenRows: 1}]) - expect(map.getRegions()).toEqual [ - {bufferRows: 5, screenRows: 5} - {bufferRows: 3, screenRows: 1} - {bufferRows: 2, screenRows: 2} - ] - - it "can replace folded regions with a folded region that surrounds them", -> - map.spliceRegions(0, 0, [ - {bufferRows: 3, screenRows: 3} - {bufferRows: 3, screenRows: 1} - {bufferRows: 1, screenRows: 1} - {bufferRows: 3, screenRows: 1} - {bufferRows: 3, screenRows: 3} - ]) - map.spliceRegions(2, 8, [{bufferRows: 8, screenRows: 1}]) - expect(map.getRegions()).toEqual [ - {bufferRows: 2, screenRows: 2} - {bufferRows: 8, screenRows: 1} - {bufferRows: 3, screenRows: 3} - ] - - it "merges adjacent rectangular regions", -> - map.spliceRegions(0, 0, [ - {bufferRows: 3, screenRows: 3} - {bufferRows: 3, screenRows: 1} - {bufferRows: 1, screenRows: 1} - {bufferRows: 3, screenRows: 1} - {bufferRows: 3, screenRows: 3} - ]) - - map.spliceRegions(3, 7, [{bufferRows: 5, screenRows: 5}]) diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee index a8a39eb54..2fe944e41 100644 --- a/spec/squirrel-update-spec.coffee +++ b/spec/squirrel-update-spec.coffee @@ -5,7 +5,7 @@ temp = require 'temp' SquirrelUpdate = require '../src/main-process/squirrel-update' Spawner = require '../src/main-process/spawner' WinPowerShell = require '../src/main-process/win-powershell' -WinRegistry = require '../src/main-process/win-registry' +WinShell = require '../src/main-process/win-shell' # Run passed callback as Spawner.spawn() would do invokeCallback = (callback) -> @@ -26,12 +26,16 @@ describe "Windows Squirrel Update", -> # do nothing on command, just run passed callback invokeCallback callback - # Prevent any actual change to Windows registry - for own method of WinRegistry - # all WinRegistry APIs share the same signature - spyOn(WinRegistry, method).andCallFake (callback) -> - # do nothing on registry, just run passed callback - invokeCallback callback + # Prevent any actual change to Windows Shell + class FakeShellOption + isRegistered: (callback) -> callback true + register: (callback) -> callback null + deregister: (callback) -> callback null, true + update: (callback) -> callback null + WinShell.fileHandler = new FakeShellOption() + WinShell.fileContextMenu = new FakeShellOption() + WinShell.folderContextMenu = new FakeShellOption() + WinShell.folderBackgroundContextMenu = new FakeShellOption() it "quits the app on all squirrel events", -> app = quit: jasmine.createSpy('quit') diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js index 8d0e458f4..93aaf21e0 100644 --- a/spec/tokenized-buffer-iterator-spec.js +++ b/spec/tokenized-buffer-iterator-spec.js @@ -4,100 +4,152 @@ import TokenizedBufferIterator from '../src/tokenized-buffer-iterator' import {Point} from 'text-buffer' describe('TokenizedBufferIterator', () => { - it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => { - const tokenizedBuffer = { - tokenizedLineForRow () { - return { - tags: [-1, -2, -1, -2], - text: '', - openScopes: [] - } - } - } - - const grammarRegistry = { - scopeForId () { - return 'foo' - } - } - - const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) - - iterator.seek(Point(0, 0)) - expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['foo']) - - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual(['foo']) - expect(iterator.getOpenTags()).toEqual(['foo']) - - iterator.moveToSuccessor() - expect(iterator.getCloseTags()).toEqual(['foo']) - expect(iterator.getOpenTags()).toEqual([]) - }) - - it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => { - const tokenizedBuffer = { - tokenizedLineForRow (row) { - if (row === 0) { + describe('seek(position)', function () { + it('seeks to the leftmost tag boundary at the given position, returning the containing tags', function () { + const tokenizedBuffer = { + tokenizedLineForRow (row) { return { - tags: [-1, 3, -2, -3], - text: 'bar', + tags: [-1, -2, -3, -4, -5, 3, -3, -4, -6], + text: 'foo', openScopes: [] } - } else if (row === 1) { + } + } + + const grammarRegistry = { + scopeForId (id) { return { - tags: [3], - text: 'baz', - openScopes: [-1] - } - } else if (row === 2) { + '-1': 'foo', '-2': 'foo', + '-3': 'bar', '-4': 'bar', + '-5': 'baz', '-6': 'baz' + }[id] + } + } + + const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) + + expect(iterator.seek(Point(0, 0))).toEqual([]) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual(['bar']) + + expect(iterator.seek(Point(0, 1))).toEqual(['baz']) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual([]) + + iterator.moveToSuccessor() + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['bar']) + + expect(iterator.seek(Point(0, 3))).toEqual(['baz']) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['bar']) + + iterator.moveToSuccessor() + expect(iterator.getCloseTags()).toEqual(['bar', 'baz']) + expect(iterator.getOpenTags()).toEqual([]) + }) + }) + + describe('moveToSuccessor()', function () { + it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => { + const tokenizedBuffer = { + tokenizedLineForRow () { return { - tags: [-2], + tags: [-1, -2, -1, -2], text: '', - openScopes: [-1] + openScopes: [] } } } - } - const grammarRegistry = { - scopeForId (id) { - if (id === -2 || id === -1) { + const grammarRegistry = { + scopeForId () { return 'foo' - } else if (id === -3) { - return 'qux' } } - } - const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) + const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) - iterator.seek(Point(0, 0)) - expect(iterator.getPosition()).toEqual(Point(0, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['foo']) + iterator.seek(Point(0, 0)) + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['foo']) - expect(iterator.getOpenTags()).toEqual(['qux']) + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual(['foo']) - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.getCloseTags()).toEqual(['qux']) - expect(iterator.getOpenTags()).toEqual([]) + iterator.moveToSuccessor() + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual([]) + }) - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.getCloseTags()).toEqual([]) - expect(iterator.getOpenTags()).toEqual(['foo']) + it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => { + const tokenizedBuffer = { + tokenizedLineForRow (row) { + if (row === 0) { + return { + tags: [-1, 3, -2, -3], + text: 'bar', + openScopes: [] + } + } else if (row === 1) { + return { + tags: [3], + text: 'baz', + openScopes: [-1] + } + } else if (row === 2) { + return { + tags: [-2], + text: '', + openScopes: [-1] + } + } + } + } - iterator.moveToSuccessor() - expect(iterator.getPosition()).toEqual(Point(2, 0)) - expect(iterator.getCloseTags()).toEqual(['foo']) - expect(iterator.getOpenTags()).toEqual([]) + const grammarRegistry = { + scopeForId (id) { + if (id === -2 || id === -1) { + return 'foo' + } else if (id === -3) { + return 'qux' + } + } + } + + const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry) + + iterator.seek(Point(0, 0)) + expect(iterator.getPosition()).toEqual(Point(0, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual(['qux']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.getCloseTags()).toEqual(['qux']) + expect(iterator.getOpenTags()).toEqual([]) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(1, 0)) + expect(iterator.getCloseTags()).toEqual([]) + expect(iterator.getOpenTags()).toEqual(['foo']) + + iterator.moveToSuccessor() + expect(iterator.getPosition()).toEqual(Point(2, 0)) + expect(iterator.getCloseTags()).toEqual(['foo']) + expect(iterator.getOpenTags()).toEqual([]) + }) }) }) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index d11c93213..6c437d953 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -816,7 +816,7 @@ describe "TokenizedBuffer", -> expect(iterator.seek(Point(0, 8))).toEqual(["source.js"]) expect(iterator.getPosition()).toEqual(Point(0, 8)) expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"]) - expect(iterator.getPosition()).toEqual(Point(1, 5)) + expect(iterator.getPosition()).toEqual(Point(1, 0)) expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"]) expect(iterator.getPosition()).toEqual(Point(1, 18)) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7ee91260e..5252321aa 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -230,11 +230,12 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - checkPortableHomeWritable = -> + checkPortableHomeWritable = => responseChannel = "check-portable-home-writable-response" ipcRenderer.on responseChannel, (event, response) -> ipcRenderer.removeAllListeners(responseChannel) - atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable + @notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable + @disposables.add new Disposable -> ipcRenderer.removeAllListeners(responseChannel) ipcRenderer.send('check-portable-home-writable', responseChannel) checkPortableHomeWritable() diff --git a/src/clipboard.coffee b/src/clipboard.coffee deleted file mode 100644 index 9511bdda9..000000000 --- a/src/clipboard.coffee +++ /dev/null @@ -1,61 +0,0 @@ -crypto = require 'crypto' -clipboard = require './safe-clipboard' - -# Extended: Represents the clipboard used for copying and pasting in Atom. -# -# An instance of this class is always available as the `atom.clipboard` global. -# -# ## Examples -# -# ```coffee -# atom.clipboard.write('hello') -# -# console.log(atom.clipboard.read()) # 'hello' -# ``` -module.exports = -class Clipboard - constructor: -> - @reset() - - reset: -> - @metadata = null - @signatureForMetadata = null - - # Creates an `md5` hash of some text. - # - # * `text` A {String} to hash. - # - # Returns a hashed {String}. - md5: (text) -> - crypto.createHash('md5').update(text, 'utf8').digest('hex') - - # Public: Write the given text to the clipboard. - # - # The metadata associated with the text is available by calling - # {::readWithMetadata}. - # - # * `text` The {String} to store. - # * `metadata` (optional) The additional info to associate with the text. - write: (text, metadata) -> - @signatureForMetadata = @md5(text) - @metadata = metadata - clipboard.writeText(text) - - # Public: Read the text from the clipboard. - # - # Returns a {String}. - read: -> - clipboard.readText() - - # Public: Read the text from the clipboard and return both the text and the - # associated metadata. - # - # Returns an {Object} with the following keys: - # * `text` The {String} clipboard text. - # * `metadata` The metadata stored by an earlier call to {::write}. - readWithMetadata: -> - text = @read() - if @signatureForMetadata is @md5(text) - {text, @metadata} - else - {text} diff --git a/src/clipboard.js b/src/clipboard.js new file mode 100644 index 000000000..34f6b1f83 --- /dev/null +++ b/src/clipboard.js @@ -0,0 +1,70 @@ +/** @babel */ + +import crypto from 'crypto' +import clipboard from './safe-clipboard' + +// Extended: Represents the clipboard used for copying and pasting in Atom. +// +// An instance of this class is always available as the `atom.clipboard` global. +// +// ## Examples +// +// ```coffee +// atom.clipboard.write('hello') +// +// console.log(atom.clipboard.read()) # 'hello' +// ``` +export default class Clipboard { + constructor () { + this.reset() + } + + reset () { + this.metadata = null + this.signatureForMetadata = null + } + + // Creates an `md5` hash of some text. + // + // * `text` A {String} to hash. + // + // Returns a hashed {String}. + md5 (text) { + return crypto.createHash('md5').update(text, 'utf8').digest('hex') + } + + // Public: Write the given text to the clipboard. + // + // The metadata associated with the text is available by calling + // {::readWithMetadata}. + // + // * `text` The {String} to store. + // * `metadata` (optional) The additional info to associate with the text. + write (text, metadata) { + this.signatureForMetadata = this.md5(text) + this.metadata = metadata + clipboard.writeText(text) + } + + // Public: Read the text from the clipboard. + // + // Returns a {String}. + read () { + return clipboard.readText() + } + + // Public: Read the text from the clipboard and return both the text and the + // associated metadata. + // + // Returns an {Object} with the following keys: + // * `text` The {String} clipboard text. + // * `metadata` The metadata stored by an earlier call to {::write}. + readWithMetadata () { + let text = this.read() + if (this.signatureForMetadata === this.md5(text)) { + return {text, metadata: this.metadata} + } else { + return {text} + } + } +} diff --git a/src/color.coffee b/src/color.coffee deleted file mode 100644 index b413b9e2c..000000000 --- a/src/color.coffee +++ /dev/null @@ -1,89 +0,0 @@ -_ = require 'underscore-plus' -ParsedColor = null - -# Essential: A simple color class returned from {Config::get} when the value -# at the key path is of type 'color'. -module.exports = -class Color - # Essential: Parse a {String} or {Object} into a {Color}. - # - # * `value` A {String} such as `'white'`, `#ff00ff`, or - # `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`, - # and `alpha` properties. - # - # Returns a {Color} or `null` if it cannot be parsed. - @parse: (value) -> - return null if _.isArray(value) or _.isFunction(value) - return null unless _.isObject(value) or _.isString(value) - - ParsedColor ?= require 'color' - - try - parsedColor = new ParsedColor(value) - catch error - return null - - new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha()) - - constructor: (red, green, blue, alpha) -> - Object.defineProperties this, - red: - set: (newRed) -> red = parseColor(newRed) - get: -> red - enumerable: true - configurable: false - green: - set: (newGreen) -> green = parseColor(newGreen) - get: -> green - enumerable: true - configurable: false - blue: - set: (newBlue) -> blue = parseColor(newBlue) - get: -> blue - enumerable: true - configurable: false - alpha: - set: (newAlpha) -> alpha = parseAlpha(newAlpha) - get: -> alpha - enumerable: true - configurable: false - - @red = red - @green = green - @blue = blue - @alpha = alpha - - # Essential: Returns a {String} in the form `'#abcdef'`. - toHexString: -> - "##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}" - - # Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`. - toRGBAString: -> - "rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})" - - isEqual: (color) -> - return true if this is color - color = Color.parse(color) unless color instanceof Color - return false unless color? - color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha - - clone: -> new Color(@red, @green, @blue, @alpha) - -parseColor = (color) -> - color = parseInt(color) - color = 0 if isNaN(color) - color = Math.max(color, 0) - color = Math.min(color, 255) - color - -parseAlpha = (alpha) -> - alpha = parseFloat(alpha) - alpha = 1 if isNaN(alpha) - alpha = Math.max(alpha, 0) - alpha = Math.min(alpha, 1) - alpha - -numberToHexString = (number) -> - hex = number.toString(16) - hex = "0#{hex}" if number < 16 - hex diff --git a/src/color.js b/src/color.js new file mode 100644 index 000000000..9db9e9b16 --- /dev/null +++ b/src/color.js @@ -0,0 +1,134 @@ +/** @babel */ + +let ParsedColor = null + +// Essential: A simple color class returned from {Config::get} when the value +// at the key path is of type 'color'. +export default class Color { + // Essential: Parse a {String} or {Object} into a {Color}. + // + // * `value` A {String} such as `'white'`, `#ff00ff`, or + // `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`, + // and `alpha` properties. + // + // Returns a {Color} or `null` if it cannot be parsed. + static parse (value) { + switch (typeof value) { + case 'string': + break + case 'object': + if (Array.isArray(value)) { return null } + break + default: + return null + } + + if (!ParsedColor) { + ParsedColor = require('color') + } + + try { + var parsedColor = new ParsedColor(value) + } catch (error) { + return null + } + + return new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha()) + } + + constructor (red, green, blue, alpha) { + this.red = red + this.green = green + this.blue = blue + this.alpha = alpha + } + + set red (red) { + this._red = parseColor(red) + } + + set green (green) { + this._green = parseColor(green) + } + + set blue (blue) { + this._blue = parseColor(blue) + } + + set alpha (alpha) { + this._alpha = parseAlpha(alpha) + } + + get red () { + return this._red + } + + get green () { + return this._green + } + + get blue () { + return this._blue + } + + get alpha () { + return this._alpha + } + + // Essential: Returns a {String} in the form `'#abcdef'`. + toHexString () { + return `#${numberToHexString(this.red)}${numberToHexString(this.green)}${numberToHexString(this.blue)}` + } + + // Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`. + toRGBAString () { + return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})` + } + + isEqual (color) { + if (this === color) { + return true + } + + if (!(color instanceof Color)) { + color = Color.parse(color) + } + + if (color == null) { + return false + } + + return color.red === this.red && color.blue === this.blue && color.green === this.green && color.alpha === this.alpha + } + + clone () { + return new Color(this.red, this.green, this.blue, this.alpha) + } +} + +function parseColor (colorString) { + const color = parseInt(colorString, 10) + if (isNaN(color)) { + return 0 + } else { + return Math.min(Math.max(color, 0), 255) + } +} + +function parseAlpha (alphaString) { + const alpha = parseFloat(alphaString) + if (isNaN(alpha)) { + return 1 + } else { + return Math.min(Math.max(alpha, 0), 1) + } +} + +function numberToHexString (number) { + const hex = number.toString(16) + if (number < 16) { + return `0${hex}` + } else { + return hex + } +} diff --git a/src/config.coffee b/src/config.coffee index 27ea1edef..1d314ada1 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -786,9 +786,10 @@ class Config rootSchema = properties[key] Object.assign rootSchema, schema - @setDefaults(keyPath, @extractDefaultsFromSchema(schema)) - @setScopedDefaultsFromSchema(keyPath, schema) - @resetSettingsForSchemaChange() + @transact => + @setDefaults(keyPath, @extractDefaultsFromSchema(schema)) + @setScopedDefaultsFromSchema(keyPath, schema) + @resetSettingsForSchemaChange() load: -> @initializeConfigDirectory() @@ -958,9 +959,10 @@ class Config setDefaults: (keyPath, defaults) -> if defaults? and isPlainObject(defaults) keys = splitKeyPath(keyPath) - for key, childValue of defaults - continue unless defaults.hasOwnProperty(key) - @setDefaults(keys.concat([key]).join('.'), childValue) + @transact => + for key, childValue of defaults + continue unless defaults.hasOwnProperty(key) + @setDefaults(keys.concat([key]).join('.'), childValue) else try defaults = @makeValueConformToSchema(keyPath, defaults) diff --git a/src/crash-reporter-start.coffee b/src/crash-reporter-start.coffee deleted file mode 100644 index 37c381473..000000000 --- a/src/crash-reporter-start.coffee +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = (extra) -> - # Breakpad on Mac OS X must be running on UI and non-UI processes - # Crashpad on Windows and Linux should only be running on non-UI process - return if process.type is 'renderer' and process.platform isnt 'darwin' - - {crashReporter} = require 'electron' - - crashReporter.start({ - productName: 'Atom', - companyName: 'GitHub', - submitURL: 'http://54.249.141.255:1127/post' - extra: extra - }) diff --git a/src/crash-reporter-start.js b/src/crash-reporter-start.js new file mode 100644 index 000000000..98b210d06 --- /dev/null +++ b/src/crash-reporter-start.js @@ -0,0 +1,10 @@ +module.exports = function (extra) { + const {crashReporter} = require('electron') + crashReporter.start({ + productName: 'Atom', + companyName: 'GitHub', + submitURL: 'https://crashreporter.atom.io', + autoSubmit: false, + extra: extra + }) +} diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee deleted file mode 100644 index 3c73a0b02..000000000 --- a/src/deserializer-manager.coffee +++ /dev/null @@ -1,68 +0,0 @@ -{Disposable} = require 'event-kit' - -# Extended: Manages the deserializers used for serialized state -# -# An instance of this class is always available as the `atom.deserializers` -# global. -# -# ## Examples -# -# ```coffee -# class MyPackageView extends View -# atom.deserializers.add(this) -# -# @deserialize: (state) -> -# new MyPackageView(state) -# -# constructor: (@state) -> -# -# serialize: -> -# @state -# ``` -module.exports = -class DeserializerManager - constructor: (@atomEnvironment) -> - @deserializers = {} - - # Public: Register the given class(es) as deserializers. - # - # * `deserializers` One or more deserializers to register. A deserializer can - # be any object with a `.name` property and a `.deserialize()` method. A - # common approach is to register a *constructor* as the deserializer for its - # instances by adding a `.deserialize()` class method. When your method is - # called, it will be passed serialized state as the first argument and the - # {Atom} environment object as the second argument, which is useful if you - # wish to avoid referencing the `atom` global. - add: (deserializers...) -> - @deserializers[deserializer.name] = deserializer for deserializer in deserializers - new Disposable => - delete @deserializers[deserializer.name] for deserializer in deserializers - return - - getDeserializerCount: -> - Object.keys(@deserializers).length - - # Public: Deserialize the state and params. - # - # * `state` The state {Object} to deserialize. - deserialize: (state) -> - return unless state? - - if deserializer = @get(state) - stateVersion = state.get?('version') ? state.version - return if deserializer.version? and deserializer.version isnt stateVersion - deserializer.deserialize(state, @atomEnvironment) - else - console.warn "No deserializer found for", state - - # Get the deserializer for the state. - # - # * `state` The state {Object} being deserialized. - get: (state) -> - return unless state? - - name = state.get?('deserializer') ? state.deserializer - @deserializers[name] - - clear: -> - @deserializers = {} diff --git a/src/deserializer-manager.js b/src/deserializer-manager.js new file mode 100644 index 000000000..f5f2e6429 --- /dev/null +++ b/src/deserializer-manager.js @@ -0,0 +1,100 @@ +/** @babel */ + +import {Disposable} from 'event-kit' + +// Extended: Manages the deserializers used for serialized state +// +// An instance of this class is always available as the `atom.deserializers` +// global. +// +// ## Examples +// +// ```coffee +// class MyPackageView extends View +// atom.deserializers.add(this) +// +// @deserialize: (state) -> +// new MyPackageView(state) +// +// constructor: (@state) -> +// +// serialize: -> +// @state +// ``` +export default class DeserializerManager { + constructor (atomEnvironment) { + this.atomEnvironment = atomEnvironment + this.deserializers = {} + } + + // Public: Register the given class(es) as deserializers. + // + // * `deserializers` One or more deserializers to register. A deserializer can + // be any object with a `.name` property and a `.deserialize()` method. A + // common approach is to register a *constructor* as the deserializer for its + // instances by adding a `.deserialize()` class method. When your method is + // called, it will be passed serialized state as the first argument and the + // {Atom} environment object as the second argument, which is useful if you + // wish to avoid referencing the `atom` global. + add (...deserializers) { + for (let i = 0; i < deserializers.length; i++) { + let deserializer = deserializers[i] + this.deserializers[deserializer.name] = deserializer + } + + return new Disposable(() => { + for (let j = 0; j < deserializers.length; j++) { + let deserializer = deserializers[j] + delete this.deserializers[deserializer.name] + } + }) + } + + getDeserializerCount () { + return Object.keys(this.deserializers).length + } + + // Public: Deserialize the state and params. + // + // * `state` The state {Object} to deserialize. + deserialize (state) { + if (state == null) { + return + } + + const deserializer = this.get(state) + if (deserializer) { + let stateVersion = ( + (typeof state.get === 'function') && state.get('version') || + state.version + ) + + if ((deserializer.version != null) && deserializer.version !== stateVersion) { + return + } + return deserializer.deserialize(state, this.atomEnvironment) + } else { + return console.warn('No deserializer found for', state) + } + } + + // Get the deserializer for the state. + // + // * `state` The state {Object} being deserialized. + get (state) { + if (state == null) { + return + } + + let stateDeserializer = ( + (typeof state.get === 'function') && state.get('deserializer') || + state.deserializer + ) + + return this.deserializers[stateDeserializer] + } + + clear () { + this.deserializers = {} + } +} diff --git a/src/git-repository-async.js b/src/git-repository-async.js deleted file mode 100644 index 66b73ba77..000000000 --- a/src/git-repository-async.js +++ /dev/null @@ -1,558 +0,0 @@ -'use babel' - -import {Repository} from 'ohnogit' -import {CompositeDisposable, Disposable} from 'event-kit' - -// For the most part, this class behaves the same as `GitRepository`, with a few -// notable differences: -// * Errors are generally propagated out to the caller instead of being -// swallowed within `GitRepositoryAsync`. -// * Methods accepting a path shouldn't be given a null path, unless it is -// specifically allowed as noted in the method's documentation. -export default class GitRepositoryAsync { - static open (path, options = {}) { - // QUESTION: Should this wrap Git.Repository and reject with a nicer message? - return new GitRepositoryAsync(path, options) - } - - static get Git () { - return Repository.Git - } - - // The name of the error thrown when an action is attempted on a destroyed - // repository. - static get DestroyedErrorName () { - return Repository.DestroyedErrorName - } - - constructor (_path, options = {}) { - this.repo = Repository.open(_path, options) - - this.subscriptions = new CompositeDisposable() - - let {refreshOnWindowFocus = true} = options - if (refreshOnWindowFocus) { - const onWindowFocus = () => this.refreshStatus() - window.addEventListener('focus', onWindowFocus) - this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) - } - - const {project, subscribeToBuffers} = options - this.project = project - if (this.project && subscribeToBuffers) { - this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) - this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) - } - } - - // This exists to provide backwards compatibility. - get _refreshingPromise () { - return this.repo._refreshingPromise - } - - get openedPath () { - return this.repo.openedPath - } - - // Public: Destroy this {GitRepositoryAsync} object. - // - // This destroys any tasks and subscriptions and releases the underlying - // libgit2 repository handle. This method is idempotent. - destroy () { - this.repo.destroy() - - if (this.subscriptions) { - this.subscriptions.dispose() - this.subscriptions = null - } - } - - // Event subscription - // ================== - - // Public: Invoke the given callback when this GitRepositoryAsync's destroy() - // method is invoked. - // - // * `callback` {Function} - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy (callback) { - return this.repo.onDidDestroy(callback) - } - - // Public: Invoke the given callback when a specific file's status has - // changed. When a file is updated, reloaded, etc, and the status changes, this - // will be fired. - // - // * `callback` {Function} - // * `event` {Object} - // * `path` {String} the old parameters the decoration used to have - // * `pathStatus` {Number} representing the status. This value can be passed to - // {::isStatusModified} or {::isStatusNew} to get more information. - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatus (callback) { - return this.repo.onDidChangeStatus(callback) - } - - // Public: Invoke the given callback when a multiple files' statuses have - // changed. For example, on window focus, the status of all the paths in the - // repo is checked. If any of them have changed, this will be fired. Call - // {::getPathStatus(path)} to get the status for your path of choice. - // - // * `callback` {Function} - // - // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatuses (callback) { - return this.repo.onDidChangeStatuses(callback) - } - - // Repository details - // ================== - - // Public: A {String} indicating the type of version control system used by - // this repository. - // - // Returns `"git"`. - getType () { - return 'git' - } - - // Public: Returns a {Promise} which resolves to the {String} path of the - // repository. - getPath () { - return this.repo.getPath() - } - - // Public: Returns a {Promise} which resolves to the {String} working - // directory path of the repository. - getWorkingDirectory (_path) { - return this.repo.getWorkingDirectory() - } - - // Public: Returns a {Promise} that resolves to true if at the root, false if - // in a subfolder of the repository. - isProjectAtRoot () { - if (!this.project) return Promise.resolve(false) - - if (!this.projectAtRoot) { - this.projectAtRoot = this.getWorkingDirectory() - .then(wd => this.project.relativize(wd) === '') - } - - return this.projectAtRoot - } - - // Public: Makes a path relative to the repository's working directory. - // - // * `path` The {String} path to relativize. - // - // Returns a {Promise} which resolves to the relative {String} path. - relativizeToWorkingDirectory (_path) { - return this.repo.relativizeToWorkingDirectory(_path) - } - - // Public: Makes a path relative to the repository's working directory. - // - // * `path` The {String} path to relativize. - // * `workingDirectory` The {String} working directory path. - // - // Returns the relative {String} path. - relativize (_path, workingDirectory) { - return this.repo.relativize(_path, workingDirectory) - } - - // Public: Returns a {Promise} which resolves to whether the given branch - // exists. - hasBranch (branch) { - return this.repo.hasBranch(branch) - } - - // Public: Retrieves a shortened version of the HEAD reference value. - // - // This removes the leading segments of `refs/heads`, `refs/tags`, or - // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 - // characters. - // - // * `path` An optional {String} path in the repository to get this information - // for, only needed if the repository contains submodules. - // - // Returns a {Promise} which resolves to a {String}. - getShortHead (_path) { - return this.repo.getShortHead(_path) - } - - // Public: Is the given path a submodule in the repository? - // - // * `path` The {String} path to check. - // - // Returns a {Promise} that resolves true if the given path is a submodule in - // the repository. - isSubmodule (_path) { - return this.repo.isSubmodule(_path) - } - - // Public: Returns the number of commits behind the current branch is from the - // its upstream remote branch. - // - // * `reference` The {String} branch reference name. - // * `path` The {String} path in the repository to get this information - // for, only needed if the repository contains submodules. - // - // Returns a {Promise} which resolves to an {Object} with the following keys: - // * `ahead` The {Number} of commits ahead. - // * `behind` The {Number} of commits behind. - getAheadBehindCount (reference, _path) { - return this.repo.getAheadBehindCount(reference, _path) - } - - // Public: Get the cached ahead/behind commit counts for the current branch's - // upstream branch. - // - // * `path` An optional {String} path in the repository to get this information - // for, only needed if the repository has submodules. - // - // Returns a {Promise} which resolves to an {Object} with the following keys: - // * `ahead` The {Number} of commits ahead. - // * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount (_path) { - return this.repo.getCachedUpstreamAheadBehindCount(_path) - } - - // Public: Returns the git configuration value specified by the key. - // - // * `path` An optional {String} path in the repository to get this information - // for, only needed if the repository has submodules. - // - // Returns a {Promise} which resolves to the {String} git configuration value - // specified by the key. - getConfigValue (key, _path) { - return this.repo.getConfigValue(key, _path) - } - - // Public: Get the URL for the 'origin' remote. - // - // * `path` (optional) {String} path in the repository to get this information - // for, only needed if the repository has submodules. - // - // Returns a {Promise} which resolves to the {String} origin url of the - // repository. - getOriginURL (_path) { - return this.repo.getOriginURL(_path) - } - - // Public: Returns the upstream branch for the current HEAD, or null if there - // is no upstream branch for the current HEAD. - // - // * `path` An optional {String} path in the repo to get this information for, - // only needed if the repository contains submodules. - // - // Returns a {Promise} which resolves to a {String} branch name such as - // `refs/remotes/origin/master`. - getUpstreamBranch (_path) { - return this.repo.getUpstreamBranch(_path) - } - - // Public: Gets all the local and remote references. - // - // * `path` An optional {String} path in the repository to get this information - // for, only needed if the repository has submodules. - // - // Returns a {Promise} which resolves to an {Object} with the following keys: - // * `heads` An {Array} of head reference names. - // * `remotes` An {Array} of remote reference names. - // * `tags` An {Array} of tag reference names. - getReferences (_path) { - return this.repo.getReferences(_path) - } - - // Public: Get the SHA for the given reference. - // - // * `reference` The {String} reference to get the target of. - // * `path` An optional {String} path in the repo to get the reference target - // for. Only needed if the repository contains submodules. - // - // Returns a {Promise} which resolves to the current {String} SHA for the - // given reference. - getReferenceTarget (reference, _path) { - return this.repo.getReferenceTarget(reference, _path) - } - - // Reading Status - // ============== - - // Public: Resolves true if the given path is modified. - // - // * `path` The {String} path to check. - // - // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` - // is modified. - isPathModified (_path) { - return this.repo.isPathModified(_path) - } - - // Public: Resolves true if the given path is new. - // - // * `path` The {String} path to check. - // - // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` - // is new. - isPathNew (_path) { - return this.repo.isPathNew(_path) - } - - // Public: Is the given path ignored? - // - // * `path` The {String} path to check. - // - // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` - // is ignored. - isPathIgnored (_path) { - return this.repo.isPathIgnored(_path) - } - - // Get the status of a directory in the repository's working directory. - // - // * `directoryPath` The {String} path to check. - // - // Returns a {Promise} resolving to a {Number} representing the status. This - // value can be passed to {::isStatusModified} or {::isStatusNew} to get more - // information. - getDirectoryStatus (directoryPath) { - return this.repo.getDirectoryStatus(directoryPath) - } - - // Refresh the status bit for the given path. - // - // Note that if the status of the path has changed, this will emit a - // 'did-change-status' event. - // - // * `path` The {String} path whose status should be refreshed. - // - // Returns a {Promise} which resolves to a {Number} which is the refreshed - // status bit for the path. - refreshStatusForPath (_path) { - return this.repo.refreshStatusForPath(_path) - } - - // Returns a Promise that resolves to the status bit of a given path if it has - // one, otherwise 'current'. - getPathStatus (_path) { - return this.refreshStatusForPath(_path) - } - - // Public: Get the cached status for the given path. - // - // * `path` A {String} path in the repository, relative or absolute. - // - // Returns a {Promise} which resolves to a status {Number} or null if the - // path is not in the cache. - getCachedPathStatus (_path) { - return this.repo.getCachedPathStatus(_path) - } - - // Public: Get the cached statuses for the repository. - // - // Returns an {Object} of {Number} statuses, keyed by {String} working - // directory-relative file names. - getCachedPathStatuses () { - return this.repo.pathStatusCache - } - - // Public: Returns true if the given status indicates modification. - // - // * `statusBit` A {Number} representing the status. - // - // Returns a {Boolean} that's true if the `statusBit` indicates modification. - isStatusModified (statusBit) { - return this.repo.isStatusModified(statusBit) - } - - // Public: Returns true if the given status indicates a new path. - // - // * `statusBit` A {Number} representing the status. - // - // Returns a {Boolean} that's true if the `statusBit` indicates a new path. - isStatusNew (statusBit) { - return this.repo.isStatusNew(statusBit) - } - - // Public: Returns true if the given status indicates the path is staged. - // - // * `statusBit` A {Number} representing the status. - // - // Returns a {Boolean} that's true if the `statusBit` indicates the path is - // staged. - isStatusStaged (statusBit) { - return this.repo.isStatusStaged(statusBit) - } - - // Public: Returns true if the given status indicates the path is ignored. - // - // * `statusBit` A {Number} representing the status. - // - // Returns a {Boolean} that's true if the `statusBit` indicates the path is - // ignored. - isStatusIgnored (statusBit) { - return this.repo.isStatusIgnored(statusBit) - } - - // Public: Returns true if the given status indicates the path is deleted. - // - // * `statusBit` A {Number} representing the status. - // - // Returns a {Boolean} that's true if the `statusBit` indicates the path is - // deleted. - isStatusDeleted (statusBit) { - return this.repo.isStatusDeleted(statusBit) - } - - // Retrieving Diffs - // ================ - // Public: Retrieves the number of lines added and removed to a path. - // - // This compares the working directory contents of the path to the `HEAD` - // version. - // - // * `path` The {String} path to check. - // - // Returns a {Promise} which resolves to an {Object} with the following keys: - // * `added` The {Number} of added lines. - // * `deleted` The {Number} of deleted lines. - getDiffStats (_path) { - return this.repo.getDiffStats(_path) - } - - // Public: Retrieves the line diffs comparing the `HEAD` version of the given - // path and the given text. - // - // * `path` The {String} path relative to the repository. - // * `text` The {String} to compare against the `HEAD` contents - // - // Returns an {Array} of hunk {Object}s with the following keys: - // * `oldStart` The line {Number} of the old hunk. - // * `newStart` The line {Number} of the new hunk. - // * `oldLines` The {Number} of lines in the old hunk. - // * `newLines` The {Number} of lines in the new hunk - getLineDiffs (_path, text) { - return this.repo.getLineDiffs(_path, text) - } - - // Checking Out - // ============ - - // Public: Restore the contents of a path in the working directory and index - // to the version at `HEAD`. - // - // This is essentially the same as running: - // - // ```sh - // git reset HEAD -- - // git checkout HEAD -- - // ``` - // - // * `path` The {String} path to checkout. - // - // Returns a {Promise} that resolves or rejects depending on whether the - // method was successful. - checkoutHead (_path) { - return this.repo.checkoutHead(_path) - } - - // Public: Checks out a branch in your repository. - // - // * `reference` The {String} reference to checkout. - // * `create` A {Boolean} value which, if true creates the new reference if - // it doesn't exist. - // - // Returns a {Promise} that resolves if the method was successful. - checkoutReference (reference, create) { - return this.repo.checkoutReference(reference, create) - } - - // Private - // ======= - - checkoutHeadForEditor (editor) { - const filePath = editor.getPath() - if (!filePath) { - return Promise.reject() - } - - if (editor.buffer.isModified()) { - editor.buffer.reload() - } - - return this.checkoutHead(filePath) - } - - // Refreshes the git status. - // - // Returns a {Promise} which will resolve to {null} when refresh is complete. - refreshStatus () { - let projectPathsPromises = [Promise.resolve('')] - if (this.project) { - projectPathsPromises = this.project.getPaths() - .map(p => this.relativizeToWorkingDirectory(p)) - } - - return Promise.all(projectPathsPromises) - .then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*')) - .then(pathspecs => this.repo.refreshStatus(pathspecs)) - } - - // Get the NodeGit repository for the given path. - // - // * `path` The optional {String} path within the repository. This is only - // needed if you want to get the repository for that path if it is a - // submodule. - // - // Returns a {Promise} which resolves to the {NodeGit.Repository}. - getRepo (_path) { - return this.repo.getRepo(_path) - } - - // Open a new instance of the underlying {NodeGit.Repository}. - // - // By opening multiple connections to the same underlying repository, users - // can safely access the same repository concurrently. - // - // Returns the new {NodeGit.Repository}. - openRepository () { - return this.repo.openRepository() - } - - // Section: Private - // ================ - - // Has the repository been destroyed? - // - // Returns a {Boolean}. - _isDestroyed () { - return this.repo._isDestroyed() - } - - // Subscribe to events on the given buffer. - subscribeToBuffer (buffer) { - const bufferSubscriptions = new CompositeDisposable() - - const refreshStatusForBuffer = () => { - const _path = buffer.getPath() - if (_path) { - this.refreshStatusForPath(_path) - } - } - - bufferSubscriptions.add( - buffer.onDidSave(refreshStatusForBuffer), - buffer.onDidReload(refreshStatusForBuffer), - buffer.onDidChangePath(refreshStatusForBuffer), - buffer.onDidDestroy(() => { - bufferSubscriptions.dispose() - this.subscriptions.remove(bufferSubscriptions) - }) - ) - - this.subscriptions.add(bufferSubscriptions) - } -} diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee index 463e2bda2..593324d0c 100644 --- a/src/git-repository-provider.coffee +++ b/src/git-repository-provider.coffee @@ -77,7 +77,7 @@ class GitRepositoryProvider unless repo repo = GitRepository.open(gitDirPath, {@project, @config}) return null unless repo - repo.async.onDidDestroy(=> delete @pathToRepository[gitDirPath]) + repo.onDidDestroy(=> delete @pathToRepository[gitDirPath]) @pathToRepository[gitDirPath] = repo repo.refreshIndex() repo.refreshStatus() diff --git a/src/git-repository.coffee b/src/git-repository.coffee index f6bacb760..85600bba7 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -3,7 +3,6 @@ _ = require 'underscore-plus' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' -GitRepositoryAsync = require './git-repository-async' GitUtils = require 'git-utils' Task = require './task' @@ -76,19 +75,11 @@ class GitRepository unless @repo? throw new Error("No Git repository found searching path: #{path}") - asyncOptions = _.clone(options) - # GitRepository itself will handle these cases by manually calling through - # to the async repo. - asyncOptions.refreshOnWindowFocus = false - asyncOptions.subscribeToBuffers = false - @async = GitRepositoryAsync.open(path, asyncOptions) - + @statuses = {} @upstream = {ahead: 0, behind: 0} for submodulePath, submoduleRepo of @repo.submodules submoduleRepo.upstream = {ahead: 0, behind: 0} - @statusesByPath = {} - {@project, @config, refreshOnWindowFocus} = options refreshOnWindowFocus ?= true @@ -126,10 +117,6 @@ class GitRepository @subscriptions.dispose() @subscriptions = null - if @async? - @async.destroy() - @async = null - # Public: Returns a {Boolean} indicating if this repository has been destroyed. isDestroyed: -> not @repo? @@ -322,7 +309,7 @@ class GitRepository getDirectoryStatus: (directoryPath) -> directoryPath = "#{@relativize(directoryPath)}/" directoryStatus = 0 - for path, status of Object.assign({}, @async.getCachedPathStatuses(), @statusesByPath) + for path, status of @statuses directoryStatus |= status if path.indexOf(directoryPath) is 0 directoryStatus @@ -335,24 +322,13 @@ class GitRepository getPathStatus: (path) -> repo = @getRepo(path) relativePath = @relativize(path) - - # This is a bit particular. If a package calls `getPathStatus` like this: - # - change the file - # - getPathStatus - # - change the file - # - getPathStatus - # We need to preserve the guarantee that each call to `getPathStatus` will - # synchronously emit 'did-change-status'. So we need to keep a cache of the - # statuses found from this call. - currentPathStatus = @getCachedRelativePathStatus(relativePath) ? 0 - - # Trigger events emitted on the async repo as well - @async.refreshStatusForPath(path) - + currentPathStatus = @statuses[relativePath] ? 0 pathStatus = repo.getStatus(repo.relativize(path)) ? 0 pathStatus = 0 if repo.isStatusIgnored(pathStatus) - @statusesByPath[relativePath] = pathStatus - + if pathStatus > 0 + @statuses[relativePath] = pathStatus + else + delete @statuses[relativePath] if currentPathStatus isnt pathStatus @emitter.emit 'did-change-status', {path, pathStatus} @@ -364,11 +340,7 @@ class GitRepository # # Returns a status {Number} or null if the path is not in the cache. getCachedPathStatus: (path) -> - relativePath = @relativize(path) - @getCachedRelativePathStatus(relativePath) - - getCachedRelativePathStatus: (relativePath) -> - @statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath] + @statuses[@relativize(path)] # Public: Returns true if the given status indicates modification. # @@ -492,42 +464,29 @@ class GitRepository # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. - # - # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> - statusesChanged = false + @handlerPath ?= require.resolve('./repository-status-handler') - # Listen for `did-change-statuses` so we know if something changed. But we - # need to wait to propagate it until after we've set the branch and cleared - # the `statusesByPath` cache. So just set a flag, and we'll emit the event - # after refresh is done. - subscription = @async.onDidChangeStatuses -> - subscription?.dispose() - subscription = null + relativeProjectPaths = @project?.getPaths() + .map (path) => @relativize(path) + .filter (path) -> path.length > 0 - statusesChanged = true + @statusTask?.terminate() + new Promise (resolve) => + @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => + statusesUnchanged = _.isEqual(statuses, @statuses) and + _.isEqual(upstream, @upstream) and + _.isEqual(branch, @branch) and + _.isEqual(submodules, @submodules) - asyncRefresh = @async.refreshStatus().then => - subscription?.dispose() - subscription = null - - @branch = @async?.branch - @statusesByPath = {} - - if statusesChanged - @emitter.emit 'did-change-statuses' - - syncRefresh = new Promise (resolve, reject) => - @handlerPath ?= require.resolve('./repository-status-handler') - - @statusTask?.terminate() - @statusTask = Task.once @handlerPath, @getPath(), ({upstream, submodules}) => + @statuses = statuses @upstream = upstream + @branch = branch @submodules = submodules for submodulePath, submoduleRepo of @getRepo().submodules submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} + unless statusesUnchanged + @emitter.emit 'did-change-statuses' resolve() - - return Promise.all([asyncRefresh, syncRefresh]) diff --git a/src/main-process/atom-portable.coffee b/src/main-process/atom-portable.coffee deleted file mode 100644 index ae4bb67ec..000000000 --- a/src/main-process/atom-portable.coffee +++ /dev/null @@ -1,35 +0,0 @@ -fs = require 'fs-plus' -path = require 'path' -{ipcMain} = require 'electron' - -module.exports = -class AtomPortable - @getPortableAtomHomePath: -> - execDirectoryPath = path.dirname(process.execPath) - path.join(execDirectoryPath, '..', '.atom') - - @setPortable: (existingAtomHome) -> - fs.copySync(existingAtomHome, @getPortableAtomHomePath()) - - @isPortableInstall: (platform, environmentAtomHome, defaultHome) -> - return false unless platform in ['linux', 'win32'] - return false if environmentAtomHome - return false if not fs.existsSync(@getPortableAtomHomePath()) - # currently checking only that the directory exists and is writable, - # probably want to do some integrity checks on contents in future - @isPortableAtomHomePathWritable(defaultHome) - - @isPortableAtomHomePathWritable: (defaultHome) -> - writable = false - message = "" - try - writePermissionTestFile = path.join(@getPortableAtomHomePath(), "write.test") - fs.writeFileSync(writePermissionTestFile, "test") if not fs.existsSync(writePermissionTestFile) - fs.removeSync(writePermissionTestFile) - writable = true - catch error - message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}" - - ipcMain.on 'check-portable-home-writable', (event) -> - event.sender.send 'check-portable-home-writable-response', {writable, message} - writable diff --git a/src/main-process/atom-portable.js b/src/main-process/atom-portable.js new file mode 100644 index 000000000..7d395c0e7 --- /dev/null +++ b/src/main-process/atom-portable.js @@ -0,0 +1,58 @@ +const fs = require('fs-plus') +const path = require('path') +const {ipcMain} = require('electron') + +module.exports = class AtomPortable { + static getPortableAtomHomePath () { + const execDirectoryPath = path.dirname(process.execPath) + return path.join(execDirectoryPath, '..', '.atom') + } + + static setPortable (existingAtomHome) { + fs.copySync(existingAtomHome, this.getPortableAtomHomePath()) + } + + static isPortableInstall (platform, environmentAtomHome, defaultHome) { + if (!['linux', 'win32'].includes(platform)) { + return false + } + + if (environmentAtomHome) { + return false + } + + if (!fs.existsSync(this.getPortableAtomHomePath())) { + return false + } + + // Currently checking only that the directory exists and is writable, + // probably want to do some integrity checks on contents in future. + return this.isPortableAtomHomePathWritable(defaultHome) + } + + static isPortableAtomHomePathWritable (defaultHome) { + let writable = false + let message = '' + try { + const writePermissionTestFile = path.join(this.getPortableAtomHomePath(), 'write.test') + + if (!fs.existsSync(writePermissionTestFile)) { + fs.writeFileSync(writePermissionTestFile, 'test') + } + + fs.removeSync(writePermissionTestFile) + writable = true + } catch (error) { + message = `Failed to use portable Atom home directory (${this.getPortableAtomHomePath()}). Using the default instead (${defaultHome}). ${error.message}.` + } + + ipcMain.on('check-portable-home-writable', function (event) { + event.sender.send('check-portable-home-writable-response', { + writable: writable, + message: message + }) + }) + + return writable + } +} diff --git a/src/main-process/main.coffee b/src/main-process/main.coffee deleted file mode 100644 index 5368710aa..000000000 --- a/src/main-process/main.coffee +++ /dev/null @@ -1,198 +0,0 @@ -global.shellStartTime = Date.now() - -process.on 'uncaughtException', (error={}) -> - console.log(error.message) if error.message? - console.log(error.stack) if error.stack? - -{app} = require 'electron' -fs = require 'fs-plus' -path = require 'path' -temp = require 'temp' -yargs = require 'yargs' -previousConsoleLog = console.log -startCrashReporter = require('../crash-reporter-start') -console.log = require 'nslog' - -start = -> - args = parseCommandLine() - args.env = process.env - setupAtomHome(args) - setupCompileCache() - if handleStartupEventWithSquirrel() - return - else if args.test and args.mainProcess - console.log = previousConsoleLog - testRunner = require(path.join(args.resourcePath, 'spec/main-process/mocha-test-runner')) - app.on 'ready', -> testRunner(args.pathsToOpen) - return - - # NB: This prevents Win10 from showing dupe items in the taskbar - app.setAppUserModelId('com.squirrel.atom.atom') - - addPathToOpen = (event, pathToOpen) -> - event.preventDefault() - args.pathsToOpen.push(pathToOpen) - - addUrlToOpen = (event, urlToOpen) -> - event.preventDefault() - args.urlsToOpen.push(urlToOpen) - - app.on 'open-file', addPathToOpen - app.on 'open-url', addUrlToOpen - app.on 'will-finish-launching', startCrashReporter - - if args.userDataDir? - app.setPath('userData', args.userDataDir) - else if args.test - app.setPath('userData', temp.mkdirSync('atom-test-data')) - - app.on 'ready', -> - app.removeListener 'open-file', addPathToOpen - app.removeListener 'open-url', addUrlToOpen - - AtomApplication = require path.join(args.resourcePath, 'src', 'main-process', 'atom-application') - AtomApplication.open(args) - - console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test - -normalizeDriveLetterName = (filePath) -> - if process.platform is 'win32' - filePath.replace /^([a-z]):/, ([driveLetter]) -> driveLetter.toUpperCase() + ":" - else - filePath - -handleStartupEventWithSquirrel = -> - return false unless process.platform is 'win32' - SquirrelUpdate = require './squirrel-update' - squirrelCommand = process.argv[1] - SquirrelUpdate.handleStartupEvent(app, squirrelCommand) - -setupAtomHome = ({setPortable}) -> - return if process.env.ATOM_HOME - - atomHome = path.join(app.getPath('home'), '.atom') - AtomPortable = require './atom-portable' - - if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome) - try - AtomPortable.setPortable(atomHome) - catch error - console.log("Failed copying portable directory '#{atomHome}' to '#{AtomPortable.getPortableAtomHomePath()}'") - console.log("#{error.message} #{error.stack}") - - if AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome) - atomHome = AtomPortable.getPortableAtomHomePath() - - try - atomHome = fs.realpathSync(atomHome) - - process.env.ATOM_HOME = atomHome - -setupCompileCache = -> - compileCache = require('../compile-cache') - compileCache.setAtomHomeDirectory(process.env.ATOM_HOME) - -writeFullVersion = -> - process.stdout.write """ - Atom : #{app.getVersion()} - Electron: #{process.versions.electron} - Chrome : #{process.versions.chrome} - Node : #{process.versions.node} - - """ - -parseCommandLine = -> - version = app.getVersion() - options = yargs(process.argv[1..]).wrap(100) - options.usage """ - Atom Editor v#{version} - - Usage: atom [options] [path ...] - - One or more paths to files or folders may be specified. If there is an - existing Atom window that contains all of the given folders, the paths - will be opened in that window. Otherwise, they will be opened in a new - window. - - Environment Variables: - - ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode. - Defaults to `~/github/atom`. - - ATOM_HOME The root path for all configuration files and folders. - Defaults to `~/.atom`. - """ - # Deprecated 1.0 API preview flag - options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported.') - options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'This option is not currently supported.') - options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.') - options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.') - options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.') - options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.') - options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.') - options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') - options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') - options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.') - options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.') - options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') - options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.') - options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).') - options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.') - options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.') - options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.') - options.string('socket-path') - options.string('user-data-dir') - options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.') - - args = options.argv - - if args.help - process.stdout.write(options.help()) - process.exit(0) - - if args.version - writeFullVersion() - process.exit(0) - - addToLastWindow = args['add'] - executedFrom = args['executed-from']?.toString() ? process.cwd() - devMode = args['dev'] - safeMode = args['safe'] - pathsToOpen = args._ - test = args['test'] - mainProcess = args['main-process'] - timeout = args['timeout'] - newWindow = args['new-window'] - pidToKillWhenClosed = args['pid'] if args['wait'] - logFile = args['log-file'] - socketPath = args['socket-path'] - userDataDir = args['user-data-dir'] - profileStartup = args['profile-startup'] - clearWindowState = args['clear-window-state'] - urlsToOpen = [] - devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom') - setPortable = args.portable - - if args['resource-path'] - devMode = true - resourcePath = args['resource-path'] - - devMode = true if test - resourcePath ?= devResourcePath if devMode - - unless fs.statSyncNoException(resourcePath) - resourcePath = path.dirname(path.dirname(__dirname)) - - # On Yosemite the $PATH is not inherited by the "open" command, so we have to - # explicitly pass it by command line, see http://git.io/YC8_Ew. - process.env.PATH = args['path-environment'] if args['path-environment'] - - resourcePath = normalizeDriveLetterName(resourcePath) - devResourcePath = normalizeDriveLetterName(devResourcePath) - - {resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test, - version, pidToKillWhenClosed, devMode, safeMode, newWindow, - logFile, socketPath, userDataDir, profileStartup, timeout, setPortable, - clearWindowState, addToLastWindow, mainProcess} - -start() diff --git a/src/main-process/main.js b/src/main-process/main.js new file mode 100644 index 000000000..432f73832 --- /dev/null +++ b/src/main-process/main.js @@ -0,0 +1,263 @@ +global.shellStartTime = Date.now() + +process.on('uncaughtException', function (error = {}) { + if (error.message != null) { + console.log(error.message) + } + + if (error.stack != null) { + console.log(error.stack) + } +}) + +const {app} = require('electron') +const fs = require('fs-plus') +const path = require('path') +const temp = require('temp') +const yargs = require('yargs') +const dedent = require('dedent') +const startCrashReporter = require('../crash-reporter-start') +const previousConsoleLog = console.log +console.log = require('nslog') + +function start () { + const args = parseCommandLine() + args.env = process.env + setupAtomHome(args) + setupCompileCache() + + if (handleStartupEventWithSquirrel()) { + return + } else if (args.test && args.mainProcess) { + console.log = previousConsoleLog + app.on('ready', function () { + const testRunner = require(path.join(args.resourcePath, 'spec/main-process/mocha-test-runner')) + testRunner(args.pathsToOpen) + }) + return + } + + // NB: This prevents Win10 from showing dupe items in the taskbar + app.setAppUserModelId('com.squirrel.atom.atom') + + function addPathToOpen (event, pathToOpen) { + event.preventDefault() + args.pathsToOpen.push(pathToOpen) + } + + function addUrlToOpen (event, urlToOpen) { + event.preventDefault() + args.urlsToOpen.push(urlToOpen) + } + + app.on('open-file', addPathToOpen) + app.on('open-url', addUrlToOpen) + app.on('will-finish-launching', startCrashReporter) + + if (args.userDataDir != null) { + app.setPath('userData', args.userDataDir) + } else if (args.test) { + app.setPath('userData', temp.mkdirSync('atom-test-data')) + } + + app.on('ready', function () { + app.removeListener('open-file', addPathToOpen) + app.removeListener('open-url', addUrlToOpen) + const AtomApplication = require(path.join(args.resourcePath, 'src', 'main-process', 'atom-application')) + AtomApplication.open(args) + + if (!args.test) { + console.log(`App load time: ${Date.now() - global.shellStartTime}ms`) + } + }) +} + +function normalizeDriveLetterName (filePath) { + if (process.platform === 'win32') { + return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':') + } else { + return filePath + } +} + +function handleStartupEventWithSquirrel () { + if (process.platform !== 'win32') { + return false + } + + const SquirrelUpdate = require('./squirrel-update') + const squirrelCommand = process.argv[1] + return SquirrelUpdate.handleStartupEvent(app, squirrelCommand) +} + +function setupAtomHome ({setPortable}) { + if (process.env.ATOM_HOME) { + return + } + + let atomHome = path.join(app.getPath('home'), '.atom') + const AtomPortable = require('./atom-portable') + + if (setPortable && !AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) { + try { + AtomPortable.setPortable(atomHome) + } catch (error) { + console.log(`Failed copying portable directory '${atomHome}' to '${AtomPortable.getPortableAtomHomePath()}'`) + console.log(`${error.message} ${error.stack}`) + } + } + + if (AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) { + atomHome = AtomPortable.getPortableAtomHomePath() + } + + try { + atomHome = fs.realpathSync(atomHome) + } finally { + process.env.ATOM_HOME = atomHome + } +} + +function setupCompileCache () { + const CompileCache = require('../compile-cache') + CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) +} + +function writeFullVersion () { + process.stdout.write( + `Atom : ${app.getVersion()}\n` + + `Electron: ${process.versions.electron}\n` + + `Chrome : ${process.versions.chrome}\n` + + `Node : ${process.versions.node}\n` + ) +} + +function parseCommandLine () { + const options = yargs(process.argv.slice(1)).wrap(100) + const version = app.getVersion() + options.usage( + dedent`Atom Editor v${version} + + Usage: atom [options] [path ...] + + One or more paths to files or folders may be specified. If there is an + existing Atom window that contains all of the given folders, the paths + will be opened in that window. Otherwise, they will be opened in a new + window. + + Environment Variables: + + ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode. + Defaults to \`~/github/atom\`. + + ATOM_HOME The root path for all configuration files and folders. + Defaults to \`~/.atom\`.` + ) + // Deprecated 1.0 API preview flag + options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported.') + options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'This option is not currently supported.') + options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.') + options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.') + options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.') + options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.') + options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.') + options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') + options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') + options.boolean('safe').describe( + 'safe', + 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.' + ) + options.boolean('portable').describe( + 'portable', + 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.' + ) + options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') + options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.') + options.string('timeout').describe( + 'timeout', + 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).' + ) + options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.') + options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.') + options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.') + options.string('socket-path') + options.string('user-data-dir') + options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.') + + const args = options.argv + + if (args.help) { + process.stdout.write(options.help()) + process.exit(0) + } + + if (args.version) { + writeFullVersion() + process.exit(0) + } + + const addToLastWindow = args['add'] + const safeMode = args['safe'] + const pathsToOpen = args._ + const test = args['test'] + const mainProcess = args['main-process'] + const timeout = args['timeout'] + const newWindow = args['new-window'] + let executedFrom = null + if (args['executed-from'] && args['executed-from'].toString()) { + executedFrom = args['executed-from'].toString() + } else { + executedFrom = process.cwd() + } + + let pidToKillWhenClosed = null + if (args['wait']) { + pidToKillWhenClosed = args['pid'] + } + + const logFile = args['log-file'] + const socketPath = args['socket-path'] + const userDataDir = args['user-data-dir'] + const profileStartup = args['profile-startup'] + const clearWindowState = args['clear-window-state'] + const urlsToOpen = [] + const setPortable = args.portable + let devMode = args['dev'] + let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom') + let resourcePath = null + + if (args['resource-path']) { + devMode = true + resourcePath = args['resource-path'] + } + + if (test) { + devMode = true + } + + if (devMode && !resourcePath) { + resourcePath = devResourcePath + } + + if (!fs.statSyncNoException(resourcePath)) { + resourcePath = path.dirname(path.dirname(__dirname)) + } + + if (args['path-environment']) { + // On Yosemite the $PATH is not inherited by the "open" command, so we have to + // explicitly pass it by command line, see http://git.io/YC8_Ew. + process.env.PATH = args['path-environment'] + } + + resourcePath = normalizeDriveLetterName(resourcePath) + devResourcePath = normalizeDriveLetterName(devResourcePath) + + return { + resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test, + version, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, socketPath, + userDataDir, profileStartup, timeout, setPortable, clearWindowState, + addToLastWindow, mainProcess + } +} + +start() diff --git a/src/main-process/squirrel-update.coffee b/src/main-process/squirrel-update.coffee index a1bfc5359..acac81457 100644 --- a/src/main-process/squirrel-update.coffee +++ b/src/main-process/squirrel-update.coffee @@ -1,7 +1,7 @@ fs = require 'fs-plus' path = require 'path' Spawner = require './spawner' -WinRegistry = require './win-registry' +WinShell = require './win-shell' WinPowerShell = require './win-powershell' appFolder = path.resolve(process.execPath, '..') @@ -125,26 +125,36 @@ exports.restartAtom = (app) -> app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args) app.quit() +updateContextMenus = (callback) -> + WinShell.fileContextMenu.update -> + WinShell.folderContextMenu.update -> + WinShell.folderBackgroundContextMenu.update -> + callback() + # Handle squirrel events denoted by --squirrel-* command line arguments. exports.handleStartupEvent = (app, squirrelCommand) -> switch squirrelCommand when '--squirrel-install' createShortcuts -> - WinRegistry.installContextMenu -> - addCommandsToPath -> - app.quit() + addCommandsToPath -> + WinShell.fileHandler.register -> + updateContextMenus -> + app.quit() true when '--squirrel-updated' updateShortcuts -> - WinRegistry.installContextMenu -> - addCommandsToPath -> + addCommandsToPath -> + updateContextMenus -> app.quit() true when '--squirrel-uninstall' removeShortcuts -> - WinRegistry.uninstallContextMenu -> - removeCommandsFromPath -> - app.quit() + removeCommandsFromPath -> + WinShell.fileHandler.deregister -> + WinShell.fileContextMenu.deregister -> + WinShell.folderContextMenu.deregister -> + WinShell.folderBackgroundContextMenu.deregister -> + app.quit() true when '--squirrel-obsolete' app.quit() diff --git a/src/main-process/win-registry.coffee b/src/main-process/win-registry.coffee deleted file mode 100644 index f4b81b377..000000000 --- a/src/main-process/win-registry.coffee +++ /dev/null @@ -1,62 +0,0 @@ -path = require 'path' -Spawner = require './spawner' - -if process.env.SystemRoot - system32Path = path.join(process.env.SystemRoot, 'System32') - regPath = path.join(system32Path, 'reg.exe') -else - regPath = 'reg.exe' - -# Registry keys used for context menu -fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom' -directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom' -backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom' -applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe' - -# Spawn reg.exe and callback when it completes -spawnReg = (args, callback) -> - Spawner.spawn(regPath, args, callback) - -# Install the Open with Atom explorer context menu items via the registry. -# -# * `callback` The {Function} to call after registry operation is done. -# It will be invoked with the same arguments provided by {Spawner.spawn}. -# -# Returns `undefined`. -exports.installContextMenu = (callback) -> - addToRegistry = (args, callback) -> - args.unshift('add') - args.push('/f') - spawnReg(args, callback) - - installFileHandler = (callback) -> - args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""] - addToRegistry(args, callback) - - installMenu = (keyPath, arg, callback) -> - args = [keyPath, '/ve', '/d', 'Open with Atom'] - addToRegistry args, -> - args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""] - addToRegistry args, -> - args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""] - addToRegistry(args, callback) - - installMenu fileKeyPath, '%1', -> - installMenu directoryKeyPath, '%1', -> - installMenu backgroundKeyPath, '%V', -> - installFileHandler(callback) - -# Uninstall the Open with Atom explorer context menu items via the registry. -# -# * `callback` The {Function} to call after registry operation is done. -# It will be invoked with the same arguments provided by {Spawner.spawn}. -# -# Returns `undefined`. -exports.uninstallContextMenu = (callback) -> - deleteFromRegistry = (keyPath, callback) -> - spawnReg(['delete', keyPath, '/f'], callback) - - deleteFromRegistry fileKeyPath, -> - deleteFromRegistry directoryKeyPath, -> - deleteFromRegistry backgroundKeyPath, -> - deleteFromRegistry(applicationsKeyPath, callback) diff --git a/src/main-process/win-shell.coffee b/src/main-process/win-shell.coffee new file mode 100644 index 000000000..2bb993e9c --- /dev/null +++ b/src/main-process/win-shell.coffee @@ -0,0 +1,58 @@ +Registry = require 'winreg' +Path = require 'path' + +exeName = Path.basename(process.execPath) +appPath = "\"#{process.execPath}\"" +isBeta = appPath.includes(' Beta') +appName = exeName.replace('atom', (if isBeta then 'Atom Beta' else 'Atom' )).replace('.exe', '') + +class ShellOption + constructor: (key, parts) -> + @key = key + @parts = parts + + isRegistered: (callback) => + new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"}) + .get @parts[0].name, (err, val) => + callback(not err? and val? and val.value is @parts[0].value) + + register: (callback) => + doneCount = @parts.length + @parts.forEach (part) => + reg = new Registry({hive: 'HKCU', key: if part.key? then "#{@key}\\#{part.key}" else @key}) + reg.create( -> reg.set part.name, Registry.REG_SZ, part.value, -> callback() if --doneCount is 0) + + deregister: (callback) => + @isRegistered (isRegistered) => + if isRegistered + new Registry({hive: 'HKCU', key: @key}).destroy -> callback null, true + else + callback null, false + + update: (callback) => + new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"}) + .get @parts[0].name, (err, val) => + if err? or not val? or val.value.includes '\\' + exeName + callback(err) + else + @register callback + +exports.appName = appName + +exports.fileHandler = new ShellOption("\\Software\\Classes\\Applications\\#{exeName}", + [{key: 'shell\\open\\command', name: '', value: "#{appPath} \"%1\""}] +) + +contextParts = [ + {key: 'command', name: '', value: "#{appPath} \"%1\""}, + {name: '', value: "Open with #{appName}"}, + {name: 'Icon', value: "#{appPath}"} +] + +exports.fileContextMenu = new ShellOption("\\Software\\Classes\\*\\shell\\#{appName}", contextParts) + +exports.folderContextMenu = new ShellOption("\\Software\\Classes\\Directory\\shell\\#{appName}", contextParts) + +exports.folderBackgroundContextMenu = new ShellOption("\\Software\\Classes\\Directory\\background\\shell\\#{appName}", + JSON.parse(JSON.stringify(contextParts).replace('%1', '%V')) +) diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee index 3d8b1895c..1cb144bdc 100644 --- a/src/notification-manager.coffee +++ b/src/notification-manager.coffee @@ -33,8 +33,20 @@ class NotificationManager # # * `message` A {String} message # * `options` (optional) An options {Object} with the following keys: - # * `detail` (optional) A {String} with additional details about the - # notification. + # * `buttons` (optional) An {Array} of {Object} where each {Object} has the + # following options: + # * `className` (optional) {String} a class name to add to the button's + # default class name (`btn btn-success`). + # * `onDidClick` (optional) {Function} callback to call when the button + # has been clicked. The context will be set to the + # {NotificationElement} instance. + # * `text` {String} inner text for the button + # * `description` (optional) A Markdown {String} containing a longer + # description about the notification. By default, this **will not** + # preserve newlines and whitespace when it is rendered. + # * `detail` (optional) A plain-text {String} containing additional details + # about the notification. By default, this **will** preserve newlines + # and whitespace when it is rendered. # * `dismissable` (optional) A {Boolean} indicating whether this # notification can be dismissed by the user. Defaults to `false`. # * `icon` (optional) A {String} name of an icon from Octicons to display @@ -46,8 +58,20 @@ class NotificationManager # # * `message` A {String} message # * `options` (optional) An options {Object} with the following keys: - # * `detail` (optional) A {String} with additional details about the - # notification. + # * `buttons` (optional) An {Array} of {Object} where each {Object} has the + # following options: + # * `className` (optional) {String} a class name to add to the button's + # default class name (`btn btn-info`). + # * `onDidClick` (optional) {Function} callback to call when the button + # has been clicked. The context will be set to the + # {NotificationElement} instance. + # * `text` {String} inner text for the button + # * `description` (optional) A Markdown {String} containing a longer + # description about the notification. By default, this **will not** + # preserve newlines and whitespace when it is rendered. + # * `detail` (optional) A plain-text {String} containing additional details + # about the notification. By default, this **will** preserve newlines + # and whitespace when it is rendered. # * `dismissable` (optional) A {Boolean} indicating whether this # notification can be dismissed by the user. Defaults to `false`. # * `icon` (optional) A {String} name of an icon from Octicons to display @@ -59,8 +83,20 @@ class NotificationManager # # * `message` A {String} message # * `options` (optional) An options {Object} with the following keys: - # * `detail` (optional) A {String} with additional details about the - # notification. + # * `buttons` (optional) An {Array} of {Object} where each {Object} has the + # following options: + # * `className` (optional) {String} a class name to add to the button's + # default class name (`btn btn-warning`). + # * `onDidClick` (optional) {Function} callback to call when the button + # has been clicked. The context will be set to the + # {NotificationElement} instance. + # * `text` {String} inner text for the button + # * `description` (optional) A Markdown {String} containing a longer + # description about the notification. By default, this **will not** + # preserve newlines and whitespace when it is rendered. + # * `detail` (optional) A plain-text {String} containing additional details + # about the notification. By default, this **will** preserve newlines + # and whitespace when it is rendered. # * `dismissable` (optional) A {Boolean} indicating whether this # notification can be dismissed by the user. Defaults to `false`. # * `icon` (optional) A {String} name of an icon from Octicons to display @@ -72,12 +108,26 @@ class NotificationManager # # * `message` A {String} message # * `options` (optional) An options {Object} with the following keys: - # * `detail` (optional) A {String} with additional details about the - # notification. + # * `buttons` (optional) An {Array} of {Object} where each {Object} has the + # following options: + # * `className` (optional) {String} a class name to add to the button's + # default class name (`btn btn-error`). + # * `onDidClick` (optional) {Function} callback to call when the button + # has been clicked. The context will be set to the + # {NotificationElement} instance. + # * `text` {String} inner text for the button + # * `description` (optional) A Markdown {String} containing a longer + # description about the notification. By default, this **will not** + # preserve newlines and whitespace when it is rendered. + # * `detail` (optional) A plain-text {String} containing additional details + # about the notification. By default, this **will** preserve newlines + # and whitespace when it is rendered. # * `dismissable` (optional) A {Boolean} indicating whether this # notification can be dismissed by the user. Defaults to `false`. # * `icon` (optional) A {String} name of an icon from Octicons to display # in the notification header. Defaults to `'flame'`. + # * `stack` (optional) A preformatted {String} with stack trace information + # describing the location of the error. addError: (message, options) -> @addNotification(new Notification('error', message, options)) @@ -85,12 +135,26 @@ class NotificationManager # # * `message` A {String} message # * `options` (optional) An options {Object} with the following keys: - # * `detail` (optional) A {String} with additional details about the - # notification. + # * `buttons` (optional) An {Array} of {Object} where each {Object} has the + # following options: + # * `className` (optional) {String} a class name to add to the button's + # default class name (`btn btn-error`). + # * `onDidClick` (optional) {Function} callback to call when the button + # has been clicked. The context will be set to the + # {NotificationElement} instance. + # * `text` {String} inner text for the button + # * `description` (optional) A Markdown {String} containing a longer + # description about the notification. By default, this **will not** + # preserve newlines and whitespace when it is rendered. + # * `detail` (optional) A plain-text {String} containing additional details + # about the notification. By default, this **will** preserve newlines + # and whitespace when it is rendered. # * `dismissable` (optional) A {Boolean} indicating whether this # notification can be dismissed by the user. Defaults to `false`. # * `icon` (optional) A {String} name of an icon from Octicons to display # in the notification header. Defaults to `'bug'`. + # * `stack` (optional) A preformatted {String} with stack trace information + # describing the location of the error. addFatalError: (message, options) -> @addNotification(new Notification('fatal', message, options)) diff --git a/src/package.coffee b/src/package.coffee index 94e759947..94763f961 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -159,6 +159,7 @@ class Package # TODO: Remove. Settings view calls this method currently. activateConfig: -> + return if @configSchemaRegisteredOnLoad @requireMainModule() @registerConfigSchemaFromMainModule() @@ -426,8 +427,8 @@ class Package return @mainModule if @mainModuleRequired unless @isCompatible() console.warn """ - Failed to require the main module of '#{@name}' because it requires an incompatible native module. - Run `apm rebuild` in the package directory to resolve. + Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}). + Run `apm rebuild` in the package directory and restart Atom to resolve. """ return mainModulePath = @getMainModulePath() diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index adae7bc4f..2fda9a335 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -5,15 +5,32 @@ module.exports = (repoPath, paths = []) -> repo = Git.open(repoPath) upstream = {} + statuses = {} submodules = {} + branch = null if repo? + # Statuses in main repo + workingDirectoryPath = repo.getWorkingDirectory() + repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus()) + for filePath, status of repoStatus + statuses[filePath] = status + + # Statuses in submodules for submodulePath, submoduleRepo of repo.submodules submodules[submodulePath] = branch: submoduleRepo.getHead() upstream: submoduleRepo.getAheadBehindCount() + workingDirectoryPath = submoduleRepo.getWorkingDirectory() + for filePath, status of submoduleRepo.getStatus() + absolutePath = path.join(workingDirectoryPath, filePath) + # Make path relative to parent repository + relativePath = repo.relativize(absolutePath) + statuses[relativePath] = status + upstream = repo.getAheadBehindCount() + branch = repo.getHead() repo.release() - {upstream, submodules} + {statuses, upstream, branch, submodules} diff --git a/src/row-map.coffee b/src/row-map.coffee deleted file mode 100644 index 5510c1421..000000000 --- a/src/row-map.coffee +++ /dev/null @@ -1,120 +0,0 @@ -{spliceWithArray} = require 'underscore-plus' - -# Used by the display buffer to map screen rows to buffer rows and vice-versa. -# This mapping may not be 1:1 due to folds and soft-wraps. This object maintains -# an array of regions, which contain `bufferRows` and `screenRows` fields. -# -# Rectangular Regions: -# If a region has the same number of buffer rows and screen rows, it is referred -# to as "rectangular", and represents one or more non-soft-wrapped, non-folded -# lines. -# -# Trapezoidal Regions: -# If a region has one buffer row and more than one screen row, it represents a -# soft-wrapped line. If a region has one screen row and more than one buffer -# row, it represents folded lines -module.exports = -class RowMap - constructor: -> - @regions = [] - - # Public: Returns a copy of all the regions in the map - getRegions: -> - @regions.slice() - - # Public: Returns an end-row-exclusive range of screen rows corresponding to - # the given buffer row. If the buffer row is soft-wrapped, the range may span - # multiple screen rows. Otherwise it will span a single screen row. - screenRowRangeForBufferRow: (targetBufferRow) -> - {region, bufferRows, screenRows} = @traverseToBufferRow(targetBufferRow) - - if region? and region.bufferRows isnt region.screenRows - [screenRows, screenRows + region.screenRows] - else - screenRows += targetBufferRow - bufferRows - [screenRows, screenRows + 1] - - # Public: Returns an end-row-exclusive range of buffer rows corresponding to - # the given screen row. If the screen row is the first line of a folded range - # of buffer rows, the range may span multiple buffer rows. Otherwise it will - # span a single buffer row. - bufferRowRangeForScreenRow: (targetScreenRow) -> - {region, screenRows, bufferRows} = @traverseToScreenRow(targetScreenRow) - if region? and region.bufferRows isnt region.screenRows - [bufferRows, bufferRows + region.bufferRows] - else - bufferRows += targetScreenRow - screenRows - [bufferRows, bufferRows + 1] - - # Public: If the given buffer row is part of a folded row range, returns that - # row range. Otherwise returns a range spanning only the given buffer row. - bufferRowRangeForBufferRow: (targetBufferRow) -> - {region, bufferRows} = @traverseToBufferRow(targetBufferRow) - if region? and region.bufferRows isnt region.screenRows - [bufferRows, bufferRows + region.bufferRows] - else - [targetBufferRow, targetBufferRow + 1] - - # Public: Given a starting buffer row, the number of buffer rows to replace, - # and an array of regions of shape {bufferRows: n, screenRows: m}, splices - # the regions at the appropriate location in the map. This method is used by - # display buffer to keep the map updated when the underlying buffer changes. - spliceRegions: (startBufferRow, bufferRowCount, regions) -> - endBufferRow = startBufferRow + bufferRowCount - {index, bufferRows} = @traverseToBufferRow(startBufferRow) - precedingRows = startBufferRow - bufferRows - - count = 0 - while region = @regions[index + count] - count++ - bufferRows += region.bufferRows - if bufferRows >= endBufferRow - followingRows = bufferRows - endBufferRow - break - - if precedingRows > 0 - regions.unshift({bufferRows: precedingRows, screenRows: precedingRows}) - - if followingRows > 0 - regions.push({bufferRows: followingRows, screenRows: followingRows}) - - spliceWithArray(@regions, index, count, regions) - @mergeAdjacentRectangularRegions(index - 1, index + regions.length) - - traverseToBufferRow: (targetBufferRow) -> - bufferRows = 0 - screenRows = 0 - for region, index in @regions - if (bufferRows + region.bufferRows) > targetBufferRow - return {region, index, screenRows, bufferRows} - bufferRows += region.bufferRows - screenRows += region.screenRows - {index, screenRows, bufferRows} - - traverseToScreenRow: (targetScreenRow) -> - bufferRows = 0 - screenRows = 0 - for region, index in @regions - if (screenRows + region.screenRows) > targetScreenRow - return {region, index, screenRows, bufferRows} - bufferRows += region.bufferRows - screenRows += region.screenRows - {index, screenRows, bufferRows} - - mergeAdjacentRectangularRegions: (startIndex, endIndex) -> - for index in [endIndex..startIndex] - if 0 < index < @regions.length - leftRegion = @regions[index - 1] - rightRegion = @regions[index] - leftIsRectangular = leftRegion.bufferRows is leftRegion.screenRows - rightIsRectangular = rightRegion.bufferRows is rightRegion.screenRows - if leftIsRectangular and rightIsRectangular - @regions.splice index - 1, 2, - bufferRows: leftRegion.bufferRows + rightRegion.bufferRows - screenRows: leftRegion.screenRows + rightRegion.screenRows - return - - # Public: Returns an array of strings describing the map's regions. - inspect: -> - for {bufferRows, screenRows} in @regions - "#{bufferRows}:#{screenRows}" diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index e6fc0dc15..3949f18e5 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -365,7 +365,12 @@ class TextEditorComponent onTextInput: (event) => event.stopPropagation() - event.preventDefault() + + # WARNING: If we call preventDefault on the input of a space character, + # then the browser interprets the spacebar keypress as a page-down command, + # causing spaces to scroll elements containing editors. This is impossible + # to test. + event.preventDefault() if event.data isnt ' ' return unless @isInputEnabled() diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee index 780156e42..591943a48 100644 --- a/src/tokenized-buffer-iterator.coffee +++ b/src/tokenized-buffer-iterator.coffee @@ -18,24 +18,31 @@ class TokenizedBufferIterator @currentLineLength = currentLine.text.length @containingTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id) currentColumn = 0 + for tag, index in @currentTags if tag >= 0 - if currentColumn >= position.column and @isAtTagBoundary() + if currentColumn is position.column @tagIndex = index break else currentColumn += tag @containingTags.pop() while @closeTags.shift() - @containingTags.push(tag) while tag = @openTags.shift() - else - scopeName = @grammarRegistry.scopeForId(tag) - if tag % 2 is 0 - if @openTags.length > 0 + @containingTags.push(openTag) while openTag = @openTags.shift() + if currentColumn > position.column @tagIndex = index break - else - @closeTags.push(scopeName) - else + else + scopeName = @grammarRegistry.scopeForId(tag) + if tag % 2 is 0 # close tag + if @openTags.length > 0 + if currentColumn is position.column + @tagIndex = index + break + else + @containingTags.pop() while @closeTags.shift() + @containingTags.push(openTag) while openTag = @openTags.shift() + @closeTags.push(scopeName) + else # open tag @openTags.push(scopeName) @tagIndex ?= @currentTags.length diff --git a/src/window.coffee b/src/window.coffee deleted file mode 100644 index 9554218ca..000000000 --- a/src/window.coffee +++ /dev/null @@ -1,27 +0,0 @@ -# Public: Measure how long a function takes to run. -# -# description - A {String} description that will be logged to the console when -# the function completes. -# fn - A {Function} to measure the duration of. -# -# Returns the value returned by the given function. -window.measure = (description, fn) -> - start = Date.now() - value = fn() - result = Date.now() - start - console.log description, result - value - -# Public: Create a dev tools profile for a function. -# -# description - A {String} description that will be available in the Profiles -# tab of the dev tools. -# fn - A {Function} to profile. -# -# Returns the value returned by the given function. -window.profile = (description, fn) -> - measure description, -> - console.profile(description) - value = fn() - console.profileEnd(description) - value diff --git a/src/window.js b/src/window.js new file mode 100644 index 000000000..c4f28ba96 --- /dev/null +++ b/src/window.js @@ -0,0 +1,30 @@ +// Public: Measure how long a function takes to run. +// +// description - A {String} description that will be logged to the console when +// the function completes. +// fn - A {Function} to measure the duration of. +// +// Returns the value returned by the given function. +window.measure = function (description, fn) { + let start = Date.now() + let value = fn() + let result = Date.now() - start + console.log(description, result) + return value +} + +// Public: Create a dev tools profile for a function. +// +// description - A {String} description that will be available in the Profiles +// tab of the dev tools. +// fn - A {Function} to profile. +// +// Returns the value returned by the given function. +window.profile = function (description, fn) { + window.measure(description, function () { + console.profile(description) + let value = fn() + console.profileEnd(description) + return value + }) +} diff --git a/src/workspace.coffee b/src/workspace.coffee index bde2a62d3..8d0ff38fd 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1097,7 +1097,7 @@ class Workspace extends Model checkoutHead = => @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) .then (repository) -> - repository?.async.checkoutHeadForEditor(editor) + repository?.checkoutHeadForEditor(editor) if @config.get('editor.confirmCheckoutHeadRevision') @applicationDelegate.confirm diff --git a/static/variables/ui-variables.less b/static/variables/ui-variables.less index 010b0b8a9..8ef4d48e7 100644 --- a/static/variables/ui-variables.less +++ b/static/variables/ui-variables.less @@ -82,3 +82,4 @@ // Other @font-family: 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif; +@use-custom-controls: true; // false uses native controls