diff --git a/.python-version b/.python-version index 4712731cc..ecc17b8e9 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.12 +2.7.13 diff --git a/.travis.yml b/.travis.yml index 1e768e76a..87559ea3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,9 @@ install: - npm install -g npm - script/build --create-debian-package --create-rpm-package --compress-artifacts -script: script/test +script: + - script/lint + - script/test cache: directories: diff --git a/appveyor.yml b/appveyor.yml index 242b19f74..29606fb4e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,6 +27,7 @@ build_script: - script\build.cmd --code-sign --create-windows-installer --compress-artifacts test_script: + - script\lint.cmd - script\test.cmd deploy: off diff --git a/benchmarks/text-editor-large-file-construction.bench.js b/benchmarks/text-editor-large-file-construction.bench.js index 694729724..64c6f4d94 100644 --- a/benchmarks/text-editor-large-file-construction.bench.js +++ b/benchmarks/text-editor-large-file-construction.bench.js @@ -26,7 +26,7 @@ export default async function ({test}) { console.log(text.length / 1024) let t0 = window.performance.now() - const buffer = new TextBuffer(text) + const buffer = new TextBuffer({text}) const editor = new TextEditor({buffer, largeFileMode: true}) atom.workspace.getActivePane().activateItem(editor) let t1 = window.performance.now() diff --git a/benchmarks/text-editor-long-lines.bench.js b/benchmarks/text-editor-long-lines.bench.js new file mode 100644 index 000000000..a99220d4e --- /dev/null +++ b/benchmarks/text-editor-long-lines.bench.js @@ -0,0 +1,97 @@ +/** @babel */ + +import path from 'path' +import fs from 'fs' +import {TextEditor, TextBuffer} from 'atom' + +const SIZES_IN_KB = [ + 512, + 1024, + 2048 +] +const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '') +const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length)) + +export default async function ({test}) { + const data = [] + + const workspaceElement = atom.views.getView(atom.workspace) + document.body.appendChild(workspaceElement) + + atom.packages.loadPackages() + await atom.packages.activate() + + console.log(atom.getLoadSettings().resourcePath); + + for (let pane of atom.workspace.getPanes()) { + pane.destroy() + } + + for (const sizeInKB of SIZES_IN_KB) { + const text = TEXT.slice(0, sizeInKB * 1024) + console.log(text.length / 1024) + + let t0 = window.performance.now() + const buffer = new TextBuffer({text}) + const editor = new TextEditor({buffer, largeFileMode: true}) + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + atom.workspace.getActivePane().activateItem(editor) + let t1 = window.performance.now() + + data.push({ + name: 'Opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + const tickDurations = [] + for (let i = 0; i < 20; i++) { + await timeout(50) + t0 = window.performance.now() + await timeout(0) + t1 = window.performance.now() + tickDurations[i] = t1 - t0 + } + + data.push({ + name: 'Max time event loop was blocked after opening a large single-line file', + x: sizeInKB, + duration: Math.max(...tickDurations) + }) + + t0 = window.performance.now() + editor.setCursorScreenPosition(editor.element.screenPositionForPixelPosition({ + top: 100, + left: 30 + })) + t1 = window.performance.now() + + data.push({ + name: 'Clicking the editor after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + t0 = window.performance.now() + editor.element.setScrollTop(editor.element.getScrollTop() + 100) + t1 = window.performance.now() + + data.push({ + name: 'Scrolling down after opening a large single-line file', + x: sizeInKB, + duration: t1 - t0 + }) + + editor.destroy() + buffer.destroy() + await timeout(10000) + } + + workspaceElement.remove() + + return data +} + +function timeout (duration) { + return new Promise((resolve) => setTimeout(resolve, duration)) +} diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 5f0211c0d..1caf740ba 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -7,7 +7,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * OS with 64-bit or 32-bit architecture * C++11 toolchain * Git -* Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) +* Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm)) * npm 3.10.x or later (run `npm install -g npm`) * Ensure node-gyp uses python2 (run `npm config set python /usr/bin/python2 -g`, use `sudo` if you didn't install node via nvm) * Development headers for [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring). diff --git a/docs/build-instructions/macOS.md b/docs/build-instructions/macOS.md index 18169435f..0d9335eea 100644 --- a/docs/build-instructions/macOS.md +++ b/docs/build-instructions/macOS.md @@ -3,7 +3,7 @@ ## Requirements * macOS 10.8 or later - * Node.js 4.4.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) + * Node.js 6.x (we recommend installing it via [nvm](https://github.com/creationix/nvm)) * npm 3.10.x or later (run `npm install -g npm`) * Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install) diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 5c8c189ef..2c231b2dc 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -2,7 +2,7 @@ ## Requirements -* Node.js 4.4.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) +* Node.js 6.x (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) * Python v2.7.x * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27` * 7zip (7z.exe available from the command line) - for creating distribution zip files diff --git a/package.json b/package.json index 312828b68..0a799e71e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.15.0-dev", + "version": "1.16.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -15,10 +15,21 @@ "electronVersion": "1.3.13", "dependencies": { "async": "0.2.6", - "atom-keymap": "7.1.19", - "atom-select-list": "0.0.6", + "atom-keymap": "7.1.20", + "atom-select-list": "0.0.12", "atom-ui": "0.4.1", - "babel-core": "5.8.38", + "babel-core": "6.22.1", + "babel-plugin-add-module-exports": "0.2.1", + "babel-plugin-transform-async-to-generator": "6.22.0", + "babel-plugin-transform-class-properties": "6.23.0", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-transform-do-expressions": "6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "6.23.0", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-plugin-transform-flow-strip-types": "6.22.0", + "babel-plugin-transform-function-bind": "6.22.0", + "babel-plugin-transform-object-rest-spread": "6.23.0", + "babel-plugin-transform-react-jsx": "6.23.0", "cached-run-in-this-context": "0.4.1", "chai": "3.5.0", "chart.js": "^2.3.0", @@ -65,7 +76,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "10.2.5", + "text-buffer": "10.3.12", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -82,85 +93,86 @@ "one-light-ui": "1.9.1", "one-dark-syntax": "1.7.1", "one-light-syntax": "1.7.1", - "solarized-dark-syntax": "1.1.1", - "solarized-light-syntax": "1.1.1", + "solarized-dark-syntax": "1.1.2", + "solarized-light-syntax": "1.1.2", "about": "1.7.2", "archive-view": "0.62.2", "autocomplete-atom-api": "0.10.0", - "autocomplete-css": "0.14.2", + "autocomplete-css": "0.15.0", "autocomplete-html": "0.7.2", "autocomplete-plus": "2.34.2", "autocomplete-snippets": "1.11.0", "autoflow": "0.29.0", "autosave": "0.24.0", "background-tips": "0.26.1", - "bookmarks": "0.44.0", - "bracket-matcher": "0.85.2", - "command-palette": "0.40.0", - "deprecation-cop": "0.56.1", + "bookmarks": "0.44.1", + "bracket-matcher": "0.85.3", + "command-palette": "0.40.2", + "dalek": "0.2.0", + "deprecation-cop": "0.56.2", "dev-live-reload": "0.47.0", - "encoding-selector": "0.23.0", - "exception-reporting": "0.40.2", - "find-and-replace": "0.206.0", + "encoding-selector": "0.23.1", + "exception-reporting": "0.41.1", + "find-and-replace": "0.206.3", "fuzzy-finder": "1.4.1", - "git-diff": "1.3.0", + "git-diff": "1.3.1", "go-to-line": "0.32.0", - "grammar-selector": "0.49.0", - "image-view": "0.60.0", - "incompatible-packages": "0.26.1", - "keybinding-resolver": "0.36.0", - "line-ending-selector": "0.6.0", + "grammar-selector": "0.49.2", + "image-view": "0.61.0", + "incompatible-packages": "0.27.0", + "keybinding-resolver": "0.36.1", + "line-ending-selector": "0.6.1", "link": "0.31.2", - "markdown-preview": "0.159.3", - "metrics": "1.1.3", + "markdown-preview": "0.159.7", + "metrics": "1.2.1", "notifications": "0.66.2", "open-on-github": "1.2.1", "package-generator": "1.1.0", - "settings-view": "0.246.0", + "settings-view": "0.247.0", "snippets": "1.0.5", - "spell-check": "0.70.2", + "spell-check": "0.71.0", "status-bar": "1.8.1", - "styleguide": "0.49.0", + "styleguide": "0.49.2", "symbols-view": "0.114.0", "tabs": "0.104.1", - "timecop": "0.34.0", - "tree-view": "0.213.2", + "timecop": "0.36.0", + "tree-view": "0.214.1", "update-package-dependencies": "0.10.0", - "welcome": "0.36.0", - "whitespace": "0.36.1", - "wrap-guide": "0.39.0", - "language-c": "0.54.1", - "language-clojure": "0.22.1", - "language-coffee-script": "0.48.2", - "language-csharp": "0.14.1", + "welcome": "0.36.1", + "whitespace": "0.36.2", + "wrap-guide": "0.39.1", + "language-c": "0.56.0", + "language-clojure": "0.22.2", + "language-coffee-script": "0.48.4", + "language-csharp": "0.14.2", "language-css": "0.42.0", "language-gfm": "0.88.0", "language-git": "0.19.0", "language-go": "0.43.1", - "language-html": "0.47.1", + "language-html": "0.47.2", "language-hyperlink": "0.16.1", - "language-java": "0.25.0", - "language-javascript": "0.125.1", + "language-java": "0.26.0", + "language-javascript": "0.126.0", "language-json": "0.18.3", "language-less": "0.30.1", "language-make": "0.22.3", "language-mustache": "0.13.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.37.3", + "language-php": "0.37.4", "language-property-list": "0.9.0", - "language-python": "0.45.1", - "language-ruby": "0.70.4", - "language-ruby-on-rails": "0.25.1", + "language-python": "0.45.2", + "language-ruby": "0.70.5", + "language-ruby-on-rails": "0.25.2", "language-sass": "0.57.1", "language-shellscript": "0.25.0", "language-source": "0.9.0", - "language-sql": "0.25.2", + "language-sql": "0.25.3", "language-text": "0.7.1", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.34.15", - "language-yaml": "0.27.2" + "language-xml": "0.34.16", + "language-yaml": "0.28.0" }, "private": true, "scripts": { diff --git a/resources/win/apm.sh b/resources/win/apm.sh index 99ccfec69..3a479f53d 100644 --- a/resources/win/apm.sh +++ b/resources/win/apm.sh @@ -1,3 +1,3 @@ #!/bin/sh -"$(dirname "$0")/../app/apm/apm.sh" "$@" +"$(dirname "$0")/../app/apm/bin/apm" "$@" diff --git a/script/lib/code-sign-on-mac.js b/script/lib/code-sign-on-mac.js index 7d70e2b6b..5e4c67707 100644 --- a/script/lib/code-sign-on-mac.js +++ b/script/lib/code-sign-on-mac.js @@ -5,16 +5,17 @@ const path = require('path') const spawnSync = require('./spawn-sync') module.exports = function (packagedAppPath) { - if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL) { + if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { console.log('Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray) return } - // This declaration is outside of the try block because it is also used for the finally block - const certPath = path.join(os.tmpdir(), 'mac.p12') - try { + let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH; + if (!certPath) { + certPath = path.join(os.tmpdir(), 'mac.p12') downloadFileFromGithub(process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) - + } + try { console.log(`Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}`) const unlockArgs = ['unlock-keychain'] // For signing on local workstations, password could be entered interactively @@ -32,6 +33,18 @@ module.exports = function (packagedAppPath) { '-T', '/usr/bin/codesign' ]) + + console.log('Running incantation to suppress dialog when signing on macOS Sierra') + try { + spawnSync('security', [ + 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', + '-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, + process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN + ]) + } catch (e) { + console.log('Incantation failed... maybe this isn\'t Sierra?'); + } + console.log(`Code-signing application at ${packagedAppPath}`) spawnSync('codesign', [ '--deep', '--force', '--verbose', @@ -39,7 +52,9 @@ module.exports = function (packagedAppPath) { '--sign', 'Developer ID Application: GitHub', packagedAppPath ], {stdio: 'inherit'}) } finally { - console.log(`Deleting certificate at ${certPath}`) - fs.removeSync(certPath) + if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { + console.log(`Deleting certificate at ${certPath}`) + fs.removeSync(certPath) + } } } diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index fbf3630a3..f3271f198 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -18,15 +18,19 @@ module.exports = function (packagedAppPath, codeSign) { iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`, loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'), outputDirectory: CONFIG.buildOutputPath, - remoteReleases: `https://atom.io/api/updates${archSuffix}`, + remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.appMetadata.version}`, setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') } - const certPath = path.join(os.tmpdir(), 'win.p12') - const signing = codeSign && process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL + const signing = codeSign && (process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL || process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) + let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH; if (signing) { - downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) + if (!certPath) { + certPath = path.join(os.tmpdir(), 'win.p12') + downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) + } + var signParams = [] signParams.push(`/f ${certPath}`) // Signing cert file signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password @@ -39,7 +43,7 @@ module.exports = function (packagedAppPath, codeSign) { } const cleanUp = function () { - if (fs.existsSync(certPath)) { + if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { console.log(`Deleting certificate at ${certPath}`) fs.removeSync(certPath) } diff --git a/script/package.json b/script/package.json index 1284fd83c..87c43f261 100644 --- a/script/package.json +++ b/script/package.json @@ -3,7 +3,6 @@ "description": "Atom build scripts", "dependencies": { "async": "2.0.1", - "babel-core": "5.8.38", "coffeelint": "1.15.7", "colors": "1.1.2", "csslint": "1.0.2", diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 9b9715a07..d967fb97b 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -142,6 +142,11 @@ describe "AtomEnvironment", -> atom.assert(false, "a == b", (e) -> error = e) expect(error).toBe errors[0] + describe "if passed metadata", -> + it "assigns the metadata on the assertion failure's error object", -> + atom.assert(false, "a == b", {foo: 'bar'}) + expect(errors[0].metadata).toEqual {foo: 'bar'} + describe "if the condition is true", -> it "does nothing", -> result = atom.assert(true, "a == b") diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index 1600da9d7..a91059efe 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -1,4 +1,5 @@ path = require 'path' +process = require 'process' _ = require 'underscore-plus' grim = require 'grim' marked = require 'marked' @@ -20,12 +21,15 @@ formatStackTrace = (spec, message='', stackTrace) -> lines.shift() if message.trim() is errorMatch?[1]?.trim() for line, index in lines - # Remove prefix of lines matching: at [object Object]. (path:1:2) - prefixMatch = line.match(/at \[object Object\]\. \(([^)]+)\)/) + # Remove prefix of lines matching: at . (path:1:2) + prefixMatch = line.match(/at \. \(([^)]+)\)/) line = "at #{prefixMatch[1]}" if prefixMatch # Relativize locations to spec directory - lines[index] = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ') + if process.platform is 'win32' + line = line.replace('file:///', '').replace(///#{path.posix.sep}///g, path.win32.sep) + line = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ') + lines[index] = line.replace("(#{spec.specDirectory}#{path.sep}", '(') # at step (path:1:2) lines = lines.map (line) -> line.trim() lines.join('\n').trim() diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee index 44da440d9..e57660a57 100644 --- a/spec/decoration-manager-spec.coffee +++ b/spec/decoration-manager-spec.coffee @@ -1,13 +1,14 @@ DecorationManager = require '../src/decoration-manager' describe "DecorationManager", -> - [decorationManager, buffer, defaultMarkerLayer] = [] + [decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = [] beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') displayLayer = buffer.addDisplayLayer() - defaultMarkerLayer = displayLayer.addMarkerLayer() - decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer) + markerLayer1 = displayLayer.addMarkerLayer() + markerLayer2 = displayLayer.addMarkerLayer() + decorationManager = new DecorationManager(displayLayer) waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -17,38 +18,53 @@ describe "DecorationManager", -> buffer.release() describe "decorations", -> - [marker, decoration, decorationProperties] = [] + [layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = [] beforeEach -> - marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]]) + layer1Marker = markerLayer1.markBufferRange([[2, 13], [3, 15]]) decorationProperties = {type: 'line-number', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + layer2Marker = markerLayer2.markBufferRange([[2, 13], [3, 15]]) + layer2MarkerDecoration = decorationManager.decorateMarker(layer2Marker, decorationProperties) it "can add decorations associated with markers and remove them", -> - expect(decoration).toBeDefined() - expect(decoration.getProperties()).toBe decorationProperties - expect(decorationManager.decorationForId(decoration.id)).toBe decoration - expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration + expect(layer1MarkerDecoration).toBeDefined() + expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration + expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual { + "#{layer1Marker.id}": [layer1MarkerDecoration], + "#{layer2Marker.id}": [layer2MarkerDecoration] + } - decoration.destroy() - expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined() - expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + layer1MarkerDecoration.destroy() + expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined() + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() + layer2MarkerDecoration.destroy() + expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined() + expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined() it "will not fail if the decoration is removed twice", -> - decoration.destroy() - decoration.destroy() - expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined() + layer1MarkerDecoration.destroy() + layer1MarkerDecoration.destroy() + expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined() it "does not allow destroyed markers to be decorated", -> - marker.destroy() + layer1Marker.destroy() expect(-> - decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')}) + decorationManager.decorateMarker(layer1Marker, {type: 'overlay', item: document.createElement('div')}) ).toThrow("Cannot decorate a destroyed marker") expect(decorationManager.getOverlayDecorations()).toEqual [] + it "does not allow destroyed marker layers to be decorated", -> + layer = displayLayer.addMarkerLayer() + layer.destroy() + expect(-> + decorationManager.decorateMarkerLayer(layer, {type: 'highlight'}) + ).toThrow("Cannot decorate a destroyed marker layer") + describe "when a decoration is updated via Decoration::update()", -> it "emits an 'updated' event containing the new and old params", -> - decoration.onDidChangeProperties updatedSpy = jasmine.createSpy() - decoration.setProperties type: 'line-number', class: 'two' + layer1MarkerDecoration.onDidChangeProperties updatedSpy = jasmine.createSpy() + layer1MarkerDecoration.setProperties type: 'line-number', class: 'two' {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] expect(oldProperties).toEqual decorationProperties @@ -56,29 +72,29 @@ describe "DecorationManager", -> describe "::getDecorations(properties)", -> it "returns decorations matching the given optional properties", -> - expect(decorationManager.getDecorations()).toEqual [decoration] + expect(decorationManager.getDecorations()).toEqual [layer1MarkerDecoration, layer2MarkerDecoration] expect(decorationManager.getDecorations(class: 'two').length).toEqual 0 - expect(decorationManager.getDecorations(class: 'one').length).toEqual 1 + expect(decorationManager.getDecorations(class: 'one').length).toEqual 2 describe "::decorateMarker", -> describe "when decorating gutters", -> - [marker] = [] + [layer1Marker] = [] beforeEach -> - marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]]) + layer1Marker = markerLayer1.markBufferRange([[1, 0], [1, 0]]) it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", -> decorationProperties = {type: 'line-number', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) - expect(decoration.isType('line-number')).toBe true - expect(decoration.isType('gutter')).toBe true - expect(decoration.getProperties().gutterName).toBe 'line-number' - expect(decoration.getProperties().class).toBe 'one' + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + expect(layer1MarkerDecoration.isType('line-number')).toBe true + expect(layer1MarkerDecoration.isType('gutter')).toBe true + expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'line-number' + expect(layer1MarkerDecoration.getProperties().class).toBe 'one' it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", -> decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'} - decoration = decorationManager.decorateMarker(marker, decorationProperties) - expect(decoration.isType('gutter')).toBe true - expect(decoration.isType('line-number')).toBe false - expect(decoration.getProperties().gutterName).toBe 'test-gutter' - expect(decoration.getProperties().class).toBe 'one' + layer1MarkerDecoration = decorationManager.decorateMarker(layer1Marker, decorationProperties) + expect(layer1MarkerDecoration.isType('gutter')).toBe true + expect(layer1MarkerDecoration.isType('line-number')).toBe false + expect(layer1MarkerDecoration.getProperties().gutterName).toBe 'test-gutter' + expect(layer1MarkerDecoration.getProperties().class).toBe 'one' diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index 821c278ee..bf23195cf 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -28,6 +28,15 @@ describe "DefaultDirectoryProvider", -> directory = provider.directoryForURISync(nonNormalizedPath) expect(directory.getPath()).toEqual tmp + it "normalizes disk drive letter in path on #win32", -> + provider = new DefaultDirectoryProvider() + nonNormalizedPath = tmp[0].toLowerCase()+tmp.slice(1) + expect(tmp).not.toMatch /^[a-z]:/ + expect(nonNormalizedPath).toMatch /^[a-z]:/ + + directory = provider.directoryForURISync(nonNormalizedPath) + expect(directory.getPath()).toEqual tmp + it "creates a Directory for its parent dir when passed a file", -> provider = new DefaultDirectoryProvider() file = path.join(tmp, "example.txt") diff --git a/spec/dom-element-pool-spec.coffee b/spec/dom-element-pool-spec.coffee deleted file mode 100644 index 2efe80beb..000000000 --- a/spec/dom-element-pool-spec.coffee +++ /dev/null @@ -1,60 +0,0 @@ -DOMElementPool = require '../src/dom-element-pool' -{contains} = require 'underscore-plus' - -describe "DOMElementPool", -> - domElementPool = null - - beforeEach -> - domElementPool = new DOMElementPool - - it "builds DOM nodes, recycling them when they are freed", -> - [div, span1, span2, span3, span4, span5, textNode] = elements = [ - domElementPool.buildElement("div") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildElement("span") - domElementPool.buildText("Hello world!") - ] - - div.appendChild(span1) - span1.appendChild(span2) - div.appendChild(span3) - span3.appendChild(span4) - span4.appendChild(textNode) - - domElementPool.freeElementAndDescendants(div) - domElementPool.freeElementAndDescendants(span5) - - expect(contains(elements, domElementPool.buildElement("div"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) - expect(contains(elements, domElementPool.buildText("another text"))).toBe(true) - - expect(contains(elements, domElementPool.buildElement("div"))).toBe(false) - expect(contains(elements, domElementPool.buildElement("span"))).toBe(false) - expect(contains(elements, domElementPool.buildText("unexisting"))).toBe(false) - - it "forgets free nodes after being cleared", -> - span = domElementPool.buildElement("span") - div = domElementPool.buildElement("div") - domElementPool.freeElementAndDescendants(span) - domElementPool.freeElementAndDescendants(div) - - domElementPool.clear() - - expect(domElementPool.buildElement("span")).not.toBe(span) - expect(domElementPool.buildElement("div")).not.toBe(div) - - it "throws an error when trying to free the same node twice", -> - div = domElementPool.buildElement("div") - domElementPool.freeElementAndDescendants(div) - expect(-> domElementPool.freeElementAndDescendants(div)).toThrow() - - it "throws an error when trying to free an invalid element", -> - expect(-> domElementPool.freeElementAndDescendants(null)).toThrow() - expect(-> domElementPool.freeElementAndDescendants(undefined)).toThrow() diff --git a/spec/dom-element-pool-spec.js b/spec/dom-element-pool-spec.js new file mode 100644 index 000000000..9de932e27 --- /dev/null +++ b/spec/dom-element-pool-spec.js @@ -0,0 +1,112 @@ +const DOMElementPool = require ('../src/dom-element-pool') + +describe('DOMElementPool', function () { + let domElementPool + + beforeEach(() => { domElementPool = new DOMElementPool() }) + + it('builds DOM nodes, recycling them when they are freed', function () { + let elements + const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [ + domElementPool.buildElement('div', 'foo'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildElement('span'), + domElementPool.buildText('Hello world!') + ]) + + expect(div.className).toBe('foo') + div.textContent = 'testing' + div.style.backgroundColor = 'red' + div.dataset.foo = 'bar' + + expect(textNode.textContent).toBe('Hello world!') + + div.appendChild(span1) + span1.appendChild(span2) + div.appendChild(span3) + span3.appendChild(span4) + span4.appendChild(textNode) + + domElementPool.freeElementAndDescendants(div) + domElementPool.freeElementAndDescendants(span5) + + expect(elements.includes(domElementPool.buildElement('div'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(true) + expect(elements.includes(domElementPool.buildText('another text'))).toBe(true) + + expect(elements.includes(domElementPool.buildElement('div'))).toBe(false) + expect(elements.includes(domElementPool.buildElement('span'))).toBe(false) + expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false) + + expect(div.className).toBe('') + expect(div.textContent).toBe('') + expect(div.style.backgroundColor).toBe('') + expect(div.dataset.foo).toBeUndefined() + + expect(textNode.textContent).toBe('another text') + }) + + it('forgets free nodes after being cleared', function () { + const span = domElementPool.buildElement('span') + const div = domElementPool.buildElement('div') + domElementPool.freeElementAndDescendants(span) + domElementPool.freeElementAndDescendants(div) + + domElementPool.clear() + + expect(domElementPool.buildElement('span')).not.toBe(span) + expect(domElementPool.buildElement('div')).not.toBe(div) + }) + + it('does not attempt to free nodes that were not created by the pool', () => { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const foreignDiv = document.createElement('div') + const div = domElementPool.buildElement('div') + div.appendChild(foreignDiv) + domElementPool.freeElementAndDescendants(div) + const span = domElementPool.buildElement('span') + span.appendChild(foreignDiv) + domElementPool.freeElementAndDescendants(span) + + expect(assertionFailure).toBeUndefined() + }) + + it('fails an assertion when freeing the same element twice', function () { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const div = domElementPool.buildElement('div') + div.textContent = 'testing' + domElementPool.freeElementAndDescendants(div) + expect(assertionFailure).toBeUndefined() + domElementPool.freeElementAndDescendants(div) + expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') + expect(assertionFailure.metadata.content).toBe('
testing
') + }) + + it('fails an assertion when freeing the same text node twice', function () { + let assertionFailure + atom.onDidFailAssertion((error) => assertionFailure = error) + + const node = domElementPool.buildText('testing') + domElementPool.freeElementAndDescendants(node) + expect(assertionFailure).toBeUndefined() + domElementPool.freeElementAndDescendants(node) + expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!') + expect(assertionFailure.metadata.content).toBe('testing') + }) + + it('throws an error when trying to free an invalid element', function () { + expect(() => domElementPool.freeElementAndDescendants(null)).toThrow() + expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow() + }) +}) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 77d7987a7..d30900b99 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -196,7 +196,9 @@ describe('AtomApplication', function () { it('persists window state based on the project directories', async function () { const tempDirPath = makeTempDir() const atomApplication = buildAtomApplication() - const window1 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'new-file')])) + const nonExistentFilePath = path.join(tempDirPath, 'new-file') + + const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath])) await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { atom.workspace.observeActivePaneItem(function (textEditor) { if (textEditor) { @@ -205,17 +207,31 @@ describe('AtomApplication', function () { } }) }) + await window1.saveState() window1.close() await window1.closedPromise - const window2 = atomApplication.launch(parseCommandLine([path.join(tempDirPath)])) + // Restore unsaved state when opening the directory itself + const window2 = atomApplication.launch(parseCommandLine([tempDirPath])) + await window2.loadedPromise const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) { - atom.workspace.observeActivePaneItem(function (textEditor) { - if (textEditor) sendBackToMainProcess(textEditor.getText()) - }) + const textEditor = atom.workspace.getActiveTextEditor() + textEditor.moveToBottom() + textEditor.insertText(' How are you?') + sendBackToMainProcess(textEditor.getText()) }) + assert.equal(window2Text, 'Hello World! How are you?') + await window2.saveState() + window2.close() + await window2.closedPromise - assert.equal(window2Text, 'Hello World!') + // Restore unsaved state when opening a path to a non-existent file in the directory + const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')])) + await window3.loadedPromise + const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) { + sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText())) + }) + assert.include(window3Texts, 'Hello World! How are you?') }) it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () { @@ -260,7 +276,7 @@ describe('AtomApplication', function () { }) assert.equal(window1EditorTitle, 'untitled') - const window2 = atomApplication.launch(parseCommandLine([])) + const window2 = atomApplication.openWithOptions(parseCommandLine([])) await focusWindow(window2) const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle()) @@ -472,7 +488,7 @@ describe('AtomApplication', function () { } let channelIdCounter = 0 - function evalInWebContents (webContents, source) { + function evalInWebContents (webContents, source, ...args) { const channelId = 'eval-result-' + channelIdCounter++ return new Promise(function (resolve) { electron.ipcMain.on(channelId, receiveResult) diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 862b7f428..4821dbc9b 100644 --- a/spec/main-process/file-recovery-service.test.js +++ b/spec/main-process/file-recovery-service.test.js @@ -112,7 +112,7 @@ describe("FileRecoveryService", () => { const mockWindow = {} const filePath = temp.path() fs.writeFileSync(filePath, "content") - fs.chmodSync(filePath, 0444) + fs.chmodSync(filePath, 0o444) let logs = [] this.stub(console, 'log', (message) => logs.push(message)) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index d548255e5..997f6054e 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -614,3 +614,7 @@ describe "Project", -> randomPath = path.join("some", "random", "path") expect(atom.project.contains(randomPath)).toBe false + + describe ".resolvePath(uri)", -> + it "normalizes disk drive letter in passed path on #win32", -> + expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt" diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index eef49f9f6..b26e64e34 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1747,11 +1747,13 @@ describe('TextEditorComponent', function () { }) describe('block decorations rendering', function () { + let markerLayer + function createBlockDecorationBeforeScreenRow(screenRow, {className}) { let item = document.createElement("div") item.className = className || "" let blockDecoration = editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), {type: "block", item: item, position: "before"} ) return [item, blockDecoration] @@ -1761,13 +1763,14 @@ describe('TextEditorComponent', function () { let item = document.createElement("div") item.className = className || "" let blockDecoration = editor.decorateMarker( - editor.markScreenPosition([screenRow, 0], {invalidate: "never"}), + markerLayer.markScreenPosition([screenRow, 0], {invalidate: "never"}), {type: "block", item: item, position: "after"} ) return [item, blockDecoration] } beforeEach(function () { + markerLayer = editor.addMarkerLayer() wrapperNode.style.height = 5 * lineHeightInPixels + 'px' editor.update({autoHeight: false}) component.measureDimensions() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 08afa6239..153cc5dc3 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -886,8 +886,12 @@ describe "Workspace", -> describe "document.title", -> describe "when there is no item open", -> - it "sets the title to 'untitled'", -> - expect(document.title).toMatch ///^untitled/// + it "sets the title to the project path", -> + expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) + + it "sets the title to 'untitled' if there is no project path", -> + atom.project.setPaths([]) + expect(document.title).toMatch /^untitled/ describe "when the active pane item's path is not inside a project path", -> beforeEach -> @@ -948,10 +952,10 @@ describe "Workspace", -> expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the last pane item is removed", -> - it "updates the title to be untitled", -> + it "updates the title to the project's first path", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toMatch ///^untitled/// + expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) describe "when an inactive pane's item changes", -> it "does not update the title", -> diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 185db5059..766ba7aa8 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -2,10 +2,12 @@ _ = require 'underscore-plus' {screen, ipcRenderer, remote, shell, webFrame} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' -{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers' +getWindowLoadSettings = require './get-window-load-settings' module.exports = class ApplicationDelegate + getWindowLoadSettings: -> getWindowLoadSettings() + open: (params) -> ipcRenderer.send('open', params) @@ -109,10 +111,7 @@ class ApplicationDelegate ipcRenderer.send("add-recent-document", filename) setRepresentedDirectoryPaths: (paths) -> - loadSettings = getWindowLoadSettings() - loadSettings['initialPaths'] = paths - setWindowLoadSettings(loadSettings) - ipcRenderer.send("did-change-paths") + ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths) setAutoHideWindowMenuBar: (autoHide) -> ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide) @@ -149,13 +148,9 @@ class ApplicationDelegate showMessageDialog: (params) -> showSaveDialog: (params) -> - if _.isString(params) - params = defaultPath: params - else - params = _.clone(params) - params.title ?= 'Save File' - params.defaultPath ?= getWindowLoadSettings().initialPaths[0] - remote.dialog.showSaveDialog remote.getCurrentWindow(), params + if typeof params is 'string' + params = {defaultPath: params} + @getCurrentWindow().showSaveDialog(params) playBeepSound: -> shell.beep() diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a740b22d5..3133b5af8 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -11,7 +11,6 @@ Model = require './model' WindowEventHandler = require './window-event-handler' StateStore = require './state-store' StorageFolder = require './storage-folder' -{getWindowLoadSettings} = require './window-load-settings-helpers' registerDefaultCommands = require './register-default-commands' {updateProcessEnv} = require './update-process-env' @@ -458,7 +457,7 @@ class AtomEnvironment extends Model # # Returns an {Object} containing all the load setting key/value pairs. getLoadSettings: -> - getWindowLoadSettings() + @applicationDelegate.getWindowLoadSettings() ### Section: Managing The Atom Window @@ -695,8 +694,14 @@ class AtomEnvironment extends Model @deserialize(state) if state? @deserializeTimings.atom = Date.now() - startTime - if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') + if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom' @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) + @document.body.classList.add('custom-title-bar') + if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom-inset' + @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) + @document.body.classList.add('custom-inset-title-bar') + if process.platform is 'darwin' and @config.get('core.titleBar') is 'hidden' + @document.body.classList.add('hidden-title-bar') @document.body.appendChild(@views.getView(@workspace)) @backgroundStylesheet?.remove() @@ -822,12 +827,17 @@ class AtomEnvironment extends Model Section: Private ### - assert: (condition, message, callback) -> + assert: (condition, message, callbackOrMetadata) -> return true if condition error = new Error("Assertion failed: #{message}") Error.captureStackTrace(error, @assert) - callback?(error) + + if callbackOrMetadata? + if typeof callbackOrMetadata is 'function' + callbackOrMetadata?(error) + else + error.metadata = callbackOrMetadata @emitter.emit 'did-fail-assertion', error diff --git a/src/babel.js b/src/babel.js index a944f2e8c..d72b29ffd 100644 --- a/src/babel.js +++ b/src/babel.js @@ -6,6 +6,7 @@ var defaultOptions = require('../static/babelrc.json') var babel = null var babelVersionDirectory = null +var options = null var PREFIXES = [ '/** @babel */', @@ -47,16 +48,27 @@ exports.compile = function (sourceCode, filePath) { var noop = function () {} Logger.prototype.debug = noop Logger.prototype.verbose = noop + + options = {ast: false, babelrc: false} + for (var key in defaultOptions) { + if (key === 'plugins') { + const plugins = [] + for (const [pluginName, pluginOptions] of defaultOptions[key]) { + plugins.push([require.resolve(`babel-plugin-${pluginName}`), pluginOptions]) + } + options[key] = plugins + } else { + options[key] = defaultOptions[key] + } + } } if (process.platform === 'win32') { filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/') } - var options = {filename: filePath} - for (var key in defaultOptions) { - options[key] = defaultOptions[key] - } + options.filename = filePath + return babel.transform(sourceCode, options).code } diff --git a/src/config-schema.js b/src/config-schema.js index 4dd99d2b4..41b5ecbb6 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -68,6 +68,12 @@ const configSchema = { default: true, description: 'Trigger the system\'s beep sound when certain actions cannot be executed or there are no results.' }, + closeDeletedFileTabs: { + type: 'boolean', + default: false, + title: 'Close Deleted File Tabs', + description: 'Close corresponding editors when a file is deleted outside Atom.' + }, destroyEmptyPanes: { type: 'boolean', default: true, @@ -492,10 +498,11 @@ if (['win32', 'linux'].includes(process.platform)) { } if (process.platform === 'darwin') { - configSchema.core.properties.useCustomTitleBar = { - type: 'boolean', - default: false, - description: 'Use custom, theme-aware title bar.
Note: This currently does not include a proxy icon.
This setting will require a relaunch of Atom to take effect.' + configSchema.core.properties.titleBar = { + type: 'string', + default: 'native', + enum: ['native', 'custom', 'custom-inset', 'hidden'], + description: 'Experimental: A `custom` title bar adapts to theme colors. Choosing `custom-inset` adds a bit more padding. The title bar can also be completely `hidden`.
Note: Switching to a custom or hidden title bar will compromise some functionality.
This setting will require a relaunch of Atom to take effect.' } } diff --git a/src/config.coffee b/src/config.coffee index 2dc537dd4..e873a1348 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -336,6 +336,31 @@ ScopeDescriptor = require './scope-descriptor' # order: 2 # ``` # +# ## Manipulating values outside your configuration schema +# +# It is possible to manipulate(`get`, `set`, `observe` etc) values that do not +# appear in your configuration schema. For example, if the config schema of the +# package 'some-package' is +# +# ```coffee +# config: +# someSetting: +# type: 'boolean' +# default: false +# ``` +# +# You can still do the following +# +# ```coffee +# let otherSetting = atom.config.get('some-package.otherSetting') +# atom.config.set('some-package.stillAnotherSetting', otherSetting * 5) +# ``` +# +# In other words, if a function asks for a `key-path`, that path doesn't have to +# be described in the config schema for the package or any package. However, as +# highlighted in the best practices section, you are advised against doing the +# above. +# # ## Best practices # # * Don't depend on (or write to) configuration keys outside of your keypath. diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee index e1096f572..05935f018 100644 --- a/src/decoration-manager.coffee +++ b/src/decoration-manager.coffee @@ -8,7 +8,7 @@ class DecorationManager extends Model didUpdateDecorationsEventScheduled: false updatedSynchronously: false - constructor: (@displayLayer, @defaultMarkerLayer) -> + constructor: (@displayLayer) -> super @emitter = new Emitter @@ -71,9 +71,11 @@ class DecorationManager extends Model decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> decorationsByMarkerId = {} - for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if decorations = @decorationsByMarkerId[marker.id] - decorationsByMarkerId[marker.id] = decorations + for layerId of @decorationCountsByLayerId + layer = @displayLayer.getMarkerLayer(layerId) + for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) + if decorations = @decorationsByMarkerId[marker.id] + decorationsByMarkerId[marker.id] = decorations decorationsByMarkerId decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> @@ -104,7 +106,14 @@ class DecorationManager extends Model decorationsState decorateMarker: (marker, decorationParams) -> - throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed() + if marker.isDestroyed() + error = new Error("Cannot decorate a destroyed marker") + error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()} + if marker.destroyStackTrace? + error.metadata.destroyStackTrace = marker.destroyStackTrace + if marker.bufferMarker?.destroyStackTrace? + error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace + throw error marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) @decorationsByMarkerId[marker.id] ?= [] @@ -117,6 +126,7 @@ class DecorationManager extends Model decoration decorateMarkerLayer: (markerLayer, decorationParams) -> + throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed() decoration = new LayerDecoration(markerLayer, this, decorationParams) @layerDecorationsByMarkerLayerId[markerLayer.id] ?= [] @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index ed4e9ba36..44d5298dd 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -15,7 +15,7 @@ class DefaultDirectoryProvider # * {Directory} if the given URI is compatible with this provider. # * `null` if the given URI is not compatibile with this provider. directoryForURISync: (uri) -> - normalizedPath = path.normalize(uri) + normalizedPath = @normalizePath(uri) {host} = url.parse(uri) directoryPath = if host uri @@ -42,3 +42,17 @@ class DefaultDirectoryProvider # * `null` if the given URI is not compatibile with this provider. directoryForURI: (uri) -> Promise.resolve(@directoryForURISync(uri)) + + # Public: Normalizes path. + # + # * `uri` {String} The path that should be normalized. + # + # Returns a {String} with normalized path. + normalizePath: (uri) -> + # Normalize disk drive letter on Windows to avoid opening two buffers for the same file + pathWithNormalizedDiskDriveLetter = + if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/) + "#{matchData[1].toUpperCase()}#{uri.slice(1)}" + else + uri + path.normalize(pathWithNormalizedDiskDriveLetter) diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee deleted file mode 100644 index f81a537f3..000000000 --- a/src/dom-element-pool.coffee +++ /dev/null @@ -1,55 +0,0 @@ -module.exports = -class DOMElementPool - constructor: -> - @freeElementsByTagName = {} - @freedElements = new Set - - clear: -> - @freedElements.clear() - for tagName, freeElements of @freeElementsByTagName - freeElements.length = 0 - return - - build: (tagName, factory, reset) -> - element = @freeElementsByTagName[tagName]?.pop() - element ?= factory() - reset(element) - @freedElements.delete(element) - element - - buildElement: (tagName, className) -> - factory = -> document.createElement(tagName) - reset = (element) -> - delete element.dataset[dataId] for dataId of element.dataset - element.removeAttribute("style") - if className? - element.className = className - else - element.removeAttribute("class") - @build(tagName, factory, reset) - - buildText: (textContent) -> - factory = -> document.createTextNode(textContent) - reset = (element) -> element.textContent = textContent - @build("#text", factory, reset) - - freeElementAndDescendants: (element) -> - @free(element) - @freeDescendants(element) - - freeDescendants: (element) -> - for descendant in element.childNodes by -1 - @free(descendant) - @freeDescendants(descendant) - return - - free: (element) -> - throw new Error("The element cannot be null or undefined.") unless element? - throw new Error("The element has already been freed!") if @freedElements.has(element) - - tagName = element.nodeName.toLowerCase() - @freeElementsByTagName[tagName] ?= [] - @freeElementsByTagName[tagName].push(element) - @freedElements.add(element) - - element.remove() diff --git a/src/dom-element-pool.js b/src/dom-element-pool.js new file mode 100644 index 000000000..0fef02dee --- /dev/null +++ b/src/dom-element-pool.js @@ -0,0 +1,89 @@ +module.exports = +class DOMElementPool { + constructor () { + this.managedElements = new Set() + this.freeElementsByTagName = new Map() + this.freedElements = new Set() + } + + clear () { + this.managedElements.clear() + this.freedElements.clear() + this.freeElementsByTagName.clear() + } + + buildElement (tagName, className) { + const elements = this.freeElementsByTagName.get(tagName) + let element = elements ? elements.pop() : null + if (element) { + for (let dataId in element.dataset) { delete element.dataset[dataId] } + element.removeAttribute('style') + if (className) { + element.className = className + } else { + element.removeAttribute('class') + } + while (element.firstChild) { + element.removeChild(element.firstChild) + } + this.freedElements.delete(element) + } else { + element = document.createElement(tagName) + if (className) { + element.className = className + } + this.managedElements.add(element) + } + return element + } + + buildText (textContent) { + const elements = this.freeElementsByTagName.get('#text') + let element = elements ? elements.pop() : null + if (element) { + element.textContent = textContent + this.freedElements.delete(element) + } else { + element = document.createTextNode(textContent) + this.managedElements.add(element) + } + return element + } + + freeElementAndDescendants (element) { + this.free(element) + element.remove() + } + + freeDescendants (element) { + while (element.firstChild) { + this.free(element.firstChild) + element.removeChild(element.firstChild) + } + } + + free (element) { + if (element == null) { throw new Error('The element cannot be null or undefined.') } + if (!this.managedElements.has(element)) return + if (this.freedElements.has(element)) { + atom.assert(false, 'The element has already been freed!', { + content: element instanceof window.Text ? element.textContent : element.outerHTML + }) + return + } + + const tagName = element.nodeName.toLowerCase() + let elements = this.freeElementsByTagName.get(tagName) + if (!elements) { + elements = [] + this.freeElementsByTagName.set(tagName, elements) + } + elements.push(element) + this.freedElements.add(element) + + for (let i = element.childNodes.length - 1; i >= 0; i--) { + const descendant = element.childNodes[i] + this.free(descendant) + } + } +} diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js new file mode 100644 index 000000000..7ee465141 --- /dev/null +++ b/src/get-window-load-settings.js @@ -0,0 +1,10 @@ +const {remote} = require('electron') + +let windowLoadSettings = null + +module.exports = () => { + if (!windowLoadSettings) { + windowLoadSettings = remote.getCurrentWindow().loadSettings + } + return windowLoadSettings +} diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index 7d3a23db7..be13ce6c6 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -3,7 +3,7 @@ module.exports = ({blobStore}) -> {updateProcessEnv} = require('./update-process-env') path = require 'path' require './window' - {getWindowLoadSettings} = require './window-load-settings-helpers' + getWindowLoadSettings = require './get-window-load-settings' {ipcRenderer} = require 'electron' {resourcePath, devMode, env} = getWindowLoadSettings() require './electron-shims' diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index 29a210904..a223e0b03 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -6,7 +6,7 @@ import ipcHelpers from './ipc-helpers' import util from 'util' export default async function () { - const {getWindowLoadSettings} = require('./window-load-settings-helpers') + const getWindowLoadSettings = require('./get-window-load-settings') const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings() try { const Clipboard = require('../src/clipboard') diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index 39a408fea..794db3174 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -18,7 +18,7 @@ module.exports = ({blobStore}) -> try path = require 'path' {ipcRenderer} = require 'electron' - {getWindowLoadSettings} = require './window-load-settings-helpers' + getWindowLoadSettings = require './get-window-load-settings' CompileCache = require './compile-cache' AtomEnvironment = require '../src/atom-environment' ApplicationDelegate = require '../src/application-delegate' diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 42358409b..93e9e3395 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -84,7 +84,13 @@ class AtomApplication initialize: (options) -> global.atomApplication = this - @config.onDidChange 'core.useCustomTitleBar', @promptForRestart.bind(this) + # DEPRECATED: This can be removed at some point (added in 1.13) + # It converts `useCustomTitleBar: true` to `titleBar: "custom"` + if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') + @config.unset('core.useCustomTitleBar') + @config.set('core.titleBar', 'custom') + + @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) @autoUpdateManager = new AutoUpdateManager( @version, options.test or options.benchmark or options.benchmarkTest, @resourcePath, @config @@ -587,8 +593,7 @@ class AtomApplication states = [] for window in @windows unless window.isSpec - if loadSettings = window.getLoadSettings() - states.push(initialPaths: loadSettings.initialPaths) + states.push({initialPaths: window.representedDirectoryPaths}) if states.length > 0 or allowEmpty @storageFolder.storeSync('application.json', states) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 9c937e4f6..03386d31a 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -43,12 +43,16 @@ class AtomWindow if process.platform is 'linux' options.icon = @constructor.iconPath - if @shouldHideTitleBar() + if @shouldAddCustomTitleBar() options.titleBarStyle = 'hidden' - @browserWindow = new BrowserWindow options - @atomApplication.addWindow(this) + if @shouldAddCustomInsetTitleBar() + options.titleBarStyle = 'hidden-inset' + if @shouldHideTitleBar() + options.frame = false + + @browserWindow = new BrowserWindow(options) @handleEvents() loadSettings = Object.assign({}, settings) @@ -60,11 +64,15 @@ class AtomWindow loadSettings.clearWindowState ?= false loadSettings.initialPaths ?= for {pathToOpen} in locationsToOpen when pathToOpen - if fs.statSyncNoException(pathToOpen).isFile?() - path.dirname(pathToOpen) - else + stat = fs.statSyncNoException(pathToOpen) or null + if stat?.isDirectory() pathToOpen - + else + parentDirectory = path.dirname(pathToOpen) + if stat?.isFile() or fs.existsSync(parentDirectory) + parentDirectory + else + pathToOpen loadSettings.initialPaths.sort() # Only send to the first non-spec window created @@ -72,33 +80,31 @@ class AtomWindow @constructor.includeShellLoadTime = false loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime + @representedDirectoryPaths = loadSettings.initialPaths + @env = loadSettings.env if loadSettings.env? + @browserWindow.loadSettings = loadSettings @browserWindow.on 'window:loaded', => @emit 'window:loaded' @resolveLoadedPromise() - @setLoadSettings(loadSettings) - @env = loadSettings.env if loadSettings.env? + @browserWindow.loadURL url.format + protocol: 'file' + pathname: "#{@resourcePath}/static/index.html" + slashes: true + + @browserWindow.showSaveDialog = @showSaveDialog.bind(this) + @browserWindow.focusOnWebView() if @isSpec @browserWindow.temporaryState = {windowDimensions} if windowDimensions? hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() - setLoadSettings: (loadSettings) -> - @browserWindow.loadURL url.format - protocol: 'file' - pathname: "#{@resourcePath}/static/index.html" - slashes: true - hash: encodeURIComponent(JSON.stringify(loadSettings)) + @atomApplication.addWindow(this) - getLoadSettings: -> - if @browserWindow.webContents? and not @browserWindow.webContents.isLoading() - hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1) - JSON.parse(decodeURIComponent(hash)) - - hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0 + hasProjectPath: -> @representedDirectoryPaths.length > 0 setupContextMenu: -> ContextMenu = require './context-menu' @@ -112,7 +118,7 @@ class AtomWindow true containsPath: (pathToCheck) -> - @getLoadSettings()?.initialPaths?.some (projectPath) -> + @representedDirectoryPaths.some (projectPath) -> if not projectPath false else if not pathToCheck @@ -226,10 +232,20 @@ class AtomWindow [width, height] = @browserWindow.getSize() {x, y, width, height} + shouldAddCustomTitleBar: -> + not @isSpec and + process.platform is 'darwin' and + @atomApplication.config.get('core.titleBar') is 'custom' + + shouldAddCustomInsetTitleBar: -> + not @isSpec and + process.platform is 'darwin' and + @atomApplication.config.get('core.titleBar') is 'custom-inset' + shouldHideTitleBar: -> not @isSpec and process.platform is 'darwin' and - @atomApplication.config.get('core.useCustomTitleBar') + @atomApplication.config.get('core.titleBar') is 'hidden' close: -> @browserWindow.close() @@ -265,6 +281,13 @@ class AtomWindow @saveState().then => @browserWindow.reload() @loadedPromise + showSaveDialog: (params) -> + params = Object.assign({ + title: 'Save File', + defaultPath: @representedDirectoryPaths[0] + }, params) + dialog.showSaveDialog(this, params) + toggleDevTools: -> @browserWindow.toggleDevTools() openDevTools: -> @browserWindow.openDevTools() @@ -275,4 +298,8 @@ class AtomWindow setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename) + setRepresentedDirectoryPaths: (@representedDirectoryPaths) -> + @representedDirectoryPaths.sort() + @atomApplication.saveState() + copy: -> @browserWindow.copy() diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index 8fdba844d..ff29dd3d6 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -22,7 +22,7 @@ class AutoUpdateManager setupAutoUpdater: -> if process.platform is 'win32' archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch - @feedUrl = "https://atom.io/api/updates#{archSuffix}" + @feedUrl = "https://atom.io/api/updates#{archSuffix}?version=#{@version}" autoUpdater = require './auto-updater-win32' else @feedUrl = "https://atom.io/api/updates?version=#{@version}" diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 4227b63ba..3f9f2523a 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -106,14 +106,14 @@ module.exports = function parseCommandLine (processArgs) { if (args['resource-path']) { devMode = true - resourcePath = args['resource-path'] + devResourcePath = args['resource-path'] } if (test) { devMode = true } - if (devMode && !resourcePath) { + if (devMode) { resourcePath = devResourcePath } diff --git a/src/project.coffee b/src/project.coffee index 522fbfbc7..a02f27dac 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -21,7 +21,6 @@ class Project extends Model constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) -> @emitter = new Emitter @buffers = [] - @paths = [] @rootDirectories = [] @repositories = [] @directoryProviders = [] @@ -32,7 +31,9 @@ class Project extends Model destroyed: -> buffer.destroy() for buffer in @buffers - @setPaths([]) + repository?.destroy() for repository in @repositories + @rootDirectories = [] + @repositories = [] reset: (packageManager) -> @emitter.dispose() @@ -62,6 +63,9 @@ class Project extends Model fs.closeSync(fs.openSync(bufferState.filePath, 'r')) catch error return unless error.code is 'ENOENT' + unless bufferState.shouldDestroyOnFileDelete? + bufferState.shouldDestroyOnFileDelete = + -> atom.config.get('core.closeDeletedFileTabs') TextBuffer.deserialize(bufferState) @subscribeToBuffer(buffer) for buffer in @buffers @@ -205,7 +209,7 @@ class Project extends Model removePath: (projectPath) -> # The projectPath may be a URI, in which case it should not be normalized. unless projectPath in @getPaths() - projectPath = path.normalize(projectPath) + projectPath = @defaultDirectoryProvider.normalizePath(projectPath) indexToRemove = null for directory, i in @rootDirectories @@ -233,11 +237,10 @@ class Project extends Model uri else if fs.isAbsolute(uri) - path.normalize(fs.resolveHome(uri)) - + @defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) # TODO: what should we do here when there are multiple directories? else if projectPath = @getPaths()[0] - path.normalize(fs.resolveHome(path.join(projectPath, uri))) + @defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) else undefined @@ -360,9 +363,14 @@ class Project extends Model else @buildBuffer(absoluteFilePath) + shouldDestroyBufferOnFileDelete: -> + atom.config.get('core.closeDeletedFileTabs') + # Still needed when deserializing a tokenized buffer buildBufferSync: (absoluteFilePath) -> - buffer = new TextBuffer({filePath: absoluteFilePath}) + buffer = new TextBuffer({ + filePath: absoluteFilePath + shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete}) @addBuffer(buffer) buffer.loadSync() buffer @@ -374,7 +382,9 @@ class Project extends Model # # Returns a {Promise} that resolves to the {TextBuffer}. buildBuffer: (absoluteFilePath) -> - buffer = new TextBuffer({filePath: absoluteFilePath}) + buffer = new TextBuffer({ + filePath: absoluteFilePath + shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete}) @addBuffer(buffer) buffer.load() .then((buffer) -> buffer) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 8c9792916..26e3bae12 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -108,7 +108,10 @@ class TextEditorElement extends HTMLElement buildModel: -> @setModel(@workspace.buildTextEditor( - buffer: new TextBuffer(@textContent) + buffer: new TextBuffer({ + text: @textContent + shouldDestroyOnFileDelete: + -> atom.config.get('core.closeDeletedFileTabs')}) softWrapped: false tabLength: 2 softTabs: true diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 3c4739ca5..1106cee09 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -622,6 +622,18 @@ class TextEditorPresenter return unless @scrollTop? and @lineHeight? @startRow = Math.max(0, @lineTopIndex.rowForPixelPosition(@scrollTop)) + atom.assert( + Number.isFinite(@startRow), + 'Invalid start row', + (error) => + error.metadata = { + startRow: @startRow?.toString(), + scrollTop: @scrollTop?.toString(), + scrollHeight: @scrollHeight?.toString(), + clientHeight: @clientHeight?.toString(), + lineHeight: @lineHeight?.toString() + } + ) updateEndRow: -> return unless @scrollTop? and @lineHeight? and @height? diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2ebd106e6..13b8a0bfa 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -16,12 +16,11 @@ TextEditorElement = require './text-editor-element' {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' ZERO_WIDTH_NBSP = '\ufeff' +MAX_SCREEN_LINE_LENGTH = 500 # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. -# If you're manipulating the state of an editor, use this class. If you're -# interested in the visual appearance of editors, use {TextEditorElement} -# instead. +# If you're manipulating the state of an editor, use this class. # # A single {TextBuffer} can belong to multiple editors. For example, if the # same file is open in two different panes, Atom creates a separate editor for @@ -162,7 +161,8 @@ class TextEditor extends Model @softWrapAtPreferredLineLength ?= false @preferredLineLength ?= 80 - @buffer ?= new TextBuffer + @buffer ?= new TextBuffer({shouldDestroyOnFileDelete: -> + atom.config.get('core.closeDeletedFileTabs')}) @tokenizedBuffer ?= new TokenizedBuffer({ grammar, tabLength, @buffer, @largeFileMode, @assert }) @@ -193,8 +193,9 @@ class TextEditor extends Model @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @defaultMarkerLayer = @displayLayer.addMarkerLayer() @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) + @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer) + @decorationManager = new DecorationManager(@displayLayer) @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) for marker in @selectionsMarkerLayer.getMarkers() @@ -2965,7 +2966,7 @@ class TextEditor extends Model else @getEditorWidthInChars() else - Infinity + MAX_SCREEN_LINE_LENGTH ### Section: Indentation diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 234f82be9..77221f52e 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -8,6 +8,8 @@ ScopeDescriptor = require './scope-descriptor' TokenizedBufferIterator = require './tokenized-buffer-iterator' NullGrammar = require './null-grammar' +MAX_LINE_LENGTH_TO_TOKENIZE = 500 + module.exports = class TokenizedBuffer extends Model grammar: null @@ -251,6 +253,8 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) + if text.length > MAX_LINE_LENGTH_TO_TOKENIZE + text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee deleted file mode 100644 index 639dc751d..000000000 --- a/src/window-load-settings-helpers.coffee +++ /dev/null @@ -1,8 +0,0 @@ -windowLoadSettings = null - -exports.getWindowLoadSettings = -> - windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1))) - -exports.setWindowLoadSettings = (settings) -> - windowLoadSettings = settings - location.hash = encodeURIComponent(JSON.stringify(settings)) diff --git a/src/workspace.coffee b/src/workspace.coffee index 9871db224..2a46ce57a 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -182,7 +182,7 @@ class Workspace extends Model projectPath = _.find projectPaths, (projectPath) -> itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) itemTitle ?= "untitled" - projectPath ?= if itemPath then path.dirname(itemPath) else null + projectPath ?= if itemPath then path.dirname(itemPath) else projectPaths[0] if projectPath? projectPath = fs.tildify(projectPath) diff --git a/static/babelrc.json b/static/babelrc.json index 26b70dc41..11474dd8d 100644 --- a/static/babelrc.json +++ b/static/babelrc.json @@ -1,7 +1,16 @@ { - "breakConfig": true, "sourceMap": "inline", - "blacklist": ["es6.forOf", "useStrict"], - "optional": ["asyncToGenerator"], - "stage": 0 + "plugins": [ + ["add-module-exports", {}], + ["transform-async-to-generator", {}], + ["transform-decorators-legacy", {}], + ["transform-class-properties", {}], + ["transform-es2015-modules-commonjs", {"strictMode": false}], + ["transform-export-extensions", {}], + ["transform-do-expressions", {}], + ["transform-function-bind", {}], + ["transform-object-rest-spread", {}], + ["transform-flow-strip-types", {}], + ["transform-react-jsx", {}] + ] } diff --git a/static/index.js b/static/index.js index 2966eafdf..aa57a594a 100644 --- a/static/index.js +++ b/static/index.js @@ -2,9 +2,8 @@ var path = require('path') var FileSystemBlobStore = require('../src/file-system-blob-store') var NativeCompileCache = require('../src/native-compile-cache') + var getWindowLoadSettings = require('../src/get-window-load-settings') - var loadSettings = null - var loadSettingsError = null var blobStore = null window.onload = function () { @@ -25,20 +24,16 @@ // Normalize to make sure drive letter case is consistent on Windows process.resourcesPath = path.normalize(process.resourcesPath) - if (loadSettingsError) { - throw loadSettingsError - } - - var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep) + var devMode = getWindowLoadSettings().devMode || !getWindowLoadSettings().resourcePath.startsWith(process.resourcesPath + path.sep) if (devMode) { setupDeprecatedPackages() } - if (loadSettings.profileStartup) { - profileStartup(loadSettings, Date.now() - startTime) + if (getWindowLoadSettings().profileStartup) { + profileStartup(Date.now() - startTime) } else { - setupWindow(loadSettings) + setupWindow() setLoadTime(Date.now() - startTime) } } catch (error) { @@ -61,23 +56,23 @@ console.error(error.stack || error) } - function setupWindow (loadSettings) { + function setupWindow () { var CompileCache = require('../src/compile-cache') CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) var ModuleCache = require('../src/module-cache') - ModuleCache.register(loadSettings) - ModuleCache.add(loadSettings.resourcePath) + ModuleCache.register(getWindowLoadSettings()) + ModuleCache.add(getWindowLoadSettings().resourcePath) // By explicitly passing the app version here, we could save the call // of "require('remote').require('app').getVersion()". var startCrashReporter = require('../src/crash-reporter-start') - startCrashReporter({_version: loadSettings.appVersion}) + startCrashReporter({_version: getWindowLoadSettings().appVersion}) setupVmCompatibility() setupCsonCache(CompileCache.getCacheDirectory()) - var initialize = require(loadSettings.windowInitializationScript) + var initialize = require(getWindowLoadSettings().windowInitializationScript) return initialize({blobStore: blobStore}).then(function () { require('electron').ipcRenderer.send('window-command', 'window:loaded') }) @@ -105,11 +100,11 @@ } } - function profileStartup (loadSettings, initialTime) { + function profileStartup (initialTime) { function profile () { console.profile('startup') var startTime = Date.now() - setupWindow(loadSettings).then(function () { + setupWindow().then(function () { setLoadTime(Date.now() - startTime + initialTime) console.profileEnd('startup') console.log('Switch to the Profiles tab to view the created startup profile') @@ -125,16 +120,6 @@ } } - function parseLoadSettings () { - var rawLoadSettings = decodeURIComponent(window.location.hash.substr(1)) - try { - loadSettings = JSON.parse(rawLoadSettings) - } catch (error) { - console.error('Failed to parse load settings: ' + rawLoadSettings) - loadSettingsError = error - } - } - var setupAtomHome = function () { if (process.env.ATOM_HOME) { return @@ -143,11 +128,10 @@ // 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 + if (getWindowLoadSettings() && getWindowLoadSettings().atomHome) { + process.env.ATOM_HOME = getWindowLoadSettings().atomHome } } - parseLoadSettings() setupAtomHome() })() diff --git a/static/title-bar.less b/static/title-bar.less index 8191bc278..3eb2c4022 100644 --- a/static/title-bar.less +++ b/static/title-bar.less @@ -1,30 +1,32 @@ @import "ui-variables"; +@title-bar-height: 22px; +@traffic-lights-width: 68px; + +@inset-title-bar-height: 38px; +@inset-traffic-lights-width: 78px; + @title-bar-text-size: 13px; -@title-bar-height: 23px; @title-bar-background-color: @base-background-color; @title-bar-border-color: @base-border-color; -body.fullscreen .title-bar { - margin-top: -@title-bar-height; -} + +// Title Bar ------------------------------- .title-bar { - height: @title-bar-height; - transition: margin-top 160ms; - - flex-shrink: 0; display: flex; + flex-shrink: 0; align-items: center; justify-content: center; - + overflow: hidden; + box-sizing: content-box; font-size: @title-bar-text-size; + background-color: @title-bar-background-color; + border-bottom: 1px solid @title-bar-border-color; + transition: margin-top 160ms; -webkit-user-select: none; -webkit-app-region: drag; - padding: 0 70px; - overflow: hidden; - .title { flex: 0 1 auto; overflow: hidden; @@ -32,10 +34,54 @@ body.fullscreen .title-bar { text-overflow: ellipsis; } - background-color: @title-bar-background-color; - border-bottom: 1px solid @title-bar-border-color; - .is-blurred & { color: @text-color-subtle; } } + + +// Custom ------------------------------- + +.custom-title-bar { + .title-bar { + height: @title-bar-height; + padding-left: @traffic-lights-width; + padding-right: @traffic-lights-width; + } + + &.fullscreen .title-bar { + margin-top: -@title-bar-height; // hide title bar in fullscreen mode + } + + atom-panel.modal { + top: @title-bar-height; // Move modals down + } +} + + +// Custom Inset ------------------------------- + +.custom-inset-title-bar { + .title-bar { + height: @inset-title-bar-height; + padding-left: @inset-traffic-lights-width; + padding-right: @inset-traffic-lights-width; + } + + &.fullscreen .title-bar { + margin-top: -@inset-title-bar-height; // hide title bar in fullscreen mode + } + + atom-panel.modal { + top: @inset-title-bar-height; // Move modals down + } +} + + +// Hidden ------------------------------- + +.hidden-title-bar { + .status-bar { + -webkit-app-region: drag; // Enable dragging + } +}