diff --git a/.travis.yml b/.travis.yml index bf1bd330e..3ce84c266 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,10 @@ install: script: script/cibuild +cache: + directories: + - node_modules + notifications: email: on_success: never diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e2254d0..057b8bcf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,9 @@ See https://atom.io/releases * The tree-view now sorts directory entries more naturally, in a locale-sensitive way. * Lines can now be moved up and down with multiple cursors. +* Improved the performance of marker-dependent code paths such as spell-check and find and replace. +* Fixed copying and pasting in native input fields. +* By default, windows with no pane items are now closed via the `core:close` command. The previous behavior can be restored via the `Close Empty Windows` option in settings. +* Fixed an issue where characters were inserted when toggling the settings view on some keyboard layouts. +* Modules can now temporarily override `Error.prepareStackTrace`. There is also an `Error.prototype.getRawStack()` method if you just need access to the raw v8 trace structure. +* Fixed a problem that caused blurry fonts on monitors that have a slightly higher resolution than 96 DPI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8170b0121..36de7b46c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,7 @@ These are just guidelines, not rules, use your best judgment and feel free to pr [How Can I Contribute?](#how-can-i-contribute) * [Reporting Bugs](#reporting-bugs) + * [Suggesting Enhancements](#suggesting-enhancements) * [Your First Code Contribution](#your-first-code-contribution) * [Pull Requests](#pull-requests) @@ -157,6 +158,60 @@ Include details about your configuration and environment: * Problem can be reliably reproduced, doesn't happen randomly: [Yes/No] * Problem happens with all files and projects, not only some files or projects: [Yes/No] +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. + +Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). If you'd like, you can use [this template](#template-for-submitting-enhancement-suggestions) to structure the information. + +#### Before Submitting An Enhancement Suggestion + +* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings). +* **Check if there's already [a package](https://atom.io/packages) which provides that enhancement.** +* **Determine [which repository the enhancement should be suggested in](#atom-and-packages).** +* **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. + +#### How Do I Submit A (Good) Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your enhancement suggestions is related to, create an issue on that repository and provide the following information: + +* **Use a clear and descriptive title** for the issue to identify the suggestion. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. +* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Atom which the suggestion is related to. You can use [this tool](http://www.cockos.com/licecap/) to record GIFs on OSX and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **Explain why this enhancement would be useful** to most Atom users and isn't something that can or should be implemented as a [community package](#atom-and-packages). +* **List some other text editors or applications where this enhancement exists.** +* **Specify which version of Atom you're using.** You can get the exact version by running `atom -v` in your terminal, or by starting Atom and running the `Application: About` command from the [Command Palette](https://github.com/atom/command-palette). +* **Specify the name and version of the OS you're using.** + +#### Template For Submitting Enhancement Suggestions + + [Short description of suggestion] + + **Steps which explain the enhancement** + + 1. [First Step] + 2. [Second Step] + 3. [Other Steps...] + + **Current and suggested behavior** + + [Describe current and suggested behavior here] + + **Why would the enhancement be useful to most users** + + [Explain why the enhancement would be useful to most users] + + [List some other text editors or applications where this enhancement exists] + + **Screenshots and GIFs** + + ![Screenshots and GIFs which demonstrate the steps or part of Atom the enhancement suggestion is related to](url) + + **Atom Version:** [Enter Atom version here] + **OS and Version:** [Enter OS name and version here] + ### Your First Code Contribution Unsure where to begin contributing to Atom? You can start by looking through these `beginner` and `help-wanted` issues: @@ -200,6 +255,7 @@ Both issue lists are sorted by total number of comments. While not perfect, numb * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") * Limit the first line to 72 characters or less * Reference issues and pull requests liberally +* When only changing documentation, include `[ci skip]` in the commit description * Consider starting the commit message with an applicable emoji: * :art: `:art:` when improving the format/structure of the code * :racehorse: `:racehorse:` when improving performance diff --git a/apm/package.json b/apm/package.json index d6ec869c3..b8dda21ea 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.1" + "atom-package-manager": "1.5.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 8dd1c573b..26d9c2f42 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -284,6 +284,7 @@ module.exports = (grunt) -> ciTasks.push('download-electron') ciTasks.push('download-electron-chromedriver') ciTasks.push('build') + ciTasks.push('fingerprint') ciTasks.push('dump-symbols') if process.platform isnt 'win32' ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar') ciTasks.push('mkdeb') if process.platform is 'linux' diff --git a/build/package.json b/build/package.json index 2ce92de17..de9053006 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.12.0", + "legal-eagle": "~0.13.0", "minidump": "~0.9", "npm": "2.13.3", "rcedit": "~0.3.0", diff --git a/build/tasks/fingerprint-task.js b/build/tasks/fingerprint-task.js new file mode 100644 index 000000000..9cfcdae76 --- /dev/null +++ b/build/tasks/fingerprint-task.js @@ -0,0 +1,7 @@ +var fingerprint = require('../../script/utils/fingerprint') + +module.exports = function (grunt) { + grunt.registerTask('fingerprint', 'Fingerpint the node_modules folder for caching on CI', function () { + fingerprint.writeFingerprint() + }) +} diff --git a/exports/atom.coffee b/exports/atom.coffee index c3601e1cc..81d1726b8 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -35,7 +35,6 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE The `TextEditor` constructor is no longer public. To construct a text editor, use `atom.workspace.buildTextEditor()`. - To check if an object is a text editor, look for for the existence of - a public method that you're using (e.g. `::getText`). + To check if an object is a text editor, use `atom.workspace.isTextEditor(object)`. """ TextEditor diff --git a/package.json b/package.json index 31086502b..1a5aa9437 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "0.34.3", + "electronVersion": "0.34.5", "dependencies": { "async": "0.2.6", "atom-keymap": "^6.1.1", @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^8.0.4", + "text-buffer": "8.0.9", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -64,10 +64,10 @@ "atom-light-ui": "0.43.0", "base16-tomorrow-dark-theme": "1.0.0", "base16-tomorrow-light-theme": "1.0.0", - "one-dark-ui": "1.1.5", + "one-dark-ui": "1.1.7", "one-dark-syntax": "1.1.1", "one-light-syntax": "1.1.1", - "one-light-ui": "1.1.5", + "one-light-ui": "1.1.7", "solarized-dark-syntax": "0.39.0", "solarized-light-syntax": "0.23.0", "about": "1.1.0", @@ -75,7 +75,7 @@ "autocomplete-atom-api": "0.9.2", "autocomplete-css": "0.11.0", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.23.0", + "autocomplete-plus": "2.23.1", "autocomplete-snippets": "1.9.0", "autoflow": "0.26.0", "autosave": "0.23.0", @@ -98,12 +98,12 @@ "line-ending-selector": "0.3.0", "link": "0.31.0", "markdown-preview": "0.156.2", - "metrics": "0.53.0", - "notifications": "0.61.0", + "metrics": "0.53.1", + "notifications": "0.62.1", "open-on-github": "0.40.0", "package-generator": "0.41.0", "release-notes": "0.53.0", - "settings-view": "0.232.0", + "settings-view": "0.232.1", "snippets": "1.0.1", "spell-check": "0.63.0", "status-bar": "0.80.0", @@ -116,20 +116,20 @@ "welcome": "0.33.0", "whitespace": "0.32.1", "wrap-guide": "0.38.1", - "language-c": "0.49.0", + "language-c": "0.50.1", "language-clojure": "0.18.0", - "language-coffee-script": "0.43.0", + "language-coffee-script": "0.46.0", "language-csharp": "0.11.0", - "language-css": "0.35.0", + "language-css": "0.35.1", "language-gfm": "0.81.0", "language-git": "0.10.0", "language-go": "0.40.0", "language-html": "0.42.0", "language-hyperlink": "0.15.0", - "language-java": "0.16.1", - "language-javascript": "0.101.1", + "language-java": "0.17.0", + "language-javascript": "0.102.2", "language-json": "0.17.1", - "language-less": "0.28.3", + "language-less": "0.29.0", "language-make": "0.20.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.0", @@ -137,16 +137,16 @@ "language-php": "0.34.0", "language-property-list": "0.8.0", "language-python": "0.42.1", - "language-ruby": "0.64.0", + "language-ruby": "0.64.1", "language-ruby-on-rails": "0.24.0", - "language-sass": "0.43.0", + "language-sass": "0.44.1", "language-shellscript": "0.20.0", "language-source": "0.9.0", "language-sql": "0.19.0", "language-text": "0.7.0", "language-todo": "0.27.0", - "language-toml": "0.16.0", - "language-xml": "0.34.0", + "language-toml": "0.17.0", + "language-xml": "0.34.1", "language-yaml": "0.24.0" }, "private": true, diff --git a/script/cibuild b/script/cibuild index c1aedddc8..b3f0b3f83 100755 --- a/script/cibuild +++ b/script/cibuild @@ -1,5 +1,7 @@ #!/usr/bin/env node var cp = require('./utils/child-process-wrapper.js'); +var crypto = require('crypto') +var fingerprint = require('./utils/fingerprint') var fs = require('fs'); var path = require('path'); @@ -42,6 +44,11 @@ function setEnvironmentVariables() { } function removeNodeModules() { + if (fingerprint.fingerprintMatches()) { + console.log('node_modules matches current fingerprint ' + fingerprint.fingerprint() + ' - not removing') + return + } + var fsPlus; try { fsPlus = require('fs-plus'); @@ -98,8 +105,8 @@ cp.safeExec.bind(global, 'npm install npm --loglevel error', {cwd: path.resolve( var async = require('async'); var gruntPath = path.join('build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : ''); var tasks = [ - cp.safeExec.bind(global, 'git clean -dff'), - cp.safeExec.bind(global, gruntPath + ' ci --gruntfile build/Gruntfile.coffee --stack --no-color'), + cp.safeExec.bind(global, 'git clean -dff -e node_modules'), // If we left them behind in removeNodeModules() they are OK to use + cp.safeExec.bind(global, gruntPath + ' ci --gruntfile build/Gruntfile.coffee --stack --no-color') ] async.series(tasks, function(error) { process.exit(error ? 1 : 0); diff --git a/script/utils/fingerprint.js b/script/utils/fingerprint.js new file mode 100644 index 000000000..2da48039f --- /dev/null +++ b/script/utils/fingerprint.js @@ -0,0 +1,31 @@ +var crypto = require('crypto') +var fs = require('fs') +var path = require('path') + +var fingerprintPath = path.resolve(__dirname, '..', '..', 'node_modules', '.atom-ci-fingerprint') + +module.exports = { + fingerprint: function () { + var packageJson = fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json')) + var body = packageJson.toString() + process.platform + process.version + return crypto.createHash('sha1').update(body).digest('hex') + }, + + writeFingerprint: function () { + var fingerprint = this.fingerprint() + fs.writeFileSync(fingerprintPath, fingerprint) + console.log('Wrote ci fingerprint:', fingerprintPath, fingerprint) + }, + + readFingerprint: function() { + if (fs.existsSync(fingerprintPath)) { + return fs.readFileSync(fingerprintPath).toString() + } else { + return null + } + }, + + fingerprintMatches: function () { + return this.readFingerprint() && this.readFingerprint() === this.fingerprint() + } +} diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 3e6536681..fd43a9616 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -45,9 +45,11 @@ describe "AtomEnvironment", -> expect(atom.config.get('editor.showInvisibles')).toBe false describe "window onerror handler", -> + devToolsPromise = null beforeEach -> - spyOn atom, 'openDevTools' - spyOn atom, 'executeJavaScriptInDevTools' + devToolsPromise = Promise.resolve() + spyOn(atom, 'openDevTools').andReturn(devToolsPromise) + spyOn(atom, 'executeJavaScriptInDevTools') it "will open the dev tools when an error is triggered", -> try @@ -55,8 +57,10 @@ describe "AtomEnvironment", -> catch e window.onerror.call(window, e.toString(), 'abc', 2, 3, e) - expect(atom.openDevTools).toHaveBeenCalled() - expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() + waitsForPromise -> devToolsPromise + runs -> + expect(atom.openDevTools).toHaveBeenCalled() + expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() describe "::onWillThrowError", -> willThrowSpy = null diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index d80e05fc5..8a6cc214e 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -69,3 +69,18 @@ describe 'CompileCache', -> CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome) expect(CSONParser.parse.callCount).toBe 1 + + describe 'overriding Error.prepareStackTrace', -> + it 'removes the override on the next tick, and always assigns the raw stack', -> + Error.prepareStackTrace = -> 'a-stack-trace' + + error = new Error("Oops") + expect(error.stack).toBe 'a-stack-trace' + expect(Array.isArray(error.getRawStack())).toBe true + + waits(1) + runs -> + error = new Error("Oops again") + console.log error.stack + expect(error.stack).toContain('compile-cache-spec.coffee') + expect(Array.isArray(error.getRawStack())).toBe true diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 9934b1917..1872b8c65 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -7,6 +7,7 @@ class FakeLinesYardstick prepareScreenRowsForMeasurement: -> @presenter.getPreMeasurementState() + @screenRows = new Set(@presenter.getScreenRows()) getScopedCharacterWidth: (scopeNames, char) -> @getScopedCharacterWidths(scopeNames)[char] @@ -34,6 +35,8 @@ class FakeLinesYardstick left = 0 column = 0 + return {top, left: 0} unless @screenRows.has(screenPosition.row) + iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() while iterator.next() characterWidths = @getScopedCharacterWidths(iterator.getScopes()) diff --git a/spec/fixtures/testdir/sample-theme-2/src/js/plugin/main.js b/spec/fixtures/testdir/sample-theme-2/src/js/plugin/main.js new file mode 100644 index 000000000..e69de29bb diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 54899a1d6..8dd34fde8 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -694,6 +694,20 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 100, contentFrameWidth: 10 * maxLineLength + 20, baseCharacterWidth: 10, verticalScrollbarWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 - 10 # subtract vertical scrollbar width + describe "when the longest screen row is the first one and it's hidden", -> + it "doesn't compute an invalid value (regression)", -> + presenter = buildPresenter(tileSize: 2, contentFrameWidth: 10, explicitHeight: 20) + editor.setText """ + a very long long long long long long line + b + c + d + e + """ + + expectStateUpdate presenter, -> presenter.setScrollTop(40) + expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + it "updates when the ::contentFrameWidth changes", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) @@ -1704,6 +1718,18 @@ describe "TextEditorPresenter", -> expectUndefinedStateForHighlight(presenter, highlight) + it "does not include highlights that end before the first visible row", -> + editor.setText("Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.") + editor.setSoftWrapped(true) + editor.setWidth(100, true) + editor.setDefaultCharWidth(10) + + marker = editor.markBufferRange([[0, 0], [0, 4]], invalidate: 'never') + highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') + presenter = buildPresenter(explicitHeight: 30, scrollTop: 10, tileSize: 2) + + expect(stateForHighlightInTile(presenter, highlight, 0)).toBeUndefined() + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[6, 2], [6, 4]], diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 39740ebd2..0cee8215a 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -168,7 +168,7 @@ describe "TextEditor", -> buffer.setPath(undefined) expect(editor.getLongTitle()).toBe 'untitled' - it "returns / when opened files has identical file names", -> + it "returns '' when opened files have identical file names", -> editor1 = null editor2 = null waitsForPromise -> @@ -177,10 +177,10 @@ describe "TextEditor", -> atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> editor2 = o runs -> - expect(editor1.getLongTitle()).toBe 'sample-theme-1/readme' - expect(editor2.getLongTitle()).toBe 'sample-theme-2/readme' + expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" + expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - it "or returns /.../ when opened files has identical file names", -> + it "returns '' when opened files have identical file and dir names", -> editor1 = null editor2 = null waitsForPromise -> @@ -189,9 +189,20 @@ describe "TextEditor", -> atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> editor2 = o runs -> - expect(editor1.getLongTitle()).toBe 'sample-theme-1/.../main.js' - expect(editor2.getLongTitle()).toBe 'sample-theme-2/.../main.js' + expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js" + expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js" + it "returns '' when opened files have identical file and same parent dir name", -> + editor1 = null + editor2 = null + waitsForPromise -> + atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> + editor1 = o + atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')).then (o) -> + editor2 = o + runs -> + expect(editor1.getLongTitle()).toBe "main.js \u2014 js" + expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin" it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> observed = [] diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index 3148942b4..a988ae7de 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -200,3 +200,34 @@ describe "WindowEventHandler", -> expect(dispatchedCommands.length).toBe 1 expect(dispatchedCommands[0].type).toBe 'foo-command' + + describe "native key bindings", -> + it "correctly dispatches them to active elements with the '.native-key-bindings' class", -> + webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"]) + spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({ + webContents: webContentsSpy + }) + + nativeKeyBindingsInput = document.createElement("input") + nativeKeyBindingsInput.classList.add("native-key-bindings") + jasmine.attachToDOM(nativeKeyBindingsInput) + nativeKeyBindingsInput.focus() + + atom.dispatchApplicationMenuCommand("core:copy") + atom.dispatchApplicationMenuCommand("core:paste") + + expect(webContentsSpy.copy).toHaveBeenCalled() + expect(webContentsSpy.paste).toHaveBeenCalled() + + webContentsSpy.copy.reset() + webContentsSpy.paste.reset() + + normalInput = document.createElement("input") + jasmine.attachToDOM(normalInput) + normalInput.focus() + + atom.dispatchApplicationMenuCommand("core:copy") + atom.dispatchApplicationMenuCommand("core:paste") + + expect(webContentsSpy.copy).not.toHaveBeenCalled() + expect(webContentsSpy.paste).not.toHaveBeenCalled() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 5d7343540..e150761af 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -76,7 +76,7 @@ describe "Workspace", -> expect(editor4.getCursorScreenPosition()).toEqual [2, 4] expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - expect(document.title).toBe "#{path.basename(editor3.getPath())} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toBe "#{path.basename(editor3.getLongTitle())} - #{atom.project.getPaths()[0]} - Atom" describe "where there are no open panes or editors", -> it "constructs the view with no open editors", -> @@ -658,6 +658,13 @@ describe "Workspace", -> waitsForPromise -> workspace.openLicense() runs -> expect(workspace.getActivePaneItem().getText()).toMatch /Copyright/ + describe "::isTextEditor(obj)", -> + it "returns true when the passed object is an instance of `TextEditor`", -> + expect(workspace.isTextEditor(atom.workspace.buildTextEditor())).toBe(true) + expect(workspace.isTextEditor({getText: ->})).toBe(false) + expect(workspace.isTextEditor(null)).toBe(false) + expect(workspace.isTextEditor(undefined)).toBe(false) + describe "::observeTextEditors()", -> it "invokes the observer with current and future text editors", -> observed = [] @@ -776,8 +783,8 @@ describe "Workspace", -> applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom) }) workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) - item = atom.workspace.getActivePaneItem() - expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]} - Atom" + item = workspace2.getActivePaneItem() + expect(document.title).toBe "#{item.getLongTitle()} - #{atom.project.getPaths()[0]} - Atom" workspace2.destroy() describe "document edited status", -> @@ -1438,11 +1445,12 @@ describe "Workspace", -> save = -> atom.workspace.saveActivePaneItem() expect(save).toThrow() - describe "::destroyActivePaneItemOrEmptyPane", -> + describe "::closeActivePaneItemOrEmptyPaneOrWindow", -> beforeEach -> + spyOn(atom, 'close') waitsForPromise -> atom.workspace.open() - it "closes the active pane item until all that remains is a single empty pane", -> + it "closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane", -> atom.config.set('core.destroyEmptyPanes', false) pane1 = atom.workspace.getActivePane() @@ -1450,19 +1458,22 @@ describe "Workspace", -> expect(atom.workspace.getPanes().length).toBe 2 expect(pane2.getItems().length).toBe 1 - atom.workspace.destroyActivePaneItemOrEmptyPane() + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getPanes().length).toBe 2 expect(pane2.getItems().length).toBe 0 - atom.workspace.destroyActivePaneItemOrEmptyPane() + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getPanes().length).toBe 1 expect(pane1.getItems().length).toBe 1 - atom.workspace.destroyActivePaneItemOrEmptyPane() + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getPanes().length).toBe 1 expect(pane1.getItems().length).toBe 0 - atom.workspace.destroyActivePaneItemOrEmptyPane() + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() expect(atom.workspace.getPanes().length).toBe 1 + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.close).toHaveBeenCalled() diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 1e05b3dbb..d6937f2d3 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -66,13 +66,42 @@ class ApplicationDelegate ipc.send("call-window-method", "setFullScreen", fullScreen) openWindowDevTools: -> - remote.getCurrentWindow().openDevTools() + new Promise (resolve) -> + # Defer DevTools interaction to the next tick, because using them during + # event handling causes some wrong input events to be triggered on + # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + process.nextTick -> + if remote.getCurrentWindow().isDevToolsOpened() + resolve() + else + remote.getCurrentWindow().once("devtools-opened", -> resolve()) + ipc.send("call-window-method", "openDevTools") + + closeWindowDevTools: -> + new Promise (resolve) -> + # Defer DevTools interaction to the next tick, because using them during + # event handling causes some wrong input events to be triggered on + # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + process.nextTick -> + unless remote.getCurrentWindow().isDevToolsOpened() + resolve() + else + remote.getCurrentWindow().once("devtools-closed", -> resolve()) + ipc.send("call-window-method", "closeDevTools") toggleWindowDevTools: -> - remote.getCurrentWindow().toggleDevTools() + new Promise (resolve) => + # Defer DevTools interaction to the next tick, because using them during + # event handling causes some wrong input events to be triggered on + # `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). + process.nextTick => + if remote.getCurrentWindow().isDevToolsOpened() + @closeWindowDevTools().then(resolve) + else + @openWindowDevTools().then(resolve) executeJavaScriptInWindowDevTools: (code) -> - remote.getCurrentWindow().executeJavaScriptInDevTools(code) + ipc.send("call-window-method", "executeJavaScriptInDevTools", code) setWindowDocumentEdited: (edited) -> ipc.send("call-window-method", "setDocumentEdited", edited) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index c56bcb493..e8d656a43 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -670,8 +670,7 @@ class AtomEnvironment extends Model @emitter.emit 'will-throw-error', eventObject if openDevTools - @openDevTools() - @executeJavaScriptInDevTools('DevToolsAPI.showConsole()') + @openDevTools().then => @executeJavaScriptInDevTools('DevToolsAPI.showConsole()') @emitter.emit 'did-throw-error', {message, url, line, column, originalError} @@ -721,10 +720,15 @@ class AtomEnvironment extends Model ### # Extended: Open the dev tools for the current window. + # + # Returns a {Promise} that resolves when the DevTools have been opened. openDevTools: -> @applicationDelegate.openWindowDevTools() # Extended: Toggle the visibility of the dev tools for the current window. + # + # Returns a {Promise} that resolves when the DevTools have been opened or + # closed. toggleDevTools: -> @applicationDelegate.toggleWindowDevTools() diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 8bb44349e..8e8e4af54 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -373,6 +373,8 @@ class AtomApplication # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) -> + devMode = Boolean(devMode) + safeMode = Boolean(safeMode) locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen) pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen) diff --git a/src/compile-cache.js b/src/compile-cache.js index f7726b4b3..bedbd2549 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -158,25 +158,33 @@ require('source-map-support').install({ } }) -var sourceMapPrepareStackTrace = Error.prepareStackTrace -var prepareStackTrace = sourceMapPrepareStackTrace +var prepareStackTraceWithSourceMapping = Error.prepareStackTrace -// Prevent coffee-script from reassigning Error.prepareStackTrace -Object.defineProperty(Error, 'prepareStackTrace', { - get: function () { return prepareStackTrace }, - set: function (newValue) {} -}) +let prepareStackTrace = prepareStackTraceWithSourceMapping -// Enable Grim to access the raw stack without reassigning Error.prepareStackTrace -Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native - prepareStackTrace = getRawStack - var result = this.stack - prepareStackTrace = sourceMapPrepareStackTrace - return result +function prepareStackTraceWithRawStackAssignment (error, frames) { + error.rawStack = frames + return prepareStackTrace(error, frames) } -function getRawStack (_, stack) { - return stack +Object.defineProperty(Error, 'prepareStackTrace', { + get: function () { + return prepareStackTraceWithRawStackAssignment + }, + + set: function (newValue) { + prepareStackTrace = newValue + process.nextTick(function () { + prepareStackTrace = prepareStackTraceWithSourceMapping + }) + } +}) + +Error.prototype.getRawStack = function () { // eslint-disable-line no-extend-native + // Access this.stack to ensure prepareStackTrace has been run on this error + // because it assigns this.rawStack as a side-effect + this.stack + return this.rawStack } Object.keys(COMPILERS).forEach(function (extension) { diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 08956d470..d9c0c1d21 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -21,7 +21,6 @@ module.exports = followSymlinks: type: 'boolean' default: true - title: 'Follow symlinks' description: 'Follow symbolic links when searching files and when opening files with the fuzzy finder.' disabledPackages: type: 'array' @@ -54,7 +53,12 @@ module.exports = destroyEmptyPanes: type: 'boolean' default: true - description: 'When the last item of a pane is removed, remove that pane as well.' + title: 'Remove Empty Panes' + description: 'When the last tab of a pane is closed, remove that pane as well.' + closeEmptyWindows: + type: 'boolean' + default: true + description: 'When a window with no open tabs or panes is given the \'Close Tab\' command, close that window.' fileEncoding: description: 'Default character set encoding to use when reading and writing files.' type: 'string' diff --git a/src/config.coffee b/src/config.coffee index 888193059..489e16016 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -699,7 +699,7 @@ class Config @endTransaction() fn(args...) result = callback() - new Promise (resolve, reject) => + new Promise (resolve, reject) -> result.then(endTransaction(resolve)).catch(endTransaction(reject)) catch error @endTransaction() diff --git a/src/default-directory-searcher.coffee b/src/default-directory-searcher.coffee index 1c9b40312..6b8ffe3e3 100644 --- a/src/default-directory-searcher.coffee +++ b/src/default-directory-searcher.coffee @@ -1,8 +1,7 @@ Task = require './task' -# Public: Searches local files for lines matching a specified regex. -# -# Implements thenable so it can be used with `Promise.all()`. +# Searches local files for lines matching a specified regex. Implements `.then()` +# so that it can be used with `Promise.all()`. class DirectorySearch constructor: (rootPaths, regex, options) -> scanHandlerOptions = @@ -22,31 +21,25 @@ class DirectorySearch @task.terminate() resolve() - # Public: Implementation of `then()` to satisfy the *thenable* contract. - # This makes it possible to use a `DirectorySearch` with `Promise.all()`. - # - # Returns `Promise`. then: (args...) -> @promise.then.apply(@promise, args) - # Public: Cancels the search. cancel: -> # This will cause @promise to reject. @task.cancel() null - # Default provider for the `atom.directory-searcher` service. module.exports = class DefaultDirectorySearcher - # Public: Determines whether this object supports search for a `Directory`. + # Determines whether this object supports search for a `Directory`. # # * `directory` {Directory} whose search needs might be supported by this object. # # Returns a `boolean` indicating whether this object can search this `Directory`. canSearchDirectory: (directory) -> true - # Public: Performs a text search for files in the specified `Directory`, subject to the + # Performs a text search for files in the specified `Directory`, subject to the # specified parameters. # # Results are streamed back to the caller by invoking methods on the specified `options`, diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 6c838b8c0..159ea1abc 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -55,7 +55,7 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'window:log-deprecation-warnings': -> Grim.logDeprecations() 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent")) 'pane:reopen-closed-item': -> @getModel().reopenItem() - 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane() + 'core:close': -> @getModel().closeActivePaneItemOrEmptyPaneOrWindow() 'core:save': -> @getModel().saveActivePaneItem() 'core:save-as': -> @getModel().saveActivePaneItemAs() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7bd66b87f..405e34548 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -377,7 +377,8 @@ class TextEditorPresenter endRow = @constrainRow(@getEndTileRow() + @tileSize) screenRows = [startRow...endRow] - if longestScreenRow = @model.getLongestScreenRow() + longestScreenRow = @model.getLongestScreenRow() + if longestScreenRow? screenRows.push(longestScreenRow) if @screenRowsToMeasure? screenRows.push(@screenRowsToMeasure...) @@ -1244,14 +1245,7 @@ class TextEditorPresenter updateHighlightState: (decorationId, properties, screenRange) -> return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements() - return if screenRange.isEmpty() - - if screenRange.start.row < @startRow - screenRange.start.row = @startRow - screenRange.start.column = 0 - if screenRange.end.row >= @endRow - screenRange.end.row = @endRow - screenRange.end.column = 0 + @constrainRangeToVisibleRowRange(screenRange) return if screenRange.isEmpty() @@ -1281,6 +1275,23 @@ class TextEditorPresenter true + constrainRangeToVisibleRowRange: (screenRange) -> + if screenRange.start.row < @startRow + screenRange.start.row = @startRow + screenRange.start.column = 0 + + if screenRange.end.row < @startRow + screenRange.end.row = @startRow + screenRange.end.column = 0 + + if screenRange.start.row >= @endRow + screenRange.start.row = @endRow + screenRange.start.column = 0 + + if screenRange.end.row >= @endRow + screenRange.end.row = @endRow + screenRange.end.column = 0 + repositionRegionWithinTile: (region, tileStartRow) -> region.top += @scrollTop - tileStartRow * @lineHeight region.left += @scrollLeft diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 29d56c6c8..a151c9dba 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -581,10 +581,7 @@ class TextEditor extends Model # # Returns a {String}. getTitle: -> - if sessionPath = @getPath() - path.basename(sessionPath) - else - 'untitled' + @getFileName() ? 'untitled' # Essential: Get unique title for display in other parts of the UI, such as # the window title. @@ -593,41 +590,52 @@ class TextEditor extends Model # 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. + # * "" when other buffers have this file name. # # Returns a {String} getLongTitle: -> - if sessionPath = @getPath() - title = @getTitle() + if @getPath() + fileName = @getFileName() - # find text editors with identical file name. - paths = [] + allPathSegments = [] 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) + if textEditor.getFileName() is fileName + allPathSegments.push(textEditor.getDirectoryPath().split(path.sep)) - # 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 + if allPathSegments.length is 0 + return fileName - directory = path.basename sessionPath - if nLevel > 1 - path.join(directory, "...", fileName) - else - path.join(directory, fileName) + ourPathSegments = @getDirectoryPath().split(path.sep) + allPathSegments.push ourPathSegments + + loop + firstSegment = ourPathSegments[0] + + commonBase = _.all(allPathSegments, (pathSegments) -> pathSegments.length > 1 and pathSegments[0] is firstSegment) + if commonBase + pathSegments.shift() for pathSegments in allPathSegments + else + break + + "#{fileName} \u2014 #{path.join(pathSegments...)}" else 'untitled' # Essential: Returns the {String} path of this editor's text buffer. getPath: -> @buffer.getPath() + getFileName: -> + if fullPath = @getPath() + path.basename(fullPath) + else + null + + getDirectoryPath: -> + if fullPath = @getPath() + path.dirname(fullPath) + else + null + # Extended: Returns the {String} character set encoding of this editor's text # buffer. getEncoding: -> @buffer.getEncoding() @@ -678,16 +686,16 @@ class TextEditor extends Model getSaveDialogOptions: -> {} checkoutHeadRevision: -> - if filePath = this.getPath() + if @getPath() checkoutHead = => - @project.repositoryForDirectory(new Directory(path.dirname(filePath))) + @project.repositoryForDirectory(new Directory(@getDirectoryPath())) .then (repository) => repository?.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 \"#{path.basename(filePath)}\" since the last Git commit?" + detailedMessage: "Are you sure you want to discard all changes to \"#{@getFileName()}\" since the last Git commit?" buttons: OK: checkoutHead Cancel: null diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index c786f7e9c..d3a231f77 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -42,9 +42,8 @@ class WindowEventHandler # `.native-key-bindings` class. handleNativeKeybindings: -> bindCommandToAction = (command, action) => - @addEventListener @document, command, (event) => - if event.target.webkitMatchesSelector('.native-key-bindings') - @applicationDelegate.getCurrentWindow().webContents[action]() + @subscriptions.add @atomEnvironment.commands.add '.native-key-bindings', command, (event) => + @applicationDelegate.getCurrentWindow().webContents[action]() bindCommandToAction('core:copy', 'copy') bindCommandToAction('core:paste', 'paste') diff --git a/src/workspace.coffee b/src/workspace.coffee index 396008201..04feef61e 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -155,7 +155,7 @@ class Workspace extends Model projectPaths = @project.getPaths() ? [] if item = @getActivePaneItem() itemPath = item.getPath?() - itemTitle = item.getTitle?() + itemTitle = item.getLongTitle?() ? item.getTitle?() projectPath = _.find projectPaths, (projectPath) -> itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) itemTitle ?= "untitled" @@ -518,6 +518,12 @@ class Workspace extends Model @project.bufferForPath(filePath, options).then (buffer) => @buildTextEditor(_.extend({buffer, largeFileMode}, options)) + # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. + # + # * `object` An {Object} you want to perform the check against. + isTextEditor: (object) -> + object instanceof TextEditor + # Extended: Create a new text editor. # # Returns a {TextEditor}. @@ -675,9 +681,15 @@ class Workspace extends Model destroyActivePane: -> @getActivePane()?.destroy() - # Destroy the active pane item or the active pane if it is empty. - destroyActivePaneItemOrEmptyPane: -> - if @getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane() + # Close the active pane item, or the active pane if it is empty, + # or the current window if there is only the empty root pane. + closeActivePaneItemOrEmptyPaneOrWindow: -> + if @getActivePaneItem()? + @destroyActivePaneItem() + else if @getPanes().length > 1 + @destroyActivePane() + else if @config.get('core.closeEmptyWindows') + atom.close() # Increase the editor font size by 1px. increaseFontSize: -> diff --git a/static/buttons.less b/static/buttons.less index cf616903a..42e04e414 100644 --- a/static/buttons.less +++ b/static/buttons.less @@ -31,12 +31,18 @@ font-size: @font-size - 2px; height: auto; line-height: 1.3em; + &.icon:before { + font-size: @font-size - 2px; + } } .btn.btn-sm, .btn-group-sm > .btn { padding: @component-padding/4 @component-padding/2; height: auto; line-height: 1.3em; + &.icon:before { + font-size: @font-size + 1px; + } } .btn.btn-lg, .btn-group-lg > .btn { @@ -44,6 +50,9 @@ padding: @component-padding - 2px @component-padding + 2px; height: auto; line-height: 1.3em; + &.icon:before { + font-size: @font-size + 6px; + } } .btn-group > .btn { @@ -63,6 +72,18 @@ border-bottom-right-radius: @component-border-radius; } +// Icon buttons +.btn.icon { + &:before { + width: initial; + height: initial; + margin-right: .3125em; + } + &:empty:before { + margin-right: 0; + } +} + .btn-toolbar { > .btn-group + .btn-group, > .btn-group + .btn, > .btn + .btn { float: none;