diff --git a/apm/package.json b/apm/package.json index 2a2e26db0..d6ec869c3 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.4.0" + "atom-package-manager": "1.4.1" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 351db9d31..a67bee93b 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -256,7 +256,7 @@ module.exports = (grunt) -> loadingGif: path.resolve(__dirname, '..', 'resources', 'win', 'loading.gif') iconUrl: "https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/#{channel}/atom.ico" setupIcon: path.resolve(__dirname, '..', 'resources', 'app-icons', channel, 'atom.ico') - remoteReleases: 'https://atom.io/api/updates' + remoteReleases: "https://atom.io/api/updates?version=#{metadata.version}" shell: 'kill-atom': diff --git a/build/package.json b/build/package.json index 308849a93..2ce92de17 100644 --- a/build/package.json +++ b/build/package.json @@ -27,7 +27,7 @@ "grunt-peg": "~1.1.0", "grunt-shell": "~0.3.1", "grunt-standard": "^1.0.2", - "legal-eagle": "~0.11.0", + "legal-eagle": "~0.12.0", "minidump": "~0.9", "npm": "2.13.3", "rcedit": "~0.3.0", diff --git a/package.json b/package.json index 2959a8020..be2fc9817 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.2.0-dev", + "version": "1.3.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -18,6 +18,7 @@ "atom-keymap": "^6.1.0", "babel-core": "^5.8.21", "bootstrap": "^3.3.4", + "cached-run-in-this-context": "0.4.0", "clear-cut": "^2.0.1", "coffee-script": "1.8.0", "color": "^0.7.3", @@ -86,12 +87,12 @@ "background-tips": "0.26.0", "bookmarks": "0.38.0", "bracket-matcher": "0.79.0", - "command-palette": "0.36.0", + "command-palette": "0.37.0", "deprecation-cop": "0.54.0", "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.37.0", - "find-and-replace": "0.189.0", + "find-and-replace": "0.190.0", "fuzzy-finder": "0.93.0", "git-diff": "0.57.0", "go-to-line": "0.30.0", @@ -113,9 +114,9 @@ "status-bar": "0.80.0", "styleguide": "0.45.0", "symbols-view": "0.110.0", - "tabs": "0.87.0", + "tabs": "0.88.0", "timecop": "0.33.0", - "tree-view": "0.194.0", + "tree-view": "0.196.0", "update-package-dependencies": "0.10.0", "welcome": "0.32.0", "whitespace": "0.32.0", @@ -124,14 +125,14 @@ "language-clojure": "0.18.0", "language-coffee-script": "0.43.0", "language-csharp": "0.11.0", - "language-css": "0.34.0", + "language-css": "0.35.0", "language-gfm": "0.81.0", "language-git": "0.10.0", - "language-go": "0.39.0", + "language-go": "0.40.0", "language-html": "0.42.0", "language-hyperlink": "0.15.0", - "language-java": "0.16.0", - "language-javascript": "0.97.0", + "language-java": "0.16.1", + "language-javascript": "0.98.0", "language-json": "0.17.1", "language-less": "0.28.3", "language-make": "0.19.0", @@ -142,15 +143,15 @@ "language-property-list": "0.8.0", "language-python": "0.41.0", "language-ruby": "0.60.0", - "language-ruby-on-rails": "0.23.0", - "language-sass": "0.42.0", - "language-shellscript": "0.19.0", + "language-ruby-on-rails": "0.24.0", + "language-sass": "0.42.1", + "language-shellscript": "0.20.0", "language-source": "0.9.0", - "language-sql": "0.18.0", + "language-sql": "0.19.0", "language-text": "0.7.0", "language-todo": "0.27.0", "language-toml": "0.16.0", - "language-xml": "0.33.1", + "language-xml": "0.34.0", "language-yaml": "0.24.0" }, "private": true, diff --git a/script/railcar b/script/railcar index 73a4b1dec..573e18b2c 100755 --- a/script/railcar +++ b/script/railcar @@ -8,18 +8,22 @@ var semver = require('semver') series([ section('Preparing to roll the railcars'), checkCleanWorkingTree, - run('git fetch origin master:master beta:beta stable:stable'), + run('git checkout master'), + run('git pull --ff-only origin master'), + run('git fetch origin beta:beta stable:stable'), run('git fetch origin --tags'), + section('Checking that merges will be fast-forwards'), + run('git branch --contains beta | grep master'), + run('git branch --contains stable | grep beta'), + section('Updating stable branch'), run('git checkout stable'), - run('git merge --ff-only origin/stable'), run('git merge --ff-only origin/beta'), bumpStableVersion, section('Updating beta branch'), run('git checkout beta'), - run('git merge --ff-only origin/beta'), run('git merge --ff-only origin/master'), run('git merge --strategy ours origin/stable'), bumpBetaVersion, @@ -44,11 +48,12 @@ function checkCleanWorkingTree (next) { } function bumpStableVersion (next) { - run('npm version patch')(next) + var newVersion = getCurrentVersion().replace(/-beta.*$/, '') + run('npm version ' + newVersion)(next) } function bumpBetaVersion (next) { - var newVersion = semver.inc(getCurrentVersion(), 'preminor', 'beta') + var newVersion = getCurrentVersion().replace(/-dev$/, '-beta0') run('npm version ' + newVersion)(next) } diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index f9dbc02f9..e12ac75c1 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -215,6 +215,17 @@ describe "AtomEnvironment", -> expect(atom.project.getPaths()).toEqual(initialPaths) describe "::unloadEditorWindow()", -> + it "saves the BlobStore so it can be loaded after reload", -> + configDirPath = temp.mkdirSync() + fakeBlobStore = jasmine.createSpyObj("blob store", ["save"]) + atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true, configDirPath, blobStore: fakeBlobStore, window, document}) + + atomEnvironment.unloadEditorWindow() + + expect(fakeBlobStore.save).toHaveBeenCalled() + + atomEnvironment.destroy() + it "saves the serialized state of the window so it can be deserialized after reload", -> atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document}) spyOn(atomEnvironment, 'saveStateSync') diff --git a/spec/atom-portable-spec.coffee b/spec/atom-portable-spec.coffee new file mode 100644 index 000000000..40c07fabe --- /dev/null +++ b/spec/atom-portable-spec.coffee @@ -0,0 +1,68 @@ +path = require "path" +fs = require 'fs-plus' +temp = require "temp" +AtomPortable = require "../src/browser/atom-portable" + +portableModeCommonPlatformBehavior = (platform) -> + describe "with ATOM_HOME environment variable", -> + it "returns false", -> + expect(AtomPortable.isPortableInstall(platform, "C:\\some\\path")).toBe false + + describe "without ATOM_HOME environment variable", -> + environmentAtomHome = undefined + portableAtomHomePath = path.join(path.dirname(process.execPath), "..", ".atom") + portableAtomHomeNaturallyExists = fs.existsSync(portableAtomHomePath) + portableAtomHomeBackupPath = "#{portableAtomHomePath}.temp" + + beforeEach -> + fs.renameSync(portableAtomHomePath, portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomePath) + + afterEach -> + if portableAtomHomeNaturallyExists + fs.renameSync(portableAtomHomeBackupPath, portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) + else + fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) + fs.removeSync(portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomeBackupPath) + + describe "with .atom directory sibling to exec", -> + beforeEach -> + fs.mkdirSync(portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) + + describe "without .atom directory sibling to exec", -> + beforeEach -> + fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) + + it "returns false", -> + expect(AtomPortable.isPortableInstall(platform, environmentAtomHome)).toBe false + +describe "Set Portable Mode on #win32", -> + portableAtomHomePath = path.join(path.dirname(process.execPath), "..", ".atom") + portableAtomHomeNaturallyExists = fs.existsSync(portableAtomHomePath) + portableAtomHomeBackupPath = "#{portableAtomHomePath}.temp" + + beforeEach -> + fs.renameSync(portableAtomHomePath, portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomePath) + + afterEach -> + if portableAtomHomeNaturallyExists + fs.renameSync(portableAtomHomeBackupPath, portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) + else + fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) + fs.removeSync(portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomeBackupPath) + + it "creates a portable home directory", -> + expect(fs.existsSync(portableAtomHomePath)).toBe false + + AtomPortable.setPortable(process.env.ATOM_HOME) + expect(fs.existsSync(portableAtomHomePath)).toBe true + +describe "Check for Portable Mode", -> + describe "Windows", -> + portableModeCommonPlatformBehavior "win32" + + describe "Mac", -> + it "returns false", -> + expect(AtomPortable.isPortableInstall("darwin", "darwin")).toBe false + + describe "Linux", -> + portableModeCommonPlatformBehavior "linux" diff --git a/spec/file-system-blob-store-spec.coffee b/spec/file-system-blob-store-spec.coffee new file mode 100644 index 000000000..c947259f6 --- /dev/null +++ b/spec/file-system-blob-store-spec.coffee @@ -0,0 +1,69 @@ +temp = require 'temp' +FileSystemBlobStore = require '../src/file-system-blob-store' + +describe "FileSystemBlobStore", -> + [storageDirectory, blobStore] = [] + + beforeEach -> + storageDirectory = temp.path() + blobStore = FileSystemBlobStore.load(storageDirectory) + + it "is empty when the file doesn't exist", -> + expect(blobStore.get("foo")).toBeUndefined() + expect(blobStore.get("bar")).toBeUndefined() + + it "allows to read and write buffers from/to memory without persisting them", -> + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) + + expect(blobStore.get("foo")).toEqual(new Buffer("foo")) + expect(blobStore.get("bar")).toEqual(new Buffer("bar")) + + it "persists buffers when saved and retrieves them on load, giving priority to in-memory ones", -> + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + + expect(blobStore.get("foo")).toEqual(new Buffer("foo")) + expect(blobStore.get("bar")).toEqual(new Buffer("bar")) + + blobStore.set("foo", new Buffer("changed")) + + expect(blobStore.get("foo")).toEqual(new Buffer("changed")) + + it "persists both in-memory and previously stored buffers when saved", -> + blobStore.set("foo", new Buffer("foo")) + blobStore.set("bar", new Buffer("bar")) + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + blobStore.set("bar", new Buffer("changed")) + blobStore.set("qux", new Buffer("qux")) + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + + expect(blobStore.get("foo")).toEqual(new Buffer("foo")) + expect(blobStore.get("bar")).toEqual(new Buffer("changed")) + expect(blobStore.get("qux")).toEqual(new Buffer("qux")) + + it "allows to delete keys from both memory and stored buffers", -> + blobStore.set("a", new Buffer("a")) + blobStore.set("b", new Buffer("b")) + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + + blobStore.set("b", new Buffer("b")) + blobStore.set("c", new Buffer("c")) + blobStore.delete("b") + blobStore.delete("c") + blobStore.save() + + blobStore = FileSystemBlobStore.load(storageDirectory) + + expect(blobStore.get("a")).toEqual(new Buffer("a")) + expect(blobStore.get("b")).toBeUndefined() + expect(blobStore.get("c")).toBeUndefined() diff --git a/spec/fixtures/native-cache/file-1.js b/spec/fixtures/native-cache/file-1.js new file mode 100644 index 000000000..ce195a18e --- /dev/null +++ b/spec/fixtures/native-cache/file-1.js @@ -0,0 +1 @@ +module.exports = function () { return 1; } diff --git a/spec/fixtures/native-cache/file-2.js b/spec/fixtures/native-cache/file-2.js new file mode 100644 index 000000000..e0cdf1485 --- /dev/null +++ b/spec/fixtures/native-cache/file-2.js @@ -0,0 +1 @@ +module.exports = function () { return 2; } diff --git a/spec/fixtures/native-cache/file-3.js b/spec/fixtures/native-cache/file-3.js new file mode 100644 index 000000000..36ca6e14a --- /dev/null +++ b/spec/fixtures/native-cache/file-3.js @@ -0,0 +1 @@ +module.exports = function () { return 3; } diff --git a/spec/fixtures/testdir/sample-theme-1/readme b/spec/fixtures/testdir/sample-theme-1/readme new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/testdir/sample-theme-1/src/js/main.js b/spec/fixtures/testdir/sample-theme-1/src/js/main.js new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/testdir/sample-theme-2/readme b/spec/fixtures/testdir/sample-theme-2/readme new file mode 100644 index 000000000..e69de29bb diff --git a/spec/fixtures/testdir/sample-theme-2/src/js/main.js b/spec/fixtures/testdir/sample-theme-2/src/js/main.js new file mode 100644 index 000000000..e69de29bb diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 022f535f4..ae85a0e9d 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -2,7 +2,7 @@ LinesYardstick = require "../src/lines-yardstick" {toArray} = require 'underscore-plus' describe "LinesYardstick", -> - [editor, mockPresenter, mockLineNodesProvider, createdLineNodes, linesYardstick] = [] + [editor, mockPresenter, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = [] beforeEach -> waitsForPromise -> @@ -49,10 +49,10 @@ describe "LinesYardstick", -> buildLineNode(screenRow) textNodesForLineIdAndScreenRow: (lineId, screenRow) -> lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) textNodes = [] - for span in lineNode.children - for textNode in span.childNodes - textNodes.push(textNode) + while textNode = iterator.nextNode() + textNodes.push(textNode) textNodes editor.setLineHeightInPixels(14) @@ -126,6 +126,33 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition([0, 9]).left).toBe 67 expect(linesYardstick.pixelPositionForScreenPosition([0, 11]).left).toBe 84 + it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", -> + # This spec documents what seems to be a bug in Chromium, because we'd + # expect that Range(0, 0).getBoundingClientRect().width to always be zero. + atom.styles.addStyleSheet """ + * { + font-size: 11px; + font-family: monospace; + } + """ + + text = " \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\" + buildLineNode = (screenRow) -> + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + # We couldn't reproduce the problem with a simple string, so we're + # attaching the full one that comes from a bug report. + lineNode.innerHTML = ' \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\' + jasmine.attachToDOM(lineNode) + createdLineNodes.push(lineNode) + lineNode + + editor.setText(text) + + expect(linesYardstick.pixelPositionForScreenPosition([0, 35]).left).toBe 230.90625 + expect(linesYardstick.pixelPositionForScreenPosition([0, 36]).left).toBe 237.5 + expect(linesYardstick.pixelPositionForScreenPosition([0, 37]).left).toBe 244.09375 + it "doesn't measure invisible lines if it is explicitly told so", -> atom.styles.addStyleSheet """ * { diff --git a/spec/native-compile-cache-spec.coffee b/spec/native-compile-cache-spec.coffee new file mode 100644 index 000000000..dd720e84d --- /dev/null +++ b/spec/native-compile-cache-spec.coffee @@ -0,0 +1,47 @@ +describe "NativeCompileCache", -> + nativeCompileCache = require '../src/native-compile-cache' + [fakeCacheStore, cachedFiles] = [] + + beforeEach -> + cachedFiles = [] + fakeCacheStore = jasmine.createSpyObj("cache store", ["set", "get", "has", "delete"]) + nativeCompileCache.setCacheStore(fakeCacheStore) + nativeCompileCache.install() + + it "writes and reads from the cache storage when requiring files", -> + fakeCacheStore.has.andReturn(false) + fakeCacheStore.set.andCallFake (filename, cacheBuffer) -> + cachedFiles.push({filename, cacheBuffer}) + + fn1 = require('./fixtures/native-cache/file-1') + fn2 = require('./fixtures/native-cache/file-2') + + expect(cachedFiles.length).toBe(2) + + expect(cachedFiles[0].filename).toBe(require.resolve('./fixtures/native-cache/file-1')) + expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) + expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) + expect(fn1()).toBe(1) + + expect(cachedFiles[1].filename).toBe(require.resolve('./fixtures/native-cache/file-2')) + expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) + expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) + expect(fn2()).toBe(2) + + fakeCacheStore.has.andReturn(true) + fakeCacheStore.get.andReturn(cachedFiles[0].cacheBuffer) + fakeCacheStore.set.reset() + + fn1 = require('./fixtures/native-cache/file-1') + + expect(fakeCacheStore.set).not.toHaveBeenCalled() + expect(fn1()).toBe(1) + + it "deletes previously cached code when the cache is not valid", -> + fakeCacheStore.has.andReturn(true) + fakeCacheStore.get.andCallFake -> new Buffer("an invalid cache") + + fn3 = require('./fixtures/native-cache/file-3') + + expect(fakeCacheStore.delete).toHaveBeenCalledWith(require.resolve('./fixtures/native-cache/file-3')) + expect(fn3()).toBe(3) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 9a2edee0a..88e730d44 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -38,6 +38,7 @@ describe "PackageManager", -> expect(-> pack.reloadStylesheets()).not.toThrow() expect(addErrorHandler.callCount).toBe 2 expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to reload the package-with-invalid-styles package stylesheets") + expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual "package-with-invalid-styles" it "returns null if the package has an invalid package.json", -> addErrorHandler = jasmine.createSpy() @@ -45,6 +46,7 @@ describe "PackageManager", -> expect(atom.packages.loadPackage("package-with-broken-package-json")).toBeNull() expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json" it "normalizes short repository urls in package.json", -> {metadata} = atom.packages.loadPackage("package-with-short-url-package-json") @@ -230,6 +232,7 @@ describe "PackageManager", -> expect(-> atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-activation-commands package") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-activation-commands" it "adds a notification when the context menu is invalid", -> addErrorHandler = jasmine.createSpy() @@ -237,6 +240,7 @@ describe "PackageManager", -> expect(-> atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-context-menu package") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-context-menu" it "adds a notification when the grammar is invalid", -> addErrorHandler = jasmine.createSpy() @@ -250,6 +254,7 @@ describe "PackageManager", -> runs -> expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load a package-with-invalid-grammar package grammar") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-grammar" it "adds a notification when the settings are invalid", -> addErrorHandler = jasmine.createSpy() @@ -263,6 +268,7 @@ describe "PackageManager", -> runs -> expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-invalid-settings package settings") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-settings" describe "when the package metadata includes `activationHooks`", -> [mainModule, promise] = [] @@ -351,6 +357,7 @@ describe "PackageManager", -> expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow() expect(addErrorHandler.callCount).toBe 1 expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-that-throws-an-exception package") + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-that-throws-an-exception" describe "when the package is not found", -> it "rejects the promise", -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 8ce026741..ed299ab54 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -168,6 +168,37 @@ describe "TextEditor", -> buffer.setPath(undefined) expect(editor.getLongTitle()).toBe 'untitled' + describe ".getUniqueTitle()", -> + it "returns file name when there is no opened file with identical name", -> + expect(editor.getUniqueTitle()).toBe 'sample.js' + buffer.setPath(undefined) + expect(editor.getLongTitle()).toBe 'untitled' + + it "returns / when opened files has identical file names", -> + editor1 = null + editor2 = null + waitsForPromise -> + atom.workspace.open(path.join('sample-theme-1', 'readme')).then (o) -> + editor1 = o + atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> + editor2 = o + runs -> + expect(editor1.getUniqueTitle()).toBe 'sample-theme-1/readme' + expect(editor2.getUniqueTitle()).toBe 'sample-theme-2/readme' + + it "or returns /.../ when opened files has identical file names", -> + editor1 = null + editor2 = null + waitsForPromise -> + atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) -> + editor1 = o + atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> + editor2 = o + runs -> + expect(editor1.getUniqueTitle()).toBe 'sample-theme-1/.../main.js' + expect(editor2.getUniqueTitle()).toBe 'sample-theme-2/.../main.js' + + it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> observed = [] editor.onDidChangeTitle (title) -> observed.push(title) @@ -827,6 +858,185 @@ describe "TextEditor", -> editor.moveToBeginningOfNextWord() expect(editor.getCursorBufferPosition()).toEqual [11, 9] + describe ".moveToPreviousSubwordBoundary", -> + it "does not move the cursor when there is no previous subword boundary", -> + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "stops at word and underscore boundaries", -> + editor.setText("sub_word \n") + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 8]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText(" word\n") + editor.setCursorBufferPosition([0, 3]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "stops at camelCase boundaries", -> + editor.setText(" getPreviousWord\n") + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "skips consecutive non-word characters", -> + editor.setText("e, => \n") + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "skips consecutive uppercase characters", -> + editor.setText(" AAADF \n") + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText("ALPhA\n") + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + + it "skips consecutive numbers", -> + editor.setText(" 88 \n") + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + it "works with multiple cursors", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveToPreviousSubwordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + + it "works with non-English characters", -> + editor.setText("supåTøåst \n") + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText("supaÖast \n") + editor.setCursorBufferPosition([0, 8]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + describe ".moveToNextSubwordBoundary", -> + it "does not move the cursor when there is no next subword boundary", -> + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + it "stops at word and underscore boundaries", -> + editor.setText(" sub_word \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 9]) + + editor.setText("word \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + it "stops at camelCase boundaries", -> + editor.setText("getPreviousWord \n") + editor.setCursorBufferPosition([0, 0]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + + it "skips consecutive non-word characters", -> + editor.setText(", => \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + it "skips consecutive uppercase characters", -> + editor.setText(" AAADF \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.setText("ALPhA\n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + + it "skips consecutive numbers", -> + editor.setText(" 88 \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + it "works with multiple cursors", -> + editor.setText("curOp\ncursorOptions\n") + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + [cursor1, cursor2] = editor.getCursors() + + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + + it "works with non-English characters", -> + editor.setText("supåTøåst \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText("supaÖast \n") + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + describe ".moveToBeginningOfNextParagraph()", -> it "moves the cursor before the first line of the next paragraph", -> editor.setCursorBufferPosition [0, 6] @@ -1299,6 +1509,128 @@ describe "TextEditor", -> expect(selection4.getBufferRange()).toEqual [[3, 30], [3, 31]] expect(selection4.isReversed()).toBeFalsy() + describe ".selectToPreviousSubwordBoundary", -> + it "selects subwords", -> + editor.setText("") + editor.insertText("_word\n") + editor.insertText(" getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToPreviousSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeTruthy() + + describe ".selectToNextSubwordBoundary", -> + it "selects subwords", -> + editor.setText("") + editor.insertText("word_\n") + editor.insertText("getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 1]) + [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToNextSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeFalsy() + + describe ".deleteToBeginningOfSubword", -> + it "deletes subwords", -> + editor.setText("") + editor.insertText("_word\n") + editor.insertText(" getPreviousWord\n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe(' getviousWord') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 1]) + expect(cursor2.getBufferPosition()).toEqual([1, 4]) + expect(cursor3.getBufferPosition()).toEqual([2, 3]) + expect(cursor4.getBufferPosition()).toEqual([3, 1]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe(' viousWord') + expect(buffer.lineForRow(2)).toBe('e ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 1]) + expect(cursor3.getBufferPosition()).toEqual([2, 1]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('viousWord') + expect(buffer.lineForRow(2)).toBe(' ') + expect(buffer.lineForRow(3)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([2, 1]) + + describe ".deleteToEndOfSubword", -> + it "deletes subwords", -> + editor.setText("") + editor.insertText("word_\n") + editor.insertText("getPreviousWord \n") + editor.insertText("e, => \n") + editor.insertText(" 88 \n") + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 0]) + [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe('PreviousWord ') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe('88 ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('Word ') + expect(buffer.lineForRow(2)).toBe('e,') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + describe ".selectWordsContainingCursors()", -> describe "when the cursor is inside a word", -> it "selects the entire word", -> @@ -4160,6 +4492,36 @@ describe "TextEditor", -> """ expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]] + describe ".moveLineUp()", -> + it "moves the line under the cursor up", -> + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {" + expect(editor.indentationForBufferRow(0)).toBe 1 + expect(editor.indentationForBufferRow(1)).toBe 0 + + it "updates the line's indentation when the editor.autoIndent setting is true", -> + atom.config.set('editor.autoIndent', true) + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.indentationForBufferRow(0)).toBe 0 + expect(editor.indentationForBufferRow(1)).toBe 0 + + describe ".moveLineDown()", -> + it "moves the line under the cursor down", -> + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {" + expect(editor.indentationForBufferRow(0)).toBe 1 + expect(editor.indentationForBufferRow(1)).toBe 0 + + it "updates the line's indentation when the editor.autoIndent setting is true", -> + atom.config.set('editor.autoIndent', true) + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.indentationForBufferRow(0)).toBe 1 + expect(editor.indentationForBufferRow(1)).toBe 2 + describe ".shouldPromptToSave()", -> it "returns false when an edit session's buffer is in use by more than one session", -> jasmine.unspy(editor, 'shouldPromptToSave') @@ -4461,307 +4823,6 @@ describe "TextEditor", -> waitsForPromise -> editor.checkoutHeadRevision() - describe ".moveToPreviousSubwordBoundary", -> - it "does not move the cursor when there is no previous subword boundary", -> - editor.setText('') - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText("sub_word \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 8]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - editor.setText(" word\n") - editor.setCursorBufferPosition([0, 3]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "stops at camelCase boundaries", -> - editor.setText(" getPreviousWord\n") - editor.setCursorBufferPosition([0, 16]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 12]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive non-word characters", -> - editor.setText("e, => \n") - editor.setCursorBufferPosition([0, 6]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 7]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 8]) - editor.addCursorAtBufferPosition([1, 13]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToPreviousSubwordBoundary() - - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 8]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToNextSubwordBoundary", -> - it "does not move the cursor when there is no next subword boundary", -> - editor.setText('') - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText(" sub_word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 9]) - - editor.setText("word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "stops at camelCase boundaries", -> - editor.setText("getPreviousWord \n") - editor.setCursorBufferPosition([0, 0]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 11]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - - it "skips consecutive non-word characters", -> - editor.setText(", => \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToNextSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".selectToPreviousSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToPreviousSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 1]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToNextSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeFalsy() - - describe ".deleteToBeginningOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe(' getviousWord') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 1]) - expect(cursor2.getBufferPosition()).toEqual([1, 4]) - expect(cursor3.getBufferPosition()).toEqual([2, 3]) - expect(cursor4.getBufferPosition()).toEqual([3, 1]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe(' viousWord') - expect(buffer.lineForRow(2)).toBe('e ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 1]) - expect(cursor3.getBufferPosition()).toEqual([2, 1]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('viousWord') - expect(buffer.lineForRow(2)).toBe(' ') - expect(buffer.lineForRow(3)).toBe('') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 0]) - expect(cursor4.getBufferPosition()).toEqual([2, 1]) - - describe ".deleteToEndOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord \n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 0]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe('PreviousWord ') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe('88 ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('Word ') - expect(buffer.lineForRow(2)).toBe('e,') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - describe 'gutters', -> describe 'the TextEditor constructor', -> it 'creates a line-number gutter', -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 004cfbc80..f98e1b835 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1,5 +1,6 @@ crypto = require 'crypto' path = require 'path' +ipc = require 'ipc' _ = require 'underscore-plus' {deprecate} = require 'grim' @@ -116,7 +117,7 @@ class AtomEnvironment extends Model # Call .loadOrCreate instead constructor: (params={}) -> - {@applicationDelegate, @window, @document, configDirPath, @enablePersistence} = params + {@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence} = params @state = {version: @constructor.version} @@ -203,6 +204,15 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() + checkPortableHomeWritable = -> + responseChannel = "check-portable-home-writable-response" + ipc.on responseChannel, (response) -> + ipc.removeAllListeners(responseChannel) + atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable + ipc.send('check-portable-home-writable', responseChannel) + + checkPortableHomeWritable() + setConfigSchema: -> @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} @@ -306,6 +316,7 @@ class AtomEnvironment extends Model @project = null @commands.clear() @stylesElement.remove() + @config.unobserveUserConfig() @uninstallWindowEventHandler() @@ -636,6 +647,7 @@ class AtomEnvironment extends Model @state.packageStates = @packages.packageStates @state.fullScreen = @isFullScreen() @saveStateSync() + @saveBlobStoreSync() openInitialEmptyEditorIfNecessary: -> return unless @config.get('core.openEmptyEditorOnStart') @@ -759,6 +771,11 @@ class AtomEnvironment extends Model showSaveDialogSync: (options={}) -> @applicationDelegate.showSaveDialog(options) + saveBlobStoreSync: -> + return unless @enablePersistence + + @blobStore.save() + saveStateSync: -> return unless @enablePersistence diff --git a/src/browser/atom-portable.coffee b/src/browser/atom-portable.coffee new file mode 100644 index 000000000..5f8f10cf6 --- /dev/null +++ b/src/browser/atom-portable.coffee @@ -0,0 +1,35 @@ +fs = require 'fs-plus' +path = require 'path' +ipc = require 'ipc' + +module.exports = +class AtomPortable + @getPortableAtomHomePath: -> + execDirectoryPath = path.dirname(process.execPath) + path.join(execDirectoryPath, '..', '.atom') + + @setPortable: (existingAtomHome) -> + fs.copySync(existingAtomHome, @getPortableAtomHomePath()) + + @isPortableInstall: (platform, environmentAtomHome, defaultHome) -> + return false unless platform in ['linux', 'win32'] + return false if environmentAtomHome + return false if not fs.existsSync(@getPortableAtomHomePath()) + # currently checking only that the directory exists and is writable, + # probably want to do some integrity checks on contents in future + @isPortableAtomHomePathWritable(defaultHome) + + @isPortableAtomHomePathWritable: (defaultHome) -> + writable = false + message = "" + try + writePermissionTestFile = path.join(@getPortableAtomHomePath(), "write.test") + fs.writeFileSync(writePermissionTestFile, "test") if not fs.existsSync(writePermissionTestFile) + fs.removeSync(writePermissionTestFile) + writable = true + catch error + message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}" + + ipc.on 'check-portable-home-writable', (event) -> + event.sender.send 'check-portable-home-writable-response', {writable, message} + writable diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 16456d274..a346f5c77 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -49,6 +49,7 @@ class AtomWindow loadSettings.resourcePath = @resourcePath loadSettings.devMode ?= false loadSettings.safeMode ?= false + loadSettings.atomHome = process.env.ATOM_HOME # Only send to the first non-spec window created if @constructor.includeShellLoadTime and not @isSpec diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 990002671..ca9d7e3ae 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -12,15 +12,14 @@ yargs = require 'yargs' console.log = require 'nslog' start = -> - setupAtomHome() + args = parseCommandLine() + setupAtomHome(args) setupCompileCache() return if handleStartupEventWithSquirrel() # NB: This prevents Win10 from showing dupe items in the taskbar app.setAppUserModelId('com.squirrel.atom.atom') - args = parseCommandLine() - addPathToOpen = (event, pathToOpen) -> event.preventDefault() args.pathsToOpen.push(pathToOpen) @@ -57,11 +56,25 @@ handleStartupEventWithSquirrel = -> setupCrashReporter = -> crashReporter.start(productName: 'Atom', companyName: 'GitHub') -setupAtomHome = -> +setupAtomHome = ({setPortable}) -> return if process.env.ATOM_HOME + atomHome = path.join(app.getHomeDir(), '.atom') + AtomPortable = require './atom-portable' + + if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome) + try + AtomPortable.setPortable(atomHome) + catch error + console.log("Failed copying portable directory '#{atomHome}' to '#{AtomPortable.getPortableAtomHomePath()}'") + console.log("#{error.message} #{error.stack}") + + if AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome) + atomHome = AtomPortable.getPortableAtomHomePath() + try atomHome = fs.realpathSync(atomHome) + process.env.ATOM_HOME = atomHome setupCompileCache = -> @@ -100,6 +113,7 @@ parseCommandLine = -> options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.') + options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.') options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).') options.alias('v', 'version').boolean('v').describe('v', 'Print the version.') @@ -129,6 +143,7 @@ parseCommandLine = -> profileStartup = args['profile-startup'] urlsToOpen = [] devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom') + setPortable = args.portable if args['resource-path'] devMode = true @@ -149,6 +164,6 @@ parseCommandLine = -> {resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, - logFile, socketPath, profileStartup, timeout} + logFile, socketPath, profileStartup, timeout, setPortable} start() diff --git a/src/file-system-blob-store.js b/src/file-system-blob-store.js new file mode 100644 index 000000000..fc6bdddf3 --- /dev/null +++ b/src/file-system-blob-store.js @@ -0,0 +1,111 @@ +'use strict' + +const fs = require('fs-plus') +const path = require('path') + +module.exports = +class FileSystemBlobStore { + static load (directory) { + let instance = new FileSystemBlobStore(directory) + instance.load() + return instance + } + + constructor (directory) { + this.inMemoryBlobs = new Map() + this.blobFilename = path.join(directory, 'BLOB') + this.blobMapFilename = path.join(directory, 'MAP') + this.lockFilename = path.join(directory, 'LOCK') + this.storedBlob = new Buffer(0) + this.storedBlobMap = {} + } + + load () { + if (!fs.existsSync(this.blobMapFilename)) { + return + } + if (!fs.existsSync(this.blobFilename)) { + return + } + this.storedBlob = fs.readFileSync(this.blobFilename) + this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename)) + } + + save () { + let dump = this.getDump() + let blobToStore = Buffer.concat(dump[0]) + let mapToStore = JSON.stringify(dump[1]) + + let acquiredLock = false + try { + fs.writeFileSync(this.lockFilename, 'LOCK', {flag: 'wx'}) + acquiredLock = true + + fs.writeFileSync(this.blobFilename, blobToStore) + fs.writeFileSync(this.blobMapFilename, mapToStore) + } catch (error) { + // Swallow the exception silently only if we fail to acquire the lock. + if (error.code !== 'EEXIST') { + throw error + } + } finally { + if (acquiredLock) { + fs.unlinkSync(this.lockFilename) + } + } + } + + has (key) { + return this.inMemoryBlobs.hasOwnProperty(key) || this.storedBlobMap.hasOwnProperty(key) + } + + get (key) { + return this.getFromMemory(key) || this.getFromStorage(key) + } + + set (key, buffer) { + return this.inMemoryBlobs.set(key, buffer) + } + + delete (key) { + this.inMemoryBlobs.delete(key) + delete this.storedBlobMap[key] + } + + getFromMemory (key) { + return this.inMemoryBlobs.get(key) + } + + getFromStorage (key) { + if (!this.storedBlobMap[key]) { + return + } + + return this.storedBlob.slice.apply(this.storedBlob, this.storedBlobMap[key]) + } + + getDump () { + let buffers = [] + let blobMap = {} + let currentBufferStart = 0 + + function dump (key, getBufferByKey) { + let buffer = getBufferByKey(key) + buffers.push(buffer) + blobMap[key] = [currentBufferStart, currentBufferStart + buffer.length] + currentBufferStart += buffer.length + } + + for (let key of this.inMemoryBlobs.keys()) { + dump(key, this.getFromMemory.bind(this)) + } + + for (let key of Object.keys(this.storedBlobMap)) { + if (!blobMap[key]) { + dump(key, this.getFromStorage.bind(this)) + } + } + + return [buffers, blobMap] + } +} diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index acf50bc5d..e301a20d0 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -1,34 +1,34 @@ # Like sands through the hourglass, so are the days of our lives. +module.exports = ({blobStore}) -> + path = require 'path' + require './window' + {getWindowLoadSettings} = require './window-load-settings-helpers' -path = require 'path' -require './window' -{getWindowLoadSettings} = require './window-load-settings-helpers' + {resourcePath, isSpec, devMode} = getWindowLoadSettings() -{resourcePath, isSpec, devMode} = getWindowLoadSettings() + # Add application-specific exports to module search path. + exportsPath = path.join(resourcePath, 'exports') + require('module').globalPaths.push(exportsPath) + process.env.NODE_PATH = exportsPath -# Add application-specific exports to module search path. -exportsPath = path.join(resourcePath, 'exports') -require('module').globalPaths.push(exportsPath) -process.env.NODE_PATH = exportsPath + # Make React faster + process.env.NODE_ENV ?= 'production' unless devMode -# Make React faster -process.env.NODE_ENV ?= 'production' unless devMode + AtomEnvironment = require './atom-environment' + ApplicationDelegate = require './application-delegate' + window.atom = new AtomEnvironment({ + window, document, blobStore, + applicationDelegate: new ApplicationDelegate, + configDirPath: process.env.ATOM_HOME + enablePersistence: true + }) -AtomEnvironment = require './atom-environment' -ApplicationDelegate = require './application-delegate' -window.atom = new AtomEnvironment({ - window, document, - applicationDelegate: new ApplicationDelegate, - configDirPath: process.env.ATOM_HOME - enablePersistence: true -}) + atom.displayWindow() + atom.loadStateSync() + atom.startEditorWindow() -atom.displayWindow() -atom.loadStateSync() -atom.startEditorWindow() - -# Workaround for focus getting cleared upon window creation -windowFocused = -> - window.removeEventListener('focus', windowFocused) - setTimeout (-> document.querySelector('atom-workspace').focus()), 0 -window.addEventListener('focus', windowFocused) + # Workaround for focus getting cleared upon window creation + windowFocused = -> + window.removeEventListener('focus', windowFocused) + setTimeout (-> document.querySelector('atom-workspace').focus()), 0 + window.addEventListener('focus', windowFocused) diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index 27ef46acc..f33cca09d 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -1,69 +1,78 @@ -# Start the crash reporter before anything else. -require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub') -remote = require 'remote' +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone -exitWithStatusCode = (status) -> - remote.require('app').emit('will-quit') - remote.process.exit(status) +module.exports = ({blobStore}) -> + # Start the crash reporter before anything else. + require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub') + remote = require 'remote' -try - path = require 'path' - ipc = require 'ipc' - {getWindowLoadSettings} = require './window-load-settings-helpers' - AtomEnvironment = require '../src/atom-environment' - ApplicationDelegate = require '../src/application-delegate' + exitWithStatusCode = (status) -> + remote.require('app').emit('will-quit') + remote.process.exit(status) - {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings() + try + path = require 'path' + ipc = require 'ipc' + {getWindowLoadSettings} = require './window-load-settings-helpers' + AtomEnvironment = require '../src/atom-environment' + ApplicationDelegate = require '../src/application-delegate' - if headless - # Override logging in headless mode so it goes to the console, regardless - # of the --enable-logging flag to Electron. - console.log = (args...) -> - ipc.send 'write-to-stdout', args.join(' ') + '\n' - console.warn = (args...) -> - ipc.send 'write-to-stderr', args.join(' ') + '\n' - console.error = (args...) -> - ipc.send 'write-to-stderr', args.join(' ') + '\n' - else - # Show window synchronously so a focusout doesn't fire on input elements - # that are focused in the very first spec run. - remote.getCurrentWindow().show() + {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings() - handleKeydown = (event) -> - # Reload: cmd-r / ctrl-r - if (event.metaKey or event.ctrlKey) and event.keyCode is 82 - ipc.send('call-window-method', 'restart') + if headless + # Override logging in headless mode so it goes to the console, regardless + # of the --enable-logging flag to Electron. + console.log = (args...) -> + ipc.send 'write-to-stdout', args.join(' ') + '\n' + console.warn = (args...) -> + ipc.send 'write-to-stderr', args.join(' ') + '\n' + console.error = (args...) -> + ipc.send 'write-to-stderr', args.join(' ') + '\n' + else + # Show window synchronously so a focusout doesn't fire on input elements + # that are focused in the very first spec run. + remote.getCurrentWindow().show() - # Toggle Dev Tools: cmd-alt-i / ctrl-alt-i - if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73 - ipc.send('call-window-method', 'toggleDevTools') + handleKeydown = (event) -> + # Reload: cmd-r / ctrl-r + if (event.metaKey or event.ctrlKey) and event.keyCode is 82 + ipc.send('call-window-method', 'restart') - # Reload: cmd-w / ctrl-w - if (event.metaKey or event.ctrlKey) and event.keyCode is 87 - ipc.send('call-window-method', 'close') + # Toggle Dev Tools: cmd-alt-i / ctrl-alt-i + if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73 + ipc.send('call-window-method', 'toggleDevTools') - window.addEventListener('keydown', handleKeydown, true) + # Reload: cmd-w / ctrl-w + if (event.metaKey or event.ctrlKey) and event.keyCode is 87 + ipc.send('call-window-method', 'close') - # Add 'exports' to module search path. - exportsPath = path.join(getWindowLoadSettings().resourcePath, 'exports') - require('module').globalPaths.push(exportsPath) - process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it. + window.addEventListener('keydown', handleKeydown, true) - document.title = "Spec Suite" + # Add 'exports' to module search path. + exportsPath = path.join(getWindowLoadSettings().resourcePath, 'exports') + require('module').globalPaths.push(exportsPath) + process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it. - testRunner = require(testRunnerPath) - legacyTestRunner = require(legacyTestRunnerPath) - buildAtomEnvironment = (params) -> new AtomEnvironment(params) - buildDefaultApplicationDelegate = (params) -> new ApplicationDelegate() + document.title = "Spec Suite" - promise = testRunner({ - logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner - }) + testRunner = require(testRunnerPath) + legacyTestRunner = require(legacyTestRunnerPath) + buildDefaultApplicationDelegate = -> new ApplicationDelegate() + buildAtomEnvironment = (params) -> + params = cloneObject(params) + params.blobStore = blobStore unless params.hasOwnProperty("blobStore") + new AtomEnvironment(params) - promise.then(exitWithStatusCode) if getWindowLoadSettings().headless -catch error - if getWindowLoadSettings().headless - console.error(error.stack ? error) - exitWithStatusCode(1) - else - throw error + promise = testRunner({ + logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner + }) + + promise.then(exitWithStatusCode) if getWindowLoadSettings().headless + catch error + if getWindowLoadSettings().headless + console.error(error.stack ? error) + exitWithStatusCode(1) + else + throw error diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index fa00bae40..54ba6cf57 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -159,9 +159,12 @@ class LinesYardstick 0 leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.setEnd(textNode, charIndex) - width = @rangeForMeasurement.getBoundingClientRect().width + if charIndex is 0 + width = 0 + else + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.setEnd(textNode, charIndex) + width = @rangeForMeasurement.getBoundingClientRect().width @rangeForMeasurement.setStart(textNode, 0) @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) diff --git a/src/native-compile-cache.js b/src/native-compile-cache.js new file mode 100644 index 000000000..a930466a5 --- /dev/null +++ b/src/native-compile-cache.js @@ -0,0 +1,101 @@ +'use strict' + +const Module = require('module') +const path = require('path') +const cachedVm = require('cached-run-in-this-context') + +class NativeCompileCache { + constructor () { + this.cacheStore = null + this.previousModuleCompile = null + } + + setCacheStore (store) { + this.cacheStore = store + } + + install () { + this.savePreviousModuleCompile() + this.overrideModuleCompile() + } + + uninstall () { + this.restorePreviousModuleCompile() + } + + savePreviousModuleCompile () { + this.previousModuleCompile = Module.prototype._compile + } + + overrideModuleCompile () { + let cacheStore = this.cacheStore + let resolvedArgv = null + // Here we override Node's module.js + // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing + // only the bits that affect compilation in order to use the cached one. + Module.prototype._compile = function (content, filename) { + let self = this + // remove shebang + content = content.replace(/^\#\!.*/, '') + function require (path) { + return self.require(path) + } + require.resolve = function (request) { + return Module._resolveFilename(request, self) + } + require.main = process.mainModule + + // Enable support to add extra extension types + require.extensions = Module._extensions + require.cache = Module._cache + + let dirname = path.dirname(filename) + + // create wrapper function + let wrapper = Module.wrap(content) + + let compiledWrapper = null + if (cacheStore.has(filename)) { + let buffer = cacheStore.get(filename) + let compilationResult = cachedVm.runInThisContextCached(wrapper, filename, buffer) + compiledWrapper = compilationResult.result + if (compilationResult.wasRejected) { + cacheStore.delete(filename) + } + } else { + let compilationResult = cachedVm.runInThisContext(wrapper, filename) + if (compilationResult.cacheBuffer) { + cacheStore.set(filename, compilationResult.cacheBuffer) + } + compiledWrapper = compilationResult.result + } + if (global.v8debug) { + if (!resolvedArgv) { + // we enter the repl if we're not given a filename argument. + if (process.argv[1]) { + resolvedArgv = Module._resolveFilename(process.argv[1], null) + } else { + resolvedArgv = 'repl' + } + } + + // Set breakpoint on module start + if (filename === resolvedArgv) { + // Installing this dummy debug event listener tells V8 to start + // the debugger. Without it, the setBreakPoint() fails with an + // 'illegal access' error. + global.v8debug.Debug.setListener(function () {}) + global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0) + } + } + let args = [self.exports, require, self, filename, dirname, process, global] + return compiledWrapper.apply(self.exports, args) + } + } + + restorePreviousModuleCompile () { + Module.prototype._compile = this.previousModuleCompile + } +} + +module.exports = new NativeCompileCache() diff --git a/src/package-manager.coffee b/src/package-manager.coffee index f6cef9465..5c0df4b70 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -467,7 +467,7 @@ class PackageManager detail = "#{error.message} in #{metadataPath}" stack = "#{error.stack}\n at #{metadataPath}:1:1" message = "Failed to load the #{path.basename(packagePath)} package" - @notificationManager.addError(message, {stack, detail, dismissable: true}) + @notificationManager.addError(message, {stack, detail, packageName: path.basename(packagePath), dismissable: true}) uninstallDirectory: (directory) -> symlinkPromise = new Promise (resolve) -> diff --git a/src/package.coffee b/src/package.coffee index 00963ef28..4cd6a18fd 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -293,7 +293,7 @@ class Package if error? detail = "#{error.message} in #{grammarPath}" stack = "#{error.stack}\n at #{grammarPath}:1:1" - @notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true}) + @notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, packageName: @name, dismissable: true}) else grammar.packageName = @name grammar.bundledPackage = @bundledPackage @@ -317,7 +317,7 @@ class Package if error? detail = "#{error.message} in #{settingsPath}" stack = "#{error.stack}\n at #{settingsPath}:1:1" - @notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true}) + @notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, packageName: @name, dismissable: true}) else @settings.push(settings) settings.activate() if @settingsActivated @@ -634,4 +634,4 @@ class Package detail = error.message stack = error.stack ? error - @notificationManager.addFatalError(message, {stack, detail, dismissable: true}) + @notificationManager.addFatalError(message, {stack, detail, packageName: @name, dismissable: true}) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 786965d41..04052751d 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -573,6 +573,45 @@ class TextEditor extends Model else 'untitled' + # Essential: Get unique title for display in other parts of the UI + # such as the window title. + # + # If the editor's buffer is unsaved, its title is "untitled" + # If the editor's buffer is saved, its unique title is formatted as one + # of the following, + # * "" when it is the only editing buffer with this file name. + # * "/.../", where the "..." may be omitted + # if the the direct parent directory is already different. + # + # Returns a {String} + getUniqueTitle: -> + if sessionPath = @getPath() + title = @getTitle() + + # find text editors with identical file name. + paths = [] + for textEditor in atom.workspace.getTextEditors() when textEditor isnt this + if textEditor.getTitle() is title + paths.push(textEditor.getPath()) + if paths.length is 0 + return title + fileName = path.basename(sessionPath) + + # find the first directory in all these paths that is unique + nLevel = 0 + while (_.some(paths, (apath) -> path.basename(apath) is path.basename(sessionPath))) + sessionPath = path.dirname(sessionPath) + paths = _.map(paths, (apath) -> path.dirname(apath)) + nLevel += 1 + + directory = path.basename sessionPath + if nLevel > 1 + path.join(directory, "...", fileName) + else + path.join(directory, fileName) + else + 'untitled' + # Essential: Get the editor's long title for display in other parts of the UI # such as the window title. # @@ -879,6 +918,7 @@ class TextEditor extends Model @foldBufferRow(foldedRow) @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true, autoscroll: true) + @autoIndentSelectedRows() if @shouldAutoIndent() # Move lines intersecting the most recent selection down by one row in screen # coordinates. @@ -935,6 +975,7 @@ class TextEditor extends Model @foldBufferRow(foldedRow) @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true, autoscroll: true) + @autoIndentSelectedRows() if @shouldAutoIndent() # Duplicate the most recent cursor's current line. duplicateLines: -> diff --git a/static/index.js b/static/index.js index 927c25266..2a5bcad3a 100644 --- a/static/index.js +++ b/static/index.js @@ -1,9 +1,11 @@ (function () { - var fs = require('fs') var path = require('path') + var FileSystemBlobStore = require('../src/file-system-blob-store') + var NativeCompileCache = require('../src/native-compile-cache') var loadSettings = null var loadSettingsError = null + var blobStore = null window.onload = function () { try { @@ -13,8 +15,11 @@ console.error('Unhandled promise rejection %o with error: %o', promise, error) }) - // Ensure ATOM_HOME is always set before anything else is required - setupAtomHome() + blobStore = FileSystemBlobStore.load( + path.join(process.env.ATOM_HOME, 'blob-store/') + ) + NativeCompileCache.setCacheStore(blobStore) + NativeCompileCache.install() // Normalize to make sure drive letter case is consistent on Windows process.resourcesPath = path.normalize(process.resourcesPath) @@ -76,28 +81,11 @@ setupVmCompatibility() setupCsonCache(CompileCache.getCacheDirectory()) - require(loadSettings.windowInitializationScript) + var initialize = require(loadSettings.windowInitializationScript) + initialize({blobStore: blobStore}) require('ipc').sendChannel('window-command', 'window:loaded') } - function setupAtomHome () { - if (!process.env.ATOM_HOME) { - var home - if (process.platform === 'win32') { - home = process.env.USERPROFILE - } else { - home = process.env.HOME - } - var atomHome = path.join(home, '.atom') - try { - atomHome = fs.realpathSync(atomHome) - } catch (error) { - // Ignore since the path might just not exist yet. - } - process.env.ATOM_HOME = atomHome - } - } - function setupCsonCache (cacheDir) { require('season').setCacheDir(path.join(cacheDir, 'cson')) } @@ -181,6 +169,20 @@ }, false) } + var setupAtomHome = function () { + if (process.env.ATOM_HOME) { + return + } + + // Ensure ATOM_HOME is always set before anything else is required + // This is because of a difference in Linux not inherited between browser and render processes + // https://github.com/atom/atom/issues/5412 + if (loadSettings && loadSettings.atomHome) { + process.env.ATOM_HOME = loadSettings.atomHome + } + } + parseLoadSettings() + setupAtomHome() setupWindowBackground() })()