diff --git a/apm/package.json b/apm/package.json index 6623876f9..4ddb4dabe 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.9.2" + "atom-package-manager": "1.9.3" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index f8ee607e5..16e0ed5d6 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -57,7 +57,7 @@ module.exports = (grunt) -> homeDir = process.env.USERPROFILE contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') - installDir ?= path.join(process.env.ProgramFiles, appName) + installDir ?= path.join(process.env.LOCALAPPDATA, appName, 'app-dev') killCommand = 'taskkill /F /IM atom.exe' else if process.platform is 'darwin' homeDir = process.env.HOME @@ -298,6 +298,7 @@ module.exports = (grunt) -> unless process.platform is 'linux' or grunt.option('no-install') defaultTasks.push 'install' grunt.registerTask('default', defaultTasks) + grunt.registerTask('build-and-sign', ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar', 'codesign:app', 'install']) getDefaultChannelAndReleaseBranch = (version) -> if version.match(/dev/) or isBuildingPR() diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index 9164f8dab..544b09753 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -54,9 +54,9 @@ module.exports = (grunt) -> # so that it doesn't becomes larger than it needs to be. ignoredPaths = [ path.join('git-utils', 'deps') - path.join('nodegit', 'vendor') - path.join('nodegit', 'node_modules', 'node-pre-gyp') - path.join('nodegit', 'node_modules', '.bin') + path.join('ohnogit', 'node_modules', 'nodegit', 'vendor') + path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', 'node-pre-gyp') + path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', '.bin') path.join('oniguruma', 'deps') path.join('less', 'dist') path.join('bootstrap', 'docs') @@ -122,9 +122,9 @@ module.exports = (grunt) -> # Ignore *.cc and *.h files from native modules ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'src') + path.sep)}.*\\.(cc|h)?" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'generate') + path.sep)}.*\\.(cc|h)?" - ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'include') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'src') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'generate') + path.sep)}.*\\.(cc|h)?" + ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'include') + path.sep)}.*\\.(cc|h)?" ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*" ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*" diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index 2d9054385..19fd3d383 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -16,10 +16,22 @@ module.exports = (grunt) -> {description} = grunt.config.get('atom.metadata') if process.platform is 'win32' - runas ?= require 'runas' - copyFolder = path.resolve 'script', 'copy-folder.cmd' - if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0 - grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") + done = @async() + fs.access(installDir, fs.W_OK, (err) -> + adminRequired = true if err + if adminRequired + grunt.log.ok("User does not have write access to #{installDir}, elevating to admin") + runas ?= require 'runas' + copyFolder = path.resolve 'script', 'copy-folder.cmd' + + if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: adminRequired) isnt 0 + grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") + else + grunt.log.ok("Installed into #{installDir}") + + done() + ) + else if process.platform is 'darwin' rm installDir mkdir path.dirname(installDir) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 3ec28f139..d0e101ba0 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -34,7 +34,7 @@ git clone https://github.com/atom/atom/ cd atom script/build ``` -This will create the Atom application in the `out\Atom` folder as well as copy it to a folder named `Atom` within `Program Files`. +This will create the Atom application in the `out\Atom` folder as well as copy it to a subfolder of your user profile (e.g. `c:\Users\Bob`) called `AppData\Local\atom\app-dev`. ### `script/build` Options * `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional): diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index 10ad345d4..fd7c4f96e 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -18,15 +18,15 @@ # 'ctrl-p': 'core:move-down' # # You can find more information about keymaps in these guides: -# * https://atom.io/docs/latest/using-atom-basic-customization#customizing-key-bindings -# * https://atom.io/docs/latest/behind-atom-keymaps-in-depth +# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings +# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ # # If you're having trouble with your keybindings not working, try the # Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the # Debugging Guide for more information: -# * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings +# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings # # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: -# https://atom.io/docs/latest/using-atom-basic-customization#cson +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson diff --git a/dot-atom/snippets.cson b/dot-atom/snippets.cson index eb8f1b22a..cd66bba04 100644 --- a/dot-atom/snippets.cson +++ b/dot-atom/snippets.cson @@ -18,4 +18,4 @@ # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: -# https://atom.io/docs/latest/using-atom-basic-customization#cson +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 7d67e2ce5..1f78739a9 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -27,6 +27,7 @@ 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' 'ctrl-S': 'core:save-as' + 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' 'ctrl-y': 'core:redo' diff --git a/menus/darwin.cson b/menus/darwin.cson index 53cc4cbc4..bb3ce0acf 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -228,10 +228,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/linux.cson b/menus/linux.cson index be11c1430..b84fc8053 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -204,10 +204,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/win32.cson b/menus/win32.cson index 738b52f00..323db5d18 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -207,10 +207,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/package.json b/package.json index 1d0d5f318..5f1cd3735 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,9 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "nodegit": "0.12.2", "normalize-package-data": "^2.0.0", "nslog": "^3", + "ohnogit": "0.0.11", "oniguruma": "^5", "pathwatcher": "~6.2", "property-accessors": "^1.1.3", @@ -54,7 +54,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "8.4.6", + "text-buffer": "8.5.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -72,12 +72,12 @@ "one-light-syntax": "1.2.0", "solarized-dark-syntax": "1.0.2", "solarized-light-syntax": "1.0.2", - "about": "1.5.0", + "about": "1.5.2", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.29.2", + "autocomplete-plus": "2.30.0", "autocomplete-snippets": "1.10.0", "autoflow": "0.27.0", "autosave": "0.23.1", @@ -89,7 +89,7 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.38.1", - "fuzzy-finder": "1.0.4", + "fuzzy-finder": "1.0.5", "git-diff": "1.0.1", "find-and-replace": "0.198.0", "go-to-line": "0.30.0", @@ -101,37 +101,37 @@ "link": "0.31.1", "markdown-preview": "0.158.0", "metrics": "0.53.1", - "notifications": "0.63.1", + "notifications": "0.63.2", "open-on-github": "1.1.0", "package-generator": "1.0.0", "settings-view": "0.235.1", "snippets": "1.0.2", "spell-check": "0.67.1", - "status-bar": "1.2.3", + "status-bar": "1.2.6", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.92.1", + "tabs": "0.93.1", "timecop": "0.33.1", - "tree-view": "0.206.0", + "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", "whitespace": "0.32.2", "wrap-guide": "0.38.1", - "language-c": "0.51.3", + "language-c": "0.51.4", "language-clojure": "0.20.0", - "language-coffee-script": "0.46.1", + "language-coffee-script": "0.47.0", "language-csharp": "0.12.1", "language-css": "0.36.1", - "language-gfm": "0.85.0", - "language-git": "0.12.1", + "language-gfm": "0.86.0", + "language-git": "0.13.0", "language-go": "0.42.0", "language-html": "0.44.1", "language-hyperlink": "0.16.0", - "language-java": "0.17.0", + "language-java": "0.18.0", "language-javascript": "0.110.0", "language-json": "0.18.0", "language-less": "0.29.3", - "language-make": "0.21.1", + "language-make": "0.22.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.1", "language-perl": "0.34.0", @@ -140,15 +140,15 @@ "language-python": "0.43.1", "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", - "language-sass": "0.46.0", - "language-shellscript": "0.21.1", + "language-sass": "0.49.0", + "language-shellscript": "0.22.0", "language-source": "0.9.0", - "language-sql": "0.20.0", + "language-sql": "0.21.0", "language-text": "0.7.1", "language-todo": "0.27.0", "language-toml": "0.18.0", - "language-xml": "0.34.4", - "language-yaml": "0.25.2" + "language-xml": "0.34.5", + "language-yaml": "0.26.0" }, "private": true, "scripts": { diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index a1af5cd53..73c4ddb01 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -2,6 +2,7 @@ SET EXPECT_OUTPUT= SET WAIT= +SET PSARGS=%* FOR %%a IN (%*) DO ( IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES @@ -25,7 +26,8 @@ FOR %%a IN (%*) DO ( IF "%EXPECT_OUTPUT%"=="YES" ( SET ELECTRON_ENABLE_LOGGING=YES IF "%WAIT%"=="YES" ( - powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event + powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event" + exit 0 ) ELSE ( "%~dp0\..\..\atom.exe" %* ) diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 7380bf122..cd90ff8fb 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -1,2 +1,5 @@ #!/bin/sh -$(dirname "$0")/atom.cmd "$@" +pushd $(dirname "$0") > /dev/null +ATOMCMD=""$(pwd -W)"/atom.cmd" +popd > /dev/null +cmd.exe //c "$ATOMCMD" "$@" diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 5fd4b11f1..846083b0e 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -28,8 +28,10 @@ describe "AtomEnvironment", -> atom.setSize(originalSize.width, originalSize.height) it 'sets the size of the window, and can retrieve the size just set', -> - atom.setSize(100, 400) - expect(atom.getSize()).toEqual width: 100, height: 400 + newWidth = originalSize.width + 12 + newHeight = originalSize.height + 23 + atom.setSize(newWidth, newHeight) + expect(atom.getSize()).toEqual width: newWidth, height: newHeight describe ".isReleasedVersion()", -> it "returns false if the version is a SHA and true otherwise", -> diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config index ab57cc5f1..323ba7d9b 100644 --- a/spec/fixtures/git/repo-with-submodules/git.git/config +++ b/spec/fixtures/git/repo-with-submodules/git.git/config @@ -5,6 +5,12 @@ logallrefupdates = true ignorecase = true precomposeunicode = true +[branch "master"] + remote = origin + merge = refs/heads/master +[remote "origin"] + url = git@github.com:atom/some-repo-i-guess.git + fetch = +refs/heads/*:refs/remotes/origin/* [submodule "jstips"] url = https://github.com/loverajoel/jstips [submodule "You-Dont-Need-jQuery"] diff --git a/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master new file mode 100644 index 000000000..3507a23dc --- /dev/null +++ b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master @@ -0,0 +1 @@ +d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 0442248e1..fcb528819 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -3,7 +3,6 @@ import fs from 'fs-plus' import path from 'path' import temp from 'temp' -import Git from 'nodegit' import {it, beforeEach, afterEach} from './async-spec-helpers' @@ -47,7 +46,7 @@ describe('GitRepositoryAsync', () => { let threw = false try { - await repo.repoPromise + await repo.getRepo() } catch (e) { threw = true } @@ -56,6 +55,14 @@ describe('GitRepositoryAsync', () => { }) }) + 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() @@ -64,19 +71,19 @@ describe('GitRepositoryAsync', () => { }) it('returns the repository when not given a path', async () => { - const nodeGitRepo1 = await repo.repoPromise + 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.repoPromise + 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.repoPromise + const nodeGitRepo1 = await repo.getRepo() const nodeGitRepo2 = await repo.getRepo('jstips') expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir()) @@ -103,7 +110,7 @@ describe('GitRepositoryAsync', () => { it('returns the repository path for a repository path', async () => { repo = openFixture('master.git') const repoPath = await repo.getPath() - expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git')) }) }) @@ -303,7 +310,7 @@ describe('GitRepositoryAsync', () => { await repo.getPathStatus(filePath) expect(statusHandler.callCount).toBe(1) - const status = Git.Status.STATUS.WT_MODIFIED + const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) fs.writeFileSync(filePath, 'abc') @@ -878,4 +885,34 @@ describe('GitRepositoryAsync', () => { }) }) }) + + 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 3afd4da75..82e371146 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -33,7 +33,7 @@ describe "GitRepository", -> waitsForPromise -> repo.async.getPath().then(onSuccess) runs -> - expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath) + expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath) describe "new GitRepository(path)", -> it "throws an exception when no repository is found", -> @@ -289,6 +289,16 @@ describe "GitRepository", -> 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') + repo.getPathStatus('file.txt') + + fs.writeFileSync(modifiedPath, originalContent) + waitsForPromise -> repo.refreshStatus() + runs -> + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() + describe "buffer events", -> [editor] = [] diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 499efd017..953cf103e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -526,7 +526,7 @@ describe "Project", -> expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true expect(atom.project.relativizePath(inputPath)).toEqual [ atom.project.getPaths()[1], - 'somewhere/something.txt' + path.join('somewhere', 'something.txt') ] describe ".contains(path)", -> diff --git a/spec/resource-pool-spec.js b/spec/resource-pool-spec.js deleted file mode 100644 index 27893360a..000000000 --- a/spec/resource-pool-spec.js +++ /dev/null @@ -1,66 +0,0 @@ -/** @babel */ - -import ResourcePool from '../src/resource-pool' - -import {it} from './async-spec-helpers' - -describe('ResourcePool', () => { - let queue - - beforeEach(() => { - queue = new ResourcePool([{}]) - }) - - describe('.enqueue', () => { - it('calls the enqueued function', async () => { - let called = false - await queue.enqueue(() => { - called = true - return Promise.resolve() - }) - expect(called).toBe(true) - }) - - it('forwards values from the inner promise', async () => { - const result = await queue.enqueue(() => Promise.resolve(42)) - expect(result).toBe(42) - }) - - it('forwards errors from the inner promise', async () => { - let threw = false - try { - await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) - } catch (e) { - threw = true - } - expect(threw).toBe(true) - }) - - it('continues to dequeue work after a promise has been rejected', async () => { - try { - await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) - } catch (e) {} - - const result = await queue.enqueue(() => Promise.resolve(42)) - expect(result).toBe(42) - }) - - it('queues up work', async () => { - let resolve = null - queue.enqueue(() => { - return new Promise((resolve_, reject) => { - resolve = resolve_ - }) - }) - - expect(queue.getQueueDepth()).toBe(0) - - queue.enqueue(() => new Promise((resolve, reject) => {})) - - expect(queue.getQueueDepth()).toBe(1) - resolve() - - waitsFor(() => queue.getQueueDepth() === 0) - }) - }) -}) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index 319e2d438..18095d6f8 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -91,3 +91,13 @@ describe "Selection", -> expect(buffer.lineForRow(0)).toBe " " expect(buffer.lineForRow(1)).toBe " " expect(buffer.lineForRow(2)).toBe "" + + it "auto-indents if only a newline is inserted", -> + selection.setBufferRange [[2, 0], [3, 0]] + selection.insertText("\n", autoIndent: true) + expect(buffer.lineForRow(2)).toBe " " + + it "auto-indents if only a carriage return + newline is inserted", -> + selection.setBufferRange [[2, 0], [3, 0]] + selection.insertText("\r\n", autoIndent: true) + expect(buffer.lineForRow(2)).toBe " " diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 1194f2f76..9c4e09da0 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -172,8 +172,8 @@ jasmine.useRealClock = -> addCustomMatchers = (spec) -> spec.addMatchers toBeInstanceOf: (expected) -> - notText = if @isNot then " not" else "" - this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class" + beOrNotBe = if @isNot then "not be" else "be" + this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class" @actual instanceof expected toHaveLength: (expected) -> @@ -181,32 +181,38 @@ addCustomMatchers = (spec) -> this.message = => "Expected object #{@actual} has no length method" false else - notText = if @isNot then " not" else "" - this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}" + haveOrNotHave = if @isNot then "not have" else "have" + this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}" @actual.length is expected toExistOnDisk: (expected) -> - notText = this.isNot and " not" or "" - @message = -> return "Expected path '" + @actual + "'" + notText + " to exist." + toOrNotTo = this.isNot and "not to" or "to" + @message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist." fs.existsSync(@actual) toHaveFocus: -> - notText = this.isNot and " not" or "" + toOrNotTo = this.isNot and "not to" or "to" if not document.hasFocus() console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner." - @message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus." + @message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus." element = @actual element = element.get(0) if element.jquery element is document.activeElement or element.contains(document.activeElement) toShow: -> - notText = if @isNot then " not" else "" + toOrNotTo = this.isNot and "not to" or "to" element = @actual element = element.get(0) if element.jquery - @message = -> return "Expected element '#{element}' or its descendants#{notText} to show." + @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show." element.style.display in ['block', 'inline-block', 'static', 'fixed'] + toEqualPath: (expected) -> + actualPath = path.normalize(@actual) + expectedPath = path.normalize(expected) + @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'." + actualPath is expectedPath + window.waitsForPromise = (args...) -> label = null if args.length > 1 diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index b1f457ddd..96122e073 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4022,15 +4022,33 @@ describe('TextEditorComponent', function () { }) }) - describe('when changing the font', async function () { - it('measures the default char, the korean char, the double width char and the half width char widths', async function () { - expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) + describe('when decreasing the fontSize', async function () { + it('decreases the widths of the korean char, the double width char and the half width char', async function () { + originalDefaultCharWidth = editor.getDefaultCharWidth() + koreanDefaultCharWidth = editor.getKoreanCharWidth() + doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() + halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() component.setFontSize(10) await nextViewUpdatePromise() - expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) - expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) - expect(editor.getDoubleWidthCharWidth()).toBe(10) - expect(editor.getHalfWidthCharWidth()).toBe(5) + expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth) + expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth) + expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth) + expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth) + }) + }) + + describe('when increasing the fontSize', function() { + it('increases the widths of the korean char, the double width char and the half width char', async function () { + originalDefaultCharWidth = editor.getDefaultCharWidth() + koreanDefaultCharWidth = editor.getKoreanCharWidth() + doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth() + halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth() + component.setFontSize(25) + await nextViewUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth) + expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth) + expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth) + expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth) }) }) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 4de7168b7..e5e58a5cc 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -182,17 +182,19 @@ describe "TextEditor", -> expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - it "returns '' when opened files have identical file and dir names", -> + it "returns '' when opened files have identical file names in subdirectories", -> editor1 = null editor2 = null + path1 = path.join('sample-theme-1', 'src', 'js') + path2 = path.join('sample-theme-2', 'src', 'js') waitsForPromise -> - atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) -> + atom.workspace.open(path.join(path1, 'main.js')).then (o) -> editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> + atom.workspace.open(path.join(path2, 'main.js')).then (o) -> editor2 = o runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js" + expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}" + expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}" it "returns '' when opened files have identical file and same parent dir name", -> editor1 = null @@ -204,7 +206,7 @@ describe "TextEditor", -> editor2 = o runs -> expect(editor1.getLongTitle()).toBe "main.js \u2014 js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin" + expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin') it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> observed = [] @@ -1187,14 +1189,10 @@ describe "TextEditor", -> cursor2 = editor.addCursorAtBufferPosition([1, 4]) expect(cursor2.marker).toBe cursor1.marker - describe '.logCursorScope()', -> - beforeEach -> - spyOn(atom.notifications, 'addInfo') - - it 'opens a notification', -> - editor.logCursorScope() - - expect(atom.notifications.addInfo).toHaveBeenCalled() + describe '.getCursorScope()', -> + it 'returns the current scope', -> + descriptor = editor.getCursorScope() + expect(descriptor.scopes).toContain('source.js') describe "selection", -> selection = null @@ -5729,28 +5727,6 @@ describe "TextEditor", -> expect(handler).toHaveBeenCalledWith 'OK' expect(editor.getPlaceholderText()).toBe 'OK' - describe ".checkoutHeadRevision()", -> - it "reverts to the version of its file checked into the project repository", -> - atom.config.set("editor.confirmCheckoutHeadRevision", false) - - editor.setCursorBufferPosition([0, 0]) - editor.insertText("---\n") - expect(editor.lineTextForBufferRow(0)).toBe "---" - - waitsForPromise -> - editor.checkoutHeadRevision() - - runs -> - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () {" - - describe "when there's no repository for the editor's file", -> - it "doesn't do anything", -> - editor = atom.workspace.buildTextEditor() - editor.setText("stuff") - editor.checkoutHeadRevision() - - waitsForPromise -> editor.checkoutHeadRevision() - describe 'gutters', -> describe 'the TextEditor constructor', -> it 'creates a line-number gutter', -> diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index ea0ca19e6..47b848809 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -175,7 +175,7 @@ describe "atom.themes", -> expect(styleElementAddedHandler).toHaveBeenCalled() element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(cssPath) + expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath) expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') # doesn't append twice @@ -194,7 +194,7 @@ describe "atom.themes", -> expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(lessPath) + expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath) expect(element.textContent).toBe """ #header { color: #4d926f; @@ -213,9 +213,9 @@ describe "atom.themes", -> it "supports requiring css and less stylesheets without an explicit extension", -> atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) document.querySelector('head style[source-path*="css.css"]').remove() document.querySelector('head style[source-path*="sample.less"]').remove() diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 88c095f68..7a1f4d221 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -445,13 +445,12 @@ describe "TokenizedBuffer", -> expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}" {tokens} = screenLine0 - expect(tokens.length).toBe 4 + expect(tokens.length).toBe 3 expect(tokens[0].value).toBe "#" expect(tokens[1].value).toBe " Econ 101" expect(tokens[2].value).toBe tabAsSpaces expect(tokens[2].scopes).toEqual tokens[1].scopes expect(tokens[2].isAtomic).toBeTruthy() - expect(tokens[3].value).toBe "" expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand" diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 97139f6bb..6fa8001aa 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -80,7 +80,8 @@ describe "Workspace", -> expect(untitledEditor.getText()).toBe("An untitled editor.") expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}/// describe "where there are no open panes or editors", -> it "constructs the view with no open editors", -> @@ -833,25 +834,29 @@ describe "Workspace", -> describe "when there is an active pane item", -> it "sets the title to the pane item's title plus the project path", -> item = atom.workspace.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the active pane's item changes", -> it "updates the title to the new item's title plus the project path", -> atom.workspace.getActivePane().activateNextItem() item = atom.workspace.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the last pane item is removed", -> it "updates the title to contain the project's path", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{pathEscaped}/// describe "when an inactive pane's item changes", -> it "does not update the title", -> @@ -875,7 +880,8 @@ describe "Workspace", -> }) workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) item = workspace2.getActivePaneItem() - expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// + pathEscaped = escapeStringRegex(atom.project.getPaths()[0]) + expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}/// workspace2.destroy() describe "document edited status", -> @@ -1610,3 +1616,48 @@ describe "Workspace", -> runs -> expect(pane.getPendingItem()).toBeFalsy() + + describe "grammar activation", -> + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + it "notifies the workspace of which grammar is used", -> + editor = null + + grammarUsed = jasmine.createSpy() + atom.workspace.handleGrammarUsed = grammarUsed + + waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o + waitsFor -> grammarUsed.callCount is 1 + runs -> + expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript' + + describe ".checkoutHeadRevision()", -> + editor = null + beforeEach -> + atom.config.set("editor.confirmCheckoutHeadRevision", false) + + waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o + + it "reverts to the version of its file checked into the project repository", -> + editor.setCursorBufferPosition([0, 0]) + editor.insertText("---\n") + expect(editor.lineTextForBufferRow(0)).toBe "---" + + waitsForPromise -> + atom.workspace.checkoutHeadRevision(editor) + + runs -> + expect(editor.lineTextForBufferRow(0)).toBe "" + + describe "when there's no repository for the editor's file", -> + it "doesn't do anything", -> + editor = atom.workspace.buildTextEditor() + editor.setText("stuff") + atom.workspace.checkoutHeadRevision(editor) + + waitsForPromise -> atom.workspace.checkoutHeadRevision(editor) + + escapeStringRegex = (str) -> + str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index ffff564ba..f50ae9d5b 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -255,7 +255,7 @@ class AtomEnvironment extends Model @deserializers.add(TextBuffer) registerDefaultCommands: -> - registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller}) + registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard}) registerDefaultViewProviders: -> @views.addViewProvider Workspace, (model, env) -> diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 69eff1845..d08990264 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -138,7 +138,11 @@ class AtomApplication return unless @socketPath? @deleteSocketFile() server = net.createServer (connection) => - connection.on 'data', (data) => + data = '' + connection.on 'data', (chunk) -> + data = data + chunk + + connection.on 'end', => options = JSON.parse(data) @openWithOptions(options) @@ -170,9 +174,6 @@ class AtomApplication @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(getLoadSettings()) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() - @on 'application:open', -> @promptForPathToOpen('all', getLoadSettings()) - @on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings()) - @on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings()) @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) @on 'application:inspect', ({x, y, atomWindow}) -> @@ -255,6 +256,14 @@ class AtomApplication ipcMain.on 'command', (event, command) => @emit(command) + ipcMain.on 'open-command', (event, command, args...) => + defaultPath = args[0] if args.length > 0 + switch command + when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath) + when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath) + when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath) + else console.log "Invalid open-command received: " + command + ipcMain.on 'window-command', (event, command, args...) -> win = BrowserWindow.fromWebContents(event.sender) win.emit(command, args...) @@ -653,11 +662,13 @@ class AtomApplication # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. # :window - An {AtomWindow} to use for opening a selected file path. - promptForPathToOpen: (type, {devMode, safeMode, window}) -> - @promptForPath type, (pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode, window}) + # :path - An optional String which controls the default path to which the + # file dialog opens. + promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) -> + @promptForPath type, ((pathsToOpen) => + @openPaths({pathsToOpen, devMode, safeMode, window})), path - promptForPath: (type, callback) -> + promptForPath: (type, callback, path) -> properties = switch type when 'file' then ['openFile'] @@ -680,8 +691,8 @@ class AtomApplication when 'folder' then 'Open Folder' else 'Open' - if process.platform is 'linux' - if projectPath = @lastFocusedWindow?.projectPath - openOptions.defaultPath = projectPath + # File dialog defaults to project directory of currently active editor + if path? + openOptions.defaultPath = path dialog.showOpenDialog(parentWindow, openOptions, callback) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index d01ad03c9..109b791a1 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -35,7 +35,6 @@ class DisplayBuffer extends Model state.config = atomEnvironment.config state.assert = atomEnvironment.assert state.grammarRegistry = atomEnvironment.grammars - state.packageManager = atomEnvironment.packages new this(state) constructor: (params={}) -> @@ -43,7 +42,7 @@ class DisplayBuffer extends Model { tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer, - ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry } = params @emitter = new Emitter @@ -51,7 +50,7 @@ class DisplayBuffer extends Model @tokenizedBuffer ?= new TokenizedBuffer({ tabLength, buffer, ignoreInvisibles, @largeFileMode, @config, - @grammarRegistry, @packageManager, @assert + @grammarRegistry, @assert }) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @@ -122,7 +121,7 @@ class DisplayBuffer extends Model foldsMarkerLayer = @foldsMarkerLayer.copy() new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager, foldsMarkerLayer + @grammarRegistry, foldsMarkerLayer }) updateAllScreenLines: -> diff --git a/src/environment-helpers.js b/src/environment-helpers.js index e2baeb26b..2d1bd5b60 100644 --- a/src/environment-helpers.js +++ b/src/environment-helpers.js @@ -58,7 +58,7 @@ function getFromShell () { function needsPatching (options = { platform: process.platform, env: process.env }) { if (options.platform === 'darwin' && !options.env.PWD) { let shell = getUserShell() - if (shell.endsWith('csh') || shell.endsWith('tcsh')) { + if (shell.endsWith('csh') || shell.endsWith('tcsh') || shell.endsWith('fish')) { return false } return true @@ -67,9 +67,20 @@ function needsPatching (options = { platform: process.platform, env: process.env return false } +// Fix for #11302 because `process.env` on Windows is a magic object that offers case-insensitive +// environment variable matching. By always cloning to `process.env` we prevent breaking the +// underlying functionality. +function clone (to, from) { + for (var key in to) { + delete to[key] + } + + Object.assign(to, from) +} + function normalize (options = {}) { if (options && options.env) { - process.env = options.env + clone(process.env, options.env) } if (!options.env) { @@ -85,8 +96,8 @@ function normalize (options = {}) { // in #4126. Retain the original in case someone needs it. let shellEnv = getFromShell() if (shellEnv && shellEnv.PATH) { - process._originalEnv = process.env - process.env = shellEnv + process._originalEnv = Object.assign({}, process.env) + clone(process.env, shellEnv) } } } @@ -96,7 +107,7 @@ function replace (env) { return } - process.env = env + clone(process.env, env) } export default { getFromShell, needsPatching, normalize, replace } diff --git a/src/git-repository-async.js b/src/git-repository-async.js index aacd482f7..66b73ba77 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -1,20 +1,7 @@ 'use babel' -import fs from 'fs-plus' -import path from 'path' -import Git from 'nodegit' -import ResourcePool from './resource-pool' -import {Emitter, CompositeDisposable, Disposable} from 'event-kit' - -const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE -const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW -const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED -const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE -const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants -const submoduleMode = 57344 // TODO: compose this from libgit2 constants - -// Just using this for _.isEqual and _.object, we should impl our own here -import _ from 'underscore-plus' +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: @@ -29,39 +16,19 @@ export default class GitRepositoryAsync { } static get Git () { - return Git + return Repository.Git } // The name of the error thrown when an action is attempted on a destroyed // repository. static get DestroyedErrorName () { - return 'GitRepositoryAsync.destroyed' + return Repository.DestroyedErrorName } constructor (_path, options = {}) { - // We'll serialize our access manually. - Git.setThreadSafetyStatus(Git.THREAD_SAFETY.DISABLED) + this.repo = Repository.open(_path, options) - this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() - this.pathStatusCache = {} - this.path = null - - // NB: These needs to happen before the following .openRepository call. - this.openedPath = _path - this._openExactPath = options.openExactPath || false - - this.repoPromise = this.openRepository() - // NB: We don't currently _use_ the pooled object. But by giving it one - // thing, we're really just serializing all the work. Down the road, we - // could open multiple connections to the repository. - this.repoPool = new ResourcePool([this.repoPromise]) - - this.isCaseInsensitive = fs.isCaseInsensitive() - this.upstream = {} - this.submodules = {} - - this._refreshingPromise = Promise.resolve() let {refreshOnWindowFocus = true} = options if (refreshOnWindowFocus) { @@ -78,23 +45,26 @@ export default class GitRepositoryAsync { } } + // 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 () { - if (this.emitter) { - this.emitter.emit('did-destroy') - this.emitter.dispose() - this.emitter = null - } + this.repo.destroy() if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null } - - this.repoPromise = null } // Event subscription @@ -107,7 +77,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy (callback) { - return this.emitter.on('did-destroy', callback) + return this.repo.onDidDestroy(callback) } // Public: Invoke the given callback when a specific file's status has @@ -122,7 +92,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatus (callback) { - return this.emitter.on('did-change-status', callback) + return this.repo.onDidChangeStatus(callback) } // Public: Invoke the given callback when a multiple files' statuses have @@ -134,7 +104,7 @@ export default class GitRepositoryAsync { // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses (callback) { - return this.emitter.on('did-change-statuses', callback) + return this.repo.onDidChangeStatuses(callback) } // Repository details @@ -151,25 +121,13 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to the {String} path of the // repository. getPath () { - return this.getRepo().then(repo => { - if (!this.path) { - this.path = repo.path().replace(/\/$/, '') - } - - return this.path - }) + return this.repo.getPath() } // Public: Returns a {Promise} which resolves to the {String} working // directory path of the repository. getWorkingDirectory (_path) { - return this.getRepo(_path).then(repo => { - if (!repo.cachedWorkdir) { - repo.cachedWorkdir = repo.workdir() - } - - return repo.cachedWorkdir - }) + return this.repo.getWorkingDirectory() } // Public: Returns a {Promise} that resolves to true if at the root, false if @@ -191,8 +149,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the relative {String} path. relativizeToWorkingDirectory (_path) { - return this.getWorkingDirectory() - .then(wd => this.relativize(_path, wd)) + return this.repo.relativizeToWorkingDirectory(_path) } // Public: Makes a path relative to the repository's working directory. @@ -202,78 +159,13 @@ export default class GitRepositoryAsync { // // Returns the relative {String} path. relativize (_path, workingDirectory) { - // The original implementation also handled null workingDirectory as it - // pulled it from a sync function that could return null. We require it - // to be passed here. - let openedWorkingDirectory - if (!_path || !workingDirectory) { - return _path - } - - // If the opened directory and the workdir differ, this is a symlinked repo - // root, so we have to do all the checks below twice--once against the realpath - // and one against the opened path - const opened = this.openedPath.replace(/\/\.git$/, '') - if (path.relative(opened, workingDirectory) !== '') { - openedWorkingDirectory = opened - } - - if (process.platform === 'win32') { - _path = _path.replace(/\\/g, '/') - } else { - if (_path[0] !== '/') { - return _path - } - } - - workingDirectory = workingDirectory.replace(/\/$/, '') - - // Depending on where the paths come from, they may have a '/private/' - // prefix. Standardize by stripping that out. - _path = _path.replace(/^\/private\//i, '/') - workingDirectory = workingDirectory.replace(/^\/private\//i, '/') - - const originalPath = _path - const originalWorkingDirectory = workingDirectory - if (this.isCaseInsensitive) { - _path = _path.toLowerCase() - workingDirectory = workingDirectory.toLowerCase() - } - - if (_path.indexOf(workingDirectory) === 0) { - return originalPath.substring(originalWorkingDirectory.length + 1) - } else if (_path === workingDirectory) { - return '' - } - - if (openedWorkingDirectory) { - openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '') - openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/') - - const originalOpenedWorkingDirectory = openedWorkingDirectory - if (this.isCaseInsensitive) { - openedWorkingDirectory = openedWorkingDirectory.toLowerCase() - } - - if (_path.indexOf(openedWorkingDirectory) === 0) { - return originalPath.substring(originalOpenedWorkingDirectory.length + 1) - } else if (_path === openedWorkingDirectory) { - return '' - } - } - - return _path + return this.repo.relativize(_path, workingDirectory) } // Public: Returns a {Promise} which resolves to whether the given branch // exists. hasBranch (branch) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.getBranch(branch)) - .then(branch => branch != null) - .catch(_ => false) - }) + return this.repo.hasBranch(branch) } // Public: Retrieves a shortened version of the HEAD reference value. @@ -287,11 +179,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to a {String}. getShortHead (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => branch.shorthand()) - }) + return this.repo.getShortHead(_path) } // Public: Is the given path a submodule in the repository? @@ -301,19 +189,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.index()) - .then(index => { - const entry = index.getByPath(relativePath) - if (!entry) return false - - return entry.mode === submoduleMode - }) - }) - }) + return this.repo.isSubmodule(_path) } // Public: Returns the number of commits behind the current branch is from the @@ -327,18 +203,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getAheadBehindCount (reference, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getBranch(reference)])) - .then(([repo, local]) => { - const upstream = Git.Branch.upstream(local) - return Promise.all([repo, local, upstream]) - }) - .then(([repo, local, upstream]) => { - return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) - }) - .catch(_ => ({ahead: 0, behind: 0})) - }) + return this.repo.getAheadBehindCount(reference, _path) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -351,15 +216,7 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getCachedUpstreamAheadBehindCount (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._submoduleForPath(_path)) - .then(submodule => { - if (submodule) { - return submodule.getCachedUpstreamAheadBehindCount(_path) - } else { - return this.upstream - } - }) + return this.repo.getCachedUpstreamAheadBehindCount(_path) } // Public: Returns the git configuration value specified by the key. @@ -370,12 +227,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} git configuration value // specified by the key. getConfigValue (key, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.configSnapshot()) - .then(config => config.getStringBuf(key)) - .catch(_ => null) - }) + return this.repo.getConfigValue(key, _path) } // Public: Get the URL for the 'origin' remote. @@ -386,7 +238,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} origin url of the // repository. getOriginURL (_path) { - return this.getConfigValue('remote.origin.url', _path) + return this.repo.getOriginURL(_path) } // Public: Returns the upstream branch for the current HEAD, or null if there @@ -398,11 +250,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => Git.Branch.upstream(branch)) - }) + return this.repo.getUpstreamBranch(_path) } // Public: Gets all the local and remote references. @@ -415,25 +263,7 @@ export default class GitRepositoryAsync { // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) - .then(refs => { - const heads = [] - const remotes = [] - const tags = [] - for (const ref of refs) { - if (ref.isTag()) { - tags.push(ref.name()) - } else if (ref.isRemote()) { - remotes.push(ref.name()) - } else if (ref.isBranch()) { - heads.push(ref.name()) - } - } - return {heads, remotes, tags} - }) - }) + return this.repo.getReferences(_path) } // Public: Get the SHA for the given reference. @@ -445,11 +275,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the current {String} SHA for the // given reference. getReferenceTarget (reference, _path) { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Git.Reference.nameToId(repo, reference)) - .then(oid => oid.tostrS()) - }) + return this.repo.getReferenceTarget(reference, _path) } // Reading Status @@ -462,9 +288,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is modified. isPathModified (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._getStatus([relativePath])) - .then(statuses => statuses.some(status => status.isModified())) + return this.repo.isPathModified(_path) } // Public: Resolves true if the given path is new. @@ -474,9 +298,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is new. isPathNew (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this._getStatus([relativePath])) - .then(statuses => statuses.some(status => status.isNew())) + return this.repo.isPathNew(_path) } // Public: Is the given path ignored? @@ -486,17 +308,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return this.getWorkingDirectory() - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => { - const relativePath = this.relativize(_path, wd) - return Git.Ignore.pathIsIgnored(repo, relativePath) - }) - .then(ignored => Boolean(ignored)) - }) - }) + return this.repo.isPathIgnored(_path) } // Get the status of a directory in the repository's working directory. @@ -507,18 +319,7 @@ export default class GitRepositoryAsync { // value can be passed to {::isStatusModified} or {::isStatusNew} to get more // information. getDirectoryStatus (directoryPath) { - return this.relativizeToWorkingDirectory(directoryPath) - .then(relativePath => { - const pathspec = relativePath + '/**' - return this._getStatus([pathspec]) - }) - .then(statuses => { - return Promise.all(statuses.map(s => s.statusBit())).then(bits => { - return bits - .filter(b => b > 0) - .reduce((status, bit) => status | bit, 0) - }) - }) + return this.repo.getDirectoryStatus(directoryPath) } // Refresh the status bit for the given path. @@ -531,27 +332,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Number} which is the refreshed // status bit for the path. refreshStatusForPath (_path) { - let relativePath - return this.getWorkingDirectory() - .then(wd => { - relativePath = this.relativize(_path, wd) - return this._getStatus([relativePath]) - }) - .then(statuses => { - const cachedStatus = this.pathStatusCache[relativePath] || 0 - const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT - if (status !== cachedStatus) { - if (status === Git.Status.STATUS.CURRENT) { - delete this.pathStatusCache[relativePath] - } else { - this.pathStatusCache[relativePath] = status - } - - this.emitter.emit('did-change-status', {path: _path, pathStatus: status}) - } - - return status - }) + return this.repo.refreshStatusForPath(_path) } // Returns a Promise that resolves to the status bit of a given path if it has @@ -567,8 +348,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a status {Number} or null if the // path is not in the cache. getCachedPathStatus (_path) { - return this.relativizeToWorkingDirectory(_path) - .then(relativePath => this.pathStatusCache[relativePath]) + return this.repo.getCachedPathStatus(_path) } // Public: Get the cached statuses for the repository. @@ -576,7 +356,7 @@ export default class GitRepositoryAsync { // Returns an {Object} of {Number} statuses, keyed by {String} working // directory-relative file names. getCachedPathStatuses () { - return this.pathStatusCache + return this.repo.pathStatusCache } // Public: Returns true if the given status indicates modification. @@ -585,7 +365,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean} that's true if the `statusBit` indicates modification. isStatusModified (statusBit) { - return (statusBit & modifiedStatusFlags) > 0 + return this.repo.isStatusModified(statusBit) } // Public: Returns true if the given status indicates a new path. @@ -594,7 +374,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean} that's true if the `statusBit` indicates a new path. isStatusNew (statusBit) { - return (statusBit & newStatusFlags) > 0 + return this.repo.isStatusNew(statusBit) } // Public: Returns true if the given status indicates the path is staged. @@ -604,7 +384,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // staged. isStatusStaged (statusBit) { - return (statusBit & indexStatusFlags) > 0 + return this.repo.isStatusStaged(statusBit) } // Public: Returns true if the given status indicates the path is ignored. @@ -614,7 +394,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // ignored. isStatusIgnored (statusBit) { - return (statusBit & ignoredStatusFlags) > 0 + return this.repo.isStatusIgnored(statusBit) } // Public: Returns true if the given status indicates the path is deleted. @@ -624,7 +404,7 @@ export default class GitRepositoryAsync { // Returns a {Boolean} that's true if the `statusBit` indicates the path is // deleted. isStatusDeleted (statusBit) { - return (statusBit & deletedStatusFlags) > 0 + return this.repo.isStatusDeleted(statusBit) } // Retrieving Diffs @@ -640,40 +420,7 @@ export default class GitRepositoryAsync { // * `added` The {Number} of added lines. // * `deleted` The {Number} of deleted lines. getDiffStats (_path) { - return this.getWorkingDirectory(_path) - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) - .then(([repo, tree]) => { - const options = new Git.DiffOptions() - options.contextLines = 0 - options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH - options.pathspec = this.relativize(_path, wd) - if (process.platform === 'win32') { - // Ignore eol of line differences on windows so that files checked in - // as LF don't report every line modified when the text contains CRLF - // endings. - options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL - } - return Git.Diff.treeToWorkdir(repo, tree, options) - }) - .then(diff => this._getDiffLines(diff)) - .then(lines => { - const stats = {added: 0, deleted: 0} - for (const line of lines) { - const origin = line.origin() - if (origin === Git.Diff.LINE.ADDITION) { - stats.added++ - } else if (origin === Git.Diff.LINE.DELETION) { - stats.deleted++ - } - } - return stats - }) - }) - }) + return this.repo.getDiffStats(_path) } // Public: Retrieves the line diffs comparing the `HEAD` version of the given @@ -688,30 +435,7 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { - return this.getWorkingDirectory(_path) - .then(wd => { - let relativePath = null - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => { - relativePath = this.relativize(_path, wd) - return repo.getHeadCommit() - }) - .then(commit => commit.getEntry(relativePath)) - .then(entry => entry.getBlob()) - .then(blob => { - const options = new Git.DiffOptions() - options.contextLines = 0 - if (process.platform === 'win32') { - // Ignore eol of line differences on windows so that files checked in - // as LF don't report every line modified when the text contains CRLF - // endings. - options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL - } - return this._diffBlobToBuffer(blob, text, options) - }) - }) - }) + return this.repo.getLineDiffs(_path, text) } // Checking Out @@ -732,19 +456,7 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves or rejects depending on whether the // method was successful. checkoutHead (_path) { - return this.getWorkingDirectory(_path) - .then(wd => { - return this.repoPool.enqueue(() => { - return this.getRepo(_path) - .then(repo => { - const checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, wd)] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) - }) - }) - }) - .then(() => this.refreshStatusForPath(_path)) + return this.repo.checkoutHead(_path) } // Public: Checks out a branch in your repository. @@ -755,19 +467,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.checkoutBranch(reference)) - }) - .catch(error => { - if (create) { - return this._createBranch(reference) - .then(_ => this.checkoutReference(reference, false)) - } else { - throw error - } - }) - .then(_ => null) + return this.repo.checkoutReference(reference, create) } // Private @@ -786,107 +486,10 @@ export default class GitRepositoryAsync { return this.checkoutHead(filePath) } - // Create a new branch with the given name. + // Refreshes the git status. // - // * `name` The {String} name of the new branch. - // - // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the - // created branch. - _createBranch (name) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, commit]) => repo.createBranch(name, commit)) - }) - } - - // Get all the hunks in the diff. - // - // * `diff` The {NodeGit.Diff} whose hunks should be retrieved. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}. - _getDiffHunks (diff) { - return diff.patches() - .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array - .then(hunks => _.flatten(hunks)) // hunks :: Array> - } - - // Get all the lines contained in the diff. - // - // * `diff` The {NodeGit.Diff} use lines should be retrieved. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}. - _getDiffLines (diff) { - return this._getDiffHunks(diff) - .then(hunks => Promise.all(hunks.map(h => h.lines()))) - .then(lines => _.flatten(lines)) // lines :: Array> - } - - // Diff the given blob and buffer with the provided options. - // - // * `blob` The {NodeGit.Blob} - // * `buffer` The {String} buffer. - // * `options` The {NodeGit.DiffOptions} - // - // Returns a {Promise} which resolves to an {Array} of {Object}s which have - // the following keys: - // * `oldStart` The {Number} of the old starting line. - // * `newStart` The {Number} of the new starting line. - // * `oldLines` The {Number} of old lines. - // * `newLines` The {Number} of new lines. - _diffBlobToBuffer (blob, buffer, options) { - const hunks = [] - const hunkCallback = (delta, hunk, payload) => { - hunks.push({ - oldStart: hunk.oldStart(), - newStart: hunk.newStart(), - oldLines: hunk.oldLines(), - newLines: hunk.newLines() - }) - } - - return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null) - .then(_ => hunks) - } - - // Get the current branch and update this.branch. - // - // Returns a {Promise} which resolves to a {boolean} indicating whether the - // branch name changed. - _refreshBranch () { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => repo.getCurrentBranch()) - .then(ref => ref.name()) - .then(branchName => { - const changed = branchName !== this.branch - this.branch = branchName - return changed - }) - }) - } - - // Refresh the cached ahead/behind count with the given branch. - // - // * `branchName` The {String} name of the branch whose ahead/behind should be - // used for the refresh. - // - // Returns a {Promise} which will resolve to a {boolean} indicating whether - // the ahead/behind count changed. - _refreshAheadBehindCount (branchName) { - return this.getAheadBehindCount(branchName) - .then(counts => { - const changed = !_.isEqual(counts, this.upstream) - this.upstream = counts - return changed - }) - } - - // Get the status for this repository. - // - // Returns a {Promise} that will resolve to an object of {String} paths to the - // {Number} status. - _getRepositoryStatus () { + // Returns a {Promise} which will resolve to {null} when refresh is complete. + refreshStatus () { let projectPathsPromises = [Promise.resolve('')] if (this.project) { projectPathsPromises = this.project.getPaths() @@ -895,163 +498,7 @@ export default class GitRepositoryAsync { return Promise.all(projectPathsPromises) .then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*')) - .then(projectPaths => { - return this._getStatus(projectPaths.length > 0 ? projectPaths : null) - }) - .then(statuses => { - const statusPairs = statuses.map(status => [status.path(), status.statusBit()]) - return _.object(statusPairs) - }) - } - - // Get the status for the given submodule. - // - // * `submodule` The {GitRepositoryAsync} for the submodule. - // - // Returns a {Promise} which resolves to an {Object}, keyed by {String} - // repo-relative {Number} statuses. - async _getSubmoduleStatus (submodule) { - // At this point, we've called submodule._refreshSubmodules(), which would - // have refreshed the status on *its* submodules, etc. So we know that its - // cached path statuses are up-to-date. - // - // Now we just need to hoist those statuses into our repository by changing - // their paths to be relative to us. - - const statuses = submodule.getCachedPathStatuses() - const repoRelativeStatuses = {} - const submoduleRepo = await submodule.getRepo() - const submoduleWorkDir = submoduleRepo.workdir() - for (const relativePath in statuses) { - const statusBit = statuses[relativePath] - const absolutePath = path.join(submoduleWorkDir, relativePath) - const repoRelativePath = await this.relativizeToWorkingDirectory(absolutePath) - repoRelativeStatuses[repoRelativePath] = statusBit - } - - return repoRelativeStatuses - } - - // Refresh the list of submodules in the repository. - // - // Returns a {Promise} which resolves to an {Object} keyed by {String} - // submodule names with {GitRepositoryAsync} values. - async _refreshSubmodules () { - const repo = await this.getRepo() - const wd = await this.getWorkingDirectory() - const submoduleNames = await repo.getSubmoduleNames() - for (const name of submoduleNames) { - const alreadyExists = Boolean(this.submodules[name]) - if (alreadyExists) continue - - const submodule = await Git.Submodule.lookup(repo, name) - const absolutePath = path.join(wd, submodule.path()) - const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false}) - this.submodules[name] = submoduleRepo - } - - for (const name in this.submodules) { - const repo = this.submodules[name] - const gone = submoduleNames.indexOf(name) < 0 - if (gone) { - repo.destroy() - delete this.submodules[name] - } else { - try { - await repo.refreshStatus() - } catch (e) { - // libgit2 will sometimes report submodules that aren't actually valid - // (https://github.com/libgit2/libgit2/issues/3580). So check the - // validity of the submodules by removing any that fail. - repo.destroy() - delete this.submodules[name] - } - } - } - - return _.values(this.submodules) - } - - // Get the status for the submodules in the repository. - // - // Returns a {Promise} that will resolve to an object of {String} paths to the - // {Number} status. - _getSubmoduleStatuses () { - return this._refreshSubmodules() - .then(repos => { - return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo))) - }) - .then(statuses => _.extend({}, ...statuses)) - } - - // Refresh the cached status. - // - // Returns a {Promise} which will resolve to a {boolean} indicating whether - // any statuses changed. - _refreshStatus () { - return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()]) - .then(([repositoryStatus, submoduleStatus]) => { - const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus) - const changed = !_.isEqual(this.pathStatusCache, statusesByPath) - this.pathStatusCache = statusesByPath - return changed - }) - } - - // Refreshes the git status. - // - // Returns a {Promise} which will resolve to {null} when refresh is complete. - refreshStatus () { - const status = this._refreshStatus() - const branch = this._refreshBranch() - const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch)) - - this._refreshingPromise = this._refreshingPromise.then(_ => { - return Promise.all([status, branch, aheadBehind]) - .then(([statusChanged, branchChanged, aheadBehindChanged]) => { - if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) { - this.emitter.emit('did-change-statuses') - } - - return null - }) - // Because all these refresh steps happen asynchronously, it's entirely - // possible the repository was destroyed while we were working. In which - // case we should just swallow the error. - .catch(e => { - if (this._isDestroyed()) { - return null - } else { - return Promise.reject(e) - } - }) - .catch(e => { - console.error('Error refreshing repository status:') - console.error(e) - return Promise.reject(e) - }) - }) - return this._refreshingPromise - } - - // Get the submodule for the given path. - // - // Returns a {Promise} which resolves to the {GitRepositoryAsync} submodule or - // null if it isn't a submodule path. - async _submoduleForPath (_path) { - let relativePath = await this.relativizeToWorkingDirectory(_path) - for (const submodulePath in this.submodules) { - const submoduleRepo = this.submodules[submodulePath] - if (relativePath === submodulePath) { - return submoduleRepo - } else if (relativePath.indexOf(`${submodulePath}/`) === 0) { - relativePath = relativePath.substring(submodulePath.length + 1) - const innerSubmodule = await submoduleRepo._submoduleForPath(relativePath) - return innerSubmodule || submoduleRepo - } - } - - return null + .then(pathspecs => this.repo.refreshStatus(pathspecs)) } // Get the NodeGit repository for the given path. @@ -1062,16 +509,7 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to the {NodeGit.Repository}. getRepo (_path) { - if (this._isDestroyed()) { - const error = new Error('Repository has been destroyed') - error.name = GitRepositoryAsync.DestroyedErrorName - return Promise.reject(error) - } - - if (!_path) return this.repoPromise - - return this._submoduleForPath(_path) - .then(submodule => submodule ? submodule.getRepo() : this.repoPromise) + return this.repo.getRepo(_path) } // Open a new instance of the underlying {NodeGit.Repository}. @@ -1081,11 +519,7 @@ export default class GitRepositoryAsync { // // Returns the new {NodeGit.Repository}. openRepository () { - if (this._openExactPath) { - return Git.Repository.open(this.openedPath) - } else { - return Git.Repository.openExt(this.openedPath, 0, '') - } + return this.repo.openRepository() } // Section: Private @@ -1095,7 +529,7 @@ export default class GitRepositoryAsync { // // Returns a {Boolean}. _isDestroyed () { - return this.repoPromise == null + return this.repo._isDestroyed() } // Subscribe to events on the given buffer. @@ -1121,28 +555,4 @@ export default class GitRepositoryAsync { this.subscriptions.add(bufferSubscriptions) } - - // Get the status for the given paths. - // - // * `paths` The {String} paths whose status is wanted. If undefined, get the - // status for the whole repository. - // - // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} - // statuses for the paths. - _getStatus (paths) { - return this.repoPool.enqueue(() => { - return this.getRepo() - .then(repo => { - const opts = { - flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS - } - - if (paths) { - opts.pathspec = paths - } - - return repo.getStatusExt(opts) - }) - }) - } } diff --git a/src/git-repository.coffee b/src/git-repository.coffee index a04124b78..fcbc40830 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -166,12 +166,7 @@ class GitRepository # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses: (callback) -> - @async.onDidChangeStatuses -> - # Defer the callback to the next tick so that we've reset - # `@statusesByPath` by the time it's called. Otherwise reads from within - # the callback could be inconsistent. - # See https://github.com/atom/atom/issues/11396 - process.nextTick callback + @emitter.on 'did-change-statuses', callback ### Section: Repository Details @@ -496,9 +491,27 @@ class GitRepository # # Returns a promise that resolves when the repository has been refreshed. refreshStatus: -> + statusesChanged = false + + # 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 + + statusesChanged = true + asyncRefresh = @async.refreshStatus().then => - @statusesByPath = {} + 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') diff --git a/src/project.coffee b/src/project.coffee index 93a3ed496..bf64753cf 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -391,6 +391,9 @@ class Project extends Model subscribeToBuffer: (buffer) -> buffer.onDidDestroy => @removeBuffer(buffer) + buffer.onDidChangePath => + unless @getPaths().length > 0 + @setPaths([path.dirname(buffer.getPath())]) buffer.onWillThrowWatchError ({error, handle}) => handle() @notificationManager.addWarning """ diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index bb3630117..1fff6f633 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -1,6 +1,6 @@ {ipcRenderer} = require 'electron' -module.exports = ({commandRegistry, commandInstaller, config}) -> +module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) -> commandRegistry.add 'atom-workspace', 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() @@ -31,9 +31,15 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') - 'application:open': -> ipcRenderer.send('command', 'application:open') - 'application:open-file': -> ipcRenderer.send('command', 'application:open-file') - 'application:open-folder': -> ipcRenderer.send('command', 'application:open-folder') + 'application:open': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open', defaultPath) + 'application:open-file': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-file', defaultPath) + 'application:open-folder': -> + defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] + ipcRenderer.send('open-command', 'application:open-folder', defaultPath) 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') 'application:add-project-folder': -> atom.addProjectFolder() @@ -187,9 +193,9 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) - 'editor:log-cursor-scope': -> @logCursorScope() - 'editor:copy-path': -> @copyPathToClipboard(false) - 'editor:copy-project-path': -> @copyPathToClipboard(true) + 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) + 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false) + 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true) 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': -> @scrollToCursorPosition() @@ -204,7 +210,7 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'editor:newline-below': -> @insertNewlineBelow() 'editor:newline-above': -> @insertNewlineAbove() 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() - 'editor:checkout-head-revision': -> @checkoutHeadRevision() + 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this) 'editor:move-line-up': -> @moveLineUp() 'editor:move-line-down': -> @moveLineDown() 'editor:move-selection-left': -> @moveSelectionLeft() @@ -232,3 +238,15 @@ stopEventPropagationAndGroupUndo = (config, commandListeners) -> model.transact config.get('editor.undoGroupingInterval'), -> commandListener.call(model, event) newCommandListeners + +showCursorScope = (descriptor, notificationManager) -> + list = descriptor.scopes.toString().split(',') + list = list.map (item) -> "* #{item}" + content = "Scopes at Cursor\n#{list.join('\n')}" + + notificationManager.addInfo(content, dismissable: true) + +copyPathToClipboard = (editor, project, clipboard, relative) -> + if filePath = editor.getPath() + filePath = project.relativize(filePath) if relative + clipboard.write(filePath) diff --git a/src/resource-pool.js b/src/resource-pool.js deleted file mode 100644 index ae7cb71d0..000000000 --- a/src/resource-pool.js +++ /dev/null @@ -1,57 +0,0 @@ -/** @babel */ - -// Manages a pool of some resource. -export default class ResourcePool { - constructor (pool) { - this.pool = pool - - this.queue = [] - } - - // Enqueue the given function. The function will be given an object from the - // pool. The function must return a {Promise}. - enqueue (fn) { - let resolve = null - let reject = null - const wrapperPromise = new Promise((resolve_, reject_) => { - resolve = resolve_ - reject = reject_ - }) - - this.queue.push(this.wrapFunction(fn, resolve, reject)) - - this.dequeueIfAble() - - return wrapperPromise - } - - wrapFunction (fn, resolve, reject) { - return (resource) => { - const promise = fn(resource) - promise - .then(result => { - resolve(result) - this.taskDidComplete(resource) - }, error => { - reject(error) - this.taskDidComplete(resource) - }) - } - } - - taskDidComplete (resource) { - this.pool.push(resource) - - this.dequeueIfAble() - } - - dequeueIfAble () { - if (!this.pool.length || !this.queue.length) return - - const fn = this.queue.shift() - const resource = this.pool.shift() - fn(resource) - } - - getQueueDepth () { return this.queue.length } -} diff --git a/src/selection.coffee b/src/selection.coffee index e208ea55a..2937baaee 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -378,7 +378,8 @@ class Selection extends Model indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis @adjustIndent(remainingLines, indentAdjustment) - if options.autoIndent and NonWhitespaceRegExp.test(text) and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 + textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text) + if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 autoIndentFirstLine = true firstLine = precedingText + firstInsertedLine desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ef1b403c3..e5a9cd589 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -459,19 +459,21 @@ class TextEditorPresenter pixelPosition = @pixelPositionForScreenPosition(screenPosition) - top = pixelPosition.top + @lineHeight - left = pixelPosition.left + @gutterWidth + # Fixed positioning. + top = @boundingClientRect.top + pixelPosition.top + @lineHeight + left = @boundingClientRect.left + pixelPosition.left + @gutterWidth if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions - rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth + rightDiff = left + itemWidth + contentMargin - @windowWidth left -= rightDiff if rightDiff > 0 - leftDiff = left + @boundingClientRect.left + contentMargin + leftDiff = left + contentMargin left -= leftDiff if leftDiff < 0 - if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0 + if top + itemHeight > @windowHeight and + top - (itemHeight + @lineHeight) >= 0 top -= itemHeight + @lineHeight pixelPosition.top = top diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 0d1b3795e..9976e5906 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -9,7 +9,6 @@ Cursor = require './cursor' Model = require './model' Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector -{Directory} = require "pathwatcher" GutterContainer = require './gutter-container' TextEditorElement = require './text-editor-element' @@ -79,14 +78,9 @@ class TextEditor extends Model state.displayBuffer = displayBuffer state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config - state.notificationManager = atomEnvironment.notifications - state.packageManager = atomEnvironment.packages state.clipboard = atomEnvironment.clipboard - state.viewRegistry = atomEnvironment.views state.grammarRegistry = atomEnvironment.grammars - state.project = atomEnvironment.project state.assert = atomEnvironment.assert.bind(atomEnvironment) - state.applicationDelegate = atomEnvironment.applicationDelegate editor = new this(state) if state.registered disposable = atomEnvironment.textEditors.add(editor) @@ -99,20 +93,15 @@ class TextEditor extends Model { @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, - @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, - @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd + @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @clipboard, @grammarRegistry, + @assert, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? - throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager? - throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? - throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? - throw new Error("Must pass a project parameter when constructing TextEditors") unless @project? - throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? + @assert ?= (condition) -> condition @firstVisibleScreenRow ?= 0 @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @@ -129,7 +118,7 @@ class TextEditor extends Model buffer ?= new TextBuffer @displayBuffer ?= new DisplayBuffer({ buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode, - @config, @assert, @grammarRegistry, @packageManager + @config, @assert, @grammarRegistry }) @buffer = @displayBuffer.buffer @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) @@ -173,8 +162,6 @@ class TextEditor extends Model subscribeToBuffer: -> @buffer.retain() @disposables.add @buffer.onDidChangePath => - unless @project.getPaths().length > 0 - @project.setPaths([path.dirname(@getPath())]) @emitter.emit 'did-change-title', @getTitle() @emitter.emit 'did-change-path', @getPath() @disposables.add @buffer.onDidChangeEncoding => @@ -487,12 +474,12 @@ class TextEditor extends Model onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - @viewRegistry.getView(this).onDidChangeScrollTop(callback) + @getElement().onDidChangeScrollTop(callback) onDidChangeScrollLeft: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - @viewRegistry.getView(this).onDidChangeScrollLeft(callback) + @getElement().onDidChangeScrollLeft(callback) onDidRequestAutoscroll: (callback) -> @displayBuffer.onDidRequestAutoscroll(callback) @@ -520,9 +507,9 @@ class TextEditor extends Model softTabs = @getSoftTabs() newEditor = new TextEditor({ @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, - suppressCursorCreation: true, @config, @notificationManager, @packageManager, + suppressCursorCreation: true, @config, @firstVisibleScreenRow, @firstVisibleScreenColumn, - @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate + @clipboard, @grammarRegistry, @assert }) newEditor @@ -682,12 +669,6 @@ class TextEditor extends Model # Essential: Returns {Boolean} `true` if this editor has no content. isEmpty: -> @buffer.isEmpty() - # Copies the current file path to the native clipboard. - copyPathToClipboard: (relative = false) -> - if filePath = @getPath() - filePath = atom.project.relativize(filePath) if relative - @clipboard.write(filePath) - ### Section: File Operations ### @@ -716,25 +697,6 @@ class TextEditor extends Model # via {Pane::saveItemAs}. getSaveDialogOptions: -> {} - checkoutHeadRevision: -> - if @getPath() - checkoutHead = => - @project.repositoryForDirectory(new Directory(@getDirectoryPath())) - .then (repository) => - repository?.async.checkoutHeadForEditor(this) - - if @config.get('editor.confirmCheckoutHeadRevision') - @applicationDelegate.confirm - message: 'Confirm Checkout HEAD Revision' - detailedMessage: "Are you sure you want to discard all changes to \"#{@getFileName()}\" since the last Git commit?" - buttons: - OK: checkoutHead - Cancel: null - else - checkoutHead() - else - Promise.resolve(false) - ### Section: Reading Text ### @@ -2827,13 +2789,9 @@ class TextEditor extends Model @commentScopeSelector ?= new TextMateScopeSelector('comment.*') @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) - logCursorScope: -> - scopeDescriptor = @getLastCursor().getScopeDescriptor() - list = scopeDescriptor.scopes.toString().split(',') - list = list.map (item) -> "* #{item}" - content = "Scopes at Cursor\n#{list.join('\n')}" - - @notificationManager.addInfo(content, dismissable: true) + # Get the scope descriptor at the cursor. + getCursorScope: -> + @getLastCursor().getScopeDescriptor() # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) @@ -3138,24 +3096,24 @@ class TextEditor extends Model scrollToTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - @viewRegistry.getView(this).scrollToTop() + @getElement().scrollToTop() scrollToBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - @viewRegistry.getView(this).scrollToBottom() + @getElement().scrollToBottom() scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) getHorizontalScrollbarHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - @viewRegistry.getView(this).getHorizontalScrollbarHeight() + @getElement().getHorizontalScrollbarHeight() getVerticalScrollbarWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - @viewRegistry.getView(this).getVerticalScrollbarWidth() + @getElement().getVerticalScrollbarWidth() pageUp: -> @moveUp(@getRowsPerPage()) @@ -3222,11 +3180,11 @@ class TextEditor extends Model pixelPositionForBufferPosition: (bufferPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition) + @getElement().pixelPositionForBufferPosition(bufferPosition) pixelPositionForScreenPosition: (screenPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) + @getElement().pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> {type: 'selection', invalidate: 'never'} @@ -3255,7 +3213,7 @@ class TextEditor extends Model @displayBuffer.setHeight(height) else Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @viewRegistry.getView(this).setHeight(height) + @getElement().setHeight(height) getHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") @@ -3268,7 +3226,7 @@ class TextEditor extends Model @displayBuffer.setWidth(width) else Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @viewRegistry.getView(this).setWidth(width) + @getElement().setWidth(width) getWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @@ -3312,77 +3270,77 @@ class TextEditor extends Model getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - @viewRegistry.getView(this).getScrollTop() + @getElement().getScrollTop() setScrollTop: (scrollTop) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - @viewRegistry.getView(this).setScrollTop(scrollTop) + @getElement().setScrollTop(scrollTop) getScrollBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - @viewRegistry.getView(this).getScrollBottom() + @getElement().getScrollBottom() setScrollBottom: (scrollBottom) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - @viewRegistry.getView(this).setScrollBottom(scrollBottom) + @getElement().setScrollBottom(scrollBottom) getScrollLeft: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - @viewRegistry.getView(this).getScrollLeft() + @getElement().getScrollLeft() setScrollLeft: (scrollLeft) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - @viewRegistry.getView(this).setScrollLeft(scrollLeft) + @getElement().setScrollLeft(scrollLeft) getScrollRight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - @viewRegistry.getView(this).getScrollRight() + @getElement().getScrollRight() setScrollRight: (scrollRight) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - @viewRegistry.getView(this).setScrollRight(scrollRight) + @getElement().setScrollRight(scrollRight) getScrollHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - @viewRegistry.getView(this).getScrollHeight() + @getElement().getScrollHeight() getScrollWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - @viewRegistry.getView(this).getScrollWidth() + @getElement().getScrollWidth() getMaxScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") - @viewRegistry.getView(this).getMaxScrollTop() + @getElement().getMaxScrollTop() intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow) + @getElement().intersectsVisibleRowRange(startRow, endRow) selectionIntersectsVisibleRowRange: (selection) -> Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection) + @getElement().selectionIntersectsVisibleRowRange(selection) screenPositionForPixelPosition: (pixelPosition) -> Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition) + @getElement().screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - @viewRegistry.getView(this).pixelRectForScreenRange(screenRange) + @getElement().pixelRectForScreenRange(screenRange) ### Section: Utility diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 5c62f9ecd..065715806 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -29,14 +29,13 @@ class TokenizedBuffer extends Model state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) state.config = atomEnvironment.config state.grammarRegistry = atomEnvironment.grammars - state.packageManager = atomEnvironment.packages state.assert = atomEnvironment.assert new this(state) constructor: (params) -> { @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config, - @grammarRegistry, @packageManager, @assert, grammarScopeName + @grammarRegistry, @assert, grammarScopeName } = params @emitter = new Emitter @@ -126,7 +125,7 @@ class TokenizedBuffer extends Model @disposables.add(@configSubscriptions) @retokenizeLines() - @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") + @emitter.emit 'did-change-grammar', grammar getGrammarSelectionContent: -> diff --git a/src/workspace.coffee b/src/workspace.coffee index b8ed79fd6..f75f00bc6 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -4,6 +4,7 @@ path = require 'path' {join} = path {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' +{Directory} = require 'pathwatcher' DefaultDirectorySearcher = require './default-directory-searcher' Model = require './model' TextEditor = require './text-editor' @@ -550,9 +551,17 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options)) disposable = atom.textEditors.add(editor) - editor.onDidDestroy -> disposable.dispose() + grammarSubscription = editor.observeGrammar(@handleGrammarUsed.bind(this)) + editor.onDidDestroy -> + grammarSubscription.dispose() + disposable.dispose() editor + handleGrammarUsed: (grammar) -> + return unless grammar? + + @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") + # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. # # * `object` An {Object} you want to perform the check against. @@ -564,8 +573,7 @@ class Workspace extends Model # Returns a {TextEditor}. buildTextEditor: (params) -> params = _.extend({ - @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, - @grammarRegistry, @project, @assert, @applicationDelegate + @config, @clipboard, @grammarRegistry, @assert }, params) new TextEditor(params) @@ -1079,3 +1087,22 @@ class Workspace extends Model inProcessFinished = true checkFinished() + + checkoutHeadRevision: (editor) -> + if editor.getPath() + checkoutHead = => + @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) + .then (repository) => + repository?.async.checkoutHeadForEditor(editor) + + if @config.get('editor.confirmCheckoutHeadRevision') + @applicationDelegate.confirm + message: 'Confirm Checkout HEAD Revision' + detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?" + buttons: + OK: checkoutHead + Cancel: null + else + checkoutHead() + else + Promise.resolve(false) diff --git a/static/text-editor-light.less b/static/text-editor-light.less index 7fafade1e..f5429fd7f 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -15,7 +15,7 @@ atom-text-editor[mini] { } atom-overlay { - position: absolute; + position: fixed; display: block; z-index: 4; }