diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15e42f708..fe9a742b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,15 @@ The following is a set of guidelines for contributing to Atom and its packages, which are hosted in the [Atom Organization](https://github.com/atom) on GitHub. -If you're unsure which package is causing your problem or if you're having an -issue with Atom core, please open an issue on the [main atom repository](https://github.com/atom/atom/issues). -These are just guidelines, not rules, use your best judgement and feel free to +These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. ## Submitting Issues -* Check the [debugging guide](https://atom.io/docs/latest/debugging) for tips +* You can create an issue [here](https://github.com/atom/atom/issues/new), but + before doing that please read the notes below on debugging and submitting issues, + and include as many details as possible with your report. +* Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging) for tips on debugging. You might be able to find the cause of the problem and fix things yourself. * Include the version of Atom you are using and the OS. @@ -24,7 +25,8 @@ propose changes to this document in a pull request. will be logged. If you can reproduce the error, use this approach to get the full stack trace and include it in the issue. * On Mac, check Console.app for stack traces to include if reporting a crash. -* Perform a cursory search to see if a similar issue has already been submitted. +* Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom) + to see if a similar issue has already been submitted. * Please setup a [profile picture](https://help.github.com/articles/how-do-i-set-up-my-profile-picture) to make yourself recognizable and so we can all get to know each other better. @@ -38,8 +40,12 @@ many packages and themes that are stored in other repos under the [language-javascript](https://github.com/atom/language-javascript), and [atom-light-ui](https://github.com/atom/atom-light-ui). +If your issue is related to a specific package, open an issue on that package's +issue tracker. If you're unsure which package is causing your problem or if +you're having an issue with Atom core, open an issue on this repository. + For more information on how to work with Atom's official packages, see -[Contributing to Atom Packages](https://atom.io/docs/latest/contributing-to-packages.html) +[Contributing to Atom Packages](https://github.com/atom/atom/blob/master/docs/contributing-to-packages.md) ## Pull Requests @@ -48,7 +54,7 @@ For more information on how to work with Atom's official packages, see [JavaScript](https://github.com/styleguide/javascript), and [CSS](https://github.com/styleguide/css) styleguides. * Include thoughtfully-worded, well-structured - [Jasmine](http://jasmine.github.io/) specs. + [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `apm test`. See the [Specs Styleguide](#specs-styleguide) below. * Document new code based on the [Documentation Styleguide](#documentation-styleguide) * End files with a newline. @@ -60,7 +66,7 @@ For more information on how to work with Atom's official packages, see * Class methods and properties (methods starting with a `@`) * Instance methods and properties * Avoid platform-dependent code: - * Use `require('atom').fs.getHomeDirectory()` to get the home directory. + * Use `require('fs-plus').getHomeDirectory()` to get the home directory. * Use `path.join()` to concatenate filenames. * Use `os.tmpdir()` rather than `/tmp` when you need to reference the temporary directory. @@ -104,6 +110,27 @@ For more information on how to work with Atom's official packages, see should be lower-case: * `getURI` instead of `getUri` * `uriToOpen` instead of `URIToOpen` +* Use `slice()` to copy an array +* Add an explicit `return` when your function ends with a `for`/`while` loop and + you don't want it to return a collected array. + +## Specs Styleguide + +- Include thoughtfully-worded, well-structured + [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. +- treat `describe` as a noun or situation. +- treat `it` as a statement about state or how an operation changes state. + +### Example + +```coffee +describe 'a dog', -> + it 'barks', -> + # spec here + describe 'when the dog is happy', -> + it 'wags its tail', -> + # spec here +``` ## Documentation Styleguide diff --git a/Dockerfile b/Dockerfile index 76fa18eae..d792c30c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # DESCRIPTION: Image to build Atom and create a .rpm file # Base docker image -FROM fedora:20 +FROM fedora:21 # Install dependencies RUN yum install -y \ @@ -12,11 +12,11 @@ RUN yum install -y \ glibc-devel \ git-core \ libgnome-keyring-devel \ - rpmdevtools + rpmdevtools \ + nodejs \ + npm -# Install node -RUN curl -sL https://rpm.nodesource.com/setup | bash - -RUN yum install -y nodejs +RUN npm install -g npm@1.4.28 --loglevel error ADD . /atom WORKDIR /atom diff --git a/LICENSE.md b/LICENSE.md index 4d231b456..bbb875dc2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2014 GitHub Inc. +Copyright (c) 2015 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 7ebdf9252..4049a9aa9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https:/ Visit [issue #3684](https://github.com/atom/atom/issues/3684) to learn more about the Atom 1.0 roadmap. +## Documentation + +If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](https://atom.io/docs/latest/) is free and available online, along with ePub, PDF and mobi versions. You can find the source to the manual in [atom/docs](https://github.com/atom/docs). + +The [API reference](https://atom.io/docs/api) for developing packages is also documented on Atom.io. + + ## Installing ### OS X @@ -55,7 +62,3 @@ repeat these steps to upgrade to future releases. * [OS X](docs/build-instructions/os-x.md) * [FreeBSD](docs/build-instructions/freebsd.md) * [Windows](docs/build-instructions/windows.md) - -## Developing - -Check out the [guides](https://atom.io/docs/latest) and the [API reference](https://atom.io/docs/api). diff --git a/apm/package.json b/apm/package.json index 7d894d573..84f73451d 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.142.0" + "atom-package-manager": "0.157.0" } } diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index ae6bab740..d831572c5 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -29,7 +29,7 @@ window.benchmark = (args...) -> else count = defaultCount [fn, options] = args - { profile, focused } = (options ? {}) + {profile, focused} = (options ? {}) method = if focused then fit else it method description, -> @@ -69,7 +69,7 @@ window.keyIdentifierForKey = (key) -> "U+00" + charCode.toString(16) window.keydownEvent = (key, properties={}) -> - $.Event "keydown", _.extend({originalEvent: { keyIdentifier: keyIdentifierForKey(key) }}, properties) + $.Event "keydown", _.extend({originalEvent: {keyIdentifier: keyIdentifierForKey(key)}}, properties) window.clickEvent = (properties={}) -> $.Event "click", properties @@ -93,7 +93,7 @@ window.pagePixelPositionForPoint = (editorView, point) -> point = Point.fromObject point top = editorView.lines.offset().top + point.row * editorView.lineHeight left = editorView.lines.offset().left + point.column * editorView.charWidth - editorView.lines.scrollLeft() - { top, left } + {top, left} window.seteditorViewWidthInChars = (editorView, widthInChars, charWidth=editorView.charWidth) -> editorView.width(charWidth * widthInChars + editorView.lines.position().left) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 7a17f22c2..7b06d9758 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -213,7 +213,7 @@ describe "TokenizedBuffer.", -> beforeEach -> editor = benchmarkFixturesProject.openSync('medium.coffee') - { languageMode, buffer } = editor + {languageMode, buffer} = editor benchmark "construction", 20, -> - new TokenizedBuffer(buffer, { languageMode, tabLength: 2}) + new TokenizedBuffer(buffer, {languageMode, tabLength: 2}) diff --git a/benchmark/browser-process-startup.coffee b/benchmark/browser-process-startup.coffee index 06f2a0d48..2b06eaaa4 100755 --- a/benchmark/browser-process-startup.coffee +++ b/benchmark/browser-process-startup.coffee @@ -8,7 +8,7 @@ _ = require 'underscore-plus' temp = require 'temp' directoryToOpen = temp.mkdirSync('browser-process-startup-') -socketPath = path.join(os.tmpdir(), 'atom.sock') +socketPath = path.join(os.tmpdir(), "atom-#{process.env.USER}.sock") numberOfRuns = 10 deleteSocketFile = -> diff --git a/.npmrc b/build/.npmrc similarity index 100% rename from .npmrc rename to build/.npmrc diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 05efaeb38..dac5c8f1f 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -92,7 +92,7 @@ module.exports = (grunt) -> prebuildLessConfig = src: [ 'static/**/*.less' - 'node_modules/bootstrap/less/bootstrap.less' + 'node_modules/atom-space-pen-views/stylesheets/**/*.less' ] csonConfig = diff --git a/build/package.json b/build/package.json index aa3742e40..212f9c3ed 100644 --- a/build/package.json +++ b/build/package.json @@ -7,12 +7,12 @@ }, "dependencies": { "async": "~0.2.9", - "donna": "1.0.7", + "donna": "1.0.10", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", "grunt": "~0.4.1", - "grunt-atom-shell-installer": "^0.25.0", + "grunt-atom-shell-installer": "^0.28.0", "grunt-cli": "~0.1.9", "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe", "grunt-contrib-coffee": "~0.12.0", diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index fcb1eb6a8..a94b38f75 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -49,6 +49,8 @@ module.exports = (grunt) -> path.join('oniguruma', 'deps') path.join('less', 'dist') path.join('bootstrap', 'docs') + path.join('bootstrap', 'dist') + path.join('bootstrap', 'fonts') path.join('bootstrap', '_config.yml') path.join('bootstrap', '_includes') path.join('bootstrap', '_layouts') diff --git a/build/tasks/compile-packages-slug-task.coffee b/build/tasks/compile-packages-slug-task.coffee index 4e24f948e..613a4a380 100644 --- a/build/tasks/compile-packages-slug-task.coffee +++ b/build/tasks/compile-packages-slug-task.coffee @@ -3,9 +3,30 @@ CSON = require 'season' fs = require 'fs-plus' _ = require 'underscore-plus' +OtherPlatforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'].filter (platform) -> platform isnt process.platform + module.exports = (grunt) -> {spawn, rm} = require('./task-helpers')(grunt) + getMenu = (appDir) -> + menusPath = path.join(appDir, 'menus') + menuPath = path.join(menusPath, "#{process.platform}.json") + menu = CSON.readFileSync(menuPath) if fs.isFileSync(menuPath) + rm menusPath + menu + + getKeymaps = (appDir) -> + keymapsPath = path.join(appDir, 'keymaps') + keymaps = {} + for keymapPath in fs.listSync(keymapsPath, ['.json']) + name = path.basename(keymapPath, path.extname(keymapPath)) + continue unless OtherPlatforms.indexOf(name) is -1 + + keymap = CSON.readFileSync(keymapPath) + keymaps[path.basename(keymapPath)] = keymap + rm keymapsPath + keymaps + grunt.registerTask 'compile-packages-slug', 'Add bundled package metadata information to the main package.json file', -> appDir = fs.realpathSync(grunt.config.get('atom.appDir')) @@ -50,5 +71,7 @@ module.exports = (grunt) -> metadata = grunt.file.readJSON(path.join(appDir, 'package.json')) metadata._atomPackages = packages + metadata._atomMenu = getMenu(appDir) + metadata._atomKeymaps = getKeymaps(appDir) grunt.file.write(path.join(appDir, 'package.json'), JSON.stringify(metadata)) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index b32ebb545..287d44b63 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -65,7 +65,7 @@ module.exports = IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ - 'jschardet@1.1.0': + 'jschardet@1.1.1': license: 'LGPL' source: 'README.md in the repository' sourceText: """ diff --git a/build/tasks/output-for-loop-returns.coffee b/build/tasks/output-for-loop-returns.coffee new file mode 100644 index 000000000..f9b036120 --- /dev/null +++ b/build/tasks/output-for-loop-returns.coffee @@ -0,0 +1,22 @@ +path = require 'path' + +module.exports = (grunt) -> + grunt.registerTask 'output-for-loop-returns', 'Log methods that end with a for loop', -> + appDir = grunt.config.get('atom.appDir') + + jsPaths = [] + grunt.file.recurse path.join(appDir, 'src'), (absolutePath, rootPath, relativePath, fileName) -> + jsPaths.push(absolutePath) if path.extname(fileName) is '.js' + + jsPaths.forEach (jsPath) -> + js = grunt.file.read(jsPath) + method = null + for line, index in js.split('\n') + [match, className, methodName] = /^\s*([a-zA-Z]+)\.(?:prototype\.)?([a-zA-Z]+)\s*=\s*function\(/.exec(line) ? [] + if className and methodName + method = "#{className}::#{methodName}" + else + [match, ctorName] = /^\s*function\s+([a-zA-Z]+)\(/.exec(line) ? [] + + if /^\s*return\s+_results;\s*$/.test(line) + console.log(method ? "#{path.basename(jsPath)}:#{index}") diff --git a/build/tasks/prebuild-less-task.coffee b/build/tasks/prebuild-less-task.coffee index bcaa0467f..5e3bd274e 100644 --- a/build/tasks/prebuild-less-task.coffee +++ b/build/tasks/prebuild-less-task.coffee @@ -1,10 +1,36 @@ path = require 'path' fs = require 'fs' - +temp = require('temp').track() LessCache = require 'less-cache' module.exports = (grunt) -> - grunt.registerMultiTask 'prebuild-less', 'Prebuild cached of compiled LESS files', -> + {rm} = require('./task-helpers')(grunt) + + compileBootstrap = -> + appDir = grunt.config.get('atom.appDir') + bootstrapLessPath = path.join(appDir, 'static', 'bootstrap.less') + bootstrapCssPath = path.join(appDir, 'static', 'bootstrap.css') + + lessCache = new LessCache + cacheDir: temp.mkdirSync('atom-less-cache') + resourcePath: path.resolve('.') + + bootstrapCss = lessCache.readFileSync(bootstrapLessPath) + grunt.file.write(bootstrapCssPath, bootstrapCss) + rm(bootstrapLessPath) + rm(path.join(appDir, 'node_modules', 'bootstrap', 'less')) + + importFallbackVariables = (lessFilePath) -> + if lessFilePath.indexOf('static') is 0 + false + else if lessFilePath.indexOf('atom-space-pen-views') isnt -1 + false + else + true + + grunt.registerMultiTask 'prebuild-less', 'Prebuild cached of compiled Less files', -> + compileBootstrap() + prebuiltConfigurations = [ ['atom-dark-ui', 'atom-dark-syntax'] ['atom-dark-ui', 'atom-light-syntax'] @@ -57,19 +83,21 @@ module.exports = (grunt) -> themeMains.push(mainPath) if grunt.file.isFile(mainPath) importPaths.unshift(stylesheetsDir) if grunt.file.isDir(stylesheetsDir) - grunt.verbose.writeln("Building LESS cache for #{configuration.join(', ').yellow}") + grunt.verbose.writeln("Building Less cache for #{configuration.join(', ').yellow}") lessCache = new LessCache cacheDir: directory resourcePath: path.resolve('.') importPaths: importPaths cssForFile = (file) -> - baseVarImports = """ - @import "variables/ui-variables"; - @import "variables/syntax-variables"; - """ less = fs.readFileSync(file, 'utf8') - lessCache.cssForFile(file, [baseVarImports, less].join('\n')) + if importFallbackVariables(file) + baseVarImports = """ + @import "variables/ui-variables"; + @import "variables/syntax-variables"; + """ + less = [baseVarImports, less].join('\n') + lessCache.cssForFile(file, less) for file in @filesSrc grunt.verbose.writeln("File #{file.cyan} created in cache.") diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee index 6f7a68c61..48b6091c4 100644 --- a/build/tasks/set-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -46,7 +46,7 @@ module.exports = (grunt) -> strings = CompanyName: 'GitHub, Inc.' FileDescription: 'Atom' - LegalCopyright: 'Copyright (C) 2014 GitHub, Inc. All rights reserved' + LegalCopyright: 'Copyright (C) 2015 GitHub, Inc. All rights reserved' ProductName: 'Atom' ProductVersion: version diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 73bb63f70..b5547fe64 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -138,4 +138,4 @@ module.exports = (grunt) -> if process.platform is 'win32' and process.env.JANKY_SHA1 done() else - done(!coreSpecFailed and failedPackages.length == 0) + done(not coreSpecFailed and failedPackages.length is 0) diff --git a/build/tasks/task-helpers.coffee b/build/tasks/task-helpers.coffee index a862f291f..2819c952a 100644 --- a/build/tasks/task-helpers.coffee +++ b/build/tasks/task-helpers.coffee @@ -55,9 +55,9 @@ module.exports = (grunt) -> proc.stderr.on 'data', (data) -> stderr.push(data.toString()) proc.on 'error', (processError) -> error ?= processError proc.on 'close', (exitCode, signal) -> - error ?= new Error(signal) if exitCode != 0 + error ?= new Error(signal) if exitCode isnt 0 results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode} - grunt.log.error results.stderr if exitCode != 0 + grunt.log.error results.stderr if exitCode isnt 0 callback(error, results, exitCode) isAtomPackage: (packagePath) -> diff --git a/coffeelint.json b/coffeelint.json index d8e19fc46..f3caf09cc 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -13,5 +13,19 @@ }, "no_debugger": { "level": "error" + }, + "prefer_english_operator": { + "level": "error" + }, + "colon_assignment_spacing": { + "spacing": { + "left": 0, + "right": 1 + }, + "level": "error" + }, + "braces_spacing": { + "spaces": 0, + "level": "error" } } diff --git a/docs/README.md b/docs/README.md index e8863f520..c66788d87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,21 @@ -# Welcome to the Atom Docs +# Atom Docs ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) -TODO: Write when docs move to a dedicated repo. +Most of the Atom user and developer documentation is contained in the [Atom Docs](https://github.com/atom/docs) repository. + +In this directory you can only find very specific build and API level documentation. Some of this may eventually move to the docs repository as well. + +## Build documentation + +Instructions for building Atom on various platforms from source. + +* [OS X](build-instructions/os-x.md) +* [Windows](build-instructions/windows.md) +* [Linux](build-instructions/linux.md) +* [FreeBSD](build-instructions/freebsd.md) + +## Other documentation here + +* [apm REST API](apm-rest-api.md) +* [Tips for contributing to packages](contributing-to-packages.md) diff --git a/docs/advanced/configuration.md b/docs/advanced/configuration.md deleted file mode 100644 index 82d821eb0..000000000 --- a/docs/advanced/configuration.md +++ /dev/null @@ -1,58 +0,0 @@ -## Configuration API - -### Reading Config Settings - -If you are writing a package that you want to make configurable, you'll need to -read config settings via the `atom.config` global. You can read the current -value of a namespaced config key with `atom.config.get`: - -```coffeescript -# read a value with `config.get` -@showInvisibles() if atom.config.get "editor.showInvisibles" -``` - -Or you can subscribe via `atom.config.observe` to track changes from any view -object. - -```coffeescript -{View} = require 'space-pen' - -class MyView extends View - attached: -> - @fontSizeObserveSubscription = - atom.config.observe 'editor.fontSize', (newValue, {previous}) => - @adjustFontSize() - - detached: -> - @fontSizeObserveSubscription.dispose() -``` - -The `atom.config.observe` method will call the given callback immediately with -the current value for the specified key path, and it will also call it in the -future whenever the value of that key path changes. If you only want to invoke -the callback when the next time the value changes, use `atom.config.onDidChange` -instead. - -Subscription methods return *disposable* subscription objects. Note in the -example above how we save the subscription to the `@fontSizeObserveSubscription` -instance variable and dispose of it when the view is detached. To group multiple -subscriptions together, you can add them all to a -[`CompositeDisposable`][composite-disposable] that you dispose when the view is -detached. - -### Writing Config Settings - -The `atom.config` database is populated on startup from `~/.atom/config.cson`, -but you can programmatically write to it with `atom.config.set`: - -```coffeescript -# basic key update -atom.config.set("core.showInvisibles", true) -``` - -If you're exposing package configuration via specific key paths, you'll want to -associate them with a schema in your package's main module. Read more about -schemas in the [config API docs][config-api]. - -[composite-disposable]: https://atom.io/docs/api/latest/CompositeDisposable -[config-api]: https://atom.io/docs/api/latest/Config diff --git a/docs/advanced/keymaps.md b/docs/advanced/keymaps.md deleted file mode 100644 index ee966a45d..000000000 --- a/docs/advanced/keymaps.md +++ /dev/null @@ -1,171 +0,0 @@ -# Keymaps In-Depth - -## Structure of a Keymap File - -Keymap files are encoded as JSON or CSON files containing nested hashes. They -work much like style sheets, but instead of applying style properties to elements -matching the selector, they specify the meaning of keystrokes on elements -matching the selector. Here is an example of some bindings that apply when -keystrokes pass through `atom-text-editor` elements: - -```coffee -'atom-text-editor': - 'cmd-delete': 'editor:delete-to-beginning-of-line' - 'alt-backspace': 'editor:delete-to-beginning-of-word' - 'ctrl-A': 'editor:select-to-first-character-of-line' - 'ctrl-shift-e': 'editor:select-to-end-of-line' - 'cmd-left': 'editor:move-to-first-character-of-line' - -'atom-text-editor:not([mini])': - 'cmd-alt-[': 'editor:fold-current-row' - 'cmd-alt-]': 'editor:unfold-current-row' -``` - -Beneath the first selector are several bindings, mapping specific *keystroke -patterns* to *commands*. When an element with the `atom-text-editor` class is focused and -`cmd-delete` is pressed, an custom DOM event called -`editor:delete-to-beginning-of-line` is emitted on the `atom-text-editor` element. - -The second selector group also targets editors, but only if they don't have the -`mini` attribute. In this example, the commands for code folding don't really -make sense on mini-editors, so the selector restricts them to regular editors. - -### Keystroke Patterns - -Keystroke patterns express one or more keystrokes combined with optional -modifier keys. For example: `ctrl-w v`, or `cmd-shift-up`. A keystroke is -composed of the following symbols, separated by a `-`. A multi-keystroke pattern -can be expressed as keystroke patterns separated by spaces. - - -| Type | Examples -| --------------------|---------------------------- -| Character literals | `a` `4` `$` -| Modifier keys | `cmd` `ctrl` `alt` `shift` -| Special keys | `enter` `escape` `backspace` `delete` `tab` `home` `end` `pageup` `pagedown` `left` `right` `up` `down` - -### Commands - -Commands are custom DOM events that are triggered when a keystroke matches a -binding. This allows user interface code to listen for named commands without -specifying the specific keybinding that triggers it. For example, the following -code creates a command to insert the current date in an editor: - -```coffee -atom.commands.add 'atom-text-editor', - 'user:insert-date': (event) -> - editor = @getModel() - editor.insertText(new Date().toLocaleString()) -``` - -`atom.commands` refers to the global {CommandRegistry} instance where all commands -are set and consequently picked up by the command palette. - -When you are looking to bind new keys, it is often useful to use the command -palette (`ctrl-shift-p`) to discover what commands are being listened for in a -given focus context. Commands are "humanized" following a simple algorithm, so a -command like `editor:fold-current-row` would appear as "Editor: Fold Current -Row". - -### "Composed" Commands - -A common question is, "How do I make a single keybinding execute two or more -commands?" There isn't any direct support for this in Atom, but it can be -achieved by creating a custom command that performs the multiple actions -you desire and then creating a keybinding for that command. For example, let's -say I want to create a "composed" command that performs a Select Line followed -by Cut. You could add the following to your `init.coffee`: - -```coffee -atom.commands.add 'atom-text-editor', 'custom:cut-line', -> - editor = atom.workspace.getActiveTextEditor() - editor.selectLinesContainingCursors() - editor.cutSelectedText() -``` - -Then let's say we want to map this custom command to `alt-ctrl-z`, you could -add the following to your keymap: - -```coffee -'atom-text-editor': - 'alt-ctrl-z': 'custom:cut-line' -``` - -### Specificity and Cascade Order - -As is the case with CSS applying styles, when multiple bindings match for a -single element, the conflict is resolved by choosing the most *specific* -selector. If two matching selectors have the same specificity, the binding -for the selector appearing later in the cascade takes precedence. - -Currently, there's no way to specify selector ordering within a single keymap, -because JSON objects do not preserve order. We eventually plan to introduce a -custom CSS-like file format for keymaps that allows for ordering within a single -file. For now, we've opted to handle cases where selector ordering is critical -by breaking the keymap into two separate files, such as `snippets-1.cson` and -`snippets-2.cson`. - -## Removing Bindings - -When the keymap system encounters a binding with the `unset!` directive as its -command, it will treat the current element as if it had no key bindings matching -the current keystroke sequence and continue searching from its parent. If you -want to remove a binding from a keymap you don't control, such as keymaps in -Atom core or in packages, use the `unset!` directive. - -For example, the following code removes the keybinding for `a` in the Tree View, -which is normally used to trigger the `tree-view:add-file` command: - -```coffee -'.tree-view': - 'a': 'unset!' -``` - -![](https://cloud.githubusercontent.com/assets/38924/3174771/e7f6ce64-ebf4-11e3-922d-f280bffb3fc5.png) - -## Forcing Chromium's Native Keystroke Handling - -If you want to force the native browser behavior for a given keystroke, use the -`native!` directive as the command of a binding. This can be useful to enable -the correct behavior in native input elements, for example. If you apply the -`.native-key-bindings` class to an element, all the keystrokes typically handled -by the browser will be assigned the `native!` directive. - -## Overloading Key Bindings - -Occasionally, it makes sense to layer multiple actions on top of the same key -binding. An example of this is the snippets package. Snippets are inserted by -typing a snippet prefix such as `for` and then pressing `tab`. Every time `tab` -is pressed, we want to execute code attempting to expand a snippet if one exists -for the text preceding the cursor. If a snippet *doesn't* exist, we want `tab` -to actually insert whitespace. - -To achieve this, the snippets package makes use of the `.abortKeyBinding()` -method on the event object representing the `snippets:expand` command. - -```coffee-script -# pseudo-code -editor.command 'snippets:expand', (e) => - if @cursorFollowsValidPrefix() - @expandSnippet() - else - e.abortKeyBinding() -``` - -When the event handler observes that the cursor does not follow a valid prefix, -it calls `e.abortKeyBinding()`, telling the keymap system to continue searching -for another matching binding. - -## Step-by-Step: How Keydown Events are Mapped to Commands - -* A keydown event occurs on a *focused* element. -* Starting at the focused element, the keymap walks upward towards the root of - the document, searching for the most specific CSS selector that matches the - current DOM element and also contains a keystroke pattern matching the keydown - event. -* When a matching keystroke pattern is found, the search is terminated and the - pattern's corresponding command is triggered on the current element. -* If `.abortKeyBinding()` is called on the triggered event object, the search - is resumed, triggering a binding on the next-most-specific CSS selector for - the same element or continuing upward to parent elements. -* If no bindings are found, the event is handled by Chromium normally. diff --git a/docs/advanced/node-modules.md b/docs/advanced/node-modules.md deleted file mode 100644 index 173713803..000000000 --- a/docs/advanced/node-modules.md +++ /dev/null @@ -1,24 +0,0 @@ -## Developing Node Modules - -Atom contains a number of packages that are Node modules instead of Atom packages. If you want to -make changes to the Node modules, for instance `atom-keymap`, you have to link them into the -development environment differently than you would a normal Atom package. - -### Linking a Node Module Into Your Atom Dev Environment - -Here are the steps to run a local version of a node module *not an apm* within Atom. We're using -`atom-keymap` as an example: - -```bash -$ git clone https://github.com/atom/atom-keymap.git -$ cd atom-keymap -$ npm install -$ npm link -$ apm rebuild # This is the special step, it makes the npm work with Atom's version of Node -$ cd WHERE-YOU-CLONED-ATOM -$ npm link atom-keymap -$ atom # Should work! -``` - -After this, you'll have to `npm install` and `apm rebuild` when you make a change to the node -module's code. diff --git a/docs/advanced/scopes-and-scope-descriptors.md b/docs/advanced/scopes-and-scope-descriptors.md deleted file mode 100644 index 7ee82995e..000000000 --- a/docs/advanced/scopes-and-scope-descriptors.md +++ /dev/null @@ -1,87 +0,0 @@ -# Scoped Settings, Scopes and Scope Descriptors - -Atom supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files. - -Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names. - -## Scope names in syntax tokens - -Each token in the editor has a collection of scope names. For example, the aformentioned JavaScript function name might have the scope names `function` and `name`. An open paren might have the scope names `punctuation`, `parameters`, `begin`. - -Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes. - -Take this piece of JavaScript: - -```js -function functionName() { - console.log('Log it out'); -} -``` - -In the dev tools, the first line's markup looks like this. - -![screen shot 2014-10-14 at 11 21 35 am](https://cloud.githubusercontent.com/assets/69169/4634321/2b1b923c-53cf-11e4-9268-6e57bcb14ec8.png) - -All the class names on the spans are scope names. Any scope name can be used to target a setting's value. - -## Scope Selectors - -Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples: - -```coffee -'.source.js' # selects all javascript tokens -'.source.js .function.name' # selects all javascript function names -'.function.name' # selects all function names in any language -``` - -[Config::set][config-set] accepts a `scopeSelector`. If you'd like to set a setting for JavaScript function names, you can give it the js function name `scopeSelector`: - -```coffee -atom.config.set('.source.js .function.name', 'my-package.my-setting', 'special value') -``` - -## Scope Descriptors - -A scope descriptor is an [Object][scope-descriptor] that wraps an `Array` of -`String`s. The Array describes a path from the root of the syntax tree to a -token including _all_ scope names for the entire path. - -In our JavaScript example above, a scope descriptor for the function name token would be: - -```coffee -['source.js', 'meta.function.js', 'entity.name.function.js'] -``` - -[Config::get][config-get] accepts a `scopeDescriptor`. You can get the value for your setting scoped to JavaScript function names via: - -```coffee -scopeDescriptor = ['source.js', 'meta.function.js', 'entity.name.function.js'] -value = atom.config.get(scopeDescriptor, 'my-package.my-setting') -``` - -But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor: - -* [Editor::getRootScopeDescriptor][editor-getRootScopeDescriptor] to get the language's descriptor. eg. `[".source.js"]` -* [Editor::scopeDescriptorForBufferPosition][editor-scopeDescriptorForBufferPosition] to get the descriptor at a specific position in the buffer. -* [Cursor::getScopeDescriptor][cursor-getScopeDescriptor] to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return `["source.js", "meta.function.js", "entity.name.function.js"]` - -Let's revisit our example using these methods: - -```coffee -editor = atom.workspace.getActiveTextEditor() -cursor = editor.getLastCursor() -valueAtCursor = atom.config.get(cursor.getScopeDescriptor(), 'my-package.my-setting') -valueForLanguage = atom.config.get(editor.getRootScopeDescriptor(), 'my-package.my-setting') -``` - - -[config]:https://atom.io/docs/api/latest/Config -[config-get]:https://atom.io/docs/api/latest/Config#instance-get -[config-set]:https://atom.io/docs/api/latest/Config#instance-set -[config-observe]:https://atom.io/docs/api/latest/Config#instance-observe - -[editor-getRootScopeDescriptor]:https://atom.io/docs/api/latest/TextEditor#instance-getRootScopeDescriptor -[editor-scopeDescriptorForBufferPosition]:https://atom.io/docs/api/latest/TextEditor#instance-scopeDescriptorForBufferPosition - -[cursor-getScopeDescriptor]:https://atom.io/docs/api/latest/Cursor#instance-getScopeDescriptor -[scope-descriptor]:https://atom.io/docs/api/latest/ScopeDescriptor diff --git a/docs/advanced/serialization.md b/docs/advanced/serialization.md deleted file mode 100644 index c2a5f303a..000000000 --- a/docs/advanced/serialization.md +++ /dev/null @@ -1,75 +0,0 @@ -## Serialization in Atom - -When a window is refreshed or restored from a previous session, the view and its -associated objects are *deserialized* from a JSON representation that was stored -during the window's previous shutdown. For your own views and objects to be -compatible with refreshing, you'll need to make them play nicely with the -serializing and deserializing. - -### Package Serialization Hook - -Your package's main module can optionally include a `serialize` method, which -will be called before your package is deactivated. You should return JSON, which -will be handed back to you as an argument to `activate` next time it is called. -In the following example, the package keeps an instance of `MyObject` in the -same state across refreshes. - -```coffee-script -module.exports = - activate: (state) -> - @myObject = - if state - atom.deserializers.deserialize(state) - else - new MyObject("Hello") - - serialize: -> - @myObject.serialize() -``` - -### Serialization Methods - -```coffee-script -class MyObject - atom.deserializers.add(this) - - @deserialize: ({data}) -> new MyObject(data) - constructor: (@data) -> - serialize: -> { deserializer: 'MyObject', data: @data } -``` - -#### .serialize() -Objects that you want to serialize should implement `.serialize()`. This method -should return a serializable object, and it must contain a key named -`deserializer` whose value is the name of a registered deserializer that can -convert the rest of the data to an object. It's usually just the name of the -class itself. - -#### @deserialize(data) -The other side of the coin is the `deserialize` method, which is usually a -class-level method on the same class that implements `serialize`. This method's -job is to convert a state object returned from a previous call `serialize` back -into a genuine object. - -#### atom.deserializers.add(klass) -You need to call the `atom.deserializers.add` method with your class in -order to make it available to the deserialization system. Now you can call the -global `deserialize` method with state returned from `serialize`, and your -class's `deserialize` method will be selected automatically. - -### Versioning - -```coffee-script -class MyObject - atom.deserializers.add(this) - - @version: 2 - @deserialize: (state) -> ... - serialize: -> { version: @constructor.version, ... } -``` - -Your serializable class can optionally have a class-level `@version` property -and include a `version` key in its serialized state. When deserializing, Atom -will only attempt to call deserialize if the two versions match, and otherwise -return undefined. We plan on implementing a migration system in the future, but -this at least protects you from improperly deserializing old state. diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index fddb941c7..3f8c27660 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -78,6 +78,7 @@ If none of this works, do install Github for Windows and use its Git shell. Make ``` $env:GYP_MSVS_VERSION=2013 ``` + * If you are using Visual Studio 2013 and the build fails with some other error message this environment variable might still be required. * Other `node-gyp` errors on first build attempt, even though the right node and python versions are installed. * Do try the build command one more time, as experience shows it often works on second try in many of these cases. diff --git a/docs/converting-a-text-mate-bundle.md b/docs/converting-a-text-mate-bundle.md deleted file mode 100644 index 3471bc83b..000000000 --- a/docs/converting-a-text-mate-bundle.md +++ /dev/null @@ -1,52 +0,0 @@ -## Converting a TextMate Bundle - -This guide will show you how to convert a [TextMate][TextMate] bundle to an -Atom package. - -Converting a TextMate bundle will allow you to use its editor preferences, -snippets, and colorization inside Atom. - -### Install apm - -The `apm` command line utility that ships with Atom supports converting -a TextMate bundle to an Atom package. - -Check that you have `apm` installed by running the following command in your -terminal: - -```sh -apm help init -``` - -You should see a message print out with details about the `apm init` command. - -If you do not, launch Atom and run the _Atom > Install Shell Commands_ menu -to install the `apm` and `atom` commands. - -### Convert the Package - -Let's convert the TextMate bundle for the [R][R] programming language. You can find other existing TextMate bundles [here][TextMateOrg]. - -You can convert the R bundle with the following command: - -```sh -apm init --package ~/.atom/packages/language-r --convert https://github.com/textmate/r.tmbundle -``` - -You can now browse to `~/.atom/packages/language-r` to see the converted bundle. - -:tada: Your new package is now ready to use, launch Atom and open a `.r` file in -the editor to see it in action! - -### Further Reading - -* Check out [Publishing a Package](publishing-a-package.html) for more information - on publishing the package you just created to [atom.io][atomio]. - -[atomio]: https://atom.io -[CSS]: https://en.wikipedia.org/wiki/Cascading_Style_Sheets -[Less]: http://lesscss.org -[plist]: https://en.wikipedia.org/wiki/Property_list -[R]: https://en.wikipedia.org/wiki/R_(programming_language) -[TextMate]: http://macromates.com -[TextMateOrg]: https://github.com/textmate diff --git a/docs/converting-a-text-mate-theme.md b/docs/converting-a-text-mate-theme.md deleted file mode 100644 index 79dacd523..000000000 --- a/docs/converting-a-text-mate-theme.md +++ /dev/null @@ -1,68 +0,0 @@ -## Converting a TextMate Theme - -This guide will show you how to convert a [TextMate][TextMate] theme to an Atom -theme. - -### Differences - -TextMate themes use [plist][plist] files while Atom themes use [CSS][CSS] or -[Less][Less] to style the UI and syntax in the editor. - -The utility that converts the theme first parses the theme's plist file and -then creates comparable CSS rules and properties that will style Atom similarly. - -### Install apm - -The `apm` command line utility that ships with Atom supports converting -a TextMate theme to an Atom theme. - -Check that you have `apm` installed by running the following command in your -terminal: - -```sh -apm help init -``` - -You should see a message print out with details about the `apm init` command. - -If you do not, launch Atom and run the _Atom > Install Shell Commands_ menu -to install the `apm` and `atom` commands. - -You can now run `apm help init` to see all the options for initializing new -packages and themes. - -### Convert the Theme - -Download the theme you wish to convert, you can browse existing TextMate themes -[here][TextMateThemes]. - -Now, let's say you've downloaded the theme to `~/Downloads/MyTheme.tmTheme`, -you can convert the theme with the following command: - -```sh -apm init --theme ~/.atom/packages/my-theme --convert ~/Downloads/MyTheme.tmTheme -``` - -You can browse to `~/.atom/packages/my-theme` to see the converted theme. - -### Activate the Theme - -Now that your theme is installed to `~/.atom/packages` you can enable it -by launching Atom and selecting the _Atom > Preferences..._ menu. - -Select the _Themes_ link on the left side and choose _My Theme_ from the -__Syntax Theme__ dropdown menu to enable your new theme. - -:tada: Your theme is now enabled, open an editor to see it in action! - -### Further Reading - -* Check out [Publishing a Package](publishing-a-package.html) for more information - on publishing the theme you just created to [atom.io][atomio]. - -[atomio]: https://atom.io -[CSS]: https://en.wikipedia.org/wiki/Cascading_Style_Sheets -[Less]: http://lesscss.org -[plist]: https://en.wikipedia.org/wiki/Property_list -[TextMate]: http://macromates.com -[TextMateThemes]: http://wiki.macromates.com/Themes/UserSubmittedThemes diff --git a/docs/creating-a-package.md b/docs/creating-a-package.md deleted file mode 100644 index d00f6ecc1..000000000 --- a/docs/creating-a-package.md +++ /dev/null @@ -1,515 +0,0 @@ -# Creating Packages - -Packages are at the core of Atom. Nearly everything outside of the main editor -is handled by a package. That includes "core" pieces like the [file tree][file-tree], -[status bar][status-bar], [syntax highlighting][cs-syntax], and more. - -A package can contain a variety of different resource types to change Atom's -behavior. The basic package layout is as follows: - -```text -my-package/ - grammars/ - keymaps/ - lib/ - menus/ - spec/ - snippets/ - styles/ - index.coffee - package.json -``` - -Not every package will have (or need) all of these directories. - -We have [a tutorial on creating your first package][first-package]. - -There are also guides for converting [TextMate bundles][convert-bundle] and -[TextMate themes][convert-theme] so they work in Atom. - -## package.json - -Similar to [npm packages][npm], Atom packages contain a _package.json_ file -in their top-level directory. This file contains metadata about the package, -such as the path to its "main" module, library dependencies, and manifests -specifying the order in which its resources should be loaded. - -In addition to the regular [npm package.json keys][npm-keys] available, Atom -package.json files have their own additions. - -- `main` (**Required**): the path to the CoffeeScript file that's the entry point -to your package. -- `styles` (**Optional**): an Array of Strings identifying the order of the -style sheets your package needs to load. If not specified, style sheets in the -_styles_ directory are added alphabetically. -- `keymaps`(**Optional**): an Array of Strings identifying the order of the -key mappings your package needs to load. If not specified, mappings in the -_keymaps_ directory are added alphabetically. -- `menus`(**Optional**): an Array of Strings identifying the order of -the menu mappings your package needs to load. If not specified, mappings -in the _menus_ directory are added alphabetically. -- `snippets` (**Optional**): an Array of Strings identifying the order of the -snippets your package needs to load. If not specified, snippets in the -_snippets_ directory are added alphabetically. -- `activationCommands` (**Optional**): an Array of Strings identifying commands that -trigger your package's activation. You can delay the loading of your package -until one of these events is triggered. -- `providedServices` (**Optional**): an Object describing the services that your -package provides, which can be used by other packages. The keys of this object -are the names of the services, and the values are Objects with the following -keys: - - `description` (**Optional**) a String describing the service - - `versions` (**Required**) an Object whose keys are Semver version strings, - and whose values are names of methods in your package's top-level module - that return a value implementing the service. -- `consumedServices` (**Optional**): an Object describing the services that your -package uses, which can be provided by other packages. The keys of this object -are the names of the services, and the values are Objects with the following -keys: - - `versions` (**Required**) an Object whose keys are Semver version ranges - and whose values are names of methods in your package's top-level module - that are called with values implementing the service. - -## Source Code - -If you want to extend Atom's behavior, your package should contain a single -top-level module, which you export from _index.coffee_ (or whichever file is -indicated by the `main` key in your _package.json_ file). The remainder of your -code should be placed in the `lib` directory, and required from your top-level -file. - -Your package's top-level module is a singleton object that manages the lifecycle -of your extensions to Atom. Even if your package creates ten different views and -appends them to different parts of the DOM, it's all managed from your top-level -object. - -Your package's top-level module should implement the following methods: - -- `activate(state)`: This **required** method is called when your -package is activated. It is passed the state data from the last time the window -was serialized if your module implements the `serialize()` method. Use this to -do initialization work when your package is started (like setting up DOM -elements or binding events). - -- `serialize()`: This **optional** method is called when the window is shutting -down, allowing you to return JSON to represent the state of your component. When -the window is later restored, the data you returned is passed to your -module's `activate` method so you can restore your view to where the user left -off. - -- `deactivate()`: This **optional** method is called when the window is shutting -down, or when your package is being updated or disabled. If your package is -watching any files, holding external resources, providing commands or subscribing -to events, release them here. - -### Simple Package Code - -Your directory would look like this: - -```text -my-package/ - package.json - index.coffee - lib/ - my-package.coffee -``` - -`index.coffee` might be: -```coffeescript -module.exports = require "./lib/my-package" -``` - -`my-package/my-package.coffee` might start: -```coffeescript -module.exports = - activate: (state) -> # ... - deactivate: -> # ... - serialize: -> # ... -``` - -Beyond this simple contract, your package has access to [Atom's API][api]. Be aware -that the Atom 1.0 API is mostly frozen. Refer to the API documentation for what -is public. That said, please collaborate with us if you need an API that doesn't -exist. Our goal is to build out Atom's API organically based on the needs of -package authors like you. - -## Style Sheets - -Style sheets for your package should be placed in the _styles_ directory. -Any style sheets in this directory will be loaded and attached to the DOM when -your package is activated. Style sheets can be written as CSS or [Less], but -Less is recommended. - -Ideally, you won't need much in the way of styling. We've provided a standard -set of components which define both the colors and UI elements for any package -that fits into Atom seamlessly. You can view all of Atom's UI components by -opening the styleguide: open the command palette (`cmd-shift-P`) and search for -_styleguide_, or just type `cmd-ctrl-shift-G`. - -If you _do_ need special styling, try to keep only structural styles in the -package style sheets. If you _must_ specify colors and sizing, these should be -taken from the active theme's [ui-variables.less][ui-variables]. For more -information, see the [theme variables docs][theme-variables]. If you follow this -guideline, your package will look good out of the box with any theme! - -An optional `styleSheets` array in your _package.json_ can list the style sheets -by name to specify a loading order; otherwise, style sheets are loaded -alphabetically. - -## Keymaps - -It's recommended that you provide key bindings for commonly used actions for -your extension, especially if you're also adding a new command: - -```coffeescript -'.tree-view-scroller': - 'ctrl-V': 'changer:magic' -``` - -Keymaps are placed in the _keymaps_ subdirectory. By default, all keymaps are -loaded in alphabetical order. An optional `keymaps` array in your _package.json_ -can specify which keymaps to load and in what order. - - -Keybindings are executed by determining which element the keypress occurred on. -In the example above, `changer:magic` command is executed when pressing `ctrl-V` -on the `.tree-view-scroller` element. - -See the [main keymaps documentation][keymaps] for more detailed information on -how keymaps work. - -## Menus - -Menus are placed in the _menus_ subdirectory. By default, all menus are loaded -in alphabetical order. An optional `menus` array in your _package.json_ can -specify which menus to load and in what order. - -### Application Menu - -It's recommended that you create an application menu item for common actions -with your package that aren't tied to a specific element: - -```coffeescript -'menu': [ - { - 'label': 'Packages' - 'submenu': [ - { - 'label': 'My Package' - 'submenu': [ - { - 'label': 'Toggle' - 'command': 'my-package:toggle' - } - ] - } - ] - } -] -``` - -To add your own item to the application menu, simply create a top level `menu` -key in any menu configuration file in _menus_. This can be a JSON or [CSON] -file. - -The menu templates you specify are merged with all other templates provided -by other packages in the order which they were loaded. - -### Context Menu - -It's recommended to specify a context menu item for commands that are linked to -specific parts of the interface, like adding a file in the tree-view: - -```coffeescript -'context-menu': - '.tree-view': [ - {label: 'Add file', command: 'tree-view:add-file'} - ] - 'atom-workspace': [ - {label: 'Inspect Element', command: 'core:inspect'} - ] -``` - -To add your own item to the application menu simply create a top level -`context-menu` key in any menu configuration file in _menus_. This can be a -JSON or [CSON] file. - -Context menus are created by determining which element was selected and then -adding all of the menu items whose selectors match that element (in the order -which they were loaded). The process is then repeated for the elements until -reaching the top of the DOM tree. - -In the example above, the `Add file` item will only appear when the focused item -or one of its parents has the `tree-view` class applied to it. - -You can also add separators and submenus to your context menus. To add a -submenu, provide a `submenu` key instead of a command. To add a separator, add -an item with a single `type: 'separator'` key/value pair. - -```coffeescript -'context-menu': - 'atom-workspace': [ - { - label: 'Text' - submenu: [ - {label: 'Inspect Element', command: 'core:inspect'} - {type: 'separator'} - {label: 'Selector All', command: 'core:select-all'} - {type: 'separator'} - {label: 'Deleted Selected Text', command: 'core:delete'} - ] - } - ] -``` - -## Snippets - -An extension can supply language snippets in the _snippets_ directory which -allows the user to enter repetitive text quickly: - -```coffeescript -".source.coffee .specs": - "Expect": - prefix: "ex" - body: "expect($1).to$2" - "Describe": - prefix: "de" - body: """ - describe "${1:description}", -> - ${2:body} - """ -``` - -A snippets file contains scope selectors at its top level (`.source.coffee -.spec`). Each scope selector contains a hash of snippets keyed by their name -(`Expect`, `Describe`). Each snippet also specifies a `prefix` and a `body` key. -The `prefix` represents the first few letters to type before hitting the `tab` -key to autocomplete. The `body` defines the autofilled text. You can use -placeholders like `$1`, `$2`, to indicate regions in the body the user can -navigate to every time they hit `tab`. - -All files in the directory are automatically loaded, unless the _package.json_ -supplies a `snippets` key. As with all scoped items, snippets loaded later take -precedence over earlier snippets when two snippets match a scope with the same -specificity. - -## Language Grammars - -If you're developing a new language grammar, you'll want to place your file in -the _grammars_ directory. Each grammar is a pairing of two keys, `match` and -`captures`. `match` is a regular expression identifying the pattern to -highlight, while `captures` is an object representing what to do with each -matching group. - -For example: - - -```coffeescript -{ - 'match': '(?:^|\\s)(__[^_]+__)' - 'captures': - '1': 'name': 'markup.bold.gfm' -} -``` - -This indicates that the first matching capture (`(__[^_]+__)`) should have the -`markup.bold.gfm` token applied to it. - -To capture a single group, simply use the `name` key instead: - -```coffeescript -{ - 'match': '^#{1,6}\\s+.+$' - 'name': 'markup.heading.gfm' -} -``` - -This indicates that Markdown header lines (`#`, `##`, `###`) should be applied -with the `markup.heading.gfm` token. - -More information about the significance of these tokens can be found in -[section 12.4 of the TextMate Manual][tm-tokens]. - -Your grammar should also include a `filetypes` array, which is a list of file -extensions your grammar supports: - -```coffeescript -'fileTypes': [ - 'markdown' - 'md' - 'mkd' - 'mkdown' - 'ron' -] -``` - -## Adding Configuration Settings - -You can support config settings in your package that are editable in the -settings view. Specify a `config` key in your package main: - -```coffeescript -module.exports = - # Your config schema! - config: - someInt: - type: 'integer' - default: 23 - minimum: 1 - activate: (state) -> # ... - # ... -``` - -To define the configuration, we use [json schema][json-schema] which allows you -to indicate the type your value should be, its default, etc. - -See the [Config API Docs](https://atom.io/docs/api/latest/Config) for more -details specifying your configuration. - -## Interacting With Other Packages Via Services - -Atom packages can interact with each other through versioned APIs called -*services*. To provide a service, in your `package.json`, specify one or more -version numbers, each paired with the name of a method on your package's main module: - -```json -{ - "providedServices": { - "my-service": { - "description": "Does a useful thing", - "versions": { - "1.2.3": "provideMyServiceV1", - "2.3.4": "provideMyServiceV2", - } - } - } -} -``` - -In your package's main module, implement the methods named above. These methods -will be called any time a package is activated that consumes their corresponding -service. They should return a value that implements the service's API. - - -```coffeescript -module.exports = - activate: -> # ... - - provideMyServiceV1: -> - adaptToLegacyAPI(myService) - - provideMyServiceV2: -> - myService -``` - -Similarly, to consume a service, specify one or more [version *ranges*][version-ranges], -each paired with the name of a method on the package's main module: - -```json -{ - "consumedServices": { - "another-service": { - "versions": { - "^1.2.3": "consumeAnotherServiceV1", - ">=2.3.4 <2.5": "consumeAnotherServiceV2", - } - } - } -} -``` - -These methods will be called any time a package is activated that *provides* their -corresponding service. They will receive the service object as an argument. You -will usually need to perform some kind of cleanup in the event that the package -providing the service is deactivated. To do this, return a `Disposable` from -your service-consuming method: - -```coffeescript -{Disposable} = require 'atom' - -module.exports = - activate: -> # ... - - consumeAnotherServiceV1: (service) -> - useService(adaptServiceFromLegacyAPI(service)) - new Disposable -> stopUsingService(service) - - consumeAnotherServiceV2: (service) -> - useService(service) - new Disposable -> stopUsingService(service) -``` - -## Bundle External Resources - -It's common to ship external resources like images and fonts in the package, to -make it easy to reference the resources in HTML or CSS, you can use the `atom` -protocol URLs to load resources in the package. - -The URLs should be in the format of -`atom://package-name/relative-path-to-package-of-resource`, for example, the -`atom://image-view/images/transparent-background.gif` would be equivalent to -`~/.atom/packages/image-view/images/transparent-background.gif`. - -You can also use the `atom` protocol URLs in themes. - -## Writing Tests - -Your package **should** have tests, and if they're placed in the _spec_ -directory, they can be run by Atom. - -Under the hood, [Jasmine] executes your tests, so you can assume that any DSL -available there is also available to your package. - -## Running Tests - -Once you've got your test suite written, you can run it by pressing -`cmd-alt-ctrl-p` or via the _Developer > Run Package Specs_ menu. - -You can also use the `apm test` command to run them from the command line. It -prints the test output and results to the console and returns the proper status -code depending on whether the tests passed or failed. - -## Publishing - -Atom bundles a command line utility called apm which can be used to publish -Atom packages to the public registry. - -Once your package is written and ready for distribution you can run the -following to publish your package: - -```sh -cd my-package -apm publish minor -``` - -This will update your `package.json` to have a new minor `version`, commit the -change, create a new [Git tag][git-tag], and then upload the package to the -registry. - -Run `apm help publish` to see all the available options and `apm help` to see -all the other available commands. - -[api]: https://atom.io/docs/api/latest -[file-tree]: https://github.com/atom/tree-view -[status-bar]: https://github.com/atom/status-bar -[cs-syntax]: https://github.com/atom/language-coffee-script -[npm]: https://en.wikipedia.org/wiki/Npm_(software) -[npm-keys]: https://docs.npmjs.com/files/package.json -[git-tag]: http://git-scm.com/book/en/Git-Basics-Tagging -[wrap-guide]: https://github.com/atom/wrap-guide/ -[keymaps]: advanced/keymaps.md -[theme-variables]: theme-variables.md -[tm-tokens]: http://manual.macromates.com/en/language_grammars.html -[spacepen]: https://github.com/nathansobo/space-pen -[path]: http://nodejs.org/docs/latest/api/path.html -[jquery]: http://jquery.com/ -[underscore]: http://underscorejs.org/ -[jasmine]: http://jasmine.github.io -[cson]: https://github.com/atom/season -[Less]: http://lesscss.org -[ui-variables]: https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less -[first-package]: your-first-package.html -[convert-bundle]: converting-a-text-mate-bundle.html -[convert-theme]: converting-a-text-mate-theme.html -[json-schema]: http://json-schema.org/ -[version-ranges]: https://docs.npmjs.com/misc/semver#ranges diff --git a/docs/creating-a-theme.md b/docs/creating-a-theme.md deleted file mode 100644 index 6e8ab1dc4..000000000 --- a/docs/creating-a-theme.md +++ /dev/null @@ -1,148 +0,0 @@ -# Creating a Theme - -Atom's interface is rendered using HTML, and it's styled via [Less] which is a -superset of CSS. Don't worry if you haven't heard of Less before; it's just like -CSS, but with a few handy extensions. - -Atom supports two types of themes: _UI_ and _syntax_. UI themes style -elements such as the tree view, the tabs, drop-down lists, and the status bar. -Syntax themes style the code inside the editor. - -Themes can be installed and changed from the settings view which you can open -by selecting the _Atom > Preferences..._ menu and navigating to the _Install_ -section and the _Themes_ section on the left hand side. - -## Getting Started - -Themes are pretty straightforward but it's still helpful to be familiar with -a few things before starting: - -* Less is a superset of CSS, but it has some really handy features like - variables. If you aren't familiar with its syntax, take a few minutes - to [familiarize yourself][less-tutorial]. -* You may also want to review the concept of a _[package.json]_, too. This file - is used to help distribute your theme to Atom users. -* Your theme's _package.json_ must contain a `"theme"` key with a value - of `"ui"` or `"syntax"` for Atom to recognize and load it as a theme. -* You can find existing themes to install or fork on - [atom.io][atomio-themes]. - -## Creating a Syntax Theme - -Let's create your first theme. - -To get started, hit `cmd-shift-P`, and start typing "Generate Syntax Theme" to -generate a new theme package. Select "Generate Syntax Theme," and you'll be -asked for the path where your theme will be created. Let's call ours -_motif-syntax_. __Tip:__ syntax themes should end with _-syntax_. - -Atom will pop open a new window, showing the _motif-syntax_ theme, with a -default set of folders and files created for us. If you open the settings view -(`cmd-,`) and navigate to the _Themes_ section on the left, you'll see the -_Motif_ theme listed in the _Syntax Theme_ drop-down. Select it from the menu to -activate it, now when you open an editor you should see that your new -_motif-syntax_ theme in action. - -Open up _styles/colors.less_ to change the various colors variables which -have been already been defined. For example, turn `@red` into `#f4c2c1`. - -Then open _styles/base.less_ and modify the various selectors that have -been already been defined. These selectors style different parts of code in the -editor such as comments, strings and the line numbers in the gutter. - -As an example, let's make the `.gutter` `background-color` into `@red`. - -Reload Atom by pressing `cmd-alt-ctrl-l` to see the changes you made reflected -in your Atom window. Pretty neat! - -__Tip:__ You can avoid reloading to see changes you make by opening an atom -window in dev mode. To open a Dev Mode Atom window run `atom --dev .` in the -terminal, use `cmd-shift-o` or use the _View > Developer > Open in Dev Mode_ -menu. When you edit your theme, changes will instantly be reflected! - -> Note: It's advised to _not_ specify a `font-family` in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we recommend you do so in your README. - -## Creating an Interface Theme - -Interface themes **must** provide a `ui-variables.less` file which contains all -of the variables provided by the [core themes][ui-variables]. - -To create an interface UI theme, do the following: - -1. Fork one of the following repositories: - * [atom-dark-ui] - * [atom-light-ui] -2. Clone the forked repository to the local filesystem -3. Open a terminal in the forked theme's directory -4. Open your new theme in a Dev Mode Atom window run `atom --dev .` in the - terminal or use the _View > Developer > Open in Dev Mode_ menu -5. Change the name of the theme in the theme's `package.json` file -6. Name your theme end with a `-ui`. i.e. `super-white-ui` -7. Run `apm link` to symlink your repository to `~/.atom/packages` -8. Reload Atom using `cmd-alt-ctrl-L` -9. Enable the theme via _UI Theme_ drop-down in the _Themes_ section of the - settings view -10. Make changes! Since you opened the theme in a Dev Mode window, changes will - be instantly reflected in the editor without having to reload. - -## Development workflow - -There are a few of tools to help make theme development faster and easier. - -### Live Reload - -Reloading by hitting `cmd-alt-ctrl-L` after you make changes to your theme is -less than ideal. Atom supports [live updating][livereload] of styles on Dev Mode -Atom windows. - -To enable a Dev Mode window: - -1. Open your theme directory in a dev window by either going to the - __View > Developer > Open in Dev Mode__ menu or by hitting the `cmd-shift-o` - shortcut -2. Make a change to your theme file and save it. Your change should be - immediately applied! - -If you'd like to reload all the styles at any time, you can use the shortcut -`cmd-ctrl-shift-r`. - -### Developer Tools - -Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You -can open them by selecting the _View > Toggle Developer Tools_ menu, or by -using the `cmd-alt-i` shortcut. - -The dev tools allow you to inspect elements and take a look at their CSS -properties. - -![devtools-img] - -Check out Google's [extensive tutorial][devtools-tutorial] for a short -introduction. - -### Atom Styleguide - -If you are creating an interface theme, you'll want a way to see how your theme -changes affect all the components in the system. The [styleguide] is a page that -renders every component Atom supports. - -To open the styleguide, open the command palette (`cmd-shift-P`) and search for -_styleguide_, or use the shortcut `cmd-ctrl-shift-g`. - -![styleguide-img] - -[atomio-themes]: https://atom.io/themes -[Less]: http://lesscss.org/ -[git]: http://git-scm.com/ -[atom]: https://atom.io/ -[package.json]: ./creating-a-package.html#package-json -[less-tutorial]: https://speakerdeck.com/danmatthews/less-css -[devtools-tutorial]: https://developer.chrome.com/devtools/docs/dom-and-styles -[ui-variables]: ./theme-variables.html -[livereload]: https://github.com/atom/dev-live-reload -[styleguide]: https://github.com/atom/styleguide -[atom-dark-ui]: https://github.com/atom/atom-dark-ui -[atom-light-ui]: https://github.com/atom/atom-light-ui -[styleguide-img]: https://f.cloud.github.com/assets/69169/1347390/2d431d98-36af-11e3-8f8e-3f4ce1e67adb.png -[devtools-img]: https://f.cloud.github.com/assets/69169/1347391/2d51f91c-36af-11e3-806f-f7b334af43e9.png -[themesettings-img]: https://f.cloud.github.com/assets/69169/1347569/3150bd0c-36b2-11e3-9d69-423503acfe3f.png diff --git a/docs/customizing-atom.md b/docs/customizing-atom.md deleted file mode 100644 index d56bd640a..000000000 --- a/docs/customizing-atom.md +++ /dev/null @@ -1,193 +0,0 @@ -# Customizing Atom - -To change a setting, configure a theme, or install a package just open the -Settings view in the current window by pressing `cmd-,`. - -## Changing The Theme - -Atom comes with both light and dark UI themes as well as several syntax themes. -You are also encouraged to [create or fork][create-theme] your own theme. - -To change the active theme just open the Settings view (`cmd-,`) and select the -`Themes` section from the left hand side. You will see a drop-down menu to -change the active _Syntax_ and _UI_ themes. - -You can also install more themes from here by browsing the featured themes or -searching for a specific theme. - -## Installing Packages - -You can install non-bundled packages by going to the `Packages` section on left -hand side of the Settings view (`cmd-,`). You will see several featured packages -and you can also search for packages from here. The packages listed here have -been published to [atom.io](http://atom.io/packages) which is the official -registry for Atom packages. - -You can also install packages from the command line using `apm`. - -Check that you have `apm` installed by running the following command in your -terminal: - -```sh -apm help install -``` - -You should see a message print out with details about the `apm install` command. - -If you do not, launch Atom and run the _Atom > Install Shell Commands_ menu -to install the `apm` and `atom` commands. - -You can also install packages by using the `apm install` command: - -* `apm install ` to install the latest version. - -* `apm install @` to install a specific version. - -For example `apm install emmet@0.1.5` installs the `0.1.5` release of the -[Emmet](https://github.com/atom/emmet) package into `~/.atom/packages`. - -You can also use `apm` to find new packages to install: - -* `apm search coffee` to search for CoffeeScript packages. - -* `apm view emmet` to see more information about a specific package. - -## Customizing Key Bindings - -Atom keymaps work similarly to style sheets. Just as style sheets use selectors -to apply styles to elements, Atom keymaps use selectors to associate keystrokes -with events in specific contexts. Here's a small example, excerpted from Atom's -built-in keymaps: - -```coffee -'atom-text-editor': - 'enter': 'editor:newline' - -'atom-text-editor[mini] input': - 'enter': 'core:confirm' -``` - -This keymap defines the meaning of `enter` in two different contexts. In a -normal editor, pressing `enter` emits the `editor:newline` event, which causes -the editor to insert a newline. But if the same keystroke occurs inside of a -select list's mini-editor, it instead emits the `core:confirm` event based on -the binding in the more-specific selector. - -By default, `~/.atom/keymap.cson` is loaded when Atom is started. It will always -be loaded last, giving you the chance to override bindings that are defined by -Atom's core keymaps or third-party packages. - -You can open this file in an editor from the _Atom > Open Your Keymap_ menu. - -You'll want to know all the commands available to you. Open the Settings panel -(`cmd-,`) and select the _Keybindings_ tab. It will show you all the keybindings -currently in use. - -## Advanced Configuration - -Atom loads configuration settings from the `config.cson` file in your _~/.atom_ -directory, which contains [CoffeeScript-style JSON][CSON] (CSON): - -```coffee -'core': - 'excludeVcsIgnoredPaths': true -'editor': - 'fontSize': 18 -``` - -The configuration itself is grouped by the package name or one of the two core -namespaces: `core` and `editor`. - -You can open this file in an editor from the _Atom > Open Your Config_ menu. - -### Custom Configuration Location - -You can override the location that Atom stores configuration files and folders -in by setting the `ATOM_HOME` environment variable. The `ATOM_HOME` path will be -used instead of `~/.atom` when it is set. - -This option can be useful when you want to make Atom portable across machines. - -### Configuration Key Reference - -- `core` - - `disabledPackages`: An array of package names to disable - - `excludeVcsIgnoredPaths`: Don't search within files specified by _.gitignore_ - - `followSymlinks`: Follow symlinks when searching and scanning root directory - - `ignoredNames`: File names to ignore across all of Atom - - `projectHome`: The directory where projects are assumed to be located - - `themes`: An array of theme names to load, in cascading order -- `editor` - - `autoIndent`: Enable/disable basic auto-indent (defaults to `true`) - - `nonWordCharacters`: A string of non-word characters to define word boundaries - - `fontSize`: The editor font size - - `fontFamily`: The editor font family - - `invisibles`: Specify characters that Atom renders for invisibles in this hash - - `tab`: Hard tab characters - - `cr`: Carriage return (for Microsoft-style line endings) - - `eol`: `\n` characters - - `space`: Leading and trailing space characters - - `preferredLineLength`: Identifies the length of a line (defaults to `80`) - - `showInvisibles`: Whether to render placeholders for invisible characters (defaults to `false`) - - `showIndentGuide`: Show/hide indent indicators within the editor - - `showLineNumbers`: Show/hide line numbers within the gutter - - `softWrap`: Enable/disable soft wrapping of text within the editor - - `softWrapAtPreferredLineLength`: Enable/disable soft line wrapping at `preferredLineLength` - - `tabLength`: Number of spaces within a tab (defaults to `2`) -- `fuzzyFinder` - - `ignoredNames`: Files to ignore *only* in the fuzzy-finder -- `whitespace` - - `ensureSingleTrailingNewline`: Whether to reduce multiple newlines to one at the end of files - - `removeTrailingWhitespace`: Enable/disable striping of whitespace at the end of lines (defaults to `true`) -- `wrap-guide` - - `columns`: Array of hashes with a `pattern` and `column` key to match the - the path of the current editor to a column position. - -### Quick Personal Hacks - -### init.coffee - -When Atom finishes loading, it will evaluate _init.coffee_ in your _~/.atom_ -directory, giving you a chance to run arbitrary personal [CoffeeScript][] code to -make customizations. You have full access to Atom's API from code in this file. -If customizations become extensive, consider [creating a package][creating-a-package]. - -You can open this file in an editor from the _Atom > Open Your Init Script_ -menu. - -For example, if you have the Audio Beep configuration setting enabled, you -could add the following code to your _~/.atom/init.coffee_ file to have Atom -greet you with an audio beep every time it loads: - -```coffee -atom.beep() -``` - -This file can also be named _init.js_ and contain JavaScript code. - -### styles.less - -If you want to apply quick-and-dirty personal styling changes without creating -an entire theme that you intend to publish, you can add styles to the -_styles.less_ file in your _~/.atom_ directory. - -You can open this file in an editor from the _Atom > Open Your Stylesheet_ menu. - -For example, to change the color of the cursor, you could add the following -rule to your _~/.atom/styles.less_ file: - -```less -atom-text-editor::shadow .cursor { - border-color: pink; -} -``` - -Unfamiliar with Less? Read more about it [here][Less]. - -This file can also be named _styles.css_ and contain CSS. - -[creating-a-package]: creating-a-package.md -[create-theme]: creating-a-theme.md -[Less]: http://www.lesscss.org -[CSON]: https://github.com/atom/season -[CoffeeScript]: http://coffeescript.org/ diff --git a/docs/debugging.md b/docs/debugging.md deleted file mode 100644 index ec202e7f8..000000000 --- a/docs/debugging.md +++ /dev/null @@ -1,133 +0,0 @@ -# Debugging - -Atom provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when [submitting issues]: - -* [Update to the latest version](#update-to-the-latest-version) -* [Check for linked packages](#check-for-linked-packages) -* [Check Atom and package settings](#check-atom-and-package-settings) -* [Check the keybindings](#check-the-keybindings) -* [Check if the problem shows up in safe mode](#check-if-the-problem-shows-up-in-safe-mode) -* [Check your config files](#check-your-config-files) -* [Check for errors in the developer tools](#check-for-errors-in-the-developer-tools) - -## Update to the latest version - -You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using. - -If you're building Atom from source, pull down the latest version of master and [re-build][building atom]. - -If you're using released version, check which version of Atom you're using: - -```shell -$ atom --version -0.99.0 -``` - -Head on over to the [list of releases][atom releases] and see if there's a more recent release. You can update to the most recent release by downloading Atom from the releases page, or with the in-app auto-updater. The in-app auto-updater checks for and downloads a new version after you restart Atom, or if you use the Atom > Check for Update menu option. - -## Check for linked packages - -If you develop or contribute to Atom packages, there may be left-over packages linked to your `~/.atom/packages` or `~/.atom/dev/packages` directories. You can use: - -```shell -$ apm links -``` - -to list all linked development packages. You can remove the links using the `apm unlink` command. See `apm unlink --help` for details. - -## Check Atom and package settings - -In some cases, unexpected behavior might be caused by misconfigured or unconfigured settings in Atom or in one of the packages. - -Open Atom's Settings View with `cmd-,` or the Atom > Preferences menu option. - -![Settings View] - -Check Atom's settings in the Settings pane, there's a description of each configuration option [here][customizing guide]. For example, if you want Atom to use hard tabs (real tabs) and not soft tabs (spaces), disable the "Soft Tabs" option. - -Since Atom ships with a set of packages and you can install additional packages yourself, check the list of packages and their settings. For example, if you'd like to get rid of the vertical line in the middle of the editor, disable the [Wrap Guide package]. And if you don't like it when Atom strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the [Whitespace packages'][whitespace package] settings. - -![Package Settings] - -## Check the keybindings - -If a command is not executing when you hit a keystroke or the wrong command is executing, there might be an issue with the keybindings for that keystroke. Atom ships with the [Keybinding resolver][keybinding resolver package], a neat package which helps you understand which keybindings are executed. - -Show the keybinding resolver with cmd-. or with "Key Binding Resolver: Show" from the Command palette. With the keybinding resolver shown, hit a keystroke: - -![Keybinding Resolver] - -The keybinding resolver shows you a list of keybindings that exist for the keystroke, where each item in the list has the following: -* the command for the keybinding, -* the CSS selector used to define the context in which the keybinding is valid, and -* the file in which the keybinding is defined. - -Of all the keybinding that are listed (grey color), at most one keybinding is matched and executed (green color). If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been defined. More keybindings are provided by [packages] and you can [define your own keybindings][customizing keybindings]. - -If multiple keybindings are matched, Atom determines which keybinding will be executed based on the [specificity of the selectors and the order in which they were loaded][specificity and order]. If the command you wanted to trigger is listed in the Keybinding resolver, but wasn't the one that was executed, this is normally explained by one of two causes: -* the keystroke was not used in the context defined by the keybinding's selector. For example, you can't trigger the "Tree View: Add File" command if the Tree View is not focused, or -* there is another keybinding that took precedence. This often happens when you install a package which defines keybinding that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones. - -Atom loads core Atom keybindings and package keybindings first, and user-defined keybindings after last. Since user-defined keybindings are loaded last, you can use your `keymap.cson` file to tweak the keybindings and sort out problems like these. For example, you can remove keybindings with [the `unset!` directive][unset directive]. - -If you notice that a package's keybindings are taking precedence over core Atom keybindings, it might be a good idea to report the issue on the package's GitHub repository. - -## Check if the problem shows up in safe mode - -A large part of Atom's functionality comes from packages you can install. In some cases, these packages might be causing unexpected behavior, problems, or performance issues. - -To determine if a package you installed is causing problems, start Atom from the terminal in safe mode: - -``` -$ atom --safe -``` - -This starts Atom, but does not load packages from `~/.atom/packages` or `~/.atom/dev/packages`. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages. - -To figure out which package is causing trouble, start Atom normally again and open Settings (`cmd-,`). Since Settings allow you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart (`cmd-q`) or reload (`cmd-ctrl-alt-l`) Atom after you disable each package to make sure it's completely gone. - -When you find the problematic package, you can disable or uninstall the package, and consider creating an issue on the package's GitHub repository. - -## Check your config files - -You might have defined some custom functionality or styles in Atom's [Init script or Stylesheet]. In some situations, these personal hacks might be causing problems so try clearing those files and restarting Atom. - -## Check for errors in the developer tools - -When an error is thrown in Atom, the developer tools are automatically shown with the error logged in the Console tab. However, if the dev tools are open before the error is triggered, a full stack trace for the error will be logged: - -![devtools error] - -If you can reproduce the error, use this approach to get the full stack trace. The stack trace might point to a problem in your [Init script][init script or stylesheet] or a specific package you installed, which you can then disable and report an issue on its GitHub repository. - -## Check that you have a build toolchain installed - -If you are having issues installing a package using `apm install`, this could be -because the package has dependencies on libraries that contain native code -and so you will need to have a C++ compiler and Python installed to be able to -install it. - -You can run `apm install --check` to see if [apm][apm] can build native code on -your machine. - -Check out the pre-requisites in the [build instructions][build-instructions] for -your platform for more details. - -[apm]: https://github.com/atom/apm -[build-instructions]: https://github.com/atom/atom/tree/master/docs/build-instructions -[submitting issues]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues -[building atom]: https://github.com/atom/atom#building -[atom releases]: https://github.com/atom/atom/releases -[customizing guide]: https://atom.io/docs/latest/customizing-atom#configuration-key-reference -[settings view]: https://f.cloud.github.com/assets/671378/2241795/ba4827d8-9ce4-11e3-93a8-6666ee100917.png -[package settings]: https://cloud.githubusercontent.com/assets/38924/3173588/7e5f6b0c-ebe8-11e3-9ec3-e8d140967e79.png -[wrap guide package]: https://atom.io/packages/wrap-guide -[whitespace package]: https://atom.io/packages/whitespace -[keybinding resolver package]: https://atom.io/packages/keybinding-resolver -[keybinding resolver]: https://f.cloud.github.com/assets/671378/2241702/5dd5a102-9cde-11e3-9e3f-1d999930492f.png -[customizing keybindings]: https://atom.io/docs/latest/customizing-atom#customizing-key-bindings -[packages]: https://atom.io/packages -[specificity and order]: https://atom.io/docs/latest/advanced/keymaps#specificity-and-cascade-order -[unset directive]: https://atom.io/docs/latest/advanced/keymaps#removing-bindings -[init script or stylesheet]: https://atom.io/docs/latest/customizing-atom#quick-personal-hacks -[devtools error]: https://cloud.githubusercontent.com/assets/38924/3177710/11b4e510-ec13-11e3-96db-a2e8a7891773.png diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 03e0a5abe..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,109 +0,0 @@ -# Getting Started - -Welcome to Atom! This guide provides a quick introduction so you can be -productive as quickly as possible. There are also guides which cover -[configuring], [theming], and [extending] Atom. - -## The Command Palette - -If there's one key-command you remember in Atom, it should be `cmd-shift-P`. You -can always press `cmd-shift-P` to bring up a list of commands (and key bindings) -that are relevant to the currently focused interface element. This is a great -way to explore the system and learn key bindings interactively. For information -about adding or changing a key binding refer to the [customizing key -bindings][key-bindings] section. - -![Command Palette] - -## The Basics - -### Working With Files - -Atom windows are scoped to a single directory on disk. If you launch Atom from -the command line via the `atom` command and don't specify a path, Atom opens a -window for the current working directory. The current window's directory will be -visible as the root of the tree view on the left, and also serve as the context -for all file-related operations. - -#### Finding Files - -The fastest way to find a file is to use the fuzzy finder. Press `cmd-t` and -begin typing the name of the file you're looking for. If you are looking for a -file that is already open press `cmd-b` to bring up a searchable list of open -files. If you are using Git you can use `cmd-shift-b` to search the list of -files modified and untracked in your project's repository. - -You can also use the tree view to navigate to a file. To open and focus the -tree view, press `ctrl-0`. The tree view can be toggled open and closed with -`cmd-\`. - -#### Adding, Moving, Deleting Files - -You can add, move, and delete files and folders by right-clicking them in the -tree view and selecting the desired operation from the context menu. You can -also perform these operations from the keyboard by selecting a file or folder -and using `a` to add, `m` to move, and `delete` to delete. - -### Searching - -#### Find and Replace - -To search within a buffer use `cmd-f`. To search the entire project use -`cmd-shift-f`. - -#### Navigating By Symbols - -To jump to a symbol such as a method definition, press `cmd-r`. This opens a -list of all symbols in the current file, which you can fuzzy filter similarly to -`cmd-t`. - -To search for symbols across your project, use `cmd-shift-r`. First you'll need -to make sure you have `tags` (or `TAGS`) file generated for your project. -This can be done by installing [ctags](http://ctags.sourceforge.net/) and -running a command such as `ctags -R src/` from the command line in your -project's root directory. Using [Homebrew](http://brew.sh/)? Just run -`brew install ctags`. - -You can customize how tags are generated by creating your own `.ctags` file -in your home directory (`~/.ctags`). Here is [a good example][ctags] to start -from. - -### Split Panes - -You can split any editor pane horizontally or vertically by using `cmd-k right` -or `cmd-k down`. Once you have a split pane, you can move focus between them -with `cmd-k cmd-right` or `cmd-k cmd-down`. To close a pane, close all its -editors with `cmd-w`, then press `cmd-w` one more time to close the pane. You -can configure panes to auto-close when empty in the Settings view. - -### Folding - -You can fold blocks of code by clicking the arrows that appear when you hover -your mouse cursor over the gutter. You can also fold and unfold from the -keyboard with `alt-cmd-[` and `alt-cmd-]`. To fold everything, use -`alt-cmd-shift-{` and to unfold everything use `alt-cmd-shift-}`. You can also -fold at a specific indentation level with `cmd-k cmd-N` where N is the -indentation depth. - -### Soft-Wrap - -If you want to toggle soft wrap, trigger the command from the command palette. -Press `cmd-shift-P` to open the palette, then type "wrap" to find the correct -command. By default, lines will wrap based on the size of the editor. If you -prefer to wrap at a specific line length, toggle "Wrap at preferred line length" -in preferences. - -## Configuration - -Press `cmd-,` to open the Settings view. This is the place to change settings, -install packages, and change the theme. - -For more advanced configuration see the [customization guide][customization]. - -[configuring]: customizing-atom.md -[theming]: creating-a-theme.md -[extending]: creating-a-package.md -[customization]: customizing-atom.md -[key-bindings]: customizing-atom.md#customizing-key-bindings -[command palette]: https://f.cloud.github.com/assets/1424/1091618/ee7c3554-166a-11e3-9955-aaa61bb5509c.png -[ctags]: https://github.com/atom/symbols-view/blob/master/lib/.ctags diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index e034e5784..000000000 --- a/docs/index.md +++ /dev/null @@ -1,30 +0,0 @@ -## Guides - -* [Getting Started](getting-started.md) -* [Customizing Atom](customizing-atom.md) -* [Creating a Package](creating-a-package.md) -* [Creating a Theme](creating-a-theme.md) -* [Publishing a Package](publishing-a-package.md) -* [Writing Specs](writing-specs.md) -* [Converting a TextMate Bundle](converting-a-text-mate-bundle.md) -* [Converting a TextMate Theme](converting-a-text-mate-theme.md) -* [Contributing](contributing.md) -* [Contributing to Core Packages](contributing-to-packages.md) -* [Debugging](debugging.md) -* [Your First Package](your-first-package.md) - -### Advanced Topics - -* [Configuration](advanced/configuration.md) -* [Developing Node Modules](advanced/node-modules.md) -* [Keymaps](advanced/keymaps.md) -* [Serialization](advanced/serialization.md) -* [Scopes and Scope Descriptors](advanced/scopes-and-scope-descriptors.md) -* [Theme Variables](theme-variables.md) -* [apm REST API](apm-rest-api.md) - -### Upgrading to 1.0 APIs - -* [Upgrading Your Package](upgrading/upgrading-your-package.md) -* [Upgrading Your UI Theme Or Package Selectors](upgrading/upgrading-your-ui-theme.md) -* [Upgrading Your Syntax Theme](upgrading/upgrading-your-syntax-theme.md) diff --git a/docs/publishing-a-package.md b/docs/publishing-a-package.md deleted file mode 100644 index 264876c7a..000000000 --- a/docs/publishing-a-package.md +++ /dev/null @@ -1,114 +0,0 @@ -## Publishing a Package - -This guide will show you how to publish a package or theme to the -[atom.io][atomio] package registry. - -Publishing a package allows other people to install it and use it in Atom. It -is a great way to share what you've made and get feedback and contributions from -others. - -This guide assumes your package's name is `my-package` but you should pick a -better name. - -### Install apm - -The `apm` command line utility that ships with Atom supports publishing packages -to the atom.io registry. - -Check that you have `apm` installed by running the following command in your -terminal: - -```sh -apm help publish -``` - -You should see a message print out with details about the `apm publish` command. - -If you do not, launch Atom and run the _Atom > Install Shell Commands_ menu -to install the `apm` and `atom` commands. - -### Prepare Your Package - -If you've followed the steps in the [your first package][your-first-package] -doc then you should be ready to publish and you can skip to the next step. - -If not, there are a few things you should check before publishing: - - * Your *package.json* file has `name`, `description`, and `repository` fields. - * Your *package.json* file has a `version` field with a value of `"0.0.0"`. - * Your *package.json* file has an `engines` field that contains an entry - for Atom such as: `"engines": {"atom": ">=0.50.0"}`. - * Your package has a `README.md` file at the root. - * Your package is in a Git repository that has been pushed to - [GitHub][github]. Follow [this guide][repo-guide] if your package isn't - already on GitHub. - -### Publish Your Package - -Before you publish a package it is a good idea to check ahead of time if -a package with the same name has already been published to atom.io. You can do -that by visiting `https://atom.io/packages/my-package` to see if the package -already exists. If it does, update your package's name to something that is -available before proceeding. - -Now let's review what the `apm publish` command does: - - 1. Registers the package name on atom.io if it is being published for the - first time. - 2. Updates the `version` field in the *package.json* file and commits it. - 3. Creates a new [Git tag][git-tag] for the version being published. - 4. Pushes the tag and current branch up to GitHub. - 5. Updates atom.io with the new version being published. - -Now run the following commands to publish your package: - -```sh -cd ~/github/my-package -apm publish minor -``` - -If this is the first package you are publishing, the `apm publish` command may -prompt you for your GitHub username and password. This is required to publish -and you only need to enter this information the first time you publish. The -credentials are stored securely in your [keychain][keychain] once you login. - -:tada: Your package is now published and available on atom.io. Head on over to -`https://atom.io/packages/my-package` to see your package's page. - -With `apm publish`, you can bump the version and publish by using -```sh -apm publish -``` -where `` can be `major`, `minor` and `patch`. - -The `major` option to the publish command tells apm to increment the first -digit of the version before publishing so the published version will be `1.0.0` -and the Git tag created will be `v1.0.0`. - -The `minor` option to the publish command tells apm to increment the second -digit of the version before publishing so the published version will be `0.1.0` -and the Git tag created will be `v0.1.0`. - -The `patch` option to the publish command tells apm to increment the third -digit of the version before publishing so the published version will be `0.0.1` -and the Git tag created will be `v0.0.1`. - -Use `major` when you make a huge change, like a rewrite, or a large change to the functionality or interface. -Use `minor` when adding or removing a feature. -Use `patch` when you make a small change like a bug fix that does not add or remove features. - -### Further Reading - -* Check out [semantic versioning][semver] to learn more about versioning your - package releases. -* Consult the [Atom.io package API docs][apm-rest-api] to learn more about how - `apm` works. - -[atomio]: https://atom.io -[github]: https://github.com -[git-tag]: http://git-scm.com/book/en/Git-Basics-Tagging -[keychain]: https://en.wikipedia.org/wiki/Keychain_(Apple) -[repo-guide]: http://guides.github.com/overviews/desktop -[semver]: http://semver.org -[your-first-package]: your-first-package.html -[apm-rest-api]: apm-rest-api.md diff --git a/docs/theme-variables.md b/docs/theme-variables.md deleted file mode 100644 index 0316fc215..000000000 --- a/docs/theme-variables.md +++ /dev/null @@ -1,116 +0,0 @@ -# Style variables - -Atom's UI provides a set of variables you can use in your own themes and packages. - -## Use in Themes - -Each custom theme must specify a `ui-variables.less` file with all of the -following variables defined. The top-most theme specified in the theme settings -will be loaded and available for import. - -## Use in Packages - -In any of your package's `.less` files, you can access the theme variables -by importing the `ui-variables` file from Atom. - -Your package should generally only specify structural styling, and these should -come from [the style guide][styleguide]. Your package shouldn't specify colors, -padding sizes, or anything in absolute pixels. You should instead use the theme -variables. If you follow this guideline, your package will look good out of the -box with any theme! - -Here's an example `.less` file that a package can define using theme variables: - -```css -@import "ui-variables"; - -.my-selector { - background-color: @base-background-color; - padding: @component-padding; -} -``` - -## Variables - -### Text colors - -* `@text-color` -* `@text-color-subtle` -* `@text-color-highlight` -* `@text-color-selected` -* `@text-color-info` - A blue -* `@text-color-success`- A green -* `@text-color-warning`- An orange or yellow -* `@text-color-error` - A red - -### Background colors - -* `@background-color-info` - A blue -* `@background-color-success` - A green -* `@background-color-warning` - An orange or yellow -* `@background-color-error` - A red -* `@background-color-highlight` -* `@background-color-selected` -* `@app-background-color` - The app's background under all the editor components - -### Component colors - -* `@base-background-color` - -* `@base-border-color` - - -* `@pane-item-background-color` - -* `@pane-item-border-color` - - -* `@input-background-color` - -* `@input-border-color` - - -* `@tool-panel-background-color` - -* `@tool-panel-border-color` - - -* `@inset-panel-background-color` - -* `@inset-panel-border-color` - - -* `@panel-heading-background-color` - -* `@panel-heading-border-color` - - -* `@overlay-background-color` - -* `@overlay-border-color` - - -* `@button-background-color` - -* `@button-background-color-hover` - -* `@button-background-color-selected` - -* `@button-border-color` - - -* `@tab-bar-background-color` - -* `@tab-bar-border-color` - -* `@tab-background-color` - -* `@tab-background-color-active` - -* `@tab-border-color` - - -* `@tree-view-background-color` - -* `@tree-view-border-color` - - -* `@ui-site-color-1` - -* `@ui-site-color-2` - -* `@ui-site-color-3` - -* `@ui-site-color-4` - -* `@ui-site-color-5` - - -### Component sizes - -* `@disclosure-arrow-size` - - -* `@component-padding` - -* `@component-icon-padding` - -* `@component-icon-size` - -* `@component-line-height` - -* `@component-border-radius` - - -* `@tab-height` - - -### Fonts - -* `@font-size` - -* `@font-family` - - -[styleguide]: https://github.com/atom/styleguide diff --git a/docs/upgrading/upgrading-your-package.md b/docs/upgrading/upgrading-your-package.md deleted file mode 100644 index e7d603a36..000000000 --- a/docs/upgrading/upgrading-your-package.md +++ /dev/null @@ -1,625 +0,0 @@ -# Upgrading Your Package - -Atom is rapidly approaching 1.0. Much of the effort leading up to the 1.0 has been cleaning up APIs in an attempt to future proof, and make a more pleasant experience developing packages. - -This document will guide you through the large bits of upgrading your package to work with 1.0 APIs. - -## TL;DR - -We've set deprecation messages and errors in strategic places to help make sure you don't miss anything. You should be able to get 95% of the way to an updated package just by fixing errors and deprecations. There are a couple of things you can do to get the full effect of all the errors and deprecations. - -### Use atom-space-pen-views - -If you use any class from `require 'atom'` with a `$` or `View` in the name, add the `atom-space-pen-views` module to your package's `package.json` file's dependencies: - -```js -{ - "dependencies": { - "atom-space-pen-views": "^2.0.3" - } -} -``` - -Then run `apm install` in your package directory. - -### Require views from atom-space-pen-views - -Anywhere you are requiring one of the following from `atom` you need to require them from `atom-space-pen-views` instead. - -```coffee -# require these from 'atom-space-pen-views' rather than 'atom' -$ -$$ -$$$ -View -TextEditorView -ScrollView -SelectListView -``` - -So this: - -```coffee -# Old way -{$, TextEditorView, View, GitRepository} = require 'atom' -``` - -Would be replaced by this: - -```coffee -# New way -{GitRepository} = require 'atom' -{$, TextEditorView, View} = require 'atom-space-pen-views' -``` - -### Run specs and test your package - -You wrote specs, right!? Here's where they shine. Run them with `cmd-shift-P`, and search for `run package specs`. It will show all the deprecation messages and errors. - -### Update the engines field in package.json - -When you are deprecation free and all done converting, upgrade the `engines` field in your package.json: - -```json -{ - "engines": { - "atom": ">=0.174.0 <2.0.0" - } -} -``` - -### Examples - -We have upgraded all the core packages. Please see [this issue](https://github.com/atom/atom/issues/4011) for a link to all the upgrade PRs. - -## Deprecations - -All of the methods in Atom core that have changes will emit deprecation messages when called. These messages are shown in two places: your **package specs**, and in **Deprecation Cop**. - -### Specs - -Just run your specs, and all the deprecations will be displayed in yellow. - -![spec-deps](https://cloud.githubusercontent.com/assets/69169/5637943/b85114ba-95b5-11e4-8681-b81ea8f556d7.png) - -### Deprecation Cop - -Run an atom window in dev mode (`atom -d`) with your package loaded, and open Deprecation Cop (search for `deprecation` in the command palette). Deprecated methods will be appear in Deprecation Cop only after they have been called. - -![dep-cop](https://cloud.githubusercontent.com/assets/69169/5637914/6e702fa2-95b5-11e4-92cc-a236ddacee21.png) - -When deprecation cop is open, and deprecated methods are called, a `Refresh` button will appear in the top right of the Deprecation Cop interface. So exercise your package, then come back to Deprecation Cop and click the `Refresh` button. - -## Upgrading your Views - -Previous to 1.0, views were baked into Atom core. These views were based on jQuery and `space-pen`. They looked something like this: - -```coffee -# The old way: getting views from atom -{$, TextEditorView, View} = require 'atom' - -module.exports = -class SomeView extends View - @content: -> - @div class: 'find-and-replace', => - @div class: 'block', => - @subview 'myEditor', new TextEditorView(mini: true) - #... -``` - -### The New - -`require 'atom'` no longer provides view helpers or jQuery. Atom core is now 'view agnostic'. The preexisting view system is available from a new npm package: `atom-space-pen-views`. - -`atom-space-pen-views` now provides jQuery, `space-pen` views, and Atom specific views: - - -```coffee -# These are now provided by atom-space-pen-views -$ -$$ -$$$ -View -TextEditorView -ScrollView -SelectListView -``` - -### Adding the module dependencies - -To use the new views, you need to specify the `atom-space-pen-views` module in your package's `package.json` file's dependencies: - -```js -{ - "dependencies": { - "atom-space-pen-views": "^2.0.3" - } -} -``` - -`space-pen` bundles jQuery. If you do not need `space-pen` or any of the views, you can require jQuery directly. - -```js -{ - "dependencies": { - "jquery": "^2" - } -} -``` - -### Converting your views - -Sometimes it is as simple as converting the requires at the top of each view page. I assume you read the 'TL;DR' section and have updated all of your requires. - -### Upgrading classes extending any space-pen View - -#### `afterAttach` and `beforeRemove` updated - -The `afterAttach` and `beforeRemove` hooks have been replaced with -`attached` and `detached` and the semantics have changed. - -`afterAttach` was called whenever the node was attached to another DOM node, even if that parent node wasn't present in the DOM. `afterAttach` also was called with a boolean indicating whether or not the element and its parents were on the DOM. Now the `attached` hook is _only_ called when the node and all of its parents are actually on the DOM, and is not called with a boolean. - -`beforeRemove` was only called when `$.fn.remove` was called, which was typically used when the node was completely removed from the DOM. The new `detached` hook is called whenever the DOM node is _detached_, which could happen if the node is being detached for reattachment later. In short, if `beforeRemove` is called the node is never coming back. With `detached` it might be attached again later. - -```coffee -# Old way -{View} = require 'atom' -class MyView extends View - afterAttach: (onDom) -> - #... - - beforeRemove: -> - #... -``` - -```coffee -# New way -{View} = require 'atom-space-pen-views' -class MyView extends View - attached: -> - # Always called with the equivalent of @afterAttach(true)! - #... - - detached: -> - #... -``` - -#### `subscribe` and `subscribeToCommand` methods removed - -The `subscribe` and `subscribeToCommand` methods have been removed. See the Eventing and Disposables section for more info. - -### Upgrading to the new TextEditorView - -All of the atom-specific methods available on the `TextEditorView` have been moved to the `TextEditor`, available via `TextEditorView::getModel`. See the [`TextEditorView` docs][TextEditorView] and [`TextEditor` docs][TextEditor] for more info. - -### Upgrading classes extending ScrollView - -The `ScrollView` has very minor changes. - -You can no longer use `@off` to remove default behavior for `core:move-up`, `core:move-down`, etc. - -```coffee -# Old way to turn off default behavior -class ResultsView extends ScrollView - initialize: (@model) -> - super() - # turn off default scrolling behavior from ScrollView - @off 'core:move-up' - @off 'core:move-down' - @off 'core:move-left' - @off 'core:move-right' -``` - -```coffee -# New way to turn off default behavior -class ResultsView extends ScrollView - initialize: (@model) -> - disposable = super() - # turn off default scrolling behavior from ScrollView - disposable.dispose() -``` - -* Check out [an example](https://github.com/atom/find-and-replace/pull/311/files#diff-9) from find-and-replace. -* See the [docs][ScrollView] for all the options. - -### Upgrading classes extending SelectListView - -Your SelectListView might look something like this: - -```coffee -# Old! -class CommandPaletteView extends SelectListView - initialize: -> - super() - @addClass('command-palette overlay from-top') - atom.workspaceView.command 'command-palette:toggle', => @toggle() - - confirmed: ({name, jQuery}) -> - @cancel() - # do something with the result - - toggle: -> - if @hasParent() - @cancel() - else - @attach() - - attach: -> - @storeFocusedElement() - - items = [] # TODO: build items - @setItems(items) - - atom.workspaceView.append(this) - @focusFilterEditor() - - confirmed: ({name, jQuery}) -> - @cancel() -``` - -This attaches and detaches itself from the dom when toggled, canceling magically detaches it from the DOM, and it uses the classes `overlay` and `from-top`. - -The new SelectListView no longer automatically detaches itself from the DOM when cancelled. It's up to you to implement whatever cancel beahavior you want. Using the new APIs to mimic the sematics of the old class, it should look like this: - -```coffee -# New! -class CommandPaletteView extends SelectListView - initialize: -> - super() - # no more need for the `overlay` and `from-top` classes - @addClass('command-palette') - atom.commands.add 'atom-workspace', 'command-palette:toggle', => @toggle() - - # You need to implement the `cancelled` method and hide. - cancelled: -> - @hide() - - confirmed: ({name, jQuery}) -> - @cancel() - # do something with the result - - toggle: -> - # Toggling now checks panel visibility, - # and hides / shows rather than attaching to / detaching from the DOM. - if @panel?.isVisible() - @cancel() - else - @show() - - show: -> - # Now you will add your select list as a modal panel to the workspace - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - - @storeFocusedElement() - - items = [] # TODO: build items - @setItems(items) - - @focusFilterEditor() - - hide: -> - @panel?.hide() -``` - -* And check out the [conversion of CommandPaletteView][selectlistview-example] as a real-world example. -* See the [SelectListView docs][SelectListView] for all options. - -## Using the model layer rather than the view layer - -The API no longer exposes any specialized view objects or view classes. `atom.workspaceView`, and all the view classes: `WorkspaceView`, `EditorView`, `PaneView`, etc. have been globally deprecated. - -Nearly all of the atom-specific actions performed by the old view objects can now be managed via the model layer. For example, here's adding a panel to the interface using the `atom.workspace` model instead of the `workspaceView`: - -```coffee -# Old! -div = document.createElement('div') -atom.workspaceView.appendToTop(div) -``` - -```coffee -# New! -div = document.createElement('div') -atom.workspace.addTopPanel(item: div) -``` - -For actions that still require the view, such as dispatching commands or munging css classes, you'll access the view via the `atom.views.getView()` method. This will return a subclass of `HTMLElement` rather than a jQuery object or an instance of a deprecated view class (e.g. `WorkspaceView`). - -```coffee -# Old! -workspaceView = atom.workspaceView -editorView = workspaceView.getActiveEditorView() -paneView = editorView.getPaneView() -``` - -```coffee -# New! -# Generally, just use the models -workspace = atom.workspace -editor = workspace.getActiveTextEditor() -pane = editor.getPane() - -# If you need views, get them with `getView` -workspaceElement = atom.views.getView(atom.workspace) -editorElement = atom.views.getView(editor) -paneElement = atom.views.getView(pane) -``` - -## Updating Specs - -`atom.workspaceView`, the `WorkspaceView` class and the `EditorView` class have been deprecated. These two objects are used heavily throughout specs, mostly to dispatch events and commands. This section will explain how to remove them while still retaining the ability to dispatch events and commands. - -### Removing WorkspaceView references - -`WorkspaceView` has been deprecated. Everything you could do on the view, you can now do on the `Workspace` model. - -Requiring `WorkspaceView` from `atom` and accessing any methods on it will throw a deprecation warning. Many specs lean heavily on `WorkspaceView` to trigger commands and fetch `EditorView` objects. - -Your specs might contain something like this: - -```coffee -# Old! -{WorkspaceView} = require 'atom' -describe 'FindView', -> - beforeEach -> - atom.workspaceView = new WorkspaceView() -``` - -Instead, we will use the `atom.views.getView()` method. This will return a plain `HTMLElement`, not a `WorkspaceView` or jQuery object. - -```coffee -# New! -describe 'FindView', -> - workspaceElement = null - beforeEach -> - workspaceElement = atom.views.getView(atom.workspace) -``` - -### Attaching the workspace to the DOM - -The workspace needs to be attached to the DOM in some cases. For example, view hooks only work (`attached()` on `View`, `attachedCallback()` on custom elements) when there is a descendant attached to the DOM. - -You might see this in your specs: - -```coffee -# Old! -atom.workspaceView.attachToDom() -``` - -Change it to: - -```coffee -# New! -jasmine.attachToDOM(workspaceElement) -``` - -### Removing EditorView references - -Like `WorkspaceView`, `EditorView` has been deprecated. Everything you needed to do on the view you are now able to do on the `TextEditor` model. - -In many cases, you will not even need to get the editor's view anymore. Any of those instances should be updated to use the `TextEditor` instance instead. You should really only need the editor's view when you plan on triggering a command on the view in a spec. - -Your specs might contain something like this: - -```coffee -# Old! -describe 'Something', -> - [editorView] = [] - beforeEach -> - editorView = atom.workspaceView.getActiveView() -``` - -We're going to use `atom.views.getView()` again to get the editor element. As in the case of the `workspaceElement`, `getView` will return a subclass of `HTMLElement` rather than an `EditorView` or jQuery object. - -```coffee -# New! -describe 'Something', -> - [editor, editorElement] = [] - beforeEach -> - editor = atom.workspace.getActiveTextEditor() - editorElement = atom.views.getView(editor) -``` - -### Dispatching commands - -Since the `editorElement` objects are no longer `jQuery` objects, they no longer support `trigger()`. Additionally, Atom has a new command dispatcher, `atom.commands`, that we use rather than commandeering jQuery's `trigger` method. - -From this: - -```coffee -# Old! -workspaceView.trigger 'a-package:toggle' -editorView.trigger 'find-and-replace:show' -``` - -To this: - -```coffee -# New! -atom.commands.dispatch workspaceElement, 'a-package:toggle' -atom.commands.dispatch editorElement, 'find-and-replace:show' -``` - -## Eventing and Disposables - -A couple large things changed with respect to events: - -1. All model events are now exposed as event subscription methods that return [`Disposable`][disposable] objects -1. The `subscribe()` method is no longer available on `space-pen` `View` objects -1. An Emitter is now provided from `require 'atom'` - -### Consuming Events - -All events from the Atom API are now methods that return a [`Disposable`][disposable] object, on which you can call `dispose()` to unsubscribe. - -```coffee -# Old! -editor.on 'changed', -> -``` - -```coffee -# New! -disposable = editor.onDidChange -> - -# You can unsubscribe at some point in the future via `dispose()` -disposable.dispose() -``` - -Deprecation warnings will guide you toward the correct methods. - -#### Using a CompositeDisposable - -You can group multiple disposables into a single disposable with a `CompositeDisposable`. - -```coffee -{CompositeDisposable} = require 'atom' - -class Something - constructor: -> - editor = atom.workspace.getActiveTextEditor() - @disposables = new CompositeDisposable - @disposables.add editor.onDidChange -> - @disposables.add editor.onDidChangePath -> - - destroy: -> - @disposables.dispose() -``` - -### Removing View::subscribe and Subscriber::subscribe calls - -There were a couple permutations of `subscribe()`. In these examples, a `CompositeDisposable` is used as it will commonly be useful where conversion is necessary. - -#### subscribe(unsubscribable) - -This one is very straight forward. - -```coffee -# Old! -@subscribe editor.on 'changed', -> -``` - -```coffee -# New! -disposables = new CompositeDisposable -disposables.add editor.onDidChange -> -``` - -#### subscribe(modelObject, event, method) - -When the modelObject is an Atom model object, the change is very simple. Just use the correct event method, and add it to your CompositeDisposable. - -```coffee -# Old! -@subscribe editor, 'changed', -> -``` - -```coffee -# New! -disposables = new CompositeDisposable -disposables.add editor.onDidChange -> -``` - -#### subscribe(jQueryObject, selector(optional), event, method) - -Things are a little more complicated when subscribing to a DOM or jQuery element. Atom no longer provides helpers for subscribing to elements. You can use jQuery or the native DOM APIs, whichever you prefer. - -```coffee -# Old! -@subscribe $(window), 'focus', -> -``` - -```coffee -# New! -{Disposable, CompositeDisposable} = require 'atom' -disposables = new CompositeDisposable - -# New with jQuery -focusCallback = -> -$(window).on 'focus', focusCallback -disposables.add new Disposable -> - $(window).off 'focus', focusCallback - -# New with native APIs -focusCallback = -> -window.addEventListener 'focus', focusCallback -disposables.add new Disposable -> - window.removeEventListener 'focus', focusCallback -``` - -### Providing Events: Using the Emitter - -You no longer need to require `emissary` to get an emitter. We now provide an `Emitter` class from `require 'atom'`. We have a specific pattern for use of the `Emitter`. Rather than mixing it in, we instantiate a member variable, and create explicit subscription methods. For more information see the [`Emitter` docs][emitter]. - -```coffee -# New! -{Emitter} = require 'atom' - -class Something - constructor: -> - @emitter = new Emitter - - destroy: -> - @emitter.dispose() - - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - methodThatFiresAChange: -> - @emitter.emit 'did-change', {data: 2} - -# Using the evented class -something = new Something -something.onDidChange (eventObject) -> - console.log eventObject.data # => 2 -something.methodThatFiresAChange() -``` - -## Subscribing To Commands - -`$.fn.command` and `View::subscribeToCommand` are no longer available. Now we use `atom.commands.add`, and collect the results in a `CompositeDisposable`. See [the docs][commands-add] for more info. - -```coffee -# Old! -atom.workspaceView.command 'core:close core:cancel', -> - -# When inside a View class, you might see this -@subscribeToCommand 'core:close core:cancel', -> -``` - -```coffee -# New! -@disposables.add atom.commands.add 'atom-workspace', - 'core:close': -> - 'core:cancel': -> - -# You can register commands directly on individual DOM elements in addition to -# using selectors. When in a View class, you should have a `@element` object -# available. `@element` is a plain HTMLElement object -@disposables.add atom.commands.add @element, - 'core:close': -> - 'core:cancel': -> -``` - -## Upgrading your stylesheet's selectors - -Many selectors have changed, and we have introduced the [Shadow DOM][shadowdom] to the editor. See the [Upgrading Your UI Theme And Package Selectors guide][upgrading-selectors] for more information in upgrading your package stylesheets. - -## Help us improve this guide! - -Did you hit something painful that wasn't in here? Want to reword some bit of it? Find something incorrect? Please edit [this file][guide], and send a pull request. Contributions are greatly appreciated. - - - - -[texteditorview]:https://github.com/atom/atom-space-pen-views#texteditorview -[scrollview]:https://github.com/atom/atom-space-pen-views#scrollview -[selectlistview]:https://github.com/atom/atom-space-pen-views#selectlistview -[selectlistview-example]:https://github.com/atom/command-palette/pull/19/files -[emitter]:https://atom.io/docs/api/latest/Emitter -[texteditor]:https://atom.io/docs/api/latest/TextEditor -[disposable]:https://atom.io/docs/api/latest/Disposable -[commands-add]:https://atom.io/docs/api/latest/CommandRegistry#instance-add -[upgrading-selectors]:https://atom.io/docs/latest/upgrading/upgrading-your-ui-theme -[shadowdom]:http://blog.atom.io/2014/11/18/avoiding-style-pollution-with-the-shadow-dom.html -[guide]:https://github.com/atom/atom/blob/master/docs/upgrading/upgrading-your-package.md diff --git a/docs/upgrading/upgrading-your-syntax-theme.md b/docs/upgrading/upgrading-your-syntax-theme.md deleted file mode 100644 index 3a8241ca0..000000000 --- a/docs/upgrading/upgrading-your-syntax-theme.md +++ /dev/null @@ -1,24 +0,0 @@ -# Upgrading Your Syntax Theme - -Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the [Shadow DOM 101][shadow-dom-101] on HTML 5 Rocks. - -Syntax themes are specifically intended to style only text editor content, so they are automatically loaded directly into the text editor's shadow DOM when it is enabled. This happens automatically when the the theme's `package.json` contains a `theme: "syntax"` declaration, so you don't need to change anything to target the appropriate context. - -When theme style sheets are loaded into the text editor's shadow DOM, selectors intended to target the editor from the *outside* no longer make sense. Styles targeting the `.editor` and `.editor-colors` classes instead need to target the `:host` pseudo-element, which matches against the containing `atom-text-editor` node. Check out the [Shadow DOM 201][host-pseudo-element] article for more information about the `:host` pseudo-element. - -Here's an example from Atom's light syntax theme. Note that the `atom-text-editor` selector intended to target the editor from the outside has been retained to allow the theme to keep working during the transition phase when it is possible to disable the shadow DOM. - -```css -atom-text-editor, :host { /* :host added */ - background-color: @syntax-background-color; - color: @syntax-text-color; - - .invisible-character { - color: @syntax-invisible-character-color; - } - /* more nested selectors... */ -} -``` - -[shadow-dom-101]: http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom -[host-pseudo-element]: http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201#toc-style-host diff --git a/docs/upgrading/upgrading-your-ui-theme.md b/docs/upgrading/upgrading-your-ui-theme.md deleted file mode 100644 index 5be0344bb..000000000 --- a/docs/upgrading/upgrading-your-ui-theme.md +++ /dev/null @@ -1,137 +0,0 @@ -# Upgrading Your UI Theme Or Package Selectors - -In addition to changes in Atom's scripting API, we'll also be making some breaking changes to Atom's DOM structure, requiring style sheets and keymaps in both packages and themes to be updated. - -## Deprecation Cop - -Deprecation cop will list usages of deprecated selector patterns to guide you. You can access it via the command palette (`cmd-shift-p`, then search for `Deprecation`). It breaks the deprecations down by package: - -![dep-cop](https://cloud.githubusercontent.com/assets/69169/5078860/d38a5df4-6e64-11e4-95b6-eb585ee9bbfc.png) - -## Custom Tags - -Rather than adding classes to standard HTML elements to indicate their role, Atom now uses custom element names. For example, `
` has now been replaced with ``. Selectors should be updated accordingly. Note that tag names have lower specificity than classes in CSS, so you'll need to take care in converting things. - -Old Selector | New Selector ---------------------|-------------------------------- -`.editor` | `atom-text-editor` -`.editor.mini` | `atom-text-editor[mini]` -`.workspace` | `atom-workspace` -`.horizontal` | `atom-workspace-axis.horizontal` -`.vertical` | `atom-workspace-axis.vertical` -`.pane-container` | `atom-pane-container` -`.pane` | `atom-pane` -`.tool-panel` | `atom-panel` -`.panel-top` | `atom-panel.top` -`.panel-bottom` | `atom-panel.bottom` -`.panel-left` | `atom-panel.left` -`.panel-right` | `atom-panel.right` -`.overlay` | `atom-panel.modal` - -## Supporting the Shadow DOM - -Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the [Shadow DOM 101][shadow-dom-101] on HTML 5 Rocks. If you need to style text editor content in a UI theme, you'll need to circumvent this protection for any rules that target the text editor's content. Some examples of the kinds of UI theme styles needing to be updated: - -* Highlight decorations -* Gutter decorations -* Line decorations -* Scrollbar styling -* Anything targeting a child selector of `.editor` - -During a transition phase, it will be possible to enable or disable the text editor's shadow DOM in the settings, so themes will need to be compatible with both approaches. - -### Shadow DOM Selectors - -Chromium provides two tools for bypassing shadow boundaries, the `::shadow` pseudo-element and the `/deep/` combinator. For an in-depth explanation of styling the shadow DOM, see the [Shadow DOM 201][shadow-dom-201] article on HTML 5 Rocks. - -#### ::shadow - -The `::shadow` pseudo-element allows you to bypass a single shadow root. For example, say you want to update a highlight decoration for a linter package. Initially, the style looks as follows: - -```css -// Without shadow DOM support -atom-text-editor .highlight.my-linter { - background: hotpink; -} -``` - -In order for this style to apply with the shadow DOM enabled, you will need to add a second selector with the `::shadow` pseudo-element. You should leave the original selector in place so your theme continues to work with the shadow DOM disabled during the transition period. - -```css -// With shadow DOM support -atom-text-editor .highlight.my-linter, -atom-text-editor::shadow .highlight.my-linter { - background: hotpink; -} -``` - -Check out the [find-and-replace][find-and-replace] package for another example of using `::shadow` to pierce the shadow DOM. - -#### /deep/ - -The `/deep/` combinator overrides *all* shadow boundaries, making it useful for rules you want to apply globally such as scrollbar styling. Here's a snippet containing scrollbar styling for the Atom Dark UI theme before shadow DOM support: - -```css -// Without shadow DOM support -.scrollbars-visible-always { - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - ::-webkit-scrollbar-track, - ::-webkit-scrollbar-corner { - background: @scrollbar-background-color; - } - - ::-webkit-scrollbar-thumb { - background: @scrollbar-color; - border-radius: 5px; - box-shadow: 0 0 1px black inset; - } -} -``` - -To style scrollbars even inside of the shadow DOM, each rule needs to be prefixed with `/deep/`. We use `/deep/` instead of `::shadow` because we don't care about the selector of the host element in this case. We just want our styling to apply everywhere. - -```css -// With shadow DOM support using /deep/ -.scrollbars-visible-always { - /deep/ ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - /deep/ ::-webkit-scrollbar-track, - /deep/ ::-webkit-scrollbar-corner { - background: @scrollbar-background-color; - } - - /deep/ ::-webkit-scrollbar-thumb { - background: @scrollbar-color; - border-radius: 5px; - box-shadow: 0 0 1px black inset; - } -} -``` - -### Context-Targeted Style Sheets - -The selector features discussed above allow you to target shadow DOM content with specific selectors, but Atom also allows you to target a specific shadow DOM context with an entire style sheet. The context into which a style sheet is loaded is based on the file name. If you want to load a style sheet into the editor, name it with the `.atom-text-editor.less` or `.atom-text-editor.css` extensions. - -``` -my-ui-theme/ - styles/ - index.less # loaded globally - index.atom-text-editor.less # loaded in the text editor shadow DOM -``` - -Check out this [style sheet](https://github.com/atom/decoration-example/blob/master/styles/decoration-example.atom-text-editor.less) from the decoration-example package for an example of context-targeting. - -Inside a context-targeted style sheet, there's no need to use the `::shadow` or `/deep/` expressions. If you want to refer to the element containing the shadow root, you can use the `::host` pseudo-element. - -During the transition phase, style sheets targeting the `atom-text-editor` context will *also* be loaded globally. Make sure you update your selectors in a way that maintains compatibility with the shadow DOM being disabled. That means if you use a `::host` pseudo element, you should also include the same style rule matches against `atom-text-editor`. - -[shadow-dom-101]: http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom -[shadow-dom-201]: http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201#toc-style-cat-hat -[find-and-replace]: https://github.com/atom/find-and-replace/blob/95351f261bc384960a69b66bf12eae8002da63f9/styles/find-and-replace.less#L10 diff --git a/docs/writing-specs.md b/docs/writing-specs.md deleted file mode 100644 index 38db57439..000000000 --- a/docs/writing-specs.md +++ /dev/null @@ -1,136 +0,0 @@ -# Writing specs - -Atom uses [Jasmine](http://jasmine.github.io/1.3/introduction.html) as its spec framework. Any new functionality should have specs to guard against regressions. - -## Create a new spec - -[Atom specs](https://github.com/atom/atom/tree/master/spec) and [package specs](https://github.com/atom/markdown-preview/tree/master/spec) are added to their respective `spec` directory. The example below creates a spec for Atom core. - -0. Create a spec file - - Spec files **must** end with `-spec` so add `sample-spec.coffee` to `atom/spec`. - -0. Add one or more `describe` methods - - The `describe` method takes two arguments, a description and a function. If the description explains a behavior it typically begins with `when`; if it is more like a unit test it begins with the method name. - - ```coffee - describe "when a test is written", -> - # contents - ``` - - or - - ```coffee - describe "Editor::moveUp", -> - # contents - ``` - -0. Add one or more `it` method - - The `it` method also takes two arguments, a description and a function. Try and make the description flow with the `it` method. For example, a description of `this should work` doesn't read well as `it this should work`. But a description of `should work` sounds great as `it should work`. - - ```coffee - describe "when a test is written", -> - it "has some expectations that should pass", -> - # Expectations - ``` - -0. Add one or more expectations - - The best way to learn about expectations is to read the [jasmine documentation](http://jasmine.github.io/1.3/introduction.html#section-Expectations) about them. Below is a simple example. - - ```coffee - describe "when a test is written", -> - it "has some expectations that should pass", -> - expect("apples").toEqual("apples") - expect("oranges").not.toEqual("apples") - ``` - -## Asynchronous specs - -Writing Asynchronous specs can be tricky at first. Some examples. - -0. Promises - - Working with promises is rather easy in Atom. You can use our `waitsForPromise` function. - - ```coffee - describe "when we open a file", -> - it "should be opened in an editor", -> - waitsForPromise -> - atom.workspace.open('c.coffee').then (editor) -> - expect(editor.getPath()).toContain 'c.coffee' - ``` - - This method can be used in the `describe`, `it`, `beforeEach` and `afterEach` functions. - - ```coffee - describe "when we open a file", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open 'c.coffee' - - it "should be opened in an editor", -> - expect(atom.workspace.getActiveTextEditor().getPath()).toContain 'c.coffee' - - ``` - - If you need to wait for multiple promises use a new `waitsForPromise` function for each promise. (Caution: Without `beforeEach` this example will fail!) - - ```coffee - describe "waiting for the packages to load", -> - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js') - waitsForPromise -> - atom.packages.activatePackage('tabs') - waitsForPromise -> - atom.packages.activatePackage('tree-view') - - it 'should have waited long enough', -> - expect(atom.packages.isPackageActive('tabs')).toBe true - expect(atom.packages.isPackageActive('tree-view')).toBe true - ``` - -0. Asynchronous functions with callbacks - - Specs for asynchronous functions can be done using the `waitsFor` and `runs` functions. A simple example. - - ```coffee - describe "fs.readdir(path, cb)", -> - it "is async", -> - spy = jasmine.createSpy('fs.readdirSpy') - - fs.readdir('/tmp/example', spy) - waitsFor -> - spy.callCount > 0 - runs -> - exp = [null, ['example.coffee']] - expect(spy.mostRecentCall.args).toEqual exp - expect(spy).toHaveBeenCalledWith(null, ['example.coffee']) - ``` - -For a more detailed documentation on asynchronous tests please visit the [jasmine documentation](http://jasmine.github.io/1.3/introduction.html#section-Asynchronous_Support). - - -## Running specs - -Most of the time you'll want to run specs by triggering the `window:run-package-specs` command. This command is not only to run package specs, it is also for Atom core specs. This will run all the specs in the current project's spec directory. If you want to run the Atom core specs and **all** the default package specs trigger the `window:run-all-specs` command. - -To run a limited subset of specs use the `fdescribe` or `fit` methods. You can use those to focus a single spec or several specs. In the example above, focusing an individual spec looks like this: - -```coffee -describe "when a test is written", -> - fit "has some expectations that should pass", -> - expect("apples").toEqual("apples") - expect("oranges").not.toEqual("apples") -``` - -### Running on CI - -It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the -[Travis CI For Your Packages](http://blog.atom.io/2014/04/25/ci-for-your-packages.html) -and [AppVeyor CI For Your Packages](http://blog.atom.io/2014/07/28/windows-ci-for-your-packages.html) -posts for more details. diff --git a/docs/your-first-package.md b/docs/your-first-package.md deleted file mode 100644 index 42aca7c31..000000000 --- a/docs/your-first-package.md +++ /dev/null @@ -1,158 +0,0 @@ -# Create Your First Package - -This tutorial will guide you though creating a simple command that replaces the -selected text with [ascii art](https://en.wikipedia.org/wiki/ASCII_art). When you -run our new command with the word "cool" selected, it will be replaced with: - -``` - ___ - /\_ \ - ___ ___ ___\//\ \ - /'___\ / __`\ / __`\\ \ \ -/\ \__//\ \L\ \/\ \L\ \\_\ \_ -\ \____\ \____/\ \____//\____\ - \/____/\/___/ \/___/ \/____/ -``` - -The final package can be viewed at -[https://github.com/atom/ascii-art](https://github.com/atom/ascii-art). - -To begin, press `cmd-shift-P` to bring up the [Command -Palette](https://github.com/atom/command-palette). Type "generate package" and -select the "Package Generator: Generate Package" command. Now we need to name -the package. Try to avoid naming your package with the *atom-* prefix, for -example we are going to call this package _ascii-art_. - -Atom will open a new window with the contents of our new _ascii-art_ package -displayed in the Tree View. Because this window is opened **after** the package -is created, the ASCII Art package will be loaded and available in our new -window. To verify this, toggle the Command Palette (`cmd-shift-P`) and type -"ASCII Art". You'll see a new `ASCII Art: Toggle` command. When triggered, this -command displays a default message. - -Now let's edit the package files to make our ASCII Art package do something -interesting. Since this package doesn't need any UI, we can remove all -view-related code. Start by opening up _lib/ascii-art.coffee_. Remove all view -code, so the `module.exports` section looks like this: - -```coffeescript -module.exports = - activate: -> -``` - -## Create a Command - -Now let's add a command. We recommend that you namespace your commands with the -package name followed by a `:`, so we'll call our command `ascii-art:convert`. -Register the command in _lib/ascii-art.coffee_: - -```coffeescript -module.exports = - activate: -> - atom.commands.add 'atom-workspace', "ascii-art:convert", => @convert() - - convert: -> - # This assumes the active pane item is an editor - editor = atom.workspace.getActivePaneItem() - editor.insertText('Hello, World!') -``` - -The `atom.commands.add` method takes a selector, command name, and a callback. -The callback executes when the command is triggered on an element matching the -selector. In this case, when the command is triggered the callback will call the -`convert` method and insert 'Hello, World!'. - -## Reload the Package - -Before we can trigger `ascii-art:convert`, we need to load the latest code for -our package by reloading the window. Run the command `window:reload` from the -command palette or by pressing `ctrl-alt-cmd-l`. - -## Trigger the Command - -Now open the command panel and search for the `ascii-art:convert` command. But -it's not there! To fix this, open _package.json_ and find the property called -`activationCommands`. Activation Commands speed up Atom's load time by allowing it to -delay a package's activation until it's needed. So remove the existing command -and add `ascii-art:convert` to the `activationCommands` array: - -```json -"activationCommands": ["ascii-art:convert"], -``` - -First, reload the window by running the command `window:reload`. Now when you -run the `ascii-art:convert` command it will output 'Hello, World!' - -## Add a Key Binding - -Now let's add a key binding to trigger the `ascii-art:convert` command. Open -_keymaps/ascii-art.cson_ and add a key binding linking `ctrl-alt-a` to the -`ascii-art:convert` command. You can delete the pre-existing key binding since -you don't need it anymore. When finished, the file will have this: - -```coffeescript -'atom-text-editor': - 'ctrl-alt-a': 'ascii-art:convert' -``` - -Notice `atom-text-editor` on the first line. Just like CSS, keymap selectors -*scope* key bindings so they only apply to specific elements. In this case, our -binding is only active for elements matching the `atom-text-editor` selector. If -the Tree View has focus, pressing `ctrl-alt-a` won't trigger the -`ascii-art:convert` command. But if the editor has focus, the -`ascii-art:convert` method *will* be triggered. More information on key bindings -can be found in the [keymaps](advanced/keymaps.html) documentation. - -Now reload the window and verify that the key binding works! You can also verify -that it **doesn't** work when the Tree View is focused. - -## Add the ASCII Art - -Now we need to convert the selected text to ASCII art. To do this we will use -the [figlet](https://npmjs.org/package/figlet) [node](http://nodejs.org/) module -from [npm](https://npmjs.org/). Open _package.json_ and add the latest version of -figlet to the dependencies: - -```json -"dependencies": { - "figlet": "1.0.8" -} -``` - -After saving the file, run the command 'update-package-dependencies:update' from -the Command Palette. This will install the package's node module dependencies, -only figlet in this case. You will need to run -'update-package-dependencies:update' whenever you update the dependencies field -in your _package.json_ file. - -Now require the figlet node module in _lib/ascii-art.coffee_ and instead of -inserting 'Hello, World!' convert the selected text to ASCII art. - -```coffeescript -convert: -> - # This assumes the active pane item is an editor - editor = atom.workspace.getActivePaneItem() - selection = editor.getLastSelection() - - figlet = require 'figlet' - figlet selection.getText(), {font: "Larry 3D 2"}, (error, asciiArt) -> - if error - console.error(error) - else - selection.insertText("\n#{asciiArt}\n") -``` - -Select some text in an editor window and hit `ctrl-alt-a`. :tada: You're now an -ASCII art professional! - -## Further reading - -* [Getting your project on GitHub guide](http://guides.github.com/overviews/desktop) - -* [Writing specs](writing-specs.md) for your package - -* [Creating a package guide](creating-a-package.html) for more information - on the mechanics of packages - -* [Publishing a package guide](publishing-a-package.html) for more information - on publishing your package to [atom.io](https://atom.io) diff --git a/dot-atom/config.cson b/dot-atom/config.cson deleted file mode 100644 index a93b83e66..000000000 --- a/dot-atom/config.cson +++ /dev/null @@ -1,7 +0,0 @@ -'editor': - 'fontSize': 16 -'core': - 'themes': [ - 'atom-dark-ui' - 'atom-dark-syntax' - ] diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index ae63892bf..dea689c12 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -17,8 +17,8 @@ # 'ctrl-p': 'core:move-down' # # You can find more information about keymaps in these guides: -# * https://atom.io/docs/latest/customizing-atom#customizing-key-bindings -# * https://atom.io/docs/latest/advanced/keymaps +# * https://atom.io/docs/latest/using-atom-basic-customization#customizing-key-bindings +# * https://atom.io/docs/latest/behind-atom-keymaps-in-depth # # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it here: diff --git a/exports/atom.coffee b/exports/atom.coffee index 5afa04022..67cd23741 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -1,7 +1,8 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer +{File, Directory} = require 'pathwatcher' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' -{deprecate} = require 'grim' +{includeDeprecatedAPIs, deprecate} = require 'grim' module.exports = BufferedNodeProcess: require '../src/buffered-node-process' @@ -11,6 +12,8 @@ module.exports = TextBuffer: TextBuffer Point: Point Range: Range + File: File + Directory: Directory Emitter: Emitter Disposable: Disposable CompositeDisposable: CompositeDisposable @@ -21,109 +24,111 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.Task = require '../src/task' module.exports.TextEditor = require '../src/text-editor' - {$, $$, $$$, View} = require '../src/space-pen-extensions' + if includeDeprecatedAPIs + {$, $$, $$$, View} = require '../src/space-pen-extensions' - Object.defineProperty module.exports, 'Workspace', get: -> - deprecate """ - Requiring `Workspace` from `atom` is no longer supported. - If you need this, please open an issue on - https://github.com/atom/atom/issues/new - And let us know what you are using it for. - """ - require '../src/workspace' + Object.defineProperty module.exports, 'Workspace', get: -> + deprecate """ + Requiring `Workspace` from `atom` is no longer supported. + If you need this, please open an issue on + https://github.com/atom/atom/issues/new + And let us know what you are using it for. + """ + require '../src/workspace' - Object.defineProperty module.exports, 'WorkspaceView', get: -> - deprecate """ - Requiring `WorkspaceView` from `atom` is no longer supported. - Use `atom.views.getView(atom.workspace)` instead. - """ - require '../src/workspace-view' + Object.defineProperty module.exports, 'WorkspaceView', get: -> + deprecate """ + Requiring `WorkspaceView` from `atom` is no longer supported. + Use `atom.views.getView(atom.workspace)` instead. + """ + require '../src/workspace-view' - Object.defineProperty module.exports, '$', get: -> - deprecate """ - Requiring `$` from `atom` is no longer supported. - If you are using `space-pen`, please require `$` from `atom-space-pen-views`. Otherwise require `jquery` instead: - `{$} = require 'atom-space-pen-views'` - or - `$ = require 'jquery'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - Or add `"jquery": "^2"` to your package dependencies. - """ - $ + Object.defineProperty module.exports, '$', get: -> + deprecate """ + Requiring `$` from `atom` is no longer supported. + If you are using `space-pen`, please require `$` from `atom-space-pen-views`. Otherwise require `jquery` instead: + `{$} = require 'atom-space-pen-views'` + or + `$ = require 'jquery'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + Or add `"jquery": "^2"` to your package dependencies. + """ + $ - Object.defineProperty module.exports, '$$', get: -> - deprecate """ - Requiring `$$` from `atom` is no longer supported. - Please require `atom-space-pen-views` instead: - `{$$} = require 'atom-space-pen-views'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - $$ + Object.defineProperty module.exports, '$$', get: -> + deprecate """ + Requiring `$$` from `atom` is no longer supported. + Please require `atom-space-pen-views` instead: + `{$$} = require 'atom-space-pen-views'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + $$ - Object.defineProperty module.exports, '$$$', get: -> - deprecate """ - Requiring `$$$` from `atom` is no longer supported. - Please require `atom-space-pen-views` instead: - `{$$$} = require 'atom-space-pen-views'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - $$$ + Object.defineProperty module.exports, '$$$', get: -> + deprecate """ + Requiring `$$$` from `atom` is no longer supported. + Please require `atom-space-pen-views` instead: + `{$$$} = require 'atom-space-pen-views'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + $$$ - Object.defineProperty module.exports, 'View', get: -> - deprecate """ - Requiring `View` from `atom` is no longer supported. - Please require `atom-space-pen-views` instead: - `{View} = require 'atom-space-pen-views'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - View + Object.defineProperty module.exports, 'View', get: -> + deprecate """ + Requiring `View` from `atom` is no longer supported. + Please require `atom-space-pen-views` instead: + `{View} = require 'atom-space-pen-views'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + View - Object.defineProperty module.exports, 'EditorView', get: -> - deprecate """ - Requiring `EditorView` from `atom` is no longer supported. - Please require `TextEditorView` from `atom-space-pen-view` instead: - `{TextEditorView} = require 'atom-space-pen-views'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - require '../src/text-editor-view' + Object.defineProperty module.exports, 'EditorView', get: -> + deprecate """ + Requiring `EditorView` from `atom` is no longer supported. + Please require `TextEditorView` from `atom-space-pen-view` instead: + `{TextEditorView} = require 'atom-space-pen-views'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + require '../src/text-editor-view' - Object.defineProperty module.exports, 'TextEditorView', get: -> - deprecate """ - Requiring `TextEditorView` from `atom` is no longer supported. - Please require `TextEditorView` from `atom-space-pen-view` instead: - `{TextEditorView} = require 'atom-space-pen-views'` - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - require '../src/text-editor-view' + Object.defineProperty module.exports, 'TextEditorView', get: -> + deprecate """ + Requiring `TextEditorView` from `atom` is no longer supported. + Please require `TextEditorView` from `atom-space-pen-view` instead: + `{TextEditorView} = require 'atom-space-pen-views'` + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + require '../src/text-editor-view' - Object.defineProperty module.exports, 'ScrollView', get: -> - deprecate """ - Requiring `ScrollView` from `atom` is no longer supported. - Please require `ScrollView` from `atom-space-pen-view` instead: - `{ScrollView} = require 'atom-space-pen-views'` - Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - require '../src/scroll-view' + Object.defineProperty module.exports, 'ScrollView', get: -> + deprecate """ + Requiring `ScrollView` from `atom` is no longer supported. + Please require `ScrollView` from `atom-space-pen-view` instead: + `{ScrollView} = require 'atom-space-pen-views'` + Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + require '../src/scroll-view' - Object.defineProperty module.exports, 'SelectListView', get: -> - deprecate """ - Requiring `SelectListView` from `atom` is no longer supported. - Please require `SelectListView` from `atom-space-pen-view` instead: - `{SelectListView} = require 'atom-space-pen-views'` - Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views - Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. - """ - require '../src/select-list-view' + Object.defineProperty module.exports, 'SelectListView', get: -> + deprecate """ + Requiring `SelectListView` from `atom` is no longer supported. + Please require `SelectListView` from `atom-space-pen-view` instead: + `{SelectListView} = require 'atom-space-pen-views'` + Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views + Add `"atom-space-pen-views": "^2.0.3"` to your package dependencies. + """ + require '../src/select-list-view' - Object.defineProperty module.exports, 'React', get: -> - deprecate "Please require `react-atom-fork` instead: `React = require 'react-atom-fork'`. Add `\"react-atom-fork\": \"^0.11\"` to your package dependencies." - require 'react-atom-fork' + Object.defineProperty module.exports, 'React', get: -> + deprecate "Please require `react-atom-fork` instead: `React = require 'react-atom-fork'`. Add `\"react-atom-fork\": \"^0.11\"` to your package dependencies." + require 'react-atom-fork' - Object.defineProperty module.exports, 'Reactionary', get: -> - deprecate "Please require `reactionary-atom-fork` instead: `Reactionary = require 'reactionary-atom-fork'`. Add `\"reactionary-atom-fork\": \"^0.9\"` to your package dependencies." - require 'reactionary-atom-fork' + Object.defineProperty module.exports, 'Reactionary', get: -> + deprecate "Please require `reactionary-atom-fork` instead: `Reactionary = require 'reactionary-atom-fork'`. Add `\"reactionary-atom-fork\": \"^0.9\"` to your package dependencies." + require 'reactionary-atom-fork' -Object.defineProperty module.exports, 'Git', get: -> - deprecate "Please require `GitRepository` instead of `Git`: `{GitRepository} = require 'atom'`" - module.exports.GitRepository +if includeDeprecatedAPIs + Object.defineProperty module.exports, 'Git', get: -> + deprecate "Please require `GitRepository` instead of `Git`: `{GitRepository} = require 'atom'`" + module.exports.GitRepository diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 5226bb5e6..34d18e159 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -18,7 +18,6 @@ 'ctrl-d': 'core:delete' # Atom Specific - 'cmd-O': 'application:open-dev' 'cmd-alt-ctrl-s': 'application:run-all-specs' 'enter': 'core:confirm' 'escape': 'core:cancel' @@ -37,6 +36,7 @@ 'cmd-N': 'application:new-window' 'cmd-W': 'window:close' 'cmd-o': 'application:open' + 'cmd-O': 'application:add-project-folder' 'cmd-T': 'pane:reopen-closed-item' 'cmd-n': 'application:new-file' 'cmd-s': 'core:save' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 59803d193..cd71d99e7 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -10,8 +10,8 @@ 'ctrl-shift-i': 'window:toggle-dev-tools' 'ctrl-alt-p': 'window:run-package-specs' 'ctrl-alt-s': 'application:run-all-specs' - 'ctrl-alt-o': 'application:open-dev' 'ctrl-shift-o': 'application:open-folder' + 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-pageup': 'pane:move-item-left' 'ctrl-shift-pagedown': 'pane:move-item-right' 'F11': 'window:toggle-full-screen' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index da43ac364..656d9ec8e 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -14,8 +14,8 @@ 'ctrl-alt-i': 'window:toggle-dev-tools' 'ctrl-alt-p': 'window:run-package-specs' 'ctrl-alt-s': 'application:run-all-specs' - 'ctrl-alt-o': 'application:open-dev' 'ctrl-shift-o': 'application:open-folder' + 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' 'F11': 'window:toggle-full-screen' diff --git a/menus/darwin.cson b/menus/darwin.cson index 7283ba16f..effd19197 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -34,6 +34,7 @@ { label: 'New Window', command: 'application:new-window' } { label: 'New File', command: 'application:new-file' } { label: 'Open...', command: 'application:open' } + { label: 'Add Project Folder...', command: 'application:add-project-folder' } { label: 'Reopen Last Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Save', command: 'core:save' } @@ -217,6 +218,7 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ @@ -225,5 +227,6 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/linux.cson b/menus/linux.cson index fc1e58785..2cfb17871 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -6,6 +6,7 @@ { label: '&New File', command: 'application:new-file' } { label: '&Open File...', command: 'application:open-file' } { label: 'Open Folder...', command: 'application:open-folder' } + { label: 'Add Project Folder...', command: 'application:add-project-folder' } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: '&Save', command: 'core:save' } @@ -95,6 +96,25 @@ { label: '&Reload', command: 'window:reload' } { label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' } { label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' } + { + label: 'Panes' + submenu: [ + { label: 'Split Up', command: 'pane:split-up' } + { label: 'Split Down', command: 'pane:split-down' } + { label: 'Split Left', command: 'pane:split-left' } + { label: 'Split Right', command: 'pane:split-right' } + { type: 'separator' } + { label: 'Focus Next Pane', command: 'window:focus-next-pane' } + { label: 'Focus Previous Pane', command: 'window:focus-previous-pane' } + { type: 'separator' } + { label: 'Focus Pane Above', command: 'window:focus-pane-above' } + { label: 'Focus Pane Below', command: 'window:focus-pane-below' } + { label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' } + { label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' } + { type: 'separator' } + { label: 'Close Pane', command: 'pane:close' } + ] + } { label: 'Developer' submenu: [ @@ -174,6 +194,7 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ @@ -182,5 +203,6 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/win32.cson b/menus/win32.cson index a3e5c8b8d..2bd013ce3 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -6,6 +6,7 @@ { label: '&New File', command: 'application:new-file' } { label: '&Open File...', command: 'application:open-file' } { label: 'Open Folder...', command: 'application:open-folder' } + { label: 'Add Project Folder...', command: 'application:add-project-folder' } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } @@ -196,6 +197,7 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ @@ -204,5 +206,6 @@ {label: 'Split Down', command: 'pane:split-down'} {label: 'Split Left', command: 'pane:split-left'} {label: 'Split Right', command: 'pane:split-right'} + {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/package.json b/package.json index 2ec0091a3..a66753883 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.187.0", + "version": "0.192.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -17,27 +17,27 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.21.3", + "atomShellVersion": "0.22.3", "dependencies": { "async": "0.2.6", - "atom-keymap": "^3.1.3", + "atom-keymap": "^5.1.2", "atom-space-pen-views": "^2.0.4", "babel-core": "^4.0.2", "bootstrap": "git+https://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372", - "clear-cut": "0.4.0", + "clear-cut": "^2.0.1", "coffee-cash": "0.8.0", "coffee-script": "1.8.0", "coffeestack": "^1.1.1", "color": "^0.7.3", "delegato": "^1", "emissary": "^1.3.3", - "event-kit": "^1.0.3", - "first-mate": "^3.0.0", - "fs-plus": "^2.5", + "event-kit": "^1.1", + "first-mate": "^3.1", + "fs-plus": "^2.6", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^3.0.0", - "grim": "1.2", + "grim": "1.2.1", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", "jquery": "^2.1.1", @@ -45,111 +45,113 @@ "marked": "^0.3", "mixto": "^1", "nslog": "^2.0.0", - "oniguruma": "^4.0.0", + "oniguruma": "^4.1", "optimist": "0.4.0", - "pathwatcher": "^3.3.3", + "pathwatcher": "^4.4", "property-accessors": "^1.1.3", "q": "^1.1.2", "random-words": "0.0.1", "react-atom-fork": "^0.11.5", "reactionary-atom-fork": "^1.0.0", "runas": "2.0.0", - "scandal": "2.0.0", - "scoped-property-store": "^0.16.2", + "scandal": "2.0.2", + "scoped-property-store": "^0.17.0", "scrollbar-style": "^2.0.0", "season": "^5.1.4", "semver": "~4.2", "serializable": "^1", - "service-hub": "^0.4.0", + "service-hub": "^0.5.0", "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", - "text-buffer": "^4.1.5", + "text-buffer": "^5.2", "theorist": "^1.0.2", + "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6" }, "packageDependencies": { "atom-dark-syntax": "0.26.0", - "atom-dark-ui": "0.47.0", + "atom-dark-ui": "0.49.0", "atom-light-syntax": "0.26.0", "atom-light-ui": "0.41.0", "base16-tomorrow-dark-theme": "0.25.0", "base16-tomorrow-light-theme": "0.8.0", - "one-dark-ui": "0.5.0", + "one-dark-ui": "0.6.3", "one-dark-syntax": "0.3.0", "one-light-syntax": "0.4.0", - "one-light-ui": "0.4.0", + "one-light-ui": "0.5.3", "solarized-dark-syntax": "0.32.0", "solarized-light-syntax": "0.19.0", - "archive-view": "0.50.0", + "archive-view": "0.55.0", "autocomplete": "0.44.0", "autoflow": "0.22.0", "autosave": "0.20.0", - "background-tips": "0.23.0", + "background-tips": "0.24.0", "bookmarks": "0.35.0", - "bracket-matcher": "0.71.0", - "command-palette": "0.34.0", - "deprecation-cop": "0.37.0", - "dev-live-reload": "0.42.0", - "encoding-selector": "0.18.0", + "bracket-matcher": "0.73.0", + "command-palette": "0.35.0", + "deprecation-cop": "0.40.0", + "dev-live-reload": "0.45.0", + "encoding-selector": "0.19.0", "exception-reporting": "0.24.0", - "find-and-replace": "0.159.0", - "fuzzy-finder": "0.70.0", + "feedback": "0.38.0", + "find-and-replace": "0.160.0", + "fuzzy-finder": "0.74.0", "git-diff": "0.54.0", "go-to-line": "0.30.0", - "grammar-selector": "0.45.0", - "image-view": "0.49.0", - "incompatible-packages": "0.22.0", - "keybinding-resolver": "0.29.0", + "grammar-selector": "0.46.0", + "image-view": "0.54.0", + "incompatible-packages": "0.24.0", + "keybinding-resolver": "0.31.0", "link": "0.30.0", - "markdown-preview": "0.139.0", + "markdown-preview": "0.148.0", "metrics": "0.45.0", - "notifications": "0.31.0", - "open-on-github": "0.34.0", + "notifications": "0.37.0", + "open-on-github": "0.36.0", "package-generator": "0.38.0", - "release-notes": "0.51.0", - "settings-view": "0.184.0", - "snippets": "0.80.0", + "release-notes": "0.52.0", + "settings-view": "0.192.0", + "snippets": "0.88.0", "spell-check": "0.55.0", - "status-bar": "0.63.0", + "status-bar": "0.68.0", "styleguide": "0.44.0", - "symbols-view": "0.88.0", + "symbols-view": "0.94.0", "tabs": "0.67.0", "timecop": "0.31.0", - "tree-view": "0.164.0", + "tree-view": "0.170.0", "update-package-dependencies": "0.9.0", - "welcome": "0.25.0", + "welcome": "0.26.0", "whitespace": "0.29.0", - "wrap-guide": "0.31.0", - "language-c": "0.41.0", - "language-clojure": "0.13.0", + "wrap-guide": "0.32.0", + "language-c": "0.44.0", + "language-clojure": "0.14.0", "language-coffee-script": "0.39.0", "language-csharp": "0.5.0", "language-css": "0.28.0", - "language-gfm": "0.67.0", + "language-gfm": "0.68.0", "language-git": "0.10.0", - "language-go": "0.21.0", - "language-html": "0.29.0", + "language-go": "0.23.0", + "language-html": "0.32.0", "language-hyperlink": "0.12.2", "language-java": "0.14.0", - "language-javascript": "0.60.0", - "language-json": "0.12.0", + "language-javascript": "0.73.0", + "language-json": "0.14.0", "language-less": "0.25.0", - "language-make": "0.13.0", + "language-make": "0.14.0", "language-mustache": "0.11.0", "language-objective-c": "0.15.0", - "language-perl": "0.13.0", - "language-php": "0.21.0", + "language-perl": "0.22.0", + "language-php": "0.22.0", "language-property-list": "0.8.0", - "language-python": "0.32.0", - "language-ruby": "0.49.0", - "language-ruby-on-rails": "0.20.0", - "language-sass": "0.35.0", - "language-shellscript": "0.12.0", + "language-python": "0.34.0", + "language-ruby": "0.52.0", + "language-ruby-on-rails": "0.21.0", + "language-sass": "0.36.0", + "language-shellscript": "0.13.0", "language-source": "0.9.0", - "language-sql": "0.14.0", + "language-sql": "0.15.0", "language-text": "0.6.0", - "language-todo": "0.16.0", + "language-todo": "0.18.0", "language-toml": "0.15.0", "language-xml": "0.28.0", "language-yaml": "0.22.0" diff --git a/script/bootstrap b/script/bootstrap index 270abff7c..4c9435852 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -35,7 +35,7 @@ function bootstrap() { var npmPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'npm'); var initialNpmCommand = fs.existsSync(npmPath) ? npmPath : 'npm'; - var npmFlags = ' --userconfig=' + path.resolve('.npmrc') + ' '; + var npmFlags = ' --userconfig=' + path.resolve(__dirname, '..', 'build', '.npmrc') + ' '; var packagesToDedupe = [ 'abbrev', diff --git a/script/cibuild b/script/cibuild index af5f789e1..08ff65c6e 100755 --- a/script/cibuild +++ b/script/cibuild @@ -46,9 +46,39 @@ function removeNodeModules() { } } +function removeTempFolders() { + var fsPlus; + try { + fsPlus = require('fs-plus'); + } catch (error) { + return; + } + + var temp = require('os').tmpdir(); + if (!fsPlus.isDirectorySync(temp)) + return; + + var deletedFolders = 0; + + fsPlus.readdirSync(temp).filter(function(folderName) { + return folderName.indexOf('npm-') === 0; + }).forEach(function(folderName) { + try { + fsPlus.removeSync(path.join(temp, folderName)); + deletedFolders++; + } catch (error) { + console.error("Failed to delete npm temp folder: " + error.message); + } + }); + + if (deletedFolders > 0) + console.log("Deleted " + deletedFolders + " npm folders from temp directory"); +} + readEnvironmentVariables(); removeNodeModules(); -cp.safeExec.bind(global, 'npm install npm', {cwd: path.resolve(__dirname, '..', 'build')}, function() { +removeTempFolders(); +cp.safeExec.bind(global, 'npm install npm --loglevel error', {cwd: path.resolve(__dirname, '..', 'build')}, function() { cp.safeExec.bind(global, 'node script/bootstrap', function(error) { if (error) process.exit(1); diff --git a/script/utils/translate-crash-log-addresses.coffee b/script/utils/translate-crash-log-addresses.coffee index 7fb01532b..fdb540d64 100755 --- a/script/utils/translate-crash-log-addresses.coffee +++ b/script/utils/translate-crash-log-addresses.coffee @@ -17,7 +17,7 @@ parse_stack_trace = (raw) -> addresses = [] for line in raw columns = line.split /\ +/ - if columns[1] == 'libcef.dylib' and /0x[a-f0-9]+/.test columns[3] + if columns[1] is 'libcef.dylib' and /0x[a-f0-9]+/.test columns[3] lines[columns[0]] = addresses.length addresses.push '0x' + parseInt(columns[5]).toString(16) + ' ' @@ -36,12 +36,12 @@ parse_log_file = (content) -> lines = content.split /\r?\n/ for line in lines - if state == 'start' + if state is 'start' if /Thread \d+ Crashed::/.test line console.log line state = 'parse' - else if state == 'parse' - break if line == '' + else if state is 'parse' + break if line is '' stack_trace.push line parse_stack_trace stack_trace @@ -53,4 +53,3 @@ process.stdin.on 'data', (chunk) -> input += chunk process.stdin.on 'end', -> parse_log_file input - diff --git a/script/utils/verify-requirements.js b/script/utils/verify-requirements.js index 554c27dd0..890172753 100644 --- a/script/utils/verify-requirements.js +++ b/script/utils/verify-requirements.js @@ -57,7 +57,7 @@ function verifyNpm(cb) { var npmMajorVersion = +versionArray[0] || 0; var npmMinorVersion = +versionArray[1] || 0; if (npmMajorVersion === 1 && npmMinorVersion < 4) - cb("npm v1.4+ is required to build Atom."); + cb("npm v1.4+ is required to build Atom. Version " + npmVersion + " was detected."); else cb(null, "npm: v" + npmVersion); }); diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 1fca9cf28..a293edccd 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -55,7 +55,7 @@ describe "the `atom` global", -> describe "loading default config", -> it 'loads the default core config', -> expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true - expect(atom.config.get('core.followSymlinks')).toBe false + expect(atom.config.get('core.followSymlinks')).toBe true expect(atom.config.get('editor.showInvisibles')).toBe false describe "window onerror handler", -> @@ -180,3 +180,19 @@ describe "the `atom` global", -> it "does not open an empty buffer", -> atom.openInitialEmptyEditorIfNecessary() expect(atom.workspace.open).not.toHaveBeenCalled() + + describe "adding a project folder", -> + it "adds a second path to the project", -> + initialPaths = atom.project.getPaths() + tempDirectory = temp.mkdirSync("a-new-directory") + spyOn(atom, "pickFolder").andCallFake (callback) -> + callback([tempDirectory]) + atom.addProjectFolder() + expect(atom.project.getPaths()).toEqual(initialPaths.concat([tempDirectory])) + + it "does nothing if the user dismisses the file picker", -> + initialPaths = atom.project.getPaths() + tempDirectory = temp.mkdirSync("a-new-directory") + spyOn(atom, "pickFolder").andCallFake (callback) -> callback(null) + atom.addProjectFolder() + expect(atom.project.getPaths()).toEqual(initialPaths) diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index 534457157..6eb6556d0 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -3,26 +3,37 @@ CSON = require 'season' CoffeeCache = require 'coffee-cash' babel = require '../src/babel' +typescript = require '../src/typescript' CompileCache = require '../src/compile-cache' describe "Compile Cache", -> describe ".addPathToCache(filePath)", -> - it "adds the path to the correct CSON, CoffeeScript, or babel cache", -> + it "adds the path to the correct CSON, CoffeeScript, babel or typescript cache", -> spyOn(CSON, 'readFileSync').andCallThrough() spyOn(CoffeeCache, 'addPathToCache').andCallThrough() spyOn(babel, 'addPathToCache').andCallThrough() + spyOn(typescript, 'addPathToCache').andCallThrough() CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'cson.cson')) expect(CSON.readFileSync.callCount).toBe 1 expect(CoffeeCache.addPathToCache.callCount).toBe 0 expect(babel.addPathToCache.callCount).toBe 0 + expect(typescript.addPathToCache.callCount).toBe 0 CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'coffee.coffee')) expect(CSON.readFileSync.callCount).toBe 1 expect(CoffeeCache.addPathToCache.callCount).toBe 1 expect(babel.addPathToCache.callCount).toBe 0 + expect(typescript.addPathToCache.callCount).toBe 0 CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'babel', 'babel-double-quotes.js')) expect(CSON.readFileSync.callCount).toBe 1 expect(CoffeeCache.addPathToCache.callCount).toBe 1 expect(babel.addPathToCache.callCount).toBe 1 + expect(typescript.addPathToCache.callCount).toBe 0 + + CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'typescript', 'valid.ts')) + expect(CSON.readFileSync.callCount).toBe 1 + expect(CoffeeCache.addPathToCache.callCount).toBe 1 + expect(babel.addPathToCache.callCount).toBe 1 + expect(typescript.addPathToCache.callCount).toBe 1 diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 4abd3b0db..4aeb46c68 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -251,9 +251,9 @@ describe "Config", -> it "removes all scoped and unscoped properties for that key-path", -> atom.config.setDefaults("foo.bar", baz: 100) - atom.config.set("foo.bar", { baz: 1, ok: 2 }, scopeSelector: ".a") - atom.config.set("foo.bar", { baz: 11, ok: 12 }, scopeSelector: ".b") - atom.config.set("foo.bar", { baz: 21, ok: 22 }) + atom.config.set("foo.bar", {baz: 1, ok: 2}, scopeSelector: ".a") + atom.config.set("foo.bar", {baz: 11, ok: 12}, scopeSelector: ".b") + atom.config.set("foo.bar", {baz: 21, ok: 22}) atom.config.unset("foo.bar.baz") @@ -987,7 +987,6 @@ describe "Config", -> expect(fs.existsSync(atom.config.configDirPath)).toBeTruthy() expect(fs.existsSync(path.join(atom.config.configDirPath, 'packages'))).toBeTruthy() expect(fs.isFileSync(path.join(atom.config.configDirPath, 'snippets.cson'))).toBeTruthy() - expect(fs.isFileSync(path.join(atom.config.configDirPath, 'config.cson'))).toBeTruthy() expect(fs.isFileSync(path.join(atom.config.configDirPath, 'init.coffee'))).toBeTruthy() expect(fs.isFileSync(path.join(atom.config.configDirPath, 'styles.less'))).toBeTruthy() diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index 780f2afd5..236283d27 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -15,8 +15,8 @@ describe "DefaultDirectoryProvider", -> provider = new DefaultDirectoryProvider() tmp = temp.mkdirSync() nonNormalizedPath = tmp + path.sep + ".." + path.sep + path.basename(tmp) - expect(tmp.contains("..")).toBe false - expect(nonNormalizedPath.contains("..")).toBe true + expect(tmp.includes("..")).toBe false + expect(nonNormalizedPath.includes("..")).toBe true directory = provider.directoryForURISync(nonNormalizedPath) expect(directory.getPath()).toEqual tmp diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 23b658c04..b5858007e 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -48,7 +48,6 @@ describe "DisplayBuffer", -> expect(displayBuffer.getLineCount()).toBe 100 + originalLineCount it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> - displayBuffer.manageScrollPosition = true displayBuffer.setHeight(50) displayBuffer.setLineHeightInPixels(10) displayBuffer.setScrollTop(80) @@ -115,6 +114,24 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items.shift(), current, left = [], ' expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' right = [];' + describe "when the only whitespace characters are at the beginning of the line", -> + beforeEach -> + displayBuffer.setEditorWidthInChars(10) + + it "wraps the line at the max length when indented with tabs", -> + buffer.setTextInRange([[0, 0], [1, 0]], '\t\tabcdefghijklmnopqrstuvwxyz') + + expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef' + expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl' + expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr' + + it "wraps the line at the max length when indented with spaces", -> + buffer.setTextInRange([[0, 0], [1, 0]], ' abcdefghijklmnopqrstuvwxyz') + + expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef' + expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl' + expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr' + describe "when there are hard tabs", -> beforeEach -> buffer.setText(buffer.getText().replace(new RegExp(' ', 'g'), '\t')) @@ -124,9 +141,42 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() describe "when a line is wrapped", -> - it "correctly tokenizes soft wrap indentation tokens", -> - expect(displayBuffer.tokenizedLineForScreenRow(4).tokens[0].isSoftWrapIndentation).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(4).tokens[1].isSoftWrapIndentation).toBeTruthy() + it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> + tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) + + expect(tokenizedLine.tokens[0].value).toBe(" ") + expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[1].value).toBe(" ") + expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeFalsy() + + describe "when editor.softWrapHangingIndent is set", -> + beforeEach -> + atom.config.set('editor.softWrapHangingIndent', 3) + + it "further indents wrapped lines", -> + expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe " return " + expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe " sort(left).concat(pivot).concat(sort(right)" + expect(displayBuffer.tokenizedLineForScreenRow(12).text).toBe " );" + + it "includes hanging indent when breaking soft-wrap indentation into tokens", -> + tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) + + expect(tokenizedLine.tokens[0].value).toBe(" ") + expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[1].value).toBe(" ") + expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[2].value).toBe(" ") # hanging indent + expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[3].value).toBe(" ") # odd space + expect(tokenizedLine.tokens[3].isSoftWrapIndentation).toBeTruthy() + + expect(tokenizedLine.tokens[4].isSoftWrapIndentation).toBeFalsy() describe "when the buffer changes", -> describe "when buffer lines are updated", -> @@ -236,7 +286,6 @@ describe "DisplayBuffer", -> it "sets ::scrollLeft to 0 and keeps it there when soft wrapping is enabled", -> displayBuffer.setDefaultCharWidth(10) displayBuffer.setWidth(85) - displayBuffer.manageScrollPosition = true displayBuffer.setSoftWrapped(false) displayBuffer.setScrollLeft(Infinity) @@ -589,8 +638,11 @@ describe "DisplayBuffer", -> expect(displayBuffer.outermostFoldsInBufferRowRange(3, 18)).toEqual [fold1, fold3, fold5] expect(displayBuffer.outermostFoldsInBufferRowRange(5, 16)).toEqual [fold3] - describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", -> + describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, clip: 'closest')", -> beforeEach -> + tabLength = 4 + + displayBuffer.setTabLength(tabLength) displayBuffer.setSoftWrapped(true) displayBuffer.setEditorWidthInChars(50) @@ -649,19 +701,43 @@ describe "DisplayBuffer", -> expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 4] expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 4] - describe "when skipAtomicTokens is false (the default)", -> - it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> + describe "when clip is 'closest' (the default)", -> + it "clips screen positions in the middle of atomic tab characters to the closest edge of the character", -> buffer.insert([0, 0], '\t') expect(displayBuffer.clipScreenPosition([0, 0])).toEqual [0, 0] expect(displayBuffer.clipScreenPosition([0, 1])).toEqual [0, 0] + expect(displayBuffer.clipScreenPosition([0, 2])).toEqual [0, 0] + expect(displayBuffer.clipScreenPosition([0, tabLength-1])).toEqual [0, tabLength] expect(displayBuffer.clipScreenPosition([0, tabLength])).toEqual [0, tabLength] - describe "when skipAtomicTokens is true", -> + describe "when clip is 'backward'", -> + it "clips screen positions in the middle of atomic tab characters to the beginning of the character", -> + buffer.insert([0, 0], '\t') + expect(displayBuffer.clipScreenPosition([0, 0], clip: 'backward')).toEqual [0, 0] + expect(displayBuffer.clipScreenPosition([0, tabLength-1], clip: 'backward')).toEqual [0, 0] + expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'backward')).toEqual [0, tabLength] + + describe "when clip is 'forward'", -> it "clips screen positions in the middle of atomic tab characters to the end of the character", -> buffer.insert([0, 0], '\t') - expect(displayBuffer.clipScreenPosition([0, 0], skipAtomicTokens: true)).toEqual [0, 0] - expect(displayBuffer.clipScreenPosition([0, 1], skipAtomicTokens: true)).toEqual [0, tabLength] - expect(displayBuffer.clipScreenPosition([0, tabLength], skipAtomicTokens: true)).toEqual [0, tabLength] + expect(displayBuffer.clipScreenPosition([0, 0], clip: 'forward')).toEqual [0, 0] + expect(displayBuffer.clipScreenPosition([0, 1], clip: 'forward')).toEqual [0, tabLength] + expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'forward')).toEqual [0, tabLength] + + describe "::screenPositionForPixelPosition(pixelPosition)", -> + it "clips pixel positions above buffer start", -> + displayBuffer.setLineHeightInPixels(20) + expect(displayBuffer.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] + expect(displayBuffer.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] + expect(displayBuffer.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] + expect(displayBuffer.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] + + it "clips pixel positions below buffer end", -> + displayBuffer.setLineHeightInPixels(20) + expect(displayBuffer.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] + expect(displayBuffer.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] + expect(displayBuffer.screenPositionForPixelPosition(top: displayBuffer.getHeight() + 1, left: 0)).toEqual [12, 2] + expect(displayBuffer.screenPositionForPixelPosition(top: displayBuffer.getHeight() - 1, left: 0)).toEqual [12, 0] describe "::screenPositionForBufferPosition(bufferPosition, options)", -> it "clips the specified buffer position", -> @@ -670,6 +746,25 @@ describe "DisplayBuffer", -> expect(displayBuffer.screenPositionForBufferPosition([100000, 0])).toEqual [12, 2] expect(displayBuffer.screenPositionForBufferPosition([100000, 100000])).toEqual [12, 2] + it "clips to the (left or right) edge of an atomic token without simply rounding up", -> + tabLength = 4 + displayBuffer.setTabLength(tabLength) + + buffer.insert([0, 0], '\t') + expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0] + expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, tabLength] + + it "clips to the edge closest to the given position when it's inside a soft tab", -> + tabLength = 4 + displayBuffer.setTabLength(tabLength) + + buffer.insert([0, 0], ' ') + expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0] + expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 0] + expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 0] + expect(displayBuffer.screenPositionForBufferPosition([0, 3])).toEqual [0, 4] + expect(displayBuffer.screenPositionForBufferPosition([0, 4])).toEqual [0, 4] + describe "position translation in the presence of hard tabs", -> it "correctly translates positions on either side of a tab", -> buffer.setText('\t') @@ -1123,7 +1218,6 @@ describe "DisplayBuffer", -> describe "::setScrollTop", -> beforeEach -> - displayBuffer.manageScrollPosition = true displayBuffer.setLineHeightInPixels(10) it "disallows negative values", -> @@ -1145,7 +1239,6 @@ describe "DisplayBuffer", -> describe "when editor.scrollPastEnd is false", -> beforeEach -> atom.config.set("editor.scrollPastEnd", false) - displayBuffer.manageScrollPosition = true displayBuffer.setLineHeightInPixels(10) it "does not add the height of the view to the scroll height", -> @@ -1158,7 +1251,6 @@ describe "DisplayBuffer", -> describe "when editor.scrollPastEnd is true", -> beforeEach -> atom.config.set("editor.scrollPastEnd", true) - displayBuffer.manageScrollPosition = true displayBuffer.setLineHeightInPixels(10) it "adds the height of the view to the scroll height", -> @@ -1170,7 +1262,6 @@ describe "DisplayBuffer", -> describe "::setScrollLeft", -> beforeEach -> - displayBuffer.manageScrollPosition = true displayBuffer.setLineHeightInPixels(10) displayBuffer.setDefaultCharWidth(10) @@ -1191,18 +1282,21 @@ describe "DisplayBuffer", -> describe "::scrollToScreenPosition(position, [options])", -> beforeEach -> - displayBuffer.manageScrollPosition = true displayBuffer.setLineHeightInPixels(10) displayBuffer.setDefaultCharWidth(10) displayBuffer.setHorizontalScrollbarHeight(0) displayBuffer.setHeight(50) - displayBuffer.setWidth(50) + displayBuffer.setWidth(150) it "sets the scroll top and scroll left so the given screen position is in view", -> displayBuffer.scrollToScreenPosition([8, 20]) expect(displayBuffer.getScrollBottom()).toBe (9 + displayBuffer.getVerticalScrollMargin()) * 10 expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10 + displayBuffer.scrollToScreenPosition([8, 20]) + expect(displayBuffer.getScrollBottom()).toBe (9 + displayBuffer.getVerticalScrollMargin()) * 10 + expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10 + describe "when the 'center' option is true", -> it "vertically scrolls to center the given position vertically", -> displayBuffer.scrollToScreenPosition([8, 20], center: true) @@ -1247,3 +1341,29 @@ describe "DisplayBuffer", -> expect(displayBuffer.getScrollWidth()).toBe 10 * 63 + operatorWidth * 2 + cursorWidth expect(changedSpy.callCount).toBe 1 + + describe "::getVisibleRowRange()", -> + beforeEach -> + displayBuffer.setLineHeightInPixels(10) + displayBuffer.setHeight(100) + + it "returns the first and the last visible rows", -> + displayBuffer.setScrollTop(0) + + expect(displayBuffer.getVisibleRowRange()).toEqual [0, 9] + + it "includes partially visible rows in the range", -> + displayBuffer.setScrollTop(5) + + expect(displayBuffer.getVisibleRowRange()).toEqual [0, 10] + + it "returns an empty range when lineHeight is 0", -> + displayBuffer.setLineHeightInPixels(0) + + expect(displayBuffer.getVisibleRowRange()).toEqual [0, 0] + + it "ends at last buffer row even if there's more space available", -> + displayBuffer.setHeight(150) + displayBuffer.setScrollTop(60) + + expect(displayBuffer.getVisibleRowRange()).toEqual [0, 13] diff --git a/spec/fixtures/packages/package-with-missing-consumed-services/index.coffee b/spec/fixtures/packages/package-with-missing-consumed-services/index.coffee new file mode 100644 index 000000000..0f1fc41e0 --- /dev/null +++ b/spec/fixtures/packages/package-with-missing-consumed-services/index.coffee @@ -0,0 +1,4 @@ +module.exports = + activate: -> + + deactivate: -> diff --git a/spec/fixtures/packages/package-with-missing-consumed-services/package.json b/spec/fixtures/packages/package-with-missing-consumed-services/package.json new file mode 100644 index 000000000..d0e0ddfcb --- /dev/null +++ b/spec/fixtures/packages/package-with-missing-consumed-services/package.json @@ -0,0 +1,11 @@ +{ + "name": "package-with-missing-consumed-services", + + "consumedServices": { + "service-1": { + "versions": { + ">=0.1": "consumeMissingService" + } + } + } +} diff --git a/spec/fixtures/packages/package-with-missing-provided-services/index.coffee b/spec/fixtures/packages/package-with-missing-provided-services/index.coffee new file mode 100644 index 000000000..0f1fc41e0 --- /dev/null +++ b/spec/fixtures/packages/package-with-missing-provided-services/index.coffee @@ -0,0 +1,4 @@ +module.exports = + activate: -> + + deactivate: -> diff --git a/spec/fixtures/packages/package-with-missing-provided-services/package.json b/spec/fixtures/packages/package-with-missing-provided-services/package.json new file mode 100644 index 000000000..a090354a2 --- /dev/null +++ b/spec/fixtures/packages/package-with-missing-provided-services/package.json @@ -0,0 +1,12 @@ +{ + "name": "package-with-missing-provided-services", + + "providedServices": { + "service-1": { + "description": "The first service", + "versions": { + "0.2.9": "provideMissingService" + } + } + } +} diff --git a/spec/fixtures/packages/package-with-provided-services/index.coffee b/spec/fixtures/packages/package-with-provided-services/index.coffee index b6413b119..86f319946 100644 --- a/spec/fixtures/packages/package-with-provided-services/index.coffee +++ b/spec/fixtures/packages/package-with-provided-services/index.coffee @@ -3,6 +3,9 @@ module.exports = deactivate: -> + provideFirstServiceV2: -> + 'first-service-v2' + provideFirstServiceV3: -> 'first-service-v3' diff --git a/spec/fixtures/packages/package-with-provided-services/package.json b/spec/fixtures/packages/package-with-provided-services/package.json index 144fc2dac..d95bbf9b7 100644 --- a/spec/fixtures/packages/package-with-provided-services/package.json +++ b/spec/fixtures/packages/package-with-provided-services/package.json @@ -5,6 +5,7 @@ "service-1": { "description": "The first service", "versions": { + "0.2.9": "provideFirstServiceV2", "0.3.1": "provideFirstServiceV3", "0.4.1": "provideFirstServiceV4" } diff --git a/spec/fixtures/typescript/invalid.ts b/spec/fixtures/typescript/invalid.ts new file mode 100644 index 000000000..7a8d0b6d0 --- /dev/null +++ b/spec/fixtures/typescript/invalid.ts @@ -0,0 +1 @@ +var foo = 123 123; // Syntax error diff --git a/spec/fixtures/typescript/valid.ts b/spec/fixtures/typescript/valid.ts new file mode 100644 index 000000000..46cf54693 --- /dev/null +++ b/spec/fixtures/typescript/valid.ts @@ -0,0 +1,2 @@ +var inc = v => v + 1 +export = inc diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee index 59e3f55af..15e1dcc60 100644 --- a/spec/git-repository-provider-spec.coffee +++ b/spec/git-repository-provider-spec.coffee @@ -16,6 +16,7 @@ describe "GitRepositoryProvider", -> expect(result).toBeInstanceOf GitRepository expect(provider.pathToRepository[result.getPath()]).toBeTruthy() expect(result.statusTask).toBeTruthy() + expect(result.getType()).toBe 'git' it "returns the same GitRepository for different Directory objects in the same repo", -> provider = new GitRepositoryProvider atom.project diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index b5f5a8c1f..4be2efab8 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -9,7 +9,7 @@ webdriverio = require "../../../build/node_modules/webdriverio" AtomPath = remote.process.argv[0] AtomLauncherPath = path.join(__dirname, "..", "helpers", "atom-launcher.sh") ChromedriverPath = path.resolve(__dirname, '..', '..', '..', 'atom-shell', 'chromedriver', 'chromedriver') -SocketPath = path.join(temp.mkdirSync("socket-dir"), "atom.sock") +SocketPath = path.join(temp.mkdirSync("socket-dir"), "atom-#{process.env.USER}.sock") ChromedriverPort = 9515 buildAtomClient = (args, env) -> diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index f3253b81e..e0df3ad9b 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -87,7 +87,7 @@ describe "Starting Atom", -> nestedDir = path.join(otherTempDirPath, "nested-dir") fs.mkdirSync(nestedDir) - runAtom [tempDirPath, otherTempDirPath, "--multi-folder"], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> client .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() @@ -100,31 +100,6 @@ describe "Starting Atom", -> .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath]) - it "opens each path in its own window unless the --multi-folder flag is passed", -> - runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> - treeViewDirs = [] - - client - .waitForExist("atom-workspace", 5000) - .waitForWindowCount(2, 10000) - .then ({value: windowHandles}) -> - - @window(windowHandles[0]) - .waitForExist("atom-workspace") - .treeViewRootDirectories() - .then ({value}) -> - expect(value).toHaveLength(1) - treeViewDirs.push(value[0]) - - .window(windowHandles[1]) - .waitForExist("atom-workspace") - .treeViewRootDirectories() - .then ({value}) -> - expect(value).toHaveLength(1) - treeViewDirs.push(value[0]) - .then -> - expect(treeViewDirs.sort()).toEqual([tempDirPath, otherTempDirPath].sort()) - describe "when there is an existing window with no project path", -> describe "opening a directory", -> it "opens the directory in the existing window", -> diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee index 7642de7d7..1e099d897 100644 --- a/spec/jasmine-helper.coffee +++ b/spec/jasmine-helper.coffee @@ -27,8 +27,15 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> fs.closeSync(logStream) if logStream? if process.env.JANKY_SHA1 grim = require 'grim' - grim.logDeprecations() if grim.getDeprecationsLength() > 0 - atom.exit(runner.results().failedCount > 0 ? 1 : 0) + + if grim.getDeprecationsLength() > 0 + grim.logDeprecations() + return atom.exit(1) + + if runner.results().failedCount > 0 + atom.exit(1) + else + atom.exit(0) else AtomReporter = require './atom-reporter' reporter = new AtomReporter() diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 07b875a91..47fa30bdf 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -124,6 +124,11 @@ describe "LanguageMode", -> // lines var sort = function(items) {}; // comment line after fn + + var nosort = function(items) { + return item; + } + }; ''' @@ -144,6 +149,9 @@ describe "LanguageMode", -> range = languageMode.rowRangeForParagraphAtBufferRow(15) expect(range).toEqual [[15,0], [15,26]] + range = languageMode.rowRangeForParagraphAtBufferRow(18) + expect(range).toEqual [[17,0], [19,3]] + describe "coffeescript", -> beforeEach -> waitsForPromise -> diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 6efb17a55..db2a76135 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -330,32 +330,32 @@ describe "PackageManager", -> element2 = $$ -> @div class: 'test-2' element3 = $$ -> @div class: 'test-3' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3[0])).toHaveLength 0 waitsForPromise -> atom.packages.activatePackage("package-with-keymaps") runs -> - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe "test-1" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element2[0])[0].command).toBe "test-2" - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element3[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1[0])[0].command).toBe "test-1" + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2[0])[0].command).toBe "test-2" + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3[0])).toHaveLength 0 describe "when the metadata contains a 'keymaps' manifest", -> it "loads only the keymaps specified by the manifest, in the specified order", -> element1 = $$ -> @div class: 'test-1' element3 = $$ -> @div class: 'test-3' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1[0])).toHaveLength 0 waitsForPromise -> atom.packages.activatePackage("package-with-keymaps-manifest") runs -> - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target:element1[0])[0].command).toBe 'keymap-1' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-n', target:element1[0])[0].command).toBe 'keymap-2' - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-y', target:element3[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1[0])[0].command).toBe 'keymap-1' + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-n', target: element1[0])[0].command).toBe 'keymap-2' + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-y', target: element3[0])).toHaveLength 0 describe "when the keymap file is empty", -> it "does not throw an error on activation", -> @@ -522,6 +522,7 @@ describe "PackageManager", -> atom.packages.activatePackage("package-with-provided-services") runs -> + expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') @@ -546,6 +547,21 @@ describe "PackageManager", -> expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() + it "ignores provided and consumed services that do not exist", -> + addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + waitsForPromise -> + atom.packages.activatePackage("package-with-missing-consumed-services") + + waitsForPromise -> + atom.packages.activatePackage("package-with-missing-provided-services") + + runs -> + expect(atom.packages.isPackageActive("package-with-missing-consumed-services")).toBe true + expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true + expect(addErrorHandler.callCount).toBe 0 + describe "::deactivatePackage(id)", -> afterEach -> atom.packages.unloadPackages() @@ -629,8 +645,8 @@ describe "PackageManager", -> runs -> atom.packages.deactivatePackage('package-with-keymaps') - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-1')[0])).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes:'ctrl-z', target: ($$ -> @div class: 'test-2')[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: ($$ -> @div class: 'test-1')[0])).toHaveLength 0 + expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: ($$ -> @div class: 'test-2')[0])).toHaveLength 0 it "removes the package's stylesheets", -> waitsForPromise -> diff --git a/spec/pane-container-view-spec.coffee b/spec/pane-container-view-spec.coffee index 6c088c343..1b9cf9813 100644 --- a/spec/pane-container-view-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -15,7 +15,7 @@ describe "PaneContainerView", -> @deserialize: ({name}) -> new TestView(name) @content: -> @div tabindex: -1 initialize: (@name) -> @text(@name) - serialize: -> { deserializer: 'TestView', @name } + serialize: -> {deserializer: 'TestView', @name} getURI: -> path.join(temp.dir, @name) save: -> @saved = true isEqual: (other) -> @name is other?.name diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 763cc9ab4..2df5aa68c 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -14,9 +14,9 @@ describe "PaneView", -> @content: ({id, text}) -> @div class: 'test-view', id: id, tabindex: -1, text initialize: ({@id, @text}) -> @emitter = new Emitter - serialize: -> { deserializer: 'TestView', @id, @text } + serialize: -> {deserializer: 'TestView', @id, @text} getURI: -> @id - isEqual: (other) -> other? and @id == other.id and @text == other.text + isEqual: (other) -> other? and @id is other.id and @text is other.text changeTitle: -> @emitter.emit 'did-change-title', 'title' onDidChangeTitle: (callback) -> @@ -222,7 +222,7 @@ describe "PaneView", -> fs.removeSync(filePath) waitsFor -> - pane.items.length == 4 + pane.items.length is 4 describe "when a pane is destroyed", -> [pane2, pane2Model] = [] diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 02ea81ff4..d89802243 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -221,7 +221,7 @@ describe "Project", -> beforeEach -> absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') - atom.project.on 'buffer-created', newBufferHandler + atom.project.onDidAddBuffer(newBufferHandler) describe "when given an absolute path that isn't currently open", -> it "returns a new edit session for the given path and emits 'buffer-created'", -> @@ -486,6 +486,11 @@ describe "Project", -> randomPath = path.join("some", "random", "path") expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath] + describe "when the given path is a URL", -> + it "returns null for the root path, and the given path unchanged", -> + url = "http://the-path" + expect(atom.project.relativizePath(url)).toEqual [null, url] + describe ".contains(path)", -> it "returns whether or not the given path is in one of the root directories", -> rootPath = atom.project.getPaths()[0] @@ -497,8 +502,12 @@ describe "Project", -> describe ".eachBuffer(callback)", -> beforeEach -> + jasmine.snapshotDeprecations() atom.project.bufferForPathSync('a') + afterEach -> + jasmine.restoreDeprecationsSnapshot() + it "invokes the callback for existing buffer", -> count = 0 count = 0 diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee index bb5028d9a..d235ebc25 100644 --- a/spec/random-editor-spec.coffee +++ b/spec/random-editor-spec.coffee @@ -4,7 +4,7 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' describe "TextEditor", -> - [editor, tokenizedBuffer, buffer, steps, previousSteps] = [] + [editor, tokenizedBuffer, buffer, steps] = [] softWrapColumn = 80 @@ -13,8 +13,6 @@ describe "TextEditor", -> atom.config.set('editor.preferredLineLength', softWrapColumn) it "properly renders soft-wrapped lines when randomly mutated", -> - previousSteps = JSON.parse(localStorage.steps ? '[]') - times 10, (i) -> buffer = new TextBuffer editor = new TextEditor({buffer}) @@ -47,6 +45,9 @@ describe "TextEditor", -> {bufferRows, screenLines} = getReferenceScreenLines() for bufferRow, screenRow in bufferRows console.log screenRow, bufferRow, screenLines[screenRow].text + console.log "==== steps to reproduce this failure: ===" + for step in steps + console.log 'editor.' + step[0] + '('+ step[1..].map((a) -> JSON.stringify(a)).join(', ') + ')' randomlyMutateEditor = -> if Math.random() < .2 @@ -79,34 +80,11 @@ describe "TextEditor", -> text getReferenceScreenLines = -> - if editor.isSoftWrapped() - screenLines = [] - bufferRows = [] - for bufferRow in [0..tokenizedBuffer.getLastRow()] - for screenLine in softWrapLine(tokenizedBuffer.tokenizedLineForRow(bufferRow)) - screenLines.push(screenLine) - bufferRows.push(bufferRow) - else - screenLines = tokenizedBuffer.tokenizedLines.slice() - bufferRows = [0..tokenizedBuffer.getLastRow()] + referenceEditor = new TextEditor({}) + referenceEditor.setEditorWidthInChars(80) + referenceEditor.setText(editor.getText()) + referenceEditor.setSoftWrapped(editor.isSoftWrapped()) + screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow()) + bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow()) + {screenLines, bufferRows} - - softWrapLine = (tokenizedLine) -> - wrappedLines = [] - while tokenizedLine.text.length > softWrapColumn and wrapScreenColumn = findWrapColumn(tokenizedLine.text) - [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) - wrappedLines.push(wrappedLine) - wrappedLines.push(tokenizedLine) - wrappedLines - - findWrapColumn = (line) -> - if /\s/.test(line[softWrapColumn]) - # search forward for the start of a word past the boundary - for column in [softWrapColumn..line.length] - return column if /\S/.test(line[column]) - return line.length - else - # search backward for the start of the word on the boundary - for column in [softWrapColumn..0] - return column + 1 if /\s/.test(line[column]) - return softWrapColumn diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index b96eb2ce9..e683d7d6e 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -24,7 +24,7 @@ TextEditorElement = require '../src/text-editor-element' TokenizedBuffer = require '../src/tokenized-buffer' TextEditorComponent = require '../src/text-editor-component' pathwatcher = require 'pathwatcher' -clipboard = require 'clipboard' +clipboard = require '../src/safe-clipboard' atom.themes.loadBaseStylesheets() atom.themes.requireStylesheet '../static/jasmine' @@ -69,10 +69,9 @@ if specDirectory specPackageName = JSON.parse(fs.readFileSync(path.join(specPackagePath, 'package.json')))?.name specProjectPath = path.join(specDirectory, 'fixtures') -isCoreSpec = specDirectory == fs.realpathSync(__dirname) +isCoreSpec = specDirectory is fs.realpathSync(__dirname) beforeEach -> - Grim.clearDeprecations() if isCoreSpec $.fx.off = true documentTitle = null projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures') @@ -234,7 +233,7 @@ addCustomMatchers = (spec) -> else notText = if @isNot then " not" else "" this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}" - @actual.length == expected + @actual.length is expected toExistOnDisk: (expected) -> notText = this.isNot and " not" or "" @@ -297,7 +296,7 @@ window.mousemoveEvent = (properties={}) -> window.waitsForPromise = (args...) -> if args.length > 1 - { shouldReject, timeout } = args[0] + {shouldReject, timeout} = args[0] else shouldReject = false fn = _.last(args) @@ -328,7 +327,7 @@ window.fakeSetTimeout = (callback, ms) -> id window.fakeClearTimeout = (idToClear) -> - window.timeouts = window.timeouts.filter ([id]) -> id != idToClear + window.timeouts = window.timeouts.filter ([id]) -> id isnt idToClear window.fakeSetInterval = (callback, ms) -> id = ++window.intervalCount @@ -358,7 +357,7 @@ window.pagePixelPositionForPoint = (editorView, point) -> point = Point.fromObject point top = editorView.renderedLines.offset().top + point.row * editorView.lineHeight left = editorView.renderedLines.offset().left + point.column * editorView.charWidth - editorView.renderedLines.scrollLeft() - { top, left } + {top, left} window.tokensText = (tokens) -> _.pluck(tokens, 'value').join('') @@ -369,7 +368,7 @@ window.setEditorWidthInChars = (editorView, widthInChars, charWidth=editorView.c window.setEditorHeightInLines = (editorView, heightInLines, lineHeight=editorView.lineHeight) -> editorView.height(editorView.getEditor().getLineHeightInPixels() * heightInLines) - editorView.component?.measureHeightAndWidth() + editorView.component?.measureDimensions() $.fn.resultOfTrigger = (type) -> event = $.Event(type) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 548c88105..769ce424f 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -50,16 +50,27 @@ describe "TextEditorComponent", -> verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() afterEach -> contentNode.style.width = '' + describe "async updates", -> + it "handles corrupted state gracefully", -> + # trigger state updates, e.g. presenter.updateLinesState + editor.insertNewline() + + # simulate state corruption + component.presenter.startRow = -1 + component.presenter.endRow = 9999 + + expect(nextAnimationFrame).not.toThrow() + describe "line rendering", -> it "renders the currently-visible lines plus the overdraw margin", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() linesNode = componentNode.querySelector('.lines') @@ -102,7 +113,7 @@ describe "TextEditorComponent", -> it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) @@ -152,7 +163,7 @@ describe "TextEditorComponent", -> it "renders the .lines div at the full height of the editor if there aren't enough lines to scroll vertically", -> editor.setText('') wrapperNode.style.height = '300px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() linesNode = componentNode.querySelector('.lines') @@ -164,7 +175,7 @@ describe "TextEditorComponent", -> lineNodes = componentNode.querySelectorAll('.line') componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(editor.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth @@ -176,7 +187,7 @@ describe "TextEditorComponent", -> expect(lineNode.style.width).toBe editor.getScrollWidth() + 'px' componentNode.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() scrollViewWidth = scrollViewNode.offsetWidth @@ -328,7 +339,7 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) nextAnimationFrame() componentNode.style.width = 16 * charWidth + editor.getVerticalScrollbarWidth() + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() it "doesn't show end of line invisibles at the end of wrapped lines", -> @@ -469,7 +480,7 @@ describe "TextEditorComponent", -> describe "gutter rendering", -> it "renders the currently-visible line numbers", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number @@ -513,7 +524,7 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode @@ -551,7 +562,7 @@ describe "TextEditorComponent", -> it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight @@ -642,7 +653,7 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) nextAnimationFrame() componentNode.style.width = 16 * charWidth + editor.getVerticalScrollbarWidth() + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() it "doesn't add the foldable class for soft-wrapped lines", -> @@ -682,11 +693,11 @@ describe "TextEditorComponent", -> describe "cursor rendering", -> it "renders the currently visible cursors", -> cursor1 = editor.getLastCursor() - cursor1.setScreenPosition([0, 5]) + cursor1.setScreenPosition([0, 5], autoscroll: false) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() cursorNodes = componentNode.querySelectorAll('.cursor') @@ -695,8 +706,8 @@ describe "TextEditorComponent", -> expect(cursorNodes[0].offsetWidth).toBe charWidth expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{5 * charWidth}px, #{0 * lineHeightInPixels}px)" - cursor2 = editor.addCursorAtScreenPosition([8, 11]) - cursor3 = editor.addCursorAtScreenPosition([4, 10]) + cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) + cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) nextAnimationFrame() cursorNodes = componentNode.querySelectorAll('.cursor') @@ -717,7 +728,7 @@ describe "TextEditorComponent", -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)" expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" - wrapperView.on 'cursor:moved', cursorMovedListener = jasmine.createSpy('cursorMovedListener') + editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) nextAnimationFrame() expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)" @@ -981,7 +992,7 @@ describe "TextEditorComponent", -> # Shrink editor vertically wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() # Add decorations that are out of range @@ -1005,7 +1016,7 @@ describe "TextEditorComponent", -> editor.setText("a line that wraps, ok") editor.setSoftWrapped(true) componentNode.style.width = 16 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() marker.destroy() @@ -1121,7 +1132,7 @@ describe "TextEditorComponent", -> it "does not render highlights for off-screen lines until they come on-screen", -> wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') @@ -1268,10 +1279,12 @@ describe "TextEditorComponent", -> expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() describe "overlay decoration rendering", -> - [item] = [] + [item, gutterWidth] = [] beforeEach -> item = document.createElement('div') item.classList.add 'overlay-test' + item.style.background = 'red' + gutterWidth = componentNode.querySelector('.gutter').offsetWidth describe "when the marker is empty", -> it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", -> @@ -1288,71 +1301,29 @@ describe "TextEditorComponent", -> overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') expect(overlay).toBe null - it "renders in the correct position on initial display and when the marker moves", -> - editor.setCursorBufferPosition([2, 5]) - - marker = editor.getLastCursor().getMarker() - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([2, 5]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.moveRight() - editor.moveRight() - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([2, 7]) - - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - describe "when the marker is not empty", -> it "renders at the head of the marker by default", -> marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) nextAnimationFrame() + nextAnimationFrame() position = wrapperNode.pixelPositionForBufferPosition([2, 10]) overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - it "renders at the head of the marker when the marker is reversed", -> - marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never', reversed: true) - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([2, 5]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - it "renders at the tail of the marker when the 'position' option is 'tail'", -> - marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([2, 5]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' + expect(overlay.style.left).toBe position.left + gutterWidth + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' describe "positioning the overlay when near the edge of the editor", -> - [itemWidth, itemHeight] = [] + [itemWidth, itemHeight, windowWidth, windowHeight] = [] beforeEach -> + atom.storeWindowDimensions() + itemWidth = 4 * editor.getDefaultCharWidth() itemHeight = 4 * editor.getLineHeightInPixels() - gutterWidth = componentNode.querySelector('.gutter').offsetWidth windowWidth = gutterWidth + 30 * editor.getDefaultCharWidth() - windowHeight = 9 * editor.getLineHeightInPixels() + windowHeight = 10 * editor.getLineHeightInPixels() item.style.width = itemWidth + 'px' item.style.height = itemHeight + 'px' @@ -1360,139 +1331,40 @@ describe "TextEditorComponent", -> wrapperNode.style.width = windowWidth + 'px' wrapperNode.style.height = windowHeight + 'px' - component.measureHeightAndWidth() + atom.setWindowDimensions({width: windowWidth, height: windowHeight}) + + component.measureDimensions() + component.measureWindowSize() nextAnimationFrame() - it "flips horizontally when near the right edge", -> + afterEach -> + atom.restoreWindowDimensions() + + # This spec should actually run on Linux as well, see TextEditorComponent#measureWindowSize for further information. + it "slides horizontally left when near the right edge on #win32 and #darwin", -> marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) nextAnimationFrame() + nextAnimationFrame() position = wrapperNode.pixelPositionForBufferPosition([0, 26]) overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' + expect(overlay.style.left).toBe position.left + gutterWidth + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' editor.insertText('a') nextAnimationFrame() - position = wrapperNode.pixelPositionForBufferPosition([0, 27]) - - expect(overlay.style.left).toBe position.left - itemWidth + 'px' + expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - it "flips vertically when near the bottom edge", -> - marker = editor.displayBuffer.markBufferRange([[4, 0], [4, 0]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + editor.insertText('b') nextAnimationFrame() - position = wrapperNode.pixelPositionForBufferPosition([4, 0]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' + expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - editor.insertNewline() - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([5, 0]) - - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top - itemHeight + 'px' - - describe "when the editor is very small", -> - beforeEach -> - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - windowWidth = gutterWidth + 6 * editor.getDefaultCharWidth() - windowHeight = 6 * editor.getLineHeightInPixels() - - wrapperNode.style.width = windowWidth + 'px' - wrapperNode.style.height = windowHeight + 'px' - - component.measureHeightAndWidth() - nextAnimationFrame() - - it "does not flip horizontally and force the overlay to have a negative left", -> - marker = editor.displayBuffer.markBufferRange([[0, 2], [0, 2]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([0, 2]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertText('a') - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([0, 3]) - - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - it "does not flip vertically and force the overlay to have a negative top", -> - marker = editor.displayBuffer.markBufferRange([[1, 0], [1, 0]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([1, 0]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertNewline() - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([2, 0]) - - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - - describe "when editor scroll position is not 0", -> - it "flips horizontally when near the right edge", -> - editor.setScrollLeft(2 * editor.getDefaultCharWidth()) - marker = editor.displayBuffer.markBufferRange([[0, 28], [0, 28]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([0, 28]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertText('a') - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([0, 29]) - - expect(overlay.style.left).toBe position.left - itemWidth + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - it "flips vertically when near the bottom edge", -> - editor.setScrollTop(2 * editor.getLineHeightInPixels()) - marker = editor.displayBuffer.markBufferRange([[6, 0], [6, 0]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([6, 0]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertNewline() - nextAnimationFrame() - - position = wrapperNode.pixelPositionForBufferPosition([7, 0]) - - expect(overlay.style.left).toBe position.left + 'px' - expect(overlay.style.top).toBe position.top - itemHeight + 'px' - describe "hidden input field", -> it "renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused", -> editor.setVerticalScrollMargin(0) @@ -1501,7 +1373,7 @@ describe "TextEditorComponent", -> inputNode = componentNode.querySelector('.hidden-input') wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(editor.getCursorScreenPosition()).toEqual [0, 0] @@ -1513,7 +1385,7 @@ describe "TextEditorComponent", -> expect(inputNode.offsetLeft).toBe 0 # In bounds, not focused - editor.setCursorBufferPosition([5, 4]) + editor.setCursorBufferPosition([5, 4], autoscroll: false) nextAnimationFrame() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 @@ -1531,7 +1403,7 @@ describe "TextEditorComponent", -> expect(inputNode.offsetLeft).toBe 0 # Out of bounds, not focused - editor.setCursorBufferPosition([1, 2]) + editor.setCursorBufferPosition([1, 2], autoscroll: false) nextAnimationFrame() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 @@ -1548,6 +1420,22 @@ describe "TextEditorComponent", -> beforeEach -> linesNode = componentNode.querySelector('.lines') + describe "when the mouse is single-clicked above the first line", -> + it "moves the cursor to the start of file buffer position", -> + editor.setText('foo') + editor.setCursorBufferPosition([0, 3]) + height = 4.5 * lineHeightInPixels + wrapperNode.style.height = height + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + nextAnimationFrame() + + coordinates = clientCoordinatesForScreenPosition([0, 2]) + coordinates.clientY = -1 + linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) + nextAnimationFrame() + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + describe "when the mouse is single-clicked below the last line", -> it "moves the cursor to the end of file buffer position", -> editor.setText('foo') @@ -1555,7 +1443,7 @@ describe "TextEditorComponent", -> height = 4.5 * lineHeightInPixels wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() coordinates = clientCoordinatesForScreenPosition([0, 2]) @@ -1569,7 +1457,7 @@ describe "TextEditorComponent", -> it "moves the cursor to the nearest screen position", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) nextAnimationFrame() @@ -1852,7 +1740,7 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) nextAnimationFrame() componentNode.style.width = 21 * charWidth + editor.getVerticalScrollbarWidth() + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() describe "when the gutter is clicked", -> @@ -1926,14 +1814,14 @@ describe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) nextAnimationFrame() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[10, 0], [20, 0]]] + expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[10, 0], [19, 0]]] it "merges overlapping selections", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(9), metaKey: true)) nextAnimationFrame() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(9), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [20, 0]]] + expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]] describe "when the gutter is shift-clicked and dragged", -> describe "when the shift-click is below the existing selection's tail", -> @@ -2018,7 +1906,7 @@ describe "TextEditorComponent", -> describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -2029,7 +1917,7 @@ describe "TextEditorComponent", -> it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> componentNode.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() linesNode = componentNode.querySelector('.lines') @@ -2043,7 +1931,7 @@ describe "TextEditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> componentNode.style.width = 30 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(editor.getScrollLeft()).toBe 0 @@ -2056,7 +1944,7 @@ describe "TextEditorComponent", -> it "does not obscure the last line with the horizontal scrollbar", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() editor.setScrollBottom(editor.getScrollHeight()) nextAnimationFrame() lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) @@ -2066,7 +1954,7 @@ describe "TextEditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears wrapperNode.style.width = 100 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = componentNode.getBoundingClientRect().bottom @@ -2075,7 +1963,7 @@ describe "TextEditorComponent", -> it "does not obscure the last character of the longest line with the vertical scrollbar", -> wrapperNode.style.height = 7 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() editor.setScrollLeft(Infinity) nextAnimationFrame() @@ -2089,21 +1977,21 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' componentNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.display).toBe 'none' @@ -2112,7 +2000,7 @@ describe "TextEditorComponent", -> it "makes the dummy scrollbar divs only as tall/wide as the actual scrollbars", -> wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() atom.styles.addStyleSheet """ @@ -2141,21 +2029,21 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.bottom).toBe '0px' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe 'none' componentNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe '' wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe '0px' @@ -2164,7 +2052,7 @@ describe "TextEditorComponent", -> it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(horizontalScrollbarNode.scrollWidth).toBe editor.getScrollWidth() @@ -2178,7 +2066,7 @@ describe "TextEditorComponent", -> beforeEach -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> @@ -2222,7 +2110,7 @@ describe "TextEditorComponent", -> it "keeps the line on the DOM if it is scrolled off-screen", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) @@ -2235,7 +2123,7 @@ describe "TextEditorComponent", -> it "does not set the mouseWheelScreenRow if scrolling horizontally", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) @@ -2278,7 +2166,7 @@ describe "TextEditorComponent", -> it "keeps the line number on the DOM if it is scrolled off-screen", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() lineNumberNode = componentNode.querySelectorAll('.line-number')[1] wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) @@ -2293,7 +2181,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() # try to scroll past the top, which is impossible @@ -2363,6 +2251,7 @@ describe "TextEditorComponent", -> expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {' it "does not handle input events when input is disabled", -> + nextAnimationFrame = noAnimationFrame # This spec is flaky on the build machine, so this. component.setInputEnabled(false) componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) expect(nextAnimationFrame).toBe noAnimationFrame @@ -2556,7 +2445,7 @@ describe "TextEditorComponent", -> initialLineHeightInPixels = editor.getLineHeightInPixels() initialCharWidth = editor.getDefaultCharWidth() - component.setFontFamily('sans-serif') + component.setFontFamily('serif') expect(editor.getDefaultCharWidth()).toBe initialCharWidth wrapperView.show() @@ -2565,7 +2454,7 @@ describe "TextEditorComponent", -> it "does not re-measure character widths until the editor is shown again", -> wrapperView.hide() - component.setFontFamily('sans-serif') + component.setFontFamily('serif') wrapperView.show() editor.setCursorBufferPosition([0, Infinity]) @@ -2689,7 +2578,7 @@ describe "TextEditorComponent", -> describe "when the wrapper view has an explicit height", -> it "does not assign a height on the component node", -> wrapperNode.style.height = '200px' - component.measureHeightAndWidth() + component.measureDimensions() nextAnimationFrame() expect(componentNode.style.height).toBe '' @@ -2927,7 +2816,7 @@ describe "TextEditorComponent", -> clipboardWrittenTo = false spyOn(require('ipc'), 'send').andCallFake (eventName, selectedText) -> if eventName is 'write-text-to-selection-clipboard' - require('clipboard').writeText(selectedText, 'selection') + require('../src/safe-clipboard').writeText(selectedText, 'selection') clipboardWrittenTo = true atom.clipboard.write('') diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 4ad319cc1..577c87401 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -29,6 +29,9 @@ describe "TextEditorPresenter", -> model: editor explicitHeight: 130 contentFrameWidth: 500 + windowWidth: 500 + windowHeight: 130 + boundingClientRect: {left: 0, top: 0, width: 500, height: 130} lineHeight: 10 baseCharacterWidth: 10 horizontalScrollbarHeight: 10 @@ -351,11 +354,11 @@ describe "TextEditorPresenter", -> expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} + expectValues presenter.getState().hiddenInput, {top: 11 * 10 - editor.getScrollTop(), left: 43 * 10 - editor.getScrollLeft()} newCursor = null expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues presenter.getState().hiddenInput, {top: (6 * 10) - 40, left: (10 * 10) - 70} + expectValues presenter.getState().hiddenInput, {top: (6 * 10) - editor.getScrollTop(), left: (10 * 10) - editor.getScrollLeft()} expectStateUpdate presenter, -> newCursor.destroy() expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} @@ -1417,33 +1420,33 @@ describe "TextEditorPresenter", -> expect(stateForSelection(presenter, 1)).toBeUndefined() # moving into view - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]]) + expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) expectValues stateForSelection(presenter, 1), { regions: [{top: 2 * 10, left: 4 * 10, width: 2 * 10, height: 10}] } # becoming empty - expectStateUpdate presenter, -> editor.getSelections()[1].clear() + expectStateUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false) expect(stateForSelection(presenter, 1)).toBeUndefined() # becoming non-empty - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]]) + expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) expectValues stateForSelection(presenter, 1), { regions: [{top: 2 * 10, left: 4 * 10, width: 2 * 10, height: 10}] } # moving out of view - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]]) + expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false) expect(stateForSelection(presenter, 1)).toBeUndefined() # adding - expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]]) + expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false) expectValues stateForSelection(presenter, 2), { regions: [{top: 1 * 10, left: 4 * 10, width: 2 * 10, height: 10}] } # moving added selection - expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]]) + expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false) expectValues stateForSelection(presenter, 2), { regions: [{top: 1 * 10, left: 4 * 10, width: 4 * 10, height: 10}] } @@ -1485,11 +1488,11 @@ describe "TextEditorPresenter", -> } describe ".overlays", -> + [item] = [] stateForOverlay = (presenter, decoration) -> presenter.getState().content.overlays[decoration.id] it "contains state for overlay decorations both initially and when their markers move", -> - item = {} marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) @@ -1497,14 +1500,14 @@ describe "TextEditorPresenter", -> # Initial state expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } # Change range expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]]) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 4 * 10, left: 6 * 10} + pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} } # Valid -> invalid @@ -1515,14 +1518,14 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.undo() expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 4 * 10, left: 6 * 10} + pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} } # Reverse direction expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } # Destroy @@ -1533,69 +1536,237 @@ describe "TextEditorPresenter", -> decoration2 = editor.decorateMarker(marker, {type: 'overlay', item}) expectValues stateForOverlay(presenter, decoration2), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } it "updates when ::baseCharacterWidth changes", -> - item = {} + scrollTop = 20 marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) + presenter = buildPresenter({explicitHeight: 30, scrollTop}) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 5} + pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 5} } it "updates when ::lineHeight changes", -> - item = {} + scrollTop = 20 marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) + presenter = buildPresenter({explicitHeight: 30, scrollTop}) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } expectStateUpdate presenter, -> presenter.setLineHeight(5) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 5, left: 13 * 10} + pixelPosition: {top: 3 * 5 - scrollTop, left: 13 * 10} } it "honors the 'position' option on overlay decorations", -> - item = {} + scrollTop = 20 marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) + presenter = buildPresenter({explicitHeight: 30, scrollTop}) expectValues stateForOverlay(presenter, decoration), { item: item - pixelPosition: {top: 2 * 10, left: 13 * 10} + pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } it "is empty until all of the required measurements are assigned", -> - item = {} marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - presenter = buildPresenter(baseCharacterWidth: null, lineHeight: null) + presenter = buildPresenter(baseCharacterWidth: null, lineHeight: null, windowWidth: null, windowHeight: null, boundingClientRect: null) expect(presenter.getState().content.overlays).toEqual({}) presenter.setBaseCharacterWidth(10) expect(presenter.getState().content.overlays).toEqual({}) presenter.setLineHeight(10) + expect(presenter.getState().content.overlays).toEqual({}) + + presenter.setWindowSize(500, 100) + expect(presenter.getState().content.overlays).toEqual({}) + + presenter.setBoundingClientRect({top: 0, left: 0, height: 100, width: 500}) expect(presenter.getState().content.overlays).not.toEqual({}) + describe "when the overlay has been measured", -> + [gutterWidth, windowWidth, windowHeight, itemWidth, itemHeight, contentMargin, boundingClientRect, contentFrameWidth] = [] + beforeEach -> + item = {} + gutterWidth = 5 * 10 # 5 chars wide + contentFrameWidth = 30 * 10 + windowWidth = gutterWidth + contentFrameWidth + windowHeight = 9 * 10 + + itemWidth = 4 * 10 + itemHeight = 4 * 10 + contentMargin = 0 + + boundingClientRect = + top: 0 + left: 0, + width: windowWidth + height: windowHeight + + it "slides horizontally left when near the right edge", -> + scrollLeft = 20 + marker = editor.markBufferPosition([0, 26], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + + presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: 26 * 10 + gutterWidth - scrollLeft} + } + + expectStateUpdate presenter, -> editor.insertText('a') + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} + } + + expectStateUpdate presenter, -> editor.insertText('b') + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} + } + + it "flips vertically when near the bottom edge", -> + scrollTop = 10 + marker = editor.markBufferPosition([5, 0], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + + presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 6 * 10 - scrollTop, left: gutterWidth} + } + + expectStateUpdate presenter, -> + editor.insertNewline() + editor.setScrollTop(scrollTop) # I'm fighting the editor + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 6 * 10 - scrollTop - itemHeight, left: gutterWidth} + } + + describe "when the overlay item has a margin", -> + beforeEach -> + itemWidth = 12 * 10 + contentMargin = -(gutterWidth + 2 * 10) + + it "slides horizontally right when near the left edge with margin", -> + editor.setCursorBufferPosition([0, 3]) + cursor = editor.getLastCursor() + marker = cursor.marker + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: 3 * 10 + gutterWidth} + } + + expectStateUpdate presenter, -> cursor.moveLeft() + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: -contentMargin} + } + + expectStateUpdate presenter, -> cursor.moveLeft() + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: -contentMargin} + } + + describe "when the editor is very small", -> + beforeEach -> + windowWidth = gutterWidth + 6 * 10 + windowHeight = 6 * 10 + contentFrameWidth = windowWidth - gutterWidth + boundingClientRect.width = windowWidth + boundingClientRect.height = windowHeight + + it "does not flip vertically and force the overlay to have a negative top", -> + marker = editor.markBufferPosition([1, 0], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 2 * 10, left: 0 * 10 + gutterWidth} + } + + expectStateUpdate presenter, -> editor.insertNewline() + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 3 * 10, left: gutterWidth} + } + + it "does not adjust horizontally and force the overlay to have a negative left", -> + itemWidth = 6 * 10 + + marker = editor.markBufferPosition([0, 0], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item}) + + presenter = buildPresenter({windowWidth, windowHeight, contentFrameWidth, boundingClientRect}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 10, left: gutterWidth} + } + + windowWidth = gutterWidth + 5 * 10 + expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 10, left: windowWidth - itemWidth} + } + + windowWidth = gutterWidth + 1 * 10 + expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 10, left: 0} + } + + windowWidth = gutterWidth + expectStateUpdate presenter, -> presenter.setWindowSize(windowWidth, windowHeight) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 10, left: 0} + } + + describe ".gutter", -> describe ".scrollHeight", -> it "is initialized based on ::lineHeight, the number of lines, and ::explicitHeight", -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index c03990bf2..5e0fef9e6 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1,4 +1,4 @@ -clipboard = require 'clipboard' +clipboard = require '../src/safe-clipboard' TextEditor = require '../src/text-editor' describe "TextEditor", -> @@ -547,7 +547,7 @@ describe "TextEditor", -> lastLine = buffer.lineForRow(lastLineIndex) expect(lastLine.length).toBeGreaterThan(0) - lastPosition = { row: lastLineIndex, column: lastLine.length } + lastPosition = {row: lastLineIndex, column: lastLine.length} editor.setCursorScreenPosition(lastPosition) editor.moveRight() @@ -831,35 +831,29 @@ describe "TextEditor", -> describe ".moveToBeginningOfNextParagraph()", -> it "moves the cursor before the first line of the next paragraph", -> - editor.setCursorBufferPosition [0,6] - cursor = editor.getLastCursor() + editor.setCursorBufferPosition [0, 6] + editor.foldBufferRow(4) editor.moveToBeginningOfNextParagraph() - - expect(cursor.getBufferPosition()).toEqual { row : 10, column : 0 } + expect(editor.getCursorBufferPosition()).toEqual [10, 0] editor.setText("") - editor.setCursorBufferPosition [0,0] - cursor = editor.getLastCursor() + editor.setCursorBufferPosition [0, 0] editor.moveToBeginningOfNextParagraph() - - expect(cursor.getBufferPosition()).toEqual [0, 0] + expect(editor.getCursorBufferPosition()).toEqual [0, 0] describe ".moveToBeginningOfPreviousParagraph()", -> it "moves the cursor before the first line of the pevious paragraph", -> - editor.setCursorBufferPosition [10,0] - cursor = editor.getLastCursor() + editor.setCursorBufferPosition [10, 0] + editor.foldBufferRow(4) editor.moveToBeginningOfPreviousParagraph() - - expect(cursor.getBufferPosition()).toEqual { row : 0, column : 0 } + expect(editor.getCursorBufferPosition()).toEqual [0, 0] editor.setText("") - editor.setCursorBufferPosition [0,0] - cursor = editor.getLastCursor() + editor.setCursorBufferPosition [0, 0] editor.moveToBeginningOfPreviousParagraph() - - expect(cursor.getBufferPosition()).toEqual [0, 0] + expect(editor.getCursorBufferPosition()).toEqual [0, 0] describe ".getCurrentParagraphBufferRange()", -> it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> @@ -921,7 +915,6 @@ describe "TextEditor", -> describe "autoscroll", -> beforeEach -> - editor.manageScrollPosition = true editor.setVerticalScrollMargin(2) editor.setHorizontalScrollMargin(2) editor.setLineHeightInPixels(10) @@ -971,9 +964,8 @@ describe "TextEditor", -> it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> editor.setScrollRight(editor.getScrollWidth()) - editor.setCursorScreenPosition([6, 62]) - expect(editor.getScrollRight()).toBe editor.getScrollWidth() + editor.setCursorScreenPosition([6, 62], autoscroll: false) editor.moveLeft() expect(editor.getScrollLeft()).toBe 59 * 10 @@ -994,6 +986,45 @@ describe "TextEditor", -> editor.undo() expect(editor.getScrollTop()).toBe 0 + it "doesn't scroll when the cursor moves into the visible area", -> + editor.setCursorBufferPosition([0, 0]) + editor.setScrollTop(40) + expect(editor.getVisibleRowRange()).toEqual([4, 9]) + editor.setCursorBufferPosition([6, 0]) + expect(editor.getScrollTop()).toBe 40 + + it "honors the autoscroll option on cursor and selection manipulation methods", -> + expect(editor.getScrollTop()).toBe 0 + editor.addCursorAtScreenPosition([11, 11], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.addCursorAtBufferPosition([11, 11], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.setCursorScreenPosition([11, 11], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.setCursorBufferPosition([11, 11], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + editor.clearSelections(autoscroll: false) + expect(editor.getScrollTop()).toBe 0 + + editor.addSelectionForScreenRange([[0, 0], [0, 4]]) + + editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) + expect(editor.getScrollTop()).toBeGreaterThan 0 + editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) + expect(editor.getScrollTop()).toBe 0 + editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) + expect(editor.getScrollTop()).toBeGreaterThan 0 + editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) + expect(editor.getScrollTop()).toBe 0 + describe '.logCursorScope()', -> beforeEach -> spyOn(atom.notifications, 'addInfo') @@ -1254,7 +1285,6 @@ describe "TextEditor", -> expect(editor.getSelectedBufferRange()).toEqual [[0,0], [2,0]] it "autoscrolls to the selection", -> - editor.manageScrollPosition = true editor.setLineHeightInPixels(10) editor.setDefaultCharWidth(10) editor.setHeight(50) @@ -1505,9 +1535,12 @@ describe "TextEditor", -> editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) expect(editor.getSelectedScreenRanges()).toEqual [[[6, 2], [6, 4]]] - it "merges intersecting selections and unfolds the fold", -> - editor.setSelectedScreenRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 2], [8, 5]]] + it "merges intersecting selections and unfolds the fold which contain them", -> + editor.foldBufferRow(0) + + # Use buffer ranges because only the first line is on screen + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] it "recyles existing selection instances", -> selection = editor.getLastSelection() @@ -1518,24 +1551,28 @@ describe "TextEditor", -> expect(selection1.getScreenRange()).toEqual [[2, 2], [3, 4]] describe ".setSelectedBufferRange(range)", -> - describe "when the 'autoscroll' option is true", -> - it "autoscrolls to the selection", -> - editor.manageScrollPosition = true - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHeight(50) - editor.setWidth(50) - editor.setHorizontalScrollbarHeight(0) + it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> + editor.setVerticalScrollMargin(2) + editor.setHorizontalScrollMargin(2) + editor.setLineHeightInPixels(10) + editor.setDefaultCharWidth(10) + editor.setHeight(70) + editor.setWidth(100) + editor.setHorizontalScrollbarHeight(0) - expect(editor.getScrollTop()).toBe 0 + expect(editor.getScrollTop()).toBe 0 - editor.setSelectedBufferRange([[5, 6], [6, 8]], autoscroll: true) - expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(editor.getScrollRight()).toBe 50 + editor.setSelectedBufferRange([[5, 6], [6, 8]]) + expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 - editor.setSelectedBufferRange([[6, 6], [6, 8]], autoscroll: true) - expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + expect(editor.getScrollTop()).toBe 0 + expect(editor.getScrollLeft()).toBe 0 + + editor.setSelectedBufferRange([[6, 6], [6, 8]]) + expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 describe ".selectMarker(marker)", -> describe "if the marker is valid", -> @@ -1557,16 +1594,15 @@ describe "TextEditor", -> expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 0]], [[3, 4], [5, 6]]] it "autoscrolls to the added selection if needed", -> - editor.manageScrollPosition = true - + editor.setVerticalScrollMargin(2) + editor.setHorizontalScrollMargin(2) editor.setLineHeightInPixels(10) editor.setDefaultCharWidth(10) - editor.setHeight(50) - editor.setWidth(50) - + editor.setHeight(80) + editor.setWidth(100) editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - expect(editor.getScrollTop()).toBe 75 - expect(editor.getScrollLeft()).toBe 160 + expect(editor.getScrollBottom()).toBe (9 * 10) + (2 * 10) + expect(editor.getScrollRight()).toBe (15 * 10) + (2 * 10) describe ".addSelectionBelow()", -> describe "when the selection is non-empty", -> @@ -1623,7 +1659,54 @@ describe "TextEditor", -> [[6, 22], [6, 28]] ] + it "can add selections to soft-wrapped line segments", -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + + editor.setSelectedScreenRange([[3, 10], [3, 15]]) + editor.addSelectionBelow() + expect(editor.getSelectedScreenRanges()).toEqual [ + [[3, 10], [3, 15]] + [[4, 10], [4, 15]] + ] + + it "takes atomic tokens into account", -> + waitsForPromise -> + atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o + + runs -> + editor.setSelectedBufferRange([[2, 1], [2, 3]]) + editor.addSelectionBelow() + + expect(editor.getSelectedBufferRanges()).toEqual [ + [[2, 1], [2, 3]] + [[3, 1], [3, 2]] + ] + describe "when the selection is empty", -> + describe "when lines are soft-wrapped", -> + beforeEach -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + + it "skips soft-wrap indentation tokens", -> + editor.setCursorScreenPosition([3, 0]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual [ + [[3, 0], [3, 0]] + [[4, 4], [4, 4]] + ] + + it "does not skip them if they're shorter than the current column", -> + editor.setCursorScreenPosition([3, 37]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual [ + [[3, 37], [3, 37]] + [[4, 26], [4, 26]] + ] + it "does not skip lines that are shorter than the current column", -> editor.setCursorBufferPosition([3, 36]) editor.addSelectionBelow() @@ -1687,7 +1770,54 @@ describe "TextEditor", -> [[3, 22], [3, 38]] ] + it "can add selections to soft-wrapped line segments", -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + + editor.setSelectedScreenRange([[4, 10], [4, 15]]) + editor.addSelectionAbove() + expect(editor.getSelectedScreenRanges()).toEqual [ + [[4, 10], [4, 15]] + [[3, 10], [3, 15]] + ] + + it "takes atomic tokens into account", -> + waitsForPromise -> + atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o + + runs -> + editor.setSelectedBufferRange([[3, 1], [3, 2]]) + editor.addSelectionAbove() + + expect(editor.getSelectedBufferRanges()).toEqual [ + [[3, 1], [3, 2]] + [[2, 1], [2, 3]] + ] + describe "when the selection is empty", -> + describe "when lines are soft-wrapped", -> + beforeEach -> + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + + it "skips soft-wrap indentation tokens", -> + editor.setCursorScreenPosition([5, 0]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual [ + [[5, 0], [5, 0]] + [[4, 4], [4, 4]] + ] + + it "does not skip them if they're shorter than the current column", -> + editor.setCursorScreenPosition([5, 29]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual [ + [[5, 29], [5, 29]] + [[4, 26], [4, 26]] + ] + it "does not skip lines that are shorter than the current column", -> editor.setCursorBufferPosition([6, 36]) editor.addSelectionAbove() @@ -1825,7 +1955,6 @@ describe "TextEditor", -> expect(cursor2.getBufferPosition()).toEqual [2, 7] it "autoscrolls to the last cursor", -> - editor.manageScrollPosition = true editor.setCursorScreenPosition([1, 2]) editor.addCursorAtScreenPosition([10, 4]) editor.setLineHeightInPixels(10) @@ -2595,6 +2724,20 @@ describe "TextEditor", -> """ + describe "when many selections get added in shuffle order", -> + it "cuts them in order", -> + editor.setSelectedBufferRanges([ + [[2,8], [2, 13]] + [[0,4], [0,13]], + [[1,6], [1, 10]], + ]) + editor.cutSelectedText() + expect(atom.clipboard.read()).toEqual """ + quicksort + sort + items + """ + describe ".cutToEndOfLine()", -> describe "when soft wrap is on", -> it "cuts up to the end of the line", -> @@ -2657,6 +2800,20 @@ describe "TextEditor", -> [[5, 8], [5, 8]] ]) + describe "when many selections get added in shuffle order", -> + it "copies them in order", -> + editor.setSelectedBufferRanges([ + [[2,8], [2, 13]] + [[0,4], [0,13]], + [[1,6], [1, 10]], + ]) + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual """ + quicksort + sort + items + """ + describe ".pasteText()", -> copyText = (text, {startColumn, textEditor}={}) -> startColumn ?= 0 @@ -2751,8 +2908,12 @@ describe "TextEditor", -> editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) editor.copySelectedText() - it "pastes each selection separately into the buffer", -> - editor.copySelectedText() + it "pastes each selection in order separately into the buffer", -> + editor.setSelectedBufferRanges([ + [[1, 6], [1, 10]] + [[0, 4], [0, 13]], + ]) + editor.moveRight() editor.insertText("_") editor.pasteText() @@ -3209,6 +3370,20 @@ describe "TextEditor", -> expect(buffer.lineForRow(0)).toBe(line2) expect(buffer.getLineCount()).toBe(count - 2) + it "deletes a line only once when multiple selections are on the same line", -> + line1 = buffer.lineForRow(1) + count = buffer.getLineCount() + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 4], [0, 5]] + ]) + expect(buffer.lineForRow(0)).not.toBe(line1) + + editor.deleteLine() + + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + it "only deletes first line if only newline is selected on second line", -> editor.setSelectedBufferRange([[0, 2], [1, 0]]) line1 = buffer.lineForRow(1) @@ -3959,7 +4134,7 @@ describe "TextEditor", -> editor.setLineHeightInPixels(10) editor.setDefaultCharWidth(10) editor.setHeight(60) - editor.setWidth(50) + editor.setWidth(130) editor.setHorizontalScrollbarHeight(0) expect(editor.getScrollTop()).toBe 0 expect(editor.getScrollLeft()).toBe 0 @@ -3975,8 +4150,6 @@ describe "TextEditor", -> describe ".pageUp/Down()", -> it "scrolls one screen height up or down and moves the cursor one page length", -> - editor.manageScrollPosition = true - editor.setLineHeightInPixels(10) editor.setHeight(50) expect(editor.getScrollHeight()).toBe 130 @@ -4000,8 +4173,6 @@ describe "TextEditor", -> describe ".selectPageUp/Down()", -> it "selects one screen height of text up or down", -> - editor.manageScrollPosition = true - editor.setLineHeightInPixels(10) editor.setHeight(50) expect(editor.getScrollHeight()).toBe 130 diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 94e5fad61..c39e4994b 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -84,7 +84,7 @@ describe "ThemeManager", -> atom.config.set('core.themes', []) waitsFor -> - didChangeActiveThemesHandler.callCount == 1 + didChangeActiveThemesHandler.callCount is 1 runs -> didChangeActiveThemesHandler.reset() @@ -92,7 +92,7 @@ describe "ThemeManager", -> atom.config.set('core.themes', ['atom-dark-ui']) waitsFor -> - didChangeActiveThemesHandler.callCount == 1 + didChangeActiveThemesHandler.callCount is 1 runs -> didChangeActiveThemesHandler.reset() @@ -101,7 +101,7 @@ describe "ThemeManager", -> atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) waitsFor -> - didChangeActiveThemesHandler.callCount == 1 + didChangeActiveThemesHandler.callCount is 1 runs -> didChangeActiveThemesHandler.reset() @@ -111,7 +111,7 @@ describe "ThemeManager", -> atom.config.set('core.themes', []) waitsFor -> - didChangeActiveThemesHandler.callCount == 1 + didChangeActiveThemesHandler.callCount is 1 runs -> didChangeActiveThemesHandler.reset() @@ -120,7 +120,7 @@ describe "ThemeManager", -> atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) waitsFor -> - didChangeActiveThemesHandler.callCount == 1 + didChangeActiveThemesHandler.callCount is 1 runs -> expect($('style[priority=1]')).toHaveLength 2 @@ -129,6 +129,7 @@ describe "ThemeManager", -> expect(importPaths[0]).toContain 'atom-dark-ui' it 'adds theme-* classes to the workspace for each active theme', -> + atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']) workspaceElement = atom.views.getView(atom.workspace) themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 0adcd2756..3cd776c2b 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -352,7 +352,7 @@ describe "TokenizedBuffer", -> tabAsSpaces = _.multiplyString(' ', tokenizedBuffer.getTabLength()) screenLine0 = tokenizedBuffer.tokenizedLineForRow(0) expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}" - { tokens } = screenLine0 + {tokens} = screenLine0 expect(tokens.length).toBe 4 expect(tokens[0].value).toBe "#" @@ -452,7 +452,7 @@ describe "TokenizedBuffer", -> it "renders each UTF-8 surrogate pair as its own atomic token", -> screenLine0 = tokenizedBuffer.tokenizedLineForRow(0) expect(screenLine0.text).toBe "'abc\uD835\uDF97def'" - { tokens } = screenLine0 + {tokens} = screenLine0 expect(tokens.length).toBe 5 expect(tokens[0].value).toBe "'" @@ -464,7 +464,7 @@ describe "TokenizedBuffer", -> screenLine1 = tokenizedBuffer.tokenizedLineForRow(1) expect(screenLine1.text).toBe "//\uD835\uDF97xyz" - { tokens } = screenLine1 + {tokens} = screenLine1 expect(tokens.length).toBe 4 expect(tokens[0].value).toBe '//' @@ -878,3 +878,25 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false + + describe "when the buffer is configured with the null grammar", -> + it "uses the placeholder tokens and does not actually tokenize using the grammar", -> + spyOn(atom.grammars.nullGrammar, 'tokenizeLine').andCallThrough() + buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') + buffer.setText('a\nb\nc') + + tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizeCallback = jasmine.createSpy('onDidTokenize') + tokenizedBuffer.onDidTokenize(tokenizeCallback) + + fullyTokenize(tokenizedBuffer) + + expect(tokenizeCallback.callCount).toBe 1 + expect(atom.grammars.nullGrammar.tokenizeLine.callCount).toBe 0 + + expect(tokenizedBuffer.tokenizedLineForRow(0).tokens.length).toBe 1 + expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0].value).toBe 'a' + expect(tokenizedBuffer.tokenizedLineForRow(1).tokens.length).toBe 1 + expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0].value).toBe 'b' + expect(tokenizedBuffer.tokenizedLineForRow(2).tokens.length).toBe 1 + expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].value).toBe 'c' diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index 74b875658..88d235398 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -1,9 +1,13 @@ TooltipManager = require '../src/tooltip-manager' {$} = require '../src/space-pen-extensions' +_ = require "underscore-plus" describe "TooltipManager", -> [manager, element] = [] + ctrlX = _.humanizeKeystroke("ctrl-x") + ctrlY = _.humanizeKeystroke("ctrl-y") + beforeEach -> manager = new TooltipManager element = document.createElement('div') @@ -35,7 +39,7 @@ describe "TooltipManager", -> hover element, -> tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "Title ⌃X ⌃Y" + expect(tooltipElement).toHaveText "Title #{ctrlX} #{ctrlY}" describe "when no title is specified", -> it "shows the key binding corresponding to the command alone", -> @@ -45,7 +49,7 @@ describe "TooltipManager", -> hover element, -> tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "⌃X ⌃Y" + expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}" describe "when a keyBindingTarget is specified", -> it "looks up the key binding relative to the target", -> @@ -57,7 +61,7 @@ describe "TooltipManager", -> hover element, -> tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "⌃X ⌃Y" + expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}" it "does not display the keybinding if there is nothing mapped to the specified keyBindingCommand", -> manager.add element, title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element diff --git a/spec/typescript-spec.coffee b/spec/typescript-spec.coffee new file mode 100644 index 000000000..493715d36 --- /dev/null +++ b/spec/typescript-spec.coffee @@ -0,0 +1,30 @@ +typescript = require '../src/typescript' +crypto = require 'crypto' + +describe "TypeScript transpiler support", -> + describe "::createTypeScriptVersionAndOptionsDigest", -> + it "returns a digest for the library version and specified options", -> + defaultOptions = + target: 1 # ES5 + module: 'commonjs' + sourceMap: true + version = '1.4.1' + shasum = crypto.createHash('sha1') + shasum.update('typescript', 'utf8') + shasum.update('\0', 'utf8') + shasum.update(version, 'utf8') + shasum.update('\0', 'utf8') + shasum.update(JSON.stringify(defaultOptions)) + expectedDigest = shasum.digest('hex') + + observedDigest = typescript.createTypeScriptVersionAndOptionsDigest(version, defaultOptions) + expect(observedDigest).toEqual expectedDigest + + describe "when there is a .ts file", -> + it "transpiles it using typescript", -> + transpiled = require('./fixtures/typescript/valid.ts') + expect(transpiled(3)).toBe 4 + + describe "when the .ts file is invalid", -> + it "does not transpile", -> + expect(-> require('./fixtures/typescript/invalid.ts')).toThrow() diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index 4d0d72abc..239f2e878 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -137,6 +137,21 @@ describe "ViewRegistry", -> advanceClock(registry.documentPollingInterval) expect(events).toEqual ['write', 'read', 'poll', 'poll'] + it "polls the document after updating when ::pollAfterNextUpdate() has been called", -> + events = [] + registry.pollDocument -> events.push('poll') + registry.updateDocument -> events.push('write') + registry.readDocument -> events.push('read') + frameRequests.shift()() + expect(events).toEqual ['write', 'read'] + + events = [] + registry.pollAfterNextUpdate() + registry.updateDocument -> events.push('write') + registry.readDocument -> events.push('read') + frameRequests.shift()() + expect(events).toEqual ['write', 'read', 'poll'] + describe "::pollDocument(fn)", -> it "calls all registered reader functions on an interval until they are disabled via a returned disposable", -> spyOn(window, 'setInterval').andCallFake(fakeSetInterval) diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 883474e85..3c1a5b4ae 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -285,19 +285,35 @@ describe "Window", -> it "adds it to the project's paths", -> pathToOpen = __filename atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - expect(atom.project.getPaths()[0]).toBe __dirname + + waitsFor -> + atom.project.getPaths().length is 1 + + runs -> + expect(atom.project.getPaths()[0]).toBe __dirname describe "when the opened path does not exist but its parent directory does", -> it "adds the parent directory to the project paths", -> pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - expect(atom.project.getPaths()[0]).toBe __dirname + + waitsFor -> + atom.project.getPaths().length is 1 + + runs -> + expect(atom.project.getPaths()[0]).toBe __dirname describe "when the opened path is a file", -> it "opens it in the workspace", -> pathToOpen = __filename atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename + + waitsFor -> + atom.workspace.open.callCount is 1 + + runs -> + expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename + describe "when the opened path is a directory", -> it "does not open it in the workspace", -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index e40ef1925..dac2849c0 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -189,7 +189,7 @@ describe "Workspace", -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> - pane2 = workspace.getPanes().filter((p) -> p != pane1)[0] + pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0] expect(workspace.getActivePane()).toBe pane2 expect(pane1.items).toEqual [] expect(pane2.items).toEqual [editor] @@ -218,7 +218,7 @@ describe "Workspace", -> workspace.open('a', split: 'right').then (o) -> editor = o runs -> - pane4 = workspace.getPanes().filter((p) -> p != pane1)[0] + pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0] expect(workspace.getActivePane()).toBe pane4 expect(pane4.items).toEqual [editor] expect(workspace.paneContainer.root.children[0]).toBe pane1 @@ -226,19 +226,19 @@ describe "Workspace", -> describe "when passed a path that matches a custom opener", -> it "returns the resource returned by the custom opener", -> - fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) - barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) + fooOpener = (pathToOpen, options) -> {foo: pathToOpen, options} if pathToOpen?.match(/\.foo/) + barOpener = (pathToOpen) -> {bar: pathToOpen} if pathToOpen?.match(/^bar:\/\//) workspace.addOpener(fooOpener) workspace.addOpener(barOpener) waitsForPromise -> pathToOpen = atom.project.getDirectories()[0]?.resolve('a.foo') workspace.open(pathToOpen, hey: "there").then (item) -> - expect(item).toEqual { foo: pathToOpen, options: {hey: "there"} } + expect(item).toEqual {foo: pathToOpen, options: {hey: "there"}} waitsForPromise -> workspace.open("bar://baz").then (item) -> - expect(item).toEqual { bar: "bar://baz" } + expect(item).toEqual {bar: "bar://baz"} it "notifies ::onDidAddTextEditor observers", -> absolutePath = require.resolve('./fixtures/dir/a') @@ -835,7 +835,7 @@ describe "Workspace", -> runs -> expect(results).toHaveLength 3 - resultForA = _.find results, ({filePath}) -> path.basename(filePath) == 'a' + resultForA = _.find results, ({filePath}) -> path.basename(filePath) is 'a' expect(resultForA.matches).toHaveLength 1 expect(resultForA.matches[0].matchText).toBe 'Elephant' diff --git a/src/atom.coffee b/src/atom.coffee index e03502dee..595dc7930 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -6,12 +6,11 @@ remote = require 'remote' shell = require 'shell' _ = require 'underscore-plus' -{deprecate} = require 'grim' -{Emitter} = require 'event-kit' -{Model} = require 'theorist' +{deprecate, includeDeprecatedAPIs} = require 'grim' +{CompositeDisposable, Emitter} = require 'event-kit' fs = require 'fs-plus' {convertStackTrace, convertLine} = require 'coffeestack' - +Model = require './model' {$} = require './space-pen-extensions' WindowEventHandler = require './window-event-handler' StylesElement = require './styles-element' @@ -34,35 +33,36 @@ class Atom extends Model atom = @deserialize(@loadState(mode)) ? new this({mode, @version}) atom.deserializeTimings.atom = Date.now() - startTime - workspaceViewDeprecationMessage = """ - atom.workspaceView is no longer available. - In most cases you will not need the view. See the Workspace docs for - alternatives: https://atom.io/docs/api/latest/Workspace. - If you do need the view, please use `atom.views.getView(atom.workspace)`, - which returns an HTMLElement. - """ + if includeDeprecatedAPIs + workspaceViewDeprecationMessage = """ + atom.workspaceView is no longer available. + In most cases you will not need the view. See the Workspace docs for + alternatives: https://atom.io/docs/api/latest/Workspace. + If you do need the view, please use `atom.views.getView(atom.workspace)`, + which returns an HTMLElement. + """ - serviceHubDeprecationMessage = """ - atom.services is no longer available. To register service providers and - consumers, use the `providedServices` and `consumedServices` fields in - your package's package.json. - """ + serviceHubDeprecationMessage = """ + atom.services is no longer available. To register service providers and + consumers, use the `providedServices` and `consumedServices` fields in + your package's package.json. + """ - Object.defineProperty atom, 'workspaceView', - get: -> - deprecate(workspaceViewDeprecationMessage) - atom.__workspaceView - set: (newValue) -> - deprecate(workspaceViewDeprecationMessage) - atom.__workspaceView = newValue + Object.defineProperty atom, 'workspaceView', + get: -> + deprecate(workspaceViewDeprecationMessage) + atom.__workspaceView + set: (newValue) -> + deprecate(workspaceViewDeprecationMessage) + atom.__workspaceView = newValue - Object.defineProperty atom, 'services', - get: -> - deprecate(serviceHubDeprecationMessage) - atom.packages.serviceHub - set: (newValue) -> - deprecate(serviceHubDeprecationMessage) - atom.packages.serviceHub = newValue + Object.defineProperty atom, 'services', + get: -> + deprecate(serviceHubDeprecationMessage) + atom.packages.serviceHub + set: (newValue) -> + deprecate(serviceHubDeprecationMessage) + atom.packages.serviceHub = newValue atom @@ -197,6 +197,7 @@ class Atom extends Model # Call .loadOrCreate instead constructor: (@state) -> @emitter = new Emitter + @disposables = new CompositeDisposable {@mode} = @state DeserializerManager = require './deserializer-manager' @deserializers = new DeserializerManager() @@ -207,12 +208,6 @@ class Atom extends Model # # Call after this instance has been assigned to the `atom` global. initialize: -> - # Disable deprecations unless in dev mode or spec mode so that regular - # editor performance isn't impacted by generating stack traces for - # deprecated calls. - unless @inDevMode() or @inSpecMode() - require('grim').deprecate = -> - sourceMapCache = {} window.onerror = => @@ -234,10 +229,12 @@ class Atom extends Model @openDevTools() @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()') - @emit 'uncaught-error', arguments... + @emit 'uncaught-error', arguments... if includeDeprecatedAPIs @emitter.emit 'did-throw-error', {message, url, line, column, originalError} - @unsubscribe() + @disposables?.dispose() + @disposables = new CompositeDisposable + @setBodyPlatformClass() @loadTime = null @@ -269,7 +266,10 @@ class Atom extends Model @config = new Config({configDirPath, resourcePath}) @keymaps = new KeymapManager({configDirPath, resourcePath}) - @keymap = @keymaps # Deprecated + + if includeDeprecatedAPIs + @keymap = @keymaps # Deprecated + @keymaps.subscribeToFileReadFailure() @tooltips = new TooltipManager @notifications = new NotificationManager @@ -285,11 +285,12 @@ class Atom extends Model @grammars = @deserializers.deserialize(@state.grammars ? @state.syntax) ? new GrammarRegistry() - Object.defineProperty this, 'syntax', get: -> - deprecate "The atom.syntax global is deprecated. Use atom.grammars instead." - @grammars + if includeDeprecatedAPIs + Object.defineProperty this, 'syntax', get: -> + deprecate "The atom.syntax global is deprecated. Use atom.grammars instead." + @grammars - @subscribe @packages.onDidActivateInitialPackages => @watchThemes() + @disposables.add @packages.onDidActivateInitialPackages => @watchThemes() Project = require './project' TextBuffer = require 'text-buffer' @@ -348,15 +349,15 @@ class Atom extends Model # Public: Is the current window in development mode? inDevMode: -> - @getLoadSettings().devMode + @devMode ?= @getLoadSettings().devMode # Public: Is the current window in safe mode? inSafeMode: -> - @getLoadSettings().safeMode + @safeMode ?= @getLoadSettings().safeMode # Public: Is the current window running specs? inSpecMode: -> - @getLoadSettings().isSpec + @specMode ?= @getLoadSettings().isSpec # Public: Get the version of the Atom application. # @@ -411,10 +412,11 @@ class Atom extends Model open: (options) -> ipc.send('open', options) - # Extended: Show the native dialog to prompt the user to select a folder. + # Extended: Prompt the user to select one or more folders. # - # * `callback` A {Function} to call once the user has selected a folder. - # * `path` {String} the path to the folder the user selected. + # * `callback` A {Function} to call once the user has confirmed the selection. + # * `paths` An {Array} of {String} paths that the user selected, or `null` + # if the user dismissed the dialog. pickFolder: (callback) -> responseChannel = "atom-pick-folder-response" ipc.on responseChannel, (path) -> @@ -497,7 +499,7 @@ class Atom extends Model # Extended: Toggle the full screen state of the current window. toggleFullScreen: -> - @setFullScreen(!@isFullScreen()) + @setFullScreen(not @isFullScreen()) # Schedule the window to be shown and focused on the next tick. # @@ -606,7 +608,7 @@ class Atom extends Model @requireUserInitScript() unless safeMode @menu.update() - @subscribe @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => + @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => @setAutoHideMenuBar(newValue) @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') @@ -719,13 +721,18 @@ class Atom extends Model deserializeWorkspaceView: -> Workspace = require './workspace' - WorkspaceView = require './workspace-view' + + if includeDeprecatedAPIs + WorkspaceView = require './workspace-view' startTime = Date.now() @workspace = Workspace.deserialize(@state.workspace) ? new Workspace workspaceElement = @views.getView(@workspace) - @__workspaceView = workspaceElement.__spacePenView + + if includeDeprecatedAPIs + @__workspaceView = workspaceElement.__spacePenView + @deserializeTimings.workspace = Date.now() - startTime @keymaps.defaultTarget = workspaceElement @@ -756,7 +763,7 @@ class Atom extends Model # Notify the browser project of the window's current project path watchProjectPath: -> - @subscribe @project.onDidChangePaths => + @disposables.add @project.onDidChangePaths => @constructor.updateLoadSetting('initialPaths', @project.getPaths()) exit: (status) -> @@ -770,11 +777,15 @@ class Atom extends Model setRepresentedFilename: (filename) -> ipc.send('call-window-method', 'setRepresentedFilename', filename) + addProjectFolder: -> + @pickFolder (selectedPaths = []) => + @project.addPath(selectedPath) for selectedPath in selectedPaths + showSaveDialog: (callback) -> callback(showSaveDialogSync()) showSaveDialogSync: (defaultPath) -> - defaultPath ?= @project?.getPath() + defaultPath ?= @project?.getPaths()[0] currentWindow = @getCurrentWindow() dialog = remote.require('dialog') dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} @@ -825,6 +836,7 @@ class Atom extends Model delete window[key] else window[key] = value + return onUpdateAvailable: (callback) -> @emitter.on 'update-available', callback @@ -832,17 +844,18 @@ class Atom extends Model updateAvailable: (details) -> @emitter.emit 'update-available', details - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClass: -> - deprecate("Callers should be converted to use atom.deserializers") - - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClasses: -> - deprecate("Callers should be converted to use atom.deserializers") - setBodyPlatformClass: -> document.body.classList.add("platform-#{process.platform}") setAutoHideMenuBar: (autoHide) -> ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide) - ipc.send('call-window-method', 'setMenuBarVisibility', !autoHide) + ipc.send('call-window-method', 'setMenuBarVisibility', not autoHide) + +if includeDeprecatedAPIs + # Deprecated: Callers should be converted to use atom.deserializers + Atom::registerRepresentationClass = -> + deprecate("Callers should be converted to use atom.deserializers") + + # Deprecated: Callers should be converted to use atom.deserializers + Atom::registerRepresentationClasses = -> + deprecate("Callers should be converted to use atom.deserializers") diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 5218ff304..d88314c89 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -82,19 +82,20 @@ class ApplicationMenu # window specific items. enableWindowSpecificItems: (enable) -> for item in @flattenMenuItems(@menu) - item.enabled = enable if item.metadata?['windowSpecific'] + item.enabled = enable if item.metadata?.windowSpecific + return # Replaces VERSION with the current version. substituteVersion: (template) -> - if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label == 'VERSION')) + if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION')) item.label = "Version #{@version}" # Sets the proper visible state the update menu items showUpdateMenuItem: (state) -> - checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Check for Update') - checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Checking for Update') - downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Downloading Update') - installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Restart and Install Update') + checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update') + checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update') + downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update') + installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update') return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem? @@ -120,11 +121,11 @@ class ApplicationMenu [ label: "Atom" submenu: [ - { label: "Check for Update", metadata: {autoUpdate: true}} - { label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload() } - { label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close() } - { label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools() } - { label: 'Quit', accelerator: 'Command+Q', click: -> app.quit() } + {label: "Check for Update", metadata: {autoUpdate: true}} + {label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()} + {label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()} + {label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()} + {label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()} ] ] @@ -145,7 +146,7 @@ class ApplicationMenu if item.command item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand) item.click = -> global.atomApplication.sendCommand(item.command) - item.metadata['windowSpecific'] = true unless /^application:/.test(item.command) + item.metadata.windowSpecific = true unless /^application:/.test(item.command) @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu template diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 7324aa460..2fee79f0b 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -18,7 +18,7 @@ DefaultSocketPath = if process.platform is 'win32' '\\\\.\\pipe\\atom-sock' else - path.join(os.tmpdir(), 'atom.sock') + path.join(os.tmpdir(), "atom-#{process.env.USER}.sock") # The application's singleton class. # @@ -60,7 +60,7 @@ class AtomApplication exit: (status) -> app.exit(status) constructor: (options) -> - {@resourcePath, @version, @devMode, @safeMode, @socketPath, @enableMultiFolderProject} = options + {@resourcePath, @version, @devMode, @safeMode, @apiPreviewMode, @socketPath} = options # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath @@ -82,20 +82,20 @@ class AtomApplication @openWithOptions(options) # Opens a new window based on the options provided. - openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile}) -> + openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, apiPreviewMode, newWindow, specDirectory, logFile}) -> if test @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) else if pathsToOpen.length > 0 - @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode}) + @openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode}) else if urlsToOpen.length > 0 - @openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen + @openUrl({urlToOpen, devMode, safeMode, apiPreviewMode}) for urlToOpen in urlsToOpen else - @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode}) # Always open a editor window if this is the first instance of Atom. + @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode}) # Always open a editor window if this is the first instance of Atom. # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> @windows.splice @windows.indexOf(window), 1 - @applicationMenu?.enableWindowSpecificItems(false) if @windows.length == 0 + @applicationMenu?.enableWindowSpecificItems(false) if @windows.length is 0 # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> @@ -146,6 +146,7 @@ class AtomApplication getLoadSettings = => devMode: @focusedWindow()?.devMode safeMode: @focusedWindow()?.safeMode + apiPreviewMode: @focusedWindow()?.apiPreviewMode @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: global.devResourcePath, safeMode: @focusedWindow()?.safeMode) @on 'application:run-benchmarks', -> @runBenchmarks() @@ -157,6 +158,8 @@ class AtomApplication @on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings()) @on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true) @on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true) + @on 'application:open-api-preview', -> @promptForPathToOpen('all', apiPreviewMode: true) + @on 'application:open-dev-api-preview', -> @promptForPathToOpen('all', {apiPreviewMode: true, devMode: true}) @on 'application:inspect', ({x,y, atomWindow}) -> atomWindow ?= @focusedWindow() atomWindow?.browserWindow.inspectElement(x, y) @@ -166,7 +169,7 @@ class AtomApplication @on 'application:open-roadmap', -> require('shell').openExternal('https://atom.io/roadmap?app') @on 'application:open-faq', -> require('shell').openExternal('https://atom.io/faq') @on 'application:open-terms-of-use', -> require('shell').openExternal('https://atom.io/terms') - @on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/issues/new') + @on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues') @on 'application:search-issues', -> require('shell').openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom') @on 'application:install-update', -> @autoUpdateManager.install() @@ -209,7 +212,7 @@ class AtomApplication app.on 'open-url', (event, urlToOpen) => event.preventDefault() - @openUrl({urlToOpen, @devMode, @safeMode}) + @openUrl({urlToOpen, @devMode, @safeMode, @apiPreviewMode}) app.on 'activate-with-no-open-windows', (event) => event.preventDefault() @@ -253,7 +256,7 @@ class AtomApplication clipboard = null ipc.on 'write-text-to-selection-clipboard', (event, selectedText) -> - clipboard ?= require 'clipboard' + clipboard ?= require '../safe-clipboard' clipboard.writeText(selectedText, 'selection') # Public: Executes the given command. @@ -333,9 +336,10 @@ class AtomApplication # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. + # :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode. # :window - {AtomWindow} to open file paths in. - openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) -> - @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, window}) + openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) -> + @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, window}) # Public: Opens a single path, in an existing window if possible. # @@ -345,14 +349,10 @@ class AtomApplication # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. + # :apiPreviewMode - Boolean to control the opened window's 1.0 API preview mode. # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. - openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> - if pathsToOpen?.length > 1 and not @enableMultiFolderProject - for pathToOpen in pathsToOpen - @openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}) - return - + openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, window}={}) -> pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) @@ -382,7 +382,7 @@ class AtomApplication bootstrapScript ?= require.resolve('../window-bootstrap') resourcePath ?= @resourcePath - openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions}) + openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, apiPreviewMode, windowDimensions}) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -393,11 +393,13 @@ class AtomApplication # Kill all processes associated with opened windows. killAllProcesses: -> @killProcess(pid) for pid of @pidsToOpenWindows + return # Kill process associated with the given opened window. killProcessForWindow: (openedWindow) -> for pid, trackedWindow of @pidsToOpenWindows @killProcess(pid) if trackedWindow is openedWindow + return # Kill the process with the given pid. killProcess: (pid) -> @@ -501,9 +503,9 @@ class AtomApplication # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. # :window - An {AtomWindow} to use for opening a selected file path. - promptForPathToOpen: (type, {devMode, safeMode, window}) -> + promptForPathToOpen: (type, {devMode, safeMode, apiPreviewMode, window}) -> @promptForPath type, (pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode, window}) + @openPaths({pathsToOpen, devMode, safeMode, apiPreviewMode, window}) promptForPath: (type, callback) -> properties = diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 66fad179e..fff98799c 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -18,7 +18,7 @@ class AtomWindow isSpec: null constructor: (settings={}) -> - {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings + {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode, @apiPreviewMode} = settings locationsToOpen ?= [{pathToOpen}] if pathToOpen locationsToOpen ?= [] @@ -47,6 +47,7 @@ class AtomWindow loadSettings.resourcePath = @resourcePath loadSettings.devMode ?= false loadSettings.safeMode ?= false + loadSettings.apiPreviewMode ?= false # Only send to the first non-spec window created if @constructor.includeShellLoadTime and not @isSpec diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index a0c1f2da8..358390889 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -33,6 +33,10 @@ class AutoUpdateManager else autoUpdater = require 'auto-updater' + autoUpdater.on 'error', (event, message) => + @setState(ErrorState) + console.error "Error Downloading Update: #{message}" + autoUpdater.setFeedUrl @feedUrl autoUpdater.on 'checking-for-update', => @@ -44,10 +48,6 @@ class AutoUpdateManager autoUpdater.on 'update-available', => @setState(DownladingState) - autoUpdater.on 'error', (event, message) => - @setState(ErrorState) - console.error "Error Downloading Update: #{message}" - autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) => @setState(UpdateAvailableState) @emitUpdateAvailableEvent(@getWindows()...) @@ -65,6 +65,7 @@ class AutoUpdateManager return unless @releaseVersion? for atomWindow in windows atomWindow.sendMessage('update-available', {@releaseVersion}) + return setState: (state) -> return if @state is state diff --git a/src/browser/main.coffee b/src/browser/main.coffee index b73556307..469d53fae 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -91,12 +91,10 @@ parseCommandLine = -> Usage: atom [options] [path ...] - One or more paths to files or folders to open may be specified. - - File paths will open in the current window. - - Folder paths will open in an existing window if that folder has already been - opened or a new window if it hasn't. + One or more paths to files or folders may be specified. If there is an + existing Atom window that contains all of the given folders, the paths + will be opened in that window. Otherwise, they will be opened in a new + window. Environment Variables: @@ -106,6 +104,7 @@ parseCommandLine = -> ATOM_HOME The root path for all configuration files and folders. Defaults to `~/.atom`. """ + options.alias('1', 'one').boolean('1').describe('1', 'Run in 1.0 API preview mode.') options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.') options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.') options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.') @@ -118,7 +117,6 @@ parseCommandLine = -> options.alias('v', 'version').boolean('v').describe('v', 'Print the version.') options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.') options.string('socket-path') - options.boolean('multi-folder') args = options.argv if args.help @@ -132,6 +130,7 @@ parseCommandLine = -> executedFrom = args['executed-from'] devMode = args['dev'] safeMode = args['safe'] + apiPreviewMode = args['one'] pathsToOpen = args._ pathsToOpen = [executedFrom] if executedFrom and pathsToOpen.length is 0 test = args['test'] @@ -140,7 +139,6 @@ parseCommandLine = -> pidToKillWhenClosed = args['pid'] if args['wait'] logFile = args['log-file'] socketPath = args['socket-path'] - enableMultiFolderProject = args['multi-folder'] if args['resource-path'] devMode = true @@ -166,6 +164,7 @@ parseCommandLine = -> process.env.PATH = args['path-environment'] if args['path-environment'] {resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, - devMode, safeMode, newWindow, specDirectory, logFile, socketPath, enableMultiFolderProject} + devMode, apiPreviewMode, safeMode, newWindow, specDirectory, logFile, + socketPath} start() diff --git a/src/clipboard.coffee b/src/clipboard.coffee index 3fba6f4e8..2412394a6 100644 --- a/src/clipboard.coffee +++ b/src/clipboard.coffee @@ -1,5 +1,5 @@ -clipboard = require 'clipboard' crypto = require 'crypto' +clipboard = require './safe-clipboard' # Extended: Represents the clipboard used for copying and pasting in Atom. # diff --git a/src/command-installer.coffee b/src/command-installer.coffee index 1d3a16777..c0ed22482 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -6,7 +6,7 @@ runas = null # defer until used symlinkCommand = (sourcePath, destinationPath, callback) -> fs.unlink destinationPath, (error) -> - if error? and error?.code != 'ENOENT' + if error? and error?.code isnt 'ENOENT' callback(error) else fs.makeTree path.dirname(destinationPath), (error) -> @@ -17,13 +17,13 @@ symlinkCommand = (sourcePath, destinationPath, callback) -> symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) -> runas ?= require 'runas' - if runas('/bin/rm', ['-f', destinationPath], admin: true) != 0 + if runas('/bin/rm', ['-f', destinationPath], admin: true) isnt 0 throw new Error("Failed to remove '#{destinationPath}'") - if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) != 0 + if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) isnt 0 throw new Error("Failed to create directory '#{destinationPath}'") - if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) != 0 + if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) isnt 0 throw new Error("Failed to symlink '#{sourcePath}' to '#{destinationPath}'") module.exports = diff --git a/src/command-registry.coffee b/src/command-registry.coffee index ead4415d2..3969dc283 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -1,11 +1,9 @@ {Emitter, Disposable, CompositeDisposable} = require 'event-kit' -{specificity} = require 'clear-cut' +{calculateSpecificity, validateSelector} = require 'clear-cut' _ = require 'underscore-plus' {$} = require './space-pen-extensions' -{validateSelector} = require './selector-validator' SequenceCount = 0 -SpecificityCache = {} # Public: Associates listener functions with commands in a # context-sensitive way using CSS selectors. You can access a global instance of @@ -50,6 +48,7 @@ class CommandRegistry destroy: -> for commandName of @registeredCommands window.removeEventListener(commandName, @handleCommandEvent, true) + return # Public: Add one or more command listeners associated with a selector. # @@ -187,6 +186,7 @@ class CommandRegistry @selectorBasedListenersByCommandName = {} for commandName, listeners of snapshot @selectorBasedListenersByCommandName[commandName] = listeners.slice() + return handleCommandEvent: (originalEvent) => propagationStopped = false @@ -239,7 +239,7 @@ class CommandRegistry class SelectorBasedListener constructor: (@selector, @callback) -> - @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) + @specificity = calculateSpecificity(@selector) @sequenceNumber = SequenceCount++ compare: (other) -> diff --git a/src/compile-cache.coffee b/src/compile-cache.coffee index c31f5bdd1..8fe8d6711 100644 --- a/src/compile-cache.coffee +++ b/src/compile-cache.coffee @@ -2,6 +2,7 @@ path = require 'path' CSON = require 'season' CoffeeCache = require 'coffee-cash' babel = require './babel' +typescript = require './typescript' # This file is required directly by apm so that files can be cached during # package install so that the first package load in Atom doesn't have to @@ -16,6 +17,7 @@ exports.addPathToCache = (filePath, atomHome) -> CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee')) CSON.setCacheDir(path.join(cacheDir, 'cson')) babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel')) + typescript.setCacheDirectory(path.join(cacheDir, 'ts')) switch path.extname(filePath) when '.coffee' @@ -24,3 +26,5 @@ exports.addPathToCache = (filePath, atomHome) -> CSON.readFileSync(filePath) when '.js' babel.addPathToCache(filePath) + when '.ts' + typescript.addPathToCache(filePath) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 1796bb64c..ba8150a26 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -9,7 +9,7 @@ module.exports = properties: ignoredNames: type: 'array' - default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"] + default: [".git", ".hg", ".svn", ".DS_Store", "._*", "Thumbs.db"] items: type: 'string' excludeVcsIgnoredPaths: @@ -18,7 +18,7 @@ module.exports = title: 'Exclude VCS Ignored Paths' followSymlinks: type: 'boolean' - default: false + default: true title: 'Follow symlinks' description: 'Used when searching and when opening files with the fuzzy finder.' disabledPackages: @@ -28,7 +28,7 @@ module.exports = type: 'string' themes: type: 'array' - default: ['atom-dark-ui', 'atom-dark-syntax'] + default: ['one-dark-ui', 'one-dark-syntax'] items: type: 'string' projectHome: @@ -99,9 +99,7 @@ module.exports = # These can be used as globals or scoped, thus defaults. completions: - type: "array" - items: - type: "string" + type: ['array', 'object'] default: [] fontFamily: type: 'string' @@ -149,6 +147,10 @@ module.exports = softWrapAtPreferredLineLength: type: 'boolean' default: false + softWrapHangingIndent: + type: 'integer' + default: 0 + minimum: 0 scrollSensitivity: type: 'integer' default: 40 diff --git a/src/config.coffee b/src/config.coffee index 937bb1307..6c440f219 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -1,6 +1,5 @@ _ = require 'underscore-plus' fs = require 'fs-plus' -EmitterMixin = require('emissary').Emitter {CompositeDisposable, Disposable, Emitter} = require 'event-kit' CSON = require 'season' path = require 'path' @@ -78,7 +77,7 @@ ScopeDescriptor = require './scope-descriptor' # # ... # ``` # -# See [Creating a Package](https://atom.io/docs/latest/creating-a-package) for +# See [package docs](https://atom.io/docs/latest/hacking-atom-package-word-count) for # more info. # # ## Config Schemas @@ -290,7 +289,6 @@ ScopeDescriptor = require './scope-descriptor' # module.exports = class Config - EmitterMixin.includeInto(this) @schemaEnforcers = {} @addSchemaEnforcer: (typeName, enforcerFunction) -> @@ -301,6 +299,7 @@ class Config for typeName, functions of filters for name, enforcerFunction of functions @addSchemaEnforcer(typeName, enforcerFunction) + return @executeSchemaEnforcers: (keyPath, value, schema) -> error = null @@ -360,7 +359,7 @@ class Config # * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from # the root of the syntax tree to a token. Get one by calling # {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. - # See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors) + # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) # for more information. # * `callback` {Function} to call when the value of the key changes. # * `value` the new value of the key @@ -370,7 +369,7 @@ class Config observe: -> if arguments.length is 2 [keyPath, callback] = arguments - else if arguments.length is 3 and (_.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor) + else if Grim.includeDeprecatedAPIs and arguments.length is 3 and (_.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor) Grim.deprecate """ Passing a scope descriptor as the first argument to Config::observe is deprecated. Pass a `scope` in an options hash as the third argument instead. @@ -379,7 +378,7 @@ class Config else if arguments.length is 3 and (_.isString(arguments[0]) and _.isObject(arguments[1])) [keyPath, options, callback] = arguments scopeDescriptor = options.scope - if options.callNow? + if Grim.includeDeprecatedAPIs and options.callNow? Grim.deprecate """ Config::observe no longer takes a `callNow` option. Use ::onDidChange instead. Note that ::onDidChange passes its callback different arguments. @@ -403,7 +402,7 @@ class Config # * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from # the root of the syntax tree to a token. Get one by calling # {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. - # See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors) + # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) # for more information. # * `callback` {Function} to call when the value of the key changes. # * `event` {Object} @@ -418,7 +417,7 @@ class Config [callback] = arguments else if arguments.length is 2 [keyPath, callback] = arguments - else if _.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor + else if Grim.includeDeprecatedAPIs and _.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor Grim.deprecate """ Passing a scope descriptor as the first argument to Config::onDidChange is deprecated. Pass a `scope` in an options hash as the third argument instead. @@ -487,7 +486,7 @@ class Config # * `scope` (optional) {ScopeDescriptor} describing a path from # the root of the syntax tree to a token. Get one by calling # {editor.getLastCursor().getScopeDescriptor()} - # See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors) + # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) # for more information. # # Returns the value from Atom's default settings, the user's configuration @@ -497,7 +496,7 @@ class Config if typeof arguments[0] is 'string' or not arguments[0]? [keyPath, options] = arguments {scope} = options - else + else if Grim.includeDeprecatedAPIs Grim.deprecate """ Passing a scope descriptor as the first argument to Config::get is deprecated. Pass a `scope` in an options hash as the final argument instead. @@ -568,7 +567,7 @@ class Config # setting to the default value. # * `options` (optional) {Object} # * `scopeSelector` (optional) {String}. eg. '.source.ruby' - # See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors) + # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) # for more information. # * `source` (optional) {String} The name of a file with which the setting # is associated. Defaults to the user's config file. @@ -577,7 +576,7 @@ class Config # * `true` if the value was set. # * `false` if the value was not able to be coerced to the type specified in the setting's schema. set: -> - if arguments[0]?[0] is '.' + if Grim.includeDeprecatedAPIs and arguments[0]?[0] is '.' Grim.deprecate """ Passing a scope selector as the first argument to Config::set is deprecated. Pass a `scopeSelector` in an options hash as the final argument instead. @@ -616,7 +615,7 @@ class Config # * `scopeSelector` (optional) {String}. See {::set} # * `source` (optional) {String}. See {::set} unset: (keyPath, options) -> - if typeof options is 'string' + if Grim.includeDeprecatedAPIs and typeof options is 'string' Grim.deprecate """ Passing a scope selector as the first argument to Config::unset is deprecated. Pass a `scopeSelector` in an options hash as the second argument instead. @@ -651,47 +650,6 @@ class Config getSources: -> _.uniq(_.pluck(@scopedSettingsStore.propertySets, 'source')).sort() - # Deprecated: Restore the global setting at `keyPath` to its default value. - # - # Returns the new value. - restoreDefault: (scopeSelector, keyPath) -> - Grim.deprecate("Use ::unset instead.") - @unset(scopeSelector, keyPath) - @get(keyPath) - - # Deprecated: Get the global default value of the key path. _Please note_ that in most - # cases calling this is not necessary! {::get} returns the default value when - # a custom value is not specified. - # - # * `scopeSelector` (optional) {String}. eg. '.source.ruby' - # * `keyPath` The {String} name of the key. - # - # Returns the default value. - getDefault: -> - Grim.deprecate("Use `::get(keyPath, {scope, excludeSources: [atom.config.getUserConfigPath()]})` instead") - if arguments.length is 1 - [keyPath] = arguments - else - [scopeSelector, keyPath] = arguments - scope = [scopeSelector] - @get(keyPath, {scope, excludeSources: [@getUserConfigPath()]}) - - # Deprecated: Is the value at `keyPath` its default value? - # - # * `scopeSelector` (optional) {String}. eg. '.source.ruby' - # * `keyPath` The {String} name of the key. - # - # Returns a {Boolean}, `true` if the current value is the default, `false` - # otherwise. - isDefault: -> - Grim.deprecate("Use `not ::get(keyPath, {scope, sources: [atom.config.getUserConfigPath()]})?` instead") - if arguments.length is 1 - [keyPath] = arguments - else - [scopeSelector, keyPath] = arguments - scope = [scopeSelector] - not @get(keyPath, {scope, sources: [@getUserConfigPath()]})? - # Extended: Retrieve the schema for a specific key path. The schema will tell # you what type the keyPath expects, and other metadata about the config # option. @@ -708,12 +666,6 @@ class Config schema = schema.properties?[key] schema - # Deprecated: Returns a new {Object} containing all of the global settings and - # defaults. Returns the scoped settings when a `scopeSelector` is specified. - getSettings: -> - Grim.deprecate "Use ::get(keyPath) instead" - _.deepExtend({}, @settings, @defaultSettings) - # Extended: Get the {String} path to the config file being used. getUserConfigPath: -> @configFilePath @@ -731,31 +683,6 @@ class Config @transactDepth-- @emitChangeEvent() - ### - Section: Deprecated - ### - - getInt: (keyPath) -> - Grim.deprecate '''Config::getInt is no longer necessary. Use ::get instead. - Make sure the config option you are accessing has specified an `integer` - schema. See the schema section of - https://atom.io/docs/api/latest/Config for more info.''' - parseInt(@get(keyPath)) - - getPositiveInt: (keyPath, defaultValue=0) -> - Grim.deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead. - Make sure the config option you are accessing has specified an `integer` - schema with `minimum: 1`. See the schema section of - https://atom.io/docs/api/latest/Config for more info.''' - Math.max(@getInt(keyPath), 0) or defaultValue - - toggle: (keyPath) -> - Grim.deprecate 'Config::toggle is no longer supported. Please remove from your code.' - @set(keyPath, !@get(keyPath)) - - unobserve: (keyPath) -> - Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' - ### Section: Internal methods used by core ### @@ -898,6 +825,7 @@ class Config @transact => @settings = {} @set(key, value, save: false) for key, value of newSettings + return getRawValue: (keyPath, options) -> unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0 @@ -958,6 +886,7 @@ class Config @setRawDefault(keyPath, defaults) catch e console.warn("'#{keyPath}' could not set the default. Attempted default: #{JSON.stringify(defaults)}; Schema: #{JSON.stringify(@getSchema(keyPath))}") + return deepClone: (object) -> if object instanceof Color @@ -1053,16 +982,6 @@ class Config @emitChangeEvent() - addScopedSettings: (source, selector, value, options) -> - Grim.deprecate("Use ::set instead") - settingsBySelector = {} - settingsBySelector[selector] = value - disposable = @scopedSettingsStore.addProperties(source, settingsBySelector, options) - @emitChangeEvent() - new Disposable => - disposable.dispose() - @emitChangeEvent() - setRawScopedValue: (keyPath, value, source, selector, options) -> if keyPath? newValue = {} @@ -1091,11 +1010,6 @@ class Config oldValue = newValue callback(event) - settingsForScopeDescriptor: (scopeDescriptor, keyPath) -> - Grim.deprecate("Use Config::getAll instead") - entries = @getAll(null, scope: scopeDescriptor) - value for {value} in entries when _.valueForKeyPath(value, keyPath)? - # Base schema enforcers. These will coerce raw input into the specified type, # and will throw an error when the value cannot be coerced. Throwing the error # will indicate that the value should not be set. @@ -1212,7 +1126,7 @@ splitKeyPath = (keyPath) -> startIndex = 0 keyPathArray = [] for char, i in keyPath - if char is '.' and (i is 0 or keyPath[i-1] != '\\') + if char is '.' and (i is 0 or keyPath[i-1] isnt '\\') keyPathArray.push keyPath.substring(startIndex, i) startIndex = i + 1 keyPathArray.push keyPath.substr(startIndex, keyPath.length) @@ -1229,3 +1143,70 @@ withoutEmptyObjects = (object) -> else resultObject = object resultObject + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(Config) + + Config::restoreDefault = (scopeSelector, keyPath) -> + Grim.deprecate("Use ::unset instead.") + @unset(scopeSelector, keyPath) + @get(keyPath) + + Config::getDefault = -> + Grim.deprecate("Use `::get(keyPath, {scope, excludeSources: [atom.config.getUserConfigPath()]})` instead") + if arguments.length is 1 + [keyPath] = arguments + else + [scopeSelector, keyPath] = arguments + scope = [scopeSelector] + @get(keyPath, {scope, excludeSources: [@getUserConfigPath()]}) + + Config::isDefault = -> + Grim.deprecate("Use `not ::get(keyPath, {scope, sources: [atom.config.getUserConfigPath()]})?` instead") + if arguments.length is 1 + [keyPath] = arguments + else + [scopeSelector, keyPath] = arguments + scope = [scopeSelector] + not @get(keyPath, {scope, sources: [@getUserConfigPath()]})? + + Config::getSettings = -> + Grim.deprecate "Use ::get(keyPath) instead" + _.deepExtend({}, @settings, @defaultSettings) + + Config::getInt = (keyPath) -> + Grim.deprecate '''Config::getInt is no longer necessary. Use ::get instead. + Make sure the config option you are accessing has specified an `integer` + schema. See the schema section of + https://atom.io/docs/api/latest/Config for more info.''' + parseInt(@get(keyPath)) + + Config::getPositiveInt = (keyPath, defaultValue=0) -> + Grim.deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead. + Make sure the config option you are accessing has specified an `integer` + schema with `minimum: 1`. See the schema section of + https://atom.io/docs/api/latest/Config for more info.''' + Math.max(@getInt(keyPath), 0) or defaultValue + + Config::toggle = (keyPath) -> + Grim.deprecate 'Config::toggle is no longer supported. Please remove from your code.' + @set(keyPath, not @get(keyPath)) + + Config::unobserve = (keyPath) -> + Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' + + Config::addScopedSettings = (source, selector, value, options) -> + Grim.deprecate("Use ::set instead") + settingsBySelector = {} + settingsBySelector[selector] = value + disposable = @scopedSettingsStore.addProperties(source, settingsBySelector, options) + @emitChangeEvent() + new Disposable => + disposable.dispose() + @emitChangeEvent() + + Config::settingsForScopeDescriptor = (scopeDescriptor, keyPath) -> + Grim.deprecate("Use Config::getAll instead") + entries = @getAll(null, scope: scopeDescriptor) + value for {value} in entries when _.valueForKeyPath(value, keyPath)? diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 3f281afb1..bd5ff943e 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -1,16 +1,13 @@ -{$} = require './space-pen-extensions' _ = require 'underscore-plus' -remote = require 'remote' path = require 'path' CSON = require 'season' fs = require 'fs-plus' -{specificity} = require 'clear-cut' +{calculateSpecificity, validateSelector} = require 'clear-cut' {Disposable} = require 'event-kit' Grim = require 'grim' MenuHelpers = require './menu-helpers' -{validateSelector} = require './selector-validator' -SpecificityCache = {} +platformContextMenu = require('../package.json')?._atomMenu?['context-menu'] # Extended: Provides a registry for commands that you'd like to appear in the # context menu. @@ -51,10 +48,13 @@ class ContextMenuManager atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() loadPlatformItems: -> - menusDirPath = path.join(@resourcePath, 'menus') - platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) - map = CSON.readFileSync(platformMenuPath) - atom.contextMenu.add(map['context-menu']) + if platformContextMenu? + @add(platformContextMenu) + else + menusDirPath = path.join(@resourcePath, 'menus') + platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) + map = CSON.readFileSync(platformMenuPath) + @add(map['context-menu']) # Public: Add context menu items scoped by CSS selectors. # @@ -101,25 +101,26 @@ class ContextMenuManager # with the following argument: # * `event` The click event that deployed the context menu. add: (itemsBySelector) -> - # Detect deprecated file path as first argument - if itemsBySelector? and typeof itemsBySelector isnt 'object' - Grim.deprecate """ - ContextMenuManager::add has changed to take a single object as its - argument. Please see - https://atom.io/docs/api/latest/ContextMenuManager for more info. - """ - itemsBySelector = arguments[1] - devMode = arguments[2]?.devMode - - # Detect deprecated format for items object - for key, value of itemsBySelector - unless _.isArray(value) + if Grim.includeDeprecatedAPIs + # Detect deprecated file path as first argument + if itemsBySelector? and typeof itemsBySelector isnt 'object' Grim.deprecate """ ContextMenuManager::add has changed to take a single object as its argument. Please see https://atom.io/docs/api/latest/ContextMenuManager for more info. """ - itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode) + itemsBySelector = arguments[1] + devMode = arguments[2]?.devMode + + # Detect deprecated format for items object + for key, value of itemsBySelector + unless _.isArray(value) + Grim.deprecate """ + ContextMenuManager::add has changed to take a single object as its + argument. Please see + https://atom.io/docs/api/latest/ContextMenuManager for more info. + """ + itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode) addedItemSets = [] @@ -132,6 +133,7 @@ class ContextMenuManager new Disposable => for itemSet in addedItemSets @itemSets.splice(@itemSets.indexOf(itemSet), 1) + return templateForElement: (target) -> @templateForEvent({target}) @@ -187,7 +189,7 @@ class ContextMenuManager menuTemplate = @templateForEvent(event) return unless menuTemplate?.length > 0 - remote.getCurrentWindow().emit('context-menu', menuTemplate) + atom.getCurrentWindow().emit('context-menu', menuTemplate) return clear: -> @@ -204,4 +206,4 @@ class ContextMenuManager class ContextMenuItemSet constructor: (@selector, @items) -> - @specificity = (SpecificityCache[@selector] ?= specificity(@selector)) + @specificity = calculateSpecificity(@selector) diff --git a/src/cursor.coffee b/src/cursor.coffee index 33f50ca9c..b54cc6bcd 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -1,8 +1,8 @@ {Point, Range} = require 'text-buffer' -{Model} = require 'theorist' {Emitter} = require 'event-kit' _ = require 'underscore-plus' Grim = require 'grim' +Model = require './model' # Extended: The `Cursor` class represents the little blinking line identifying # where text can be inserted. @@ -15,7 +15,6 @@ class Cursor extends Model bufferPosition: null goalColumn: null visible: true - needsAutoscroll: null # Instantiated by a {TextEditor} constructor: ({@editor, @marker, id}) -> @@ -30,10 +29,6 @@ class Cursor extends Model {textChanged} = e return if oldHeadScreenPosition.isEqual(newHeadScreenPosition) - # Supports old editor view - @needsAutoscroll ?= @isLastCursor() and !textChanged - @autoscroll() if @editor.manageScrollPosition and @isLastCursor() and textChanged - @goalColumn = null movedEvent = @@ -44,16 +39,15 @@ class Cursor extends Model textChanged: textChanged cursor: this - @emit 'moved', movedEvent + @emit 'moved', movedEvent if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-position', movedEvent @editor.cursorMoved(movedEvent) @marker.onDidDestroy => @destroyed = true @editor.removeCursor(this) - @emit 'destroyed' + @emit 'destroyed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-destroy' @emitter.dispose() - @needsAutoscroll = true destroy: -> @marker.destroy() @@ -95,13 +89,13 @@ class Cursor extends Model @emitter.on 'did-change-visibility', callback on: (eventName) -> + return unless Grim.includeDeprecatedAPIs + switch eventName when 'moved' Grim.deprecate("Use Cursor::onDidChangePosition instead") when 'destroyed' Grim.deprecate("Use Cursor::onDidDestroy instead") - when 'destroyed' - Grim.deprecate("Use Cursor::onDidDestroy instead") else Grim.deprecate("::on is no longer supported. Use the event subscription methods instead") super @@ -128,8 +122,9 @@ class Cursor extends Model # # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. # * `options` (optional) {Object} with the following keys: - # * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever - # the cursor moves to. + # * `autoscroll` {Boolean} indicating whether to autoscroll to the new + # position. Defaults to `true` if this is the most recently added cursor, + # `false` otherwise. setBufferPosition: (bufferPosition, options={}) -> @changePosition options, => @marker.setHeadBufferPosition(bufferPosition, options) @@ -161,7 +156,7 @@ class Cursor extends Model # Public: Returns whether the cursor is at the start of a line. isAtBeginningOfLine: -> - @getBufferPosition().column == 0 + @getBufferPosition().column is 0 # Public: Returns whether the cursor is on the line return character. isAtEndOfLine: -> @@ -215,7 +210,7 @@ class Cursor extends Model isInsideWord: (options) -> {row, column} = @getBufferPosition() range = [[row, column], [row, Infinity]] - @editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) == 0 + @editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0 # Public: Returns the indentation level of the current line. getIndentLevel: -> @@ -229,9 +224,6 @@ class Cursor extends Model # Returns a {ScopeDescriptor} getScopeDescriptor: -> @editor.scopeDescriptorForBufferPosition(@getBufferPosition()) - getScopes: -> - Grim.deprecate 'Use Cursor::getScopeDescriptor() instead' - @getScopeDescriptor().getScopesArray() # Public: Returns true if this cursor has no non-whitespace characters before # its current position. @@ -251,7 +243,7 @@ class Cursor extends Model # # Returns a {Boolean}. isLastCursor: -> - this == @editor.getLastCursor() + this is @editor.getLastCursor() ### Section: Moving the Cursor @@ -266,9 +258,9 @@ class Cursor extends Model moveUp: (rowCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() - { row, column } = range.start + {row, column} = range.start else - { row, column } = @getScreenPosition() + {row, column} = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true) @@ -283,9 +275,9 @@ class Cursor extends Model moveDown: (rowCount=1, {moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() - { row, column } = range.end + {row, column} = range.end else - { row, column } = @getScreenPosition() + {row, column} = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true) @@ -310,7 +302,7 @@ class Cursor extends Model columnCount-- # subtract 1 for the row move column = column - columnCount - @setScreenPosition({row, column}) + @setScreenPosition({row, column}, clip: 'backward') # Public: Moves the cursor right one screen column. # @@ -323,7 +315,7 @@ class Cursor extends Model if moveToEndOfSelection and not range.isEmpty() @setScreenPosition(range.end) else - { row, column } = @getScreenPosition() + {row, column} = @getScreenPosition() maxLines = @editor.getScreenLineCount() rowLength = @editor.lineTextForScreenRow(row).length columnsRemainingInLine = rowLength - column @@ -337,7 +329,7 @@ class Cursor extends Model columnsRemainingInLine = rowLength column = column + columnCount - @setScreenPosition({row, column}, skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true) + @setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true) # Public: Moves the cursor to the top of the buffer. moveToTop: -> @@ -483,10 +475,6 @@ class Cursor extends Model endOfWordPosition or currentBufferPosition - getMoveNextWordBoundaryBufferPosition: (options) -> - Grim.deprecate 'Use `::getNextWordBoundaryBufferPosition(options)` instead' - @getNextWordBoundaryBufferPosition(options) - # Public: Retrieves the buffer position of where the current word starts. # # * `options` (optional) An {Object} with the following keys: @@ -598,10 +586,9 @@ class Cursor extends Model # Public: Sets whether the cursor is visible. setVisible: (visible) -> - if @visible != visible + if @visible isnt visible @visible = visible - @needsAutoscroll ?= true if @visible and @isLastCursor() - @emit 'visibility-changed', @visible + @emit 'visibility-changed', @visible if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-visibility', @visible # Public: Returns the visibility of the cursor. @@ -628,11 +615,10 @@ class Cursor extends Model # Public: Prevents this cursor from causing scrolling. clearAutoscroll: -> - @needsAutoscroll = null # Public: Deselects the current selection. - clearSelection: -> - @selection?.clear() + clearSelection: (options) -> + @selection?.clear(options) # Public: Get the RegExp used by the cursor to determine what a "word" is. # @@ -655,12 +641,9 @@ class Cursor extends Model ### changePosition: (options, fn) -> - @clearSelection() - @needsAutoscroll = options.autoscroll ? @isLastCursor() + @clearSelection(autoscroll: false) fn() - if @needsAutoscroll - @emit 'autoscrolled' # Support legacy editor - @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view + @autoscroll() if options.autoscroll ? @isLastCursor() getPixelRect: -> @editor.pixelRectForScreenRange(@getScreenRange()) @@ -681,10 +664,10 @@ class Cursor extends Model position = new Point(row, column - 1) @editor.scanInBufferRange /^\n*$/g, scanRange, ({range, stop}) -> - if !range.start.isEqual(start) + unless range.start.isEqual(start) position = range.start stop() - @editor.screenPositionForBufferPosition(position) + position getBeginningOfPreviousParagraphBufferPosition: -> start = @getBufferPosition() @@ -694,7 +677,16 @@ class Cursor extends Model position = new Point(0, 0) zero = new Point(0,0) @editor.backwardsScanInBufferRange /^\n*$/g, scanRange, ({range, stop}) -> - if !range.start.isEqual(zero) + unless range.start.isEqual(zero) position = range.start stop() - @editor.screenPositionForBufferPosition(position) + position + +if Grim.includeDeprecatedAPIs + Cursor::getScopes = -> + Grim.deprecate 'Use Cursor::getScopeDescriptor() instead' + @getScopeDescriptor().getScopesArray() + + Cursor::getMoveNextWordBoundaryBufferPosition = (options) -> + Grim.deprecate 'Use `::getNextWordBoundaryBufferPosition(options)` instead' + @getNextWordBoundaryBufferPosition(options) diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index f4f5d749f..bd530ce2b 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -35,6 +35,8 @@ class CursorsComponent @domNode.appendChild(cursorNode) @updateCursorNode(id, cursorState) + return + updateCursorNode: (id, newCursorState) -> cursorNode = @cursorNodesById[id] oldCursorState = (@oldState.cursors[id] ?= {}) diff --git a/src/custom-event-mixin.coffee b/src/custom-event-mixin.coffee index 1a3bb4d88..12785c89c 100644 --- a/src/custom-event-mixin.coffee +++ b/src/custom-event-mixin.coffee @@ -7,9 +7,11 @@ CustomEventMixin = for name, listeners in @customEventListeners for listener in listeners @getDOMNode().removeEventListener(name, listener) + return addCustomEventListeners: (customEventListeners) -> for name, listener of customEventListeners @customEventListeners[name] ?= [] @customEventListeners[name].push(listener) @getDOMNode().addEventListener(name, listener) + return diff --git a/src/decoration.coffee b/src/decoration.coffee index fdaaa285d..48b26e81f 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -1,5 +1,4 @@ _ = require 'underscore-plus' -EmitterMixin = require('emissary').Emitter {Emitter} = require 'event-kit' Grim = require 'grim' @@ -20,7 +19,7 @@ nextId = -> idCounter++ # decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'}) # ``` # -# Best practice for destorying the decoration is by destroying the {Marker}. +# Best practice for destroying the decoration is by destroying the {Marker}. # # ```coffee # marker.destroy() @@ -30,7 +29,6 @@ nextId = -> idCounter++ # the marker. module.exports = class Decoration - EmitterMixin.includeInto(this) # Private: Check if the `decorationProperties.type` matches `type` # @@ -56,7 +54,6 @@ class Decoration @properties.id = @id @flashQueue = null @destroyed = false - @markerDestroyDisposable = @marker.onDidDestroy => @destroy() # Essential: Destroy this marker. @@ -68,7 +65,7 @@ class Decoration @markerDestroyDisposable.dispose() @markerDestroyDisposable = null @destroyed = true - @emit 'destroyed' + @emit 'destroyed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-destroy' @emitter.dispose() @@ -124,9 +121,6 @@ class Decoration # Essential: Returns the {Decoration}'s properties. getProperties: -> @properties - getParams: -> - Grim.deprecate 'Use Decoration::getProperties instead' - @getProperties() # Essential: Update the marker with new Properties. Allows you to change the decoration's class. # @@ -142,11 +136,8 @@ class Decoration oldProperties = @properties @properties = newProperties @properties.id = @id - @emit 'updated', {oldParams: oldProperties, newParams: newProperties} + @emit 'updated', {oldParams: oldProperties, newParams: newProperties} if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-properties', {oldProperties, newProperties} - update: (newProperties) -> - Grim.deprecate 'Use Decoration::setProperties instead' - @setProperties(newProperties) ### Section: Private methods @@ -155,7 +146,7 @@ class Decoration matchesPattern: (decorationPattern) -> return false unless decorationPattern? for key, value of decorationPattern - return false if @properties[key] != value + return false if @properties[key] isnt value true onDidFlash: (callback) -> @@ -165,14 +156,18 @@ class Decoration flashObject = {class: klass, duration} @flashQueue ?= [] @flashQueue.push(flashObject) - @emit 'flash' + @emit 'flash' if Grim.includeDeprecatedAPIs @emitter.emit 'did-flash' consumeNextFlash: -> return @flashQueue.shift() if @flashQueue?.length > 0 null - on: (eventName) -> +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(Decoration) + + Decoration::on = (eventName) -> switch eventName when 'updated' Grim.deprecate 'Use Decoration::onDidChangeProperties instead' @@ -184,3 +179,11 @@ class Decoration Grim.deprecate 'Decoration::on is deprecated. Use event subscription methods instead.' EmitterMixin::on.apply(this, arguments) + + Decoration::getParams = -> + Grim.deprecate 'Use Decoration::getProperties instead' + @getProperties() + + Decoration::update = -> (newProperties) -> + Grim.deprecate 'Use Decoration::setProperties instead' + @setProperties(newProperties) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index a05c532a9..9e25e097b 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -16,7 +16,7 @@ class DefaultDirectoryProvider directoryForURISync: (uri) -> projectPath = path.normalize(uri) - directoryPath = if !fs.isDirectorySync(projectPath) and fs.isDirectorySync(path.dirname(projectPath)) + directoryPath = if not fs.isDirectorySync(projectPath) and fs.isDirectorySync(path.dirname(projectPath)) path.dirname(projectPath) else projectPath diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 50becb31a..fab84539c 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -35,10 +35,7 @@ class DeserializerManager @deserializers[deserializer.name] = deserializer for deserializer in deserializers new Disposable => delete @deserializers[deserializer.name] for deserializer in deserializers - - remove: (classes...) -> - Grim.deprecate("Call .dispose() on the Disposable return from ::add instead") - delete @deserializers[name] for {name} in classes + return # Public: Deserialize the state and params. # @@ -63,3 +60,9 @@ class DeserializerManager name = state.get?('deserializer') ? state.deserializer @deserializers[name] + +if Grim.includeDeprecatedAPIs + DeserializerManager::remove = (classes...) -> + Grim.deprecate("Call .dispose() on the Disposable return from ::add instead") + delete @deserializers[name] for {name} in classes + return diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index e09f1580c..d86fd13be 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1,12 +1,11 @@ _ = require 'underscore-plus' -EmitterMixin = require('emissary').Emitter Serializable = require 'serializable' -{Model} = require 'theorist' {CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' TokenizedBuffer = require './tokenized-buffer' RowMap = require './row-map' Fold = require './fold' +Model = require './model' Token = require './token' Decoration = require './decoration' Marker = require './marker' @@ -21,20 +20,6 @@ module.exports = class DisplayBuffer extends Model Serializable.includeInto(this) - @properties - manageScrollPosition: false - softWrapped: null - editorWidthInChars: null - lineHeightInPixels: null - defaultCharWidth: null - height: null - width: null - scrollTop: 0 - scrollLeft: 0 - scrollWidth: 0 - verticalScrollbarWidth: 15 - horizontalScrollbarHeight: 15 - verticalScrollMargin: 2 horizontalScrollMargin: 6 scopedCharacterWidthsChangeCount: 0 @@ -43,6 +28,7 @@ class DisplayBuffer extends Model super @emitter = new Emitter + @disposables = new CompositeDisposable @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles}) @buffer = @tokenizedBuffer.buffer @@ -51,10 +37,10 @@ class DisplayBuffer extends Model @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} - @subscribe @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings - @subscribe @tokenizedBuffer.onDidChange @handleTokenizedBufferChange - @subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated - @subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated + @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings + @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange + @disposables.add @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated + @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) @@ -69,12 +55,17 @@ class DisplayBuffer extends Model scrollPastEnd: atom.config.get('editor.scrollPastEnd', scope: scopeDescriptor) softWrap: atom.config.get('editor.softWrap', scope: scopeDescriptor) softWrapAtPreferredLineLength: atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) + softWrapHangingIndent: atom.config.get('editor.softWrapHangingIndent', scope: scopeDescriptor) preferredLineLength: atom.config.get('editor.preferredLineLength', scope: scopeDescriptor) subscriptions.add atom.config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrap = newValue @updateWrappedScreenLines() + subscriptions.add atom.config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) => + @configSettings.softWrapHangingIndent = newValue + @updateWrappedScreenLines() + subscriptions.add atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrapAtPreferredLineLength = newValue @updateWrappedScreenLines() if @isSoftWrapped() @@ -131,6 +122,20 @@ class DisplayBuffer extends Model onDidChangeCharacterWidths: (callback) -> @emitter.on 'did-change-character-widths', callback + onDidChangeScrollTop: (callback) -> + @emitter.on 'did-change-scroll-top', callback + + onDidChangeScrollLeft: (callback) -> + @emitter.on 'did-change-scroll-left', callback + + observeScrollTop: (callback) -> + callback(@scrollTop) + @onDidChangeScrollTop(callback) + + observeScrollLeft: (callback) -> + callback(@scrollLeft) + @onDidChangeScrollLeft(callback) + observeDecorations: (callback) -> callback(decoration) for decoration in @getDecorations() @onDidAddDecoration(callback) @@ -147,38 +152,11 @@ class DisplayBuffer extends Model onDidUpdateMarkers: (callback) -> @emitter.on 'did-update-markers', callback - on: (eventName) -> - switch eventName - when 'changed' - Grim.deprecate("Use DisplayBuffer::onDidChange instead") - when 'grammar-changed' - Grim.deprecate("Use DisplayBuffer::onDidChangeGrammar instead") - when 'soft-wrap-changed' - Grim.deprecate("Use DisplayBuffer::onDidChangeSoftWrap instead") - when 'character-widths-changed' - Grim.deprecate("Use DisplayBuffer::onDidChangeCharacterWidths instead") - when 'decoration-added' - Grim.deprecate("Use DisplayBuffer::onDidAddDecoration instead") - when 'decoration-removed' - Grim.deprecate("Use DisplayBuffer::onDidRemoveDecoration instead") - when 'decoration-changed' - Grim.deprecate("Use decoration.getMarker().onDidChange() instead") - when 'decoration-updated' - Grim.deprecate("Use Decoration::onDidChangeProperties instead") - when 'marker-created' - Grim.deprecate("Use Decoration::onDidCreateMarker instead") - when 'markers-updated' - Grim.deprecate("Use Decoration::onDidUpdateMarkers instead") - else - Grim.deprecate("DisplayBuffer::on is deprecated. Use event subscription methods instead.") - - EmitterMixin::on.apply(this, arguments) - emitDidChange: (eventProperties, refreshMarkers=true) -> if refreshMarkers @pauseMarkerChangeEvents() @refreshMarkerScreenPositions() - @emit 'changed', eventProperties + @emit 'changed', eventProperties if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', eventProperties @resumeMarkerChangeEvents() @@ -188,19 +166,29 @@ class DisplayBuffer extends Model @updateAllScreenLines() screenDelta = @getLastRow() - end bufferDelta = 0 - @emitDidChange({ start, end, screenDelta, bufferDelta }) + @emitDidChange({start, end, screenDelta, bufferDelta}) # Sets the visibility of the tokenized buffer. # # visible - A {Boolean} indicating of the tokenized buffer is shown setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) - getVerticalScrollMargin: -> @verticalScrollMargin + getVerticalScrollMargin: -> Math.min(@verticalScrollMargin, (@getHeight() - @getLineHeightInPixels()) / 2) setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - getHorizontalScrollMargin: -> @horizontalScrollMargin + getVerticalScrollMarginInPixels: -> + scrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels() + maxScrollMarginInPixels = (@getHeight() - @getLineHeightInPixels()) / 2 + Math.min(scrollMarginInPixels, maxScrollMarginInPixels) + + getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, (@getWidth() - @getDefaultCharWidth()) / 2) setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin + getHorizontalScrollMarginInPixels: -> + scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth() + maxScrollMarginInPixels = (@getWidth() - @getDefaultCharWidth()) / 2 + Math.min(scrollMarginInPixels, maxScrollMarginInPixels) + getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight setHorizontalScrollbarHeight: (@horizontalScrollbarHeight) -> @horizontalScrollbarHeight @@ -263,26 +251,27 @@ class DisplayBuffer extends Model getScrollTop: -> @scrollTop setScrollTop: (scrollTop) -> - if @manageScrollPosition - @scrollTop = Math.round(Math.max(0, Math.min(@getMaxScrollTop(), scrollTop))) - else - @scrollTop = Math.round(scrollTop) + scrollTop = Math.round(Math.max(0, Math.min(@getMaxScrollTop(), scrollTop))) + unless scrollTop is @scrollTop + @scrollTop = scrollTop + @emitter.emit 'did-change-scroll-top', @scrollTop + @scrollTop getMaxScrollTop: -> @getScrollHeight() - @getClientHeight() - getScrollBottom: -> @scrollTop + @height + getScrollBottom: -> @scrollTop + @getClientHeight() setScrollBottom: (scrollBottom) -> @setScrollTop(scrollBottom - @getClientHeight()) @getScrollBottom() getScrollLeft: -> @scrollLeft setScrollLeft: (scrollLeft) -> - if @manageScrollPosition - @scrollLeft = Math.round(Math.max(0, Math.min(@getScrollWidth() - @getClientWidth(), scrollLeft))) - @scrollLeft - else - @scrollLeft = Math.round(scrollLeft) + scrollLeft = Math.round(Math.max(0, Math.min(@getScrollWidth() - @getClientWidth(), scrollLeft))) + unless scrollLeft is @scrollLeft + @scrollLeft = scrollLeft + @emitter.emit 'did-change-scroll-left', @scrollLeft + @scrollLeft getMaxScrollLeft: -> @getScrollWidth() - @getClientWidth() @@ -329,7 +318,7 @@ class DisplayBuffer extends Model characterWidthsChanged: -> @computeScrollWidth() - @emit 'character-widths-changed', @scopedCharacterWidthsChangeCount + @emit 'character-widths-changed', @scopedCharacterWidthsChangeCount if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount clearScopedCharWidths: -> @@ -348,12 +337,13 @@ class DisplayBuffer extends Model getScrollWidth: -> @scrollWidth + # Returns an {Array} of two numbers representing the first and the last visible rows. getVisibleRowRange: -> return [0, 0] unless @getLineHeightInPixels() > 0 - heightInLines = Math.ceil(@getHeight() / @getLineHeightInPixels()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeightInPixels()) - endRow = Math.min(@getLineCount(), startRow + heightInLines) + endRow = Math.ceil((@getScrollTop() + @getHeight()) / @getLineHeightInPixels()) - 1 + endRow = Math.min(@getLineCount(), endRow) [startRow, endRow] @@ -366,15 +356,16 @@ class DisplayBuffer extends Model @intersectsVisibleRowRange(start.row, end.row + 1) scrollToScreenRange: (screenRange, options) -> - verticalScrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels() - horizontalScrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth() + verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() + horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() - {top, left, height, width} = @pixelRectForScreenRange(screenRange) - bottom = top + height - right = left + width + {top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) + {top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) + bottom = endTop + endHeight + right = endLeft if options?.center - desiredScrollCenter = top + height / 2 + desiredScrollCenter = (top + bottom) / 2 unless @getScrollTop() < desiredScrollCenter < @getScrollBottom() desiredScrollTop = desiredScrollCenter - @getHeight() / 2 desiredScrollBottom = desiredScrollCenter + @getHeight() / 2 @@ -385,15 +376,26 @@ class DisplayBuffer extends Model desiredScrollLeft = left - horizontalScrollMarginInPixels desiredScrollRight = right + horizontalScrollMarginInPixels - if desiredScrollTop < @getScrollTop() - @setScrollTop(desiredScrollTop) - else if desiredScrollBottom > @getScrollBottom() - @setScrollBottom(desiredScrollBottom) + if options?.reversed ? true + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom) + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop) - if desiredScrollLeft < @getScrollLeft() - @setScrollLeft(desiredScrollLeft) - else if desiredScrollRight > @getScrollRight() - @setScrollRight(desiredScrollRight) + if desiredScrollRight > @getScrollRight() + @setScrollRight(desiredScrollRight) + if desiredScrollLeft < @getScrollLeft() + @setScrollLeft(desiredScrollLeft) + else + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop) + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom) + + if desiredScrollLeft < @getScrollLeft() + @setScrollLeft(desiredScrollLeft) + if desiredScrollRight > @getScrollRight() + @setScrollRight(desiredScrollRight) scrollToScreenPosition: (screenPosition, options) -> @scrollToScreenRange(new Range(screenPosition, screenPosition), options) @@ -434,7 +436,7 @@ class DisplayBuffer extends Model @softWrapped = softWrapped @updateWrappedScreenLines() softWrapped = @isSoftWrapped() - @emit 'soft-wrap-changed', softWrapped + @emit 'soft-wrap-changed', softWrapped if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-soft-wrapped', softWrapped softWrapped else @@ -532,6 +534,7 @@ class DisplayBuffer extends Model # bufferRow - The buffer row {Number} to check against unfoldBufferRow: (bufferRow) -> fold.destroy() for fold in @foldsContainingBufferRow(bufferRow) + return # Given a buffer row, this returns the largest fold that starts there. # @@ -671,6 +674,7 @@ class DisplayBuffer extends Model targetLeft = pixelPosition.left defaultCharWidth = @defaultCharWidth row = Math.floor(targetTop / @getLineHeightInPixels()) + targetLeft = 0 if row < 0 targetLeft = Infinity if row > @getLastRow() row = Math.min(row, @getLastRow()) row = Math.max(0, row) @@ -736,7 +740,7 @@ class DisplayBuffer extends Model screenPositionForBufferPosition: (bufferPosition, options) -> throw new Error("This TextEditor has been destroyed") if @isDestroyed() - { row, column } = @buffer.clipPosition(bufferPosition) + {row, column} = @buffer.clipPosition(bufferPosition) [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row) for screenRow in [startScreenRow...endScreenRow] screenLine = @screenLines[screenRow] @@ -770,7 +774,7 @@ class DisplayBuffer extends Model # # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> - { row, column } = @clipScreenPosition(Point.fromObject(screenPosition), options) + {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options) [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row) new Point(bufferRow, @screenLines[row].bufferColumnForScreenColumn(column)) @@ -824,8 +828,8 @@ class DisplayBuffer extends Model # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> - { wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation } = options - { row, column } = Point.fromObject(screenPosition) + {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options + {row, column} = Point.fromObject(screenPosition) if row < 0 row = 0 @@ -858,6 +862,18 @@ class DisplayBuffer extends Model column = screenLine.clipScreenColumn(column, options) new Point(row, column) + # Clip the start and end of the given range to valid positions on screen. + # See {::clipScreenPosition} for more information. + # + # * `range` The {Range} to clip. + # * `options` (optional) See {::clipScreenPosition} `options`. + # Returns a {Range}. + clipScreenRange: (range, options) -> + start = @clipScreenPosition(range.start, options) + end = @clipScreenPosition(range.end, options) + + new Range(start, end) + # Calculates a {Range} representing the start of the {TextBuffer} until the end. # # Returns a {Range}. @@ -900,11 +916,11 @@ class DisplayBuffer extends Model decorateMarker: (marker, decorationParams) -> marker = @getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) - @subscribe decoration.onDidDestroy => @removeDecoration(decoration) + @disposables.add decoration.onDidDestroy => @removeDecoration(decoration) @decorationsByMarkerId[marker.id] ?= [] @decorationsByMarkerId[marker.id].push(decoration) @decorationsById[decoration.id] = decoration - @emit 'decoration-added', decoration + @emit 'decoration-added', decoration if Grim.includeDeprecatedAPIs @emitter.emit 'did-add-decoration', decoration decoration @@ -916,7 +932,7 @@ class DisplayBuffer extends Model if index > -1 decorations.splice(index, 1) delete @decorationsById[decoration.id] - @emit 'decoration-removed', decoration + @emit 'decoration-removed', decoration if Grim.includeDeprecatedAPIs @emitter.emit 'did-remove-decoration', decoration delete @decorationsByMarkerId[marker.id] if decorations.length is 0 @@ -1064,26 +1080,29 @@ class DisplayBuffer extends Model pauseMarkerChangeEvents: -> marker.pauseChangeEvents() for marker in @getMarkers() + return resumeMarkerChangeEvents: -> marker.resumeChangeEvents() for marker in @getMarkers() - @emit 'markers-updated' + @emit 'markers-updated' if Grim.includeDeprecatedAPIs @emitter.emit 'did-update-markers' refreshMarkerScreenPositions: -> for marker in @getMarkers() marker.notifyObservers(textChanged: false) + return destroyed: -> - marker.unsubscribe() for id, marker of @markers + marker.disposables.dispose() for id, marker of @markers @scopedConfigSubscriptions.dispose() - @unsubscribe() + @disposables.dispose() @tokenizedBuffer.destroy() logLines: (start=0, end=@getLastRow()) -> for row in [start..end] line = @tokenizedLineForScreenRow(row).text console.log row, @bufferRowForScreenRow(row), line, line.length + return getRootScopeDescriptor: -> @tokenizedBuffer.rootScopeDescriptor @@ -1091,7 +1110,7 @@ class DisplayBuffer extends Model handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange @updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?) - @setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if @manageScrollPosition and delta < 0 + @setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if delta < 0 updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0] @@ -1144,7 +1163,10 @@ class DisplayBuffer extends Model softWraps = 0 if @isSoftWrapped() while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumn()) - [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) + [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt( + wrapScreenColumn, + @configSettings.softWrapHangingIndent + ) break if wrappedLine.hasOnlySoftWrapIndentation() screenLines.push(wrappedLine) softWraps++ @@ -1204,7 +1226,7 @@ class DisplayBuffer extends Model if marker = @getMarker(textBufferMarker.id) # The marker might have been removed in some other handler called before # this one. Only emit when the marker still exists. - @emit 'marker-created', marker + @emit 'marker-created', marker if Grim.includeDeprecatedAPIs @emitter.emit 'did-create-marker', marker createFoldForMarker: (marker) -> @@ -1213,3 +1235,58 @@ class DisplayBuffer extends Model foldForMarker: (marker) -> @foldsByMarkerId[marker.id] + +if Grim.includeDeprecatedAPIs + DisplayBuffer.properties + softWrapped: null + editorWidthInChars: null + lineHeightInPixels: null + defaultCharWidth: null + height: null + width: null + scrollTop: 0 + scrollLeft: 0 + scrollWidth: 0 + verticalScrollbarWidth: 15 + horizontalScrollbarHeight: 15 + + EmitterMixin = require('emissary').Emitter + + DisplayBuffer::on = (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use DisplayBuffer::onDidChange instead") + when 'grammar-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeGrammar instead") + when 'soft-wrap-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeSoftWrap instead") + when 'character-widths-changed' + Grim.deprecate("Use DisplayBuffer::onDidChangeCharacterWidths instead") + when 'decoration-added' + Grim.deprecate("Use DisplayBuffer::onDidAddDecoration instead") + when 'decoration-removed' + Grim.deprecate("Use DisplayBuffer::onDidRemoveDecoration instead") + when 'decoration-changed' + Grim.deprecate("Use decoration.getMarker().onDidChange() instead") + when 'decoration-updated' + Grim.deprecate("Use Decoration::onDidChangeProperties instead") + when 'marker-created' + Grim.deprecate("Use Decoration::onDidCreateMarker instead") + when 'markers-updated' + Grim.deprecate("Use Decoration::onDidUpdateMarkers instead") + else + Grim.deprecate("DisplayBuffer::on is deprecated. Use event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) +else + DisplayBuffer::softWrapped = null + DisplayBuffer::editorWidthInChars = null + DisplayBuffer::lineHeightInPixels = null + DisplayBuffer::defaultCharWidth = null + DisplayBuffer::height = null + DisplayBuffer::width = null + DisplayBuffer::scrollTop = 0 + DisplayBuffer::scrollLeft = 0 + DisplayBuffer::scrollWidth = 0 + DisplayBuffer::verticalScrollbarWidth = 15 + DisplayBuffer::horizontalScrollbarHeight = 15 diff --git a/src/git-repository.coffee b/src/git-repository.coffee index d523b562f..145b2bf96 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -1,11 +1,10 @@ {basename, join} = require 'path' _ = require 'underscore-plus' -EmitterMixin = require('emissary').Emitter {Emitter, Disposable, CompositeDisposable} = require 'event-kit' fs = require 'fs-plus' GitUtils = require 'git-utils' -{deprecate} = require 'grim' +{includeDeprecatedAPIs, deprecate} = require 'grim' Task = require './task' @@ -43,8 +42,6 @@ Task = require './task' # ``` module.exports = class GitRepository - EmitterMixin.includeInto(this) - @exists: (path) -> if git = @open(path) git.destroy() @@ -96,7 +93,8 @@ class GitRepository @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus) if @project? - @subscriptions.add @project.eachBuffer (buffer) => @subscribeToBuffer(buffer) + @project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer) + @subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer) # Public: Destroy this {GitRepository} object. # @@ -154,20 +152,16 @@ class GitRepository onDidChangeStatuses: (callback) -> @emitter.on 'did-change-statuses', callback - on: (eventName) -> - switch eventName - when 'status-changed' - deprecate 'Use GitRepository::onDidChangeStatus instead' - when 'statuses-changed' - deprecate 'Use GitRepository::onDidChangeStatuses instead' - else - deprecate 'GitRepository::on is deprecated. Use event subscription methods instead.' - EmitterMixin::on.apply(this, arguments) - ### Section: Repository Details ### + # Public: A {String} indicating the type of version control system used by + # this repository. + # + # Returns `"git"`. + getType: -> 'git' + # Public: Returns the {String} path of the repository. getPath: -> @path ?= fs.absolute(@getRepo().getPath()) @@ -245,9 +239,6 @@ class GitRepository # * `path` (optional) {String} path in the repository to get this information # for, only needed if the repository has submodules. getOriginURL: (path) -> @getConfigValue('remote.origin.url', path) - getOriginUrl: (path) -> - deprecate 'Use ::getOriginURL instead.' - @getOriginURL(path) # Public: Returns the upstream branch for the current HEAD, or null if there # is no upstream branch for the current HEAD. @@ -322,7 +313,7 @@ class GitRepository else delete @statuses[relativePath] if currentPathStatus isnt pathStatus - @emit 'status-changed', path, pathStatus + @emit 'status-changed', path, pathStatus if includeDeprecatedAPIs @emitter.emit 'did-change-status', {path, pathStatus} pathStatus @@ -483,5 +474,23 @@ class GitRepository submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} unless statusesUnchanged - @emit 'statuses-changed' + @emit 'statuses-changed' if includeDeprecatedAPIs @emitter.emit 'did-change-statuses' + +if includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(GitRepository) + + GitRepository::on = (eventName) -> + switch eventName + when 'status-changed' + deprecate 'Use GitRepository::onDidChangeStatus instead' + when 'statuses-changed' + deprecate 'Use GitRepository::onDidChangeStatuses instead' + else + deprecate 'GitRepository::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + GitRepository::getOriginUrl = (path) -> + deprecate 'Use ::getOriginURL instead.' + @getOriginURL(path) diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index acdc863bc..568de483a 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -1,13 +1,6 @@ -_ = require 'underscore-plus' -{deprecate} = require 'grim' -{specificity} = require 'clear-cut' -{Subscriber} = require 'emissary' +{Emitter} = require 'event-kit' +{includeDeprecatedAPIs, deprecate} = require 'grim' FirstMate = require 'first-mate' -{ScopeSelector} = FirstMate -ScopedPropertyStore = require 'scoped-property-store' -PropertyAccessors = require 'property-accessors' - -{$, $$} = require './space-pen-extensions' Token = require './token' # Extended: Syntax class holding the grammars used for tokenizing. @@ -18,16 +11,12 @@ Token = require './token' # language-specific comment regexes. See {::getProperty} for more details. module.exports = class GrammarRegistry extends FirstMate.GrammarRegistry - PropertyAccessors.includeInto(this) - Subscriber.includeInto(this) - @deserialize: ({grammarOverridesByPath}) -> grammarRegistry = new GrammarRegistry() grammarRegistry.grammarOverridesByPath = grammarOverridesByPath grammarRegistry atom.deserializers.add(this) - atom.deserializers.add(name: 'Syntax', deserialize: @deserialize) # Support old serialization constructor: -> super(maxTokensPerLine: 100) @@ -48,24 +37,38 @@ class GrammarRegistry extends FirstMate.GrammarRegistry # Returns a {Grammar}, never null. selectGrammar: (filePath, fileContents) -> super + clearObservers: -> + @off() if includeDeprecatedAPIs + @emitter = new Emitter + +if includeDeprecatedAPIs + PropertyAccessors = require 'property-accessors' + PropertyAccessors.includeInto(GrammarRegistry) + + {Subscriber} = require 'emissary' + Subscriber.includeInto(GrammarRegistry) + + # Support old serialization + atom.deserializers.add(name: 'Syntax', deserialize: GrammarRegistry.deserialize) + # Deprecated: Used by settings-view to display snippets for packages - @::accessor 'propertyStore', -> + GrammarRegistry::accessor 'propertyStore', -> deprecate("Do not use this. Use a public method on Config") atom.config.scopedSettingsStore - addProperties: (args...) -> - args.unshift(null) if args.length == 2 + GrammarRegistry::addProperties = (args...) -> + args.unshift(null) if args.length is 2 deprecate 'Consider using atom.config.set() instead. A direct (but private) replacement is available at atom.config.addScopedSettings().' atom.config.addScopedSettings(args...) - removeProperties: (name) -> + GrammarRegistry::removeProperties = (name) -> deprecate 'atom.config.addScopedSettings() now returns a disposable you can call .dispose() on' atom.config.scopedSettingsStore.removeProperties(name) - getProperty: (scope, keyPath) -> + GrammarRegistry::getProperty = (scope, keyPath) -> deprecate 'A direct (but private) replacement is available at atom.config.getRawScopedValue().' atom.config.getRawScopedValue(scope, keyPath) - propertiesForScope: (scope, keyPath) -> + GrammarRegistry::propertiesForScope = (scope, keyPath) -> deprecate 'Use atom.config.getAll instead.' atom.config.settingsForScopeDescriptor(scope, keyPath) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 6eb11ff1f..072ac99a0 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -84,6 +84,8 @@ class GutterComponent delete @lineNumberNodesById[id] delete @oldState.lineNumbers[id] + return + buildLineNumberHTML: (lineNumberState) -> {screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState if screenRow? diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 3bd5197fe..e2c629cb9 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -39,6 +39,8 @@ class HighlightsComponent @domNode.appendChild(highlightNode) @updateHighlightNode(id, highlightState) + return + updateHighlightNode: (id, newHighlightState) -> highlightNode = @highlightNodesById[id] oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0}) @@ -92,6 +94,8 @@ class HighlightsComponent else regionNode.style[property] = '' + return + flashHighlightNodeIfRequested: (id, newHighlightState) -> oldHighlightState = @oldState[id] return unless newHighlightState.flashCount > oldHighlightState.flashCount diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index 821abbc0c..7ffd3d7ef 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -3,13 +3,23 @@ path = require 'path' KeymapManager = require 'atom-keymap' CSON = require 'season' {jQuery} = require 'space-pen' +Grim = require 'grim' + +bundledKeymaps = require('../package.json')?._atomKeymaps KeymapManager::onDidLoadBundledKeymaps = (callback) -> @emitter.on 'did-load-bundled-keymaps', callback KeymapManager::loadBundledKeymaps = -> - @loadKeymap(path.join(@resourcePath, 'keymaps')) - @emit 'bundled-keymaps-loaded' + keymapsPath = path.join(@resourcePath, 'keymaps') + if bundledKeymaps? + for keymapName, keymap of bundledKeymaps + keymapPath = path.join(keymapsPath, keymapName) + @add(keymapPath, keymap) + else + @loadKeymap(keymapsPath) + + @emit 'bundled-keymaps-loaded' if Grim.includeDeprecatedAPIs @emitter.emit 'did-load-bundled-keymaps' KeymapManager::getUserKeymapPath = -> @@ -50,7 +60,7 @@ KeymapManager::subscribeToFileReadFailure = -> else error.message - atom.notifications.addError(message, {detail: detail, dismissable: true}) + atom.notifications.addError(message, {detail, dismissable: true}) # This enables command handlers registered via jQuery to call # `.abortKeyBinding()` on the `jQuery.Event` object passed to the handler. diff --git a/src/language-mode.coffee b/src/language-mode.coffee index b7a52280b..b5529a05e 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -1,14 +1,10 @@ {Range} = require 'text-buffer' _ = require 'underscore-plus' {OnigRegExp} = require 'oniguruma' -{Emitter, Subscriber} = require 'emissary' ScopeDescriptor = require './scope-descriptor' module.exports = class LanguageMode - Emitter.includeInto(this) - Subscriber.includeInto(this) - # Sets up a `LanguageMode` for the given {TextEditor}. # # editor - The {TextEditor} to associate with @@ -16,7 +12,6 @@ class LanguageMode {@buffer} = @editor destroy: -> - @unsubscribe() toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) @@ -29,14 +24,8 @@ class LanguageMode # endRow - The row {Number} to end at toggleLineCommentsForBufferRows: (start, end) -> scope = @editor.scopeDescriptorForBufferPosition([start, 0]) - commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0] - - return unless commentStartEntry? - - commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) -> - entry.scopeSelector is commentStartEntry.scopeSelector - commentStartString = commentStartEntry?.value - commentEndString = commentEndEntry?.value + {commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope) + return unless commentStartString? buffer = @editor.buffer commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') @@ -104,11 +93,13 @@ class LanguageMode [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? @editor.createFold(startRow, endRow) + return # Unfolds all the foldable lines in the buffer. unfoldAll: -> for row in [@buffer.getLastRow()..0] fold.destroy() for fold in @editor.displayBuffer.foldsStartingAtBufferRow(row) + return # Fold all comment and code blocks at a given indentLevel # @@ -120,8 +111,9 @@ class LanguageMode continue unless startRow? # assumption: startRow will always be the min indent level for the entire range - if @editor.indentationForBufferRow(startRow) == indentLevel + if @editor.indentationForBufferRow(startRow) is indentLevel @editor.createFold(startRow, endRow) + return # Given a buffer row, creates a fold at it. # @@ -175,7 +167,7 @@ class LanguageMode continue if @editor.isBufferRowBlank(row) indentation = @editor.indentationForBufferRow(row) if indentation <= startIndentLevel - includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row)) + includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row)) foldEndRow = row if includeRowInFold break @@ -192,11 +184,24 @@ class LanguageMode return false unless 0 <= bufferRow <= @editor.getLastBufferRow() @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() - # Find a row range for a 'paragraph' around specified bufferRow. - # Right now, a paragraph is a block of text bounded by and empty line or a - # block of text that is not the same type (comments next to source code). + # Find a row range for a 'paragraph' around specified bufferRow. A paragraph + # is a block of text bounded by and empty line or a block of text that is not + # the same type (comments next to source code). rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless /\w/.test(@editor.lineTextForBufferRow(bufferRow)) + scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) + {commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope) + commentStartRegex = null + if commentStartString? and not commentEndString? + commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') + commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + + filterCommentStart = (line) -> + if commentStartRegex? + matches = commentStartRegex.searchSync(line) + line = line.substring(matches[0].end) if matches?.length + line + + return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow))) if @isLineCommentedAtBufferRow(bufferRow) isOriginalRowComment = true @@ -208,15 +213,15 @@ class LanguageMode startRow = bufferRow while startRow > firstRow - break if @isLineCommentedAtBufferRow(startRow - 1) != isOriginalRowComment - break unless /\w/.test(@editor.lineTextForBufferRow(startRow - 1)) + break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment + break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1))) startRow-- endRow = bufferRow lastRow = @editor.getLastBufferRow() while endRow < lastRow - break if @isLineCommentedAtBufferRow(endRow + 1) != isOriginalRowComment - break unless /\w/.test(@editor.lineTextForBufferRow(endRow + 1)) + break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment + break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1))) endRow++ new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length]) @@ -276,6 +281,7 @@ class LanguageMode # endRow - The row {Number} to end at autoIndentBufferRows: (startRow, endRow) -> @autoIndentBufferRow(row) for row in [startRow..endRow] + return # Given a buffer row, this indents it. # @@ -320,3 +326,11 @@ class LanguageMode foldEndRegexForScopeDescriptor: (scopeDescriptor) -> @getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern') + + commentStartAndEndStringsForScope: (scope) -> + commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0] + commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) -> + entry.scopeSelector is commentStartEntry.scopeSelector + commentStartString = commentStartEntry?.value + commentEndString = commentEndEntry?.value + {commentStartString, commentEndString} diff --git a/src/lines-component.coffee b/src/lines-component.coffee index fa4ab23b0..5004d4060 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -4,7 +4,6 @@ _ = require 'underscore-plus' CursorsComponent = require './cursors-component' HighlightsComponent = require './highlights-component' -OverlayManager = require './overlay-manager' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -40,13 +39,6 @@ class LinesComponent insertionPoint.setAttribute('select', '.overlayer') @domNode.appendChild(insertionPoint) - insertionPoint = document.createElement('content') - insertionPoint.setAttribute('select', 'atom-overlay') - @overlayManager = new OverlayManager(@presenter, @hostElement) - @domNode.appendChild(insertionPoint) - else - @overlayManager = new OverlayManager(@presenter, @domNode) - updateSync: (state) -> @newState = state.content @oldState ?= {lines: {}} @@ -82,13 +74,12 @@ class LinesComponent @cursorsComponent.updateSync(state) @highlightsComponent.updateSync(state) - @overlayManager?.render(state) - @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth removeLineNodes: -> @removeLineNode(id) for id of @oldState.lines + return removeLineNode: (id) -> @lineNodesByLineId[id].remove() @@ -126,6 +117,8 @@ class LinesComponent @lineNodesByLineId[id] = lineNode @domNode.appendChild(lineNode) + return + buildLineHTML: (id) -> {scrollWidth} = @newState {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id] diff --git a/src/marker.coffee b/src/marker.coffee index 98e3f0265..5d1e35570 100644 --- a/src/marker.coffee +++ b/src/marker.coffee @@ -1,8 +1,5 @@ -{Range} = require 'text-buffer' _ = require 'underscore-plus' -{Subscriber} = require 'emissary' -EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' +{CompositeDisposable, Emitter} = require 'event-kit' Grim = require 'grim' # Essential: Represents a buffer annotation that remains logically stationary @@ -45,9 +42,6 @@ Grim = require 'grim' # See {TextEditor::markBufferRange} for usage. module.exports = class Marker - EmitterMixin.includeInto(this) - Subscriber.includeInto(this) - bufferMarkerSubscription: null oldHeadBufferPosition: null oldHeadScreenPosition: null @@ -62,6 +56,7 @@ class Marker constructor: ({@bufferMarker, @displayBuffer}) -> @emitter = new Emitter + @disposables = new CompositeDisposable @id = @bufferMarker.id @oldHeadBufferPosition = @getHeadBufferPosition() @oldHeadScreenPosition = @getHeadScreenPosition() @@ -69,14 +64,14 @@ class Marker @oldTailScreenPosition = @getTailScreenPosition() @wasValid = @isValid() - @subscribe @bufferMarker.onDidDestroy => @destroyed() - @subscribe @bufferMarker.onDidChange (event) => @notifyObservers(event) + @disposables.add @bufferMarker.onDidDestroy => @destroyed() + @disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event) # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once # destroyed, a marker cannot be restored by undo/redo operations. destroy: -> @bufferMarker.destroy() - @unsubscribe() + @disposables.dispose() # Essential: Creates and returns a new {Marker} with the same properties as this # marker. @@ -118,15 +113,6 @@ class Marker onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback - on: (eventName) -> - switch eventName - when 'changed' - Grim.deprecate("Use Marker::onDidChange instead") - when 'destroyed' - Grim.deprecate("Use Marker::onDidDestroy instead") - - EmitterMixin::on.apply(this, arguments) - ### Section: Marker Details ### @@ -159,9 +145,6 @@ class Marker # the marker. getProperties: -> @bufferMarker.getProperties() - getAttributes: -> - Grim.deprecate 'Use Marker::getProperties instead' - @getProperties() # Essential: Merges an {Object} containing new properties into the marker's # existing properties. @@ -169,16 +152,10 @@ class Marker # * `properties` {Object} setProperties: (properties) -> @bufferMarker.setProperties(properties) - setAttributes: (properties) -> - Grim.deprecate 'Use Marker::getProperties instead' - @setProperties(properties) matchesProperties: (attributes) -> attributes = @displayBuffer.translateToBufferMarkerParams(attributes) @bufferMarker.matchesParams(attributes) - matchesAttributes: (attributes) -> - Grim.deprecate 'Use Marker::matchesProperties instead' - @matchesProperties(attributes) ### Section: Comparing to other markers @@ -284,7 +261,6 @@ class Marker # * `screenPosition` The new {Point} to use # * `properties` (optional) {Object} properties to associate with the marker. setHeadScreenPosition: (screenPosition, properties) -> - screenPosition = @displayBuffer.clipScreenPosition(screenPosition, properties) @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties)) # Extended: Retrieves the buffer position of the marker's tail. @@ -311,7 +287,6 @@ class Marker # * `screenPosition` The new {Point} to use # * `properties` (optional) {Object} properties to associate with the marker. setTailScreenPosition: (screenPosition, options) -> - screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) # Extended: Returns a {Boolean} indicating whether the marker has a tail. @@ -344,7 +319,7 @@ class Marker destroyed: -> delete @displayBuffer.markers[@id] - @emit 'destroyed' + @emit 'destroyed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-destroy' @emitter.dispose() @@ -375,7 +350,7 @@ class Marker if @deferredChangeEvents? @deferredChangeEvents.push(changeEvent) else - @emit 'changed', changeEvent + @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', changeEvent @oldHeadBufferPosition = newHeadBufferPosition @@ -392,8 +367,36 @@ class Marker @deferredChangeEvents = null for event in deferredChangeEvents - @emit 'changed', event + @emit 'changed', event if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', event + return getPixelRange: -> @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false) + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(Marker) + + Marker::on = (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use Marker::onDidChange instead") + when 'destroyed' + Grim.deprecate("Use Marker::onDidDestroy instead") + else + Grim.deprecate("Marker::on is deprecated. Use documented event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) + + Marker::getAttributes = -> + Grim.deprecate 'Use Marker::getProperties instead' + @getProperties() + + Marker::setAttributes = (properties) -> + Grim.deprecate 'Use Marker::setProperties instead' + @setProperties(properties) + + Marker::matchesAttributes = (attributes) -> + Grim.deprecate 'Use Marker::matchesProperties instead' + @matchesProperties(attributes) diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee index a91523b82..aa346200c 100644 --- a/src/menu-helpers.coffee +++ b/src/menu-helpers.coffee @@ -17,6 +17,8 @@ merge = (menu, item, itemSpecificity=Infinity) -> else unless item.type is 'separator' and _.last(menu)?.type is 'separator' menu.push(item) + return + unmerge = (menu, item) -> matchingItemIndex = findMatchingItemIndex(menu, item) matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1 diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index 1991cba95..b5c9a80e3 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -8,6 +8,8 @@ fs = require 'fs-plus' MenuHelpers = require './menu-helpers' +platformMenu = require('../package.json')?._atomMenu?.menu + # Extended: Provides a registry for menu items that you'd like to appear in the # application menu. # @@ -144,10 +146,13 @@ class MenuManager @sendToBrowserProcess(@template, keystrokesByCommand) loadPlatformItems: -> - menusDirPath = path.join(@resourcePath, 'menus') - platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) - {menu} = CSON.readFileSync(platformMenuPath) - @add(menu) + if platformMenu? + @add(platformMenu) + else + menusDirPath = path.join(@resourcePath, 'menus') + platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) + {menu} = CSON.readFileSync(platformMenuPath) + @add(menu) # Merges an item in a submenu aware way such that new items are always # appended to the bottom of existing menus where possible. @@ -164,7 +169,7 @@ class MenuManager filtered = {} for key, bindings of keystrokesByCommand for binding in bindings - continue if binding.indexOf(' ') != -1 + continue if binding.indexOf(' ') isnt -1 filtered[key] ?= [] filtered[key].push(binding) diff --git a/src/model.coffee b/src/model.coffee new file mode 100644 index 000000000..7b38c0eef --- /dev/null +++ b/src/model.coffee @@ -0,0 +1,34 @@ +Grim = require 'grim' +if Grim.includeDeprecatedAPIs + module.exports = require('theorist').Model + return + +PropertyAccessors = require 'property-accessors' + +nextInstanceId = 1 + +module.exports = +class Model + PropertyAccessors.includeInto(this) + + @resetNextInstanceId: -> nextInstanceId = 1 + + alive: true + + constructor: (params) -> + @assignId(params?.id) + + assignId: (id) -> + @id ?= id ? nextInstanceId++ + + @::advisedAccessor 'id', + set: (id) -> nextInstanceId = id + 1 if id >= nextInstanceId + + destroy: -> + return unless @isAlive() + @alive = false + @destroyed?() + + isAlive: -> @alive + + isDestroyed: -> not @isAlive() diff --git a/src/notification.coffee b/src/notification.coffee index 0ca82e724..43044c182 100644 --- a/src/notification.coffee +++ b/src/notification.coffee @@ -27,9 +27,9 @@ class Notification getDetail: -> @options.detail isEqual: (other) -> - @getMessage() == other.getMessage() \ - and @getType() == other.getType() \ - and @getDetail() == other.getDetail() + @getMessage() is other.getMessage() \ + and @getType() is other.getType() \ + and @getDetail() is other.getDetail() dismiss: -> return unless @isDismissable() and not @isDismissed() diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee index c8b5da0e8..21a484fbe 100644 --- a/src/overlay-manager.coffee +++ b/src/overlay-manager.coffee @@ -1,39 +1,44 @@ module.exports = class OverlayManager constructor: (@presenter, @container) -> - @overlayNodesById = {} + @overlaysById = {} render: (state) -> - for decorationId, {pixelPosition, item} of state.content.overlays - @renderOverlay(state, decorationId, item, pixelPosition) + for decorationId, overlay of state.content.overlays + if @shouldUpdateOverlay(decorationId, overlay) + @renderOverlay(state, decorationId, overlay) - for id, overlayNode of @overlayNodesById + for id, {overlayNode} of @overlaysById unless state.content.overlays.hasOwnProperty(id) - delete @overlayNodesById[id] + delete @overlaysById[id] overlayNode.remove() - return + shouldUpdateOverlay: (decorationId, overlay) -> + cachedOverlay = @overlaysById[decorationId] + return true unless cachedOverlay? + cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or + cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left - renderOverlay: (state, decorationId, item, pixelPosition) -> - item = atom.views.getView(item) - unless overlayNode = @overlayNodesById[decorationId] - overlayNode = @overlayNodesById[decorationId] = document.createElement('atom-overlay') - overlayNode.appendChild(item) + measureOverlays: -> + for decorationId, {itemView} of @overlaysById + @measureOverlay(decorationId, itemView) + + measureOverlay: (decorationId, itemView) -> + contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0 + @presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin) + + renderOverlay: (state, decorationId, {item, pixelPosition}) -> + itemView = atom.views.getView(item) + cachedOverlay = @overlaysById[decorationId] + unless overlayNode = cachedOverlay?.overlayNode + overlayNode = document.createElement('atom-overlay') @container.appendChild(overlayNode) + @overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView} - itemWidth = item.offsetWidth - itemHeight = item.offsetHeight + # The same node may be used in more than one overlay. This steals the node + # back if it has been displayed in another overlay. + overlayNode.appendChild(itemView) if overlayNode.childNodes.length is 0 - - {scrollTop, scrollLeft} = state.content - - left = pixelPosition.left - if left + itemWidth - scrollLeft > @presenter.contentFrameWidth and left - itemWidth >= scrollLeft - left -= itemWidth - - top = pixelPosition.top + @presenter.lineHeight - if top + itemHeight - scrollTop > @presenter.height and top - itemHeight - @presenter.lineHeight >= scrollTop - top -= itemHeight + @presenter.lineHeight - - overlayNode.style.top = top + 'px' - overlayNode.style.left = left + 'px' + cachedOverlay.pixelPosition = pixelPosition + overlayNode.style.top = pixelPosition.top + 'px' + overlayNode.style.left = pixelPosition.left + 'px' diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 9f53d45e2..386a65e68 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -1,7 +1,6 @@ path = require 'path' _ = require 'underscore-plus' -EmitterMixin = require('emissary').Emitter {Emitter} = require 'event-kit' fs = require 'fs-plus' Q = require 'q' @@ -28,8 +27,6 @@ ThemePackage = require './theme-package' # settings and also by calling `enablePackage()/disablePackage()`. module.exports = class PackageManager - EmitterMixin.includeInto(this) - constructor: ({configDirPath, @devMode, safeMode, @resourcePath}) -> @emitter = new Emitter @packageDirPaths = [] @@ -57,11 +54,6 @@ class PackageManager # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidLoadInitialPackages: (callback) -> @emitter.on 'did-load-initial-packages', callback - @emitter.on 'did-load-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone - - onDidLoadAll: (callback) -> - Grim.deprecate("Use `::onDidLoadInitialPackages` instead.") - @onDidLoadInitialPackages(callback) # Public: Invoke the given callback when all packages have been activated. # @@ -70,11 +62,6 @@ class PackageManager # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidActivateInitialPackages: (callback) -> @emitter.on 'did-activate-initial-packages', callback - @emitter.on 'did-activate-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone - - onDidActivateAll: (callback) -> - Grim.deprecate("Use `::onDidActivateInitialPackages` instead.") - @onDidActivateInitialPackages(callback) # Public: Invoke the given callback when a package is activated. # @@ -112,16 +99,6 @@ class PackageManager onDidUnloadPackage: (callback) -> @emitter.on 'did-unload-package', callback - on: (eventName) -> - switch eventName - when 'loaded' - Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead' - when 'activated' - Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead' - else - Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.' - EmitterMixin::on.apply(this, arguments) - ### Section: Package system data ### @@ -299,8 +276,7 @@ class PackageManager getPackageDependencies: -> unless @packageDependencies? try - metadataPath = path.join(@resourcePath, 'package.json') - {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {} + @packageDependencies = require('../package.json')?.packageDependencies @packageDependencies ?= {} @packageDependencies @@ -331,7 +307,7 @@ class PackageManager packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath)) packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath) @loadPackage(packagePath) for packagePath in packagePaths - @emit 'loaded' + @emit 'loaded' if Grim.includeDeprecatedAPIs @emitter.emit 'did-load-initial-packages' loadPackage: (nameOrPath) -> @@ -380,7 +356,7 @@ class PackageManager packages = @getLoadedPackagesForTypes(types) promises = promises.concat(activator.activatePackages(packages)) Q.all(promises).then => - @emit 'activated' + @emit 'activated' if Grim.includeDeprecatedAPIs @emitter.emit 'did-activate-initial-packages' # another type of package manager can handle other package types. @@ -394,6 +370,7 @@ class PackageManager for pack in packages promise = @activatePackage(pack.name) promises.push(promise) unless pack.hasActivationCommands() + return @observeDisabledPackages() promises @@ -413,6 +390,7 @@ class PackageManager deactivatePackages: -> atom.config.transact => @deactivatePackage(pack.name) for pack in @getLoadedPackages() + return @unobserveDisabledPackages() # Deactivate the package with the given name @@ -430,3 +408,25 @@ class PackageManager stack = "#{error.stack}\n at #{metadataPath}:1:1" message = "Failed to load the #{path.basename(packagePath)} package" atom.notifications.addError(message, {stack, detail, dismissable: true}) + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(PackageManager) + + PackageManager::on = (eventName) -> + switch eventName + when 'loaded' + Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead' + when 'activated' + Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead' + else + Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + PackageManager::onDidLoadAll = (callback) -> + Grim.deprecate("Use `::onDidLoadInitialPackages` instead.") + @onDidLoadInitialPackages(callback) + + PackageManager::onDidActivateAll = (callback) -> + Grim.deprecate("Use `::onDidActivateInitialPackages` instead.") + @onDidActivateInitialPackages(callback) diff --git a/src/package.coffee b/src/package.coffee index 758894952..74c4f0eb8 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -4,25 +4,19 @@ _ = require 'underscore-plus' async = require 'async' CSON = require 'season' fs = require 'fs-plus' -EmitterMixin = require('emissary').Emitter {Emitter, CompositeDisposable} = require 'event-kit' Q = require 'q' -{deprecate} = require 'grim' +{includeDeprecatedAPIs, deprecate} = require 'grim' ModuleCache = require './module-cache' ScopedProperties = require './scoped-properties' -try - packagesCache = require('../package.json')?._atomPackages ? {} -catch error - packagesCache = {} +packagesCache = require('../package.json')?._atomPackages ? {} # Loads and activates a package's main module and resources such as # stylesheets, keymaps, grammar, editor properties, and menus. module.exports = class Package - EmitterMixin.includeInto(this) - @isBundledPackagePath: (packagePath) -> if atom.packages.devMode return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}") @@ -43,11 +37,11 @@ class Package metadata ?= {} metadata.name = packageName - if metadata.stylesheetMain? + if includeDeprecatedAPIs and metadata.stylesheetMain? deprecate("Use the `mainStyleSheet` key instead of `stylesheetMain` in the `package.json` of `#{packageName}`", {packageName}) metadata.mainStyleSheet = metadata.stylesheetMain - if metadata.stylesheets? + if includeDeprecatedAPIs and metadata.stylesheets? deprecate("Use the `styleSheets` key instead of `stylesheets` in the `package.json` of `#{packageName}`", {packageName}) metadata.styleSheets = metadata.stylesheets @@ -87,14 +81,6 @@ class Package onDidDeactivate: (callback) -> @emitter.on 'did-deactivate', callback - on: (eventName) -> - switch eventName - when 'deactivated' - deprecate 'Use Package::onDidDeactivate instead' - else - deprecate 'Package::on is deprecated. Use event subscription methods instead.' - EmitterMixin::on.apply(this, arguments) - ### Section: Instance Methods ### @@ -174,9 +160,9 @@ class Package if @mainModule? if @mainModule.config? and typeof @mainModule.config is 'object' atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} - else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' + else if includeDeprecatedAPIs and @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' deprecate """Use a config schema instead. See the configuration section - of https://atom.io/docs/latest/creating-a-package and + of https://atom.io/docs/latest/hacking-atom-package-word-count and https://atom.io/docs/api/latest/Config for more details""" atom.config.setDefaults(@name, @mainModule.configDefaults) @mainModule.activateConfig?() @@ -223,12 +209,16 @@ class Package activateServices: -> for name, {versions} of @metadata.providedServices + servicesByVersion = {} for version, methodName of versions - @activationDisposables.add atom.packages.serviceHub.provide(name, version, @mainModule[methodName]()) + if typeof @mainModule[methodName] is 'function' + servicesByVersion[version] = @mainModule[methodName]() + @activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion) for name, {versions} of @metadata.consumedServices for version, methodName of versions - @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) + if typeof @mainModule[methodName] is 'function' + @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return loadKeymaps: -> @@ -236,12 +226,14 @@ class Package @keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps) else @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}] + return loadMenus: -> if @bundledPackage and packagesCache[@name]? @menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus) else @menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}] + return getKeymapPaths: -> keymapsDirPath = path.join(@path, 'keymaps') @@ -262,7 +254,7 @@ class Package [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)] getStylesheetsPath: -> - if fs.isDirectorySync(path.join(@path, 'stylesheets')) + if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'stylesheets')) deprecate("Store package style sheets in the `styles/` directory instead of `stylesheets/` in the `#{@name}` package", packageName: @name) path.join(@path, 'stylesheets') else @@ -333,7 +325,7 @@ class Package deferred = Q.defer() - if fs.isDirectorySync(path.join(@path, 'scoped-properties')) + if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'scoped-properties')) settingsDirPath = path.join(@path, 'scoped-properties') deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name) else @@ -361,7 +353,7 @@ class Package @mainModule?.deactivate?() catch e console.error "Error deactivating package '#{@name}'", e.stack - @emit 'deactivated' + @emit 'deactivated' if includeDeprecatedAPIs @emitter.emit 'did-deactivate' deactivateConfig: -> @@ -445,6 +437,8 @@ class Package @activateNow() break currentTarget = currentTarget.parentElement + return + return getActivationCommands: -> return @activationCommands if @activationCommands? @@ -459,7 +453,7 @@ class Package else if _.isArray(commands) @activationCommands[selector].push(commands...) - if @metadata.activationEvents? + if includeDeprecatedAPIs and @metadata.activationEvents? deprecate """ Use `activationCommands` instead of `activationEvents` in your package.json Commands should be grouped by selector as follows: @@ -503,6 +497,7 @@ class Package for modulePath in fs.listSync(nodeModulesPath) nativeModulePaths.push(modulePath) if @isNativeModule(modulePath) traversePath(path.join(modulePath, 'node_modules')) + return traversePath(path.join(@path, 'node_modules')) nativeModulePaths @@ -563,8 +558,28 @@ class Package SyntaxError: #{error.message} at #{location} """ + else if error.less and error.filename and error.column? and error.line? + # Less errors + location = "#{error.filename}:#{error.line}:#{error.column}" + detail = "#{error.message} in #{location}" + stack = """ + LessError: #{error.message} + at #{location} + """ else detail = error.message stack = error.stack ? error atom.notifications.addFatalError(message, {stack, detail, dismissable: true}) + +if includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(Package) + + Package::on = (eventName) -> + switch eventName + when 'deactivated' + deprecate 'Use Package::onDidDeactivate instead' + else + deprecate 'Package::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee index aaa8c6207..f9dd15697 100644 --- a/src/pane-axis-element.coffee +++ b/src/pane-axis-element.coffee @@ -53,7 +53,7 @@ class PaneAxisElement extends HTMLElement siblingView.remove() view.remove() - childReplaced: ({index, oldChild, newChild}) -> + childReplaced: ({index, oldChild, newChild}) -> focusedElement = document.activeElement if @hasFocus() @childRemoved({child: oldChild, index}) @childAdded({child: newChild, index}) diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index 9d946b391..9489d3222 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -1,7 +1,7 @@ -{Model} = require 'theorist' {Emitter, CompositeDisposable} = require 'event-kit' {flatten} = require 'underscore-plus' Serializable = require 'serializable' +Model = require './model' module.exports = class PaneAxis extends Model diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee index 65ee9ecea..94b008255 100644 --- a/src/pane-container-element.coffee +++ b/src/pane-container-element.coffee @@ -1,4 +1,5 @@ {CompositeDisposable} = require 'event-kit' +Grim = require 'grim' {callAttachHooks} = require './space-pen-extensions' PaneContainerView = null _ = require 'underscore-plus' @@ -8,12 +9,14 @@ class PaneContainerElement extends HTMLElement createdCallback: -> @subscriptions = new CompositeDisposable @classList.add 'panes' - PaneContainerView ?= require './pane-container-view' - @__spacePenView = new PaneContainerView(this) + + if Grim.includeDeprecatedAPIs + PaneContainerView ?= require './pane-container-view' + @__spacePenView = new PaneContainerView(this) initialize: (@model) -> @subscriptions.add @model.observeRoot(@rootChanged.bind(this)) - @__spacePenView.setModel(@model) + @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs this rootChanged: (root) -> diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 14fa25a44..e6eb97075 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -1,7 +1,8 @@ {find, flatten} = require 'underscore-plus' -{Model} = require 'theorist' +Grim = require 'grim' {Emitter, CompositeDisposable} = require 'event-kit' Serializable = require 'serializable' +Model = require './model' Pane = require './pane' PaneElement = require './pane-element' PaneContainerElement = require './pane-container-element' @@ -18,19 +19,14 @@ class PaneContainer extends Model @version: 1 - @properties - activePane: null - root: null - @behavior 'activePaneItem', -> - @$activePane - .switch((activePane) -> activePane?.$activeItem) - .distinctUntilChanged() - constructor: (params) -> super + unless Grim.includeDeprecatedAPIs + @activePane = params?.activePane + @emitter = new Emitter @subscriptions = new CompositeDisposable @@ -151,6 +147,7 @@ class PaneContainer extends Model saveAll: -> pane.saveItems() for pane in @getPanes() + return confirmClose: (options) -> allSaved = true @@ -186,6 +183,7 @@ class PaneContainer extends Model destroyEmptyPanes: -> pane.destroy() for pane in @getPanes() when pane.items.length is 0 + return willDestroyPaneItem: (event) -> @emitter.emit 'will-destroy-pane-item', event @@ -234,3 +232,12 @@ class PaneContainer extends Model removedPaneItem: (item) -> @itemRegistry.removeItem(item) + +if Grim.includeDeprecatedAPIs + PaneContainer.properties + activePane: null + + PaneContainer.behavior 'activePaneItem', -> + @$activePane + .switch((activePane) -> activePane?.$activeItem) + .distinctUntilChanged() diff --git a/src/pane-element.coffee b/src/pane-element.coffee index ac49c86ab..0efcc5528 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -1,6 +1,7 @@ {CompositeDisposable} = require 'event-kit' +Grim = require 'grim' {$, callAttachHooks, callRemoveHooks} = require './space-pen-extensions' -PaneView = require './pane-view' +PaneView = null class PaneElement extends HTMLElement attached: false @@ -12,7 +13,7 @@ class PaneElement extends HTMLElement @initializeContent() @subscribeToDOMEvents() - @createSpacePenShim() + @createSpacePenShim() if Grim.includeDeprecatedAPIs attachedCallback: -> @attached = true @@ -54,6 +55,7 @@ class PaneElement extends HTMLElement @addEventListener 'drop', handleDrop createSpacePenShim: -> + PaneView ?= require './pane-view' @__spacePenView = new PaneView(this) initialize: (@model) -> @@ -62,8 +64,7 @@ class PaneElement extends HTMLElement @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this)) @subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this)) @subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this)) - @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this)) - @__spacePenView.setModel(@model) + @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs this getModel: -> @model diff --git a/src/pane.coffee b/src/pane.coffee index 7e8374efe..38c8d9201 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,11 +1,10 @@ {find, compact, extend, last} = require 'underscore-plus' -{Model} = require 'theorist' {Emitter} = require 'event-kit' Serializable = require 'serializable' Grim = require 'grim' +Model = require './model' PaneAxis = require './pane-axis' TextEditor = require './text-editor' -PaneView = null # Extended: A container for presenting content in the center of the workspace. # Panes can contain multiple items, one of which is *active* at a given time. @@ -16,25 +15,15 @@ class Pane extends Model atom.deserializers.add(this) Serializable.includeInto(this) - @properties - container: undefined - activeItem: undefined - focused: false - flexScale: 1 - - # Public: Only one pane is considered *active* at a time. A pane is activated - # when it is focused, and when focus returns to the pane container after - # moving to another element such as a panel, it returns to the active pane. - @behavior 'active', -> - @$container - .switch((container) -> container?.$activePane) - .map((activePane) => activePane is this) - .distinctUntilChanged() - constructor: (params) -> super + unless Grim.includeDeprecatedAPIs + @container = params?.container + @activeItem = params?.activeItem + @emitter = new Emitter + @itemSubscriptions = new WeakMap @items = [] @addItems(compact(params?.items ? [])) @@ -45,7 +34,7 @@ class Pane extends Model serializeParams: -> if typeof @activeItem?.getURI is 'function' activeItemURI = @activeItem.getURI() - else if typeof @activeItem?.getUri is 'function' + else if Grim.includeDeprecatedAPIs and typeof @activeItem?.getUri is 'function' activeItemURI = @activeItem.getUri() id: @id @@ -62,7 +51,7 @@ class Pane extends Model params.activeItem = find params.items, (item) -> if typeof item.getURI is 'function' itemURI = item.getURI() - else if typeof item.getUri is 'function' + else if Grim.includeDeprecatedAPIs and typeof item.getUri is 'function' itemURI = item.getUri() itemURI is activeItemURI @@ -231,39 +220,6 @@ class Pane extends Model onWillDestroyItem: (callback) -> @emitter.on 'will-destroy-item', callback - on: (eventName) -> - switch eventName - when 'activated' - Grim.deprecate("Use Pane::onDidActivate instead") - when 'destroyed' - Grim.deprecate("Use Pane::onDidDestroy instead") - when 'item-added' - Grim.deprecate("Use Pane::onDidAddItem instead") - when 'item-removed' - Grim.deprecate("Use Pane::onDidRemoveItem instead") - when 'item-moved' - Grim.deprecate("Use Pane::onDidMoveItem instead") - when 'before-item-destroyed' - Grim.deprecate("Use Pane::onWillDestroyItem instead") - else - Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") - super - - behavior: (behaviorName) -> - switch behaviorName - when 'active' - Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.") - when 'container' - Grim.deprecate("The $container behavior property is deprecated.") - when 'activeItem' - Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.") - when 'focused' - Grim.deprecate("The $focused behavior property is deprecated.") - else - Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.") - - super - # Called by the view layer to indicate that the pane has gained focus. focus: -> @focused = true @@ -278,6 +234,10 @@ class Pane extends Model getPanes: -> [this] + unsubscribeFromItem: (item) -> + @itemSubscriptions.get(item)?.dispose() + @itemSubscriptions.delete(item) + ### Section: Items ### @@ -369,11 +329,13 @@ class Pane extends Model addItem: (item, index=@getActiveItemIndex() + 1) -> return if item in @items - if typeof item.on is 'function' + if typeof item.onDidDestroy is 'function' + @itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, true) + else if Grim.includeDeprecatedAPIs and typeof item.on is 'function' @subscribe item, 'destroyed', => @removeItem(item, true) @items.splice(index, 0, item) - @emit 'item-added', item, index + @emit 'item-added', item, index if Grim.includeDeprecatedAPIs @emitter.emit 'did-add-item', {item, index} @setActiveItem(item) unless @getActiveItem()? item @@ -396,8 +358,9 @@ class Pane extends Model index = @items.indexOf(item) return if index is -1 - if typeof item.on is 'function' + if Grim.includeDeprecatedAPIs and typeof item.on is 'function' @unsubscribe item + @unsubscribeFromItem(item) if item is @activeItem if @items.length is 1 @@ -407,7 +370,7 @@ class Pane extends Model else @activatePreviousItem() @items.splice(index, 1) - @emit 'item-removed', item, index, destroyed + @emit 'item-removed', item, index, destroyed if Grim.includeDeprecatedAPIs @emitter.emit 'did-remove-item', {item, index, destroyed} @container?.didDestroyPaneItem({item, index, pane: this}) if destroyed @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes') @@ -420,7 +383,7 @@ class Pane extends Model oldIndex = @items.indexOf(item) @items.splice(oldIndex, 1) @items.splice(newIndex, 0, item) - @emit 'item-moved', item, newIndex + @emit 'item-moved', item, newIndex if Grim.includeDeprecatedAPIs @emitter.emit 'did-move-item', {item, oldIndex, newIndex} # Public: Move the given item to the given index on another pane. @@ -448,7 +411,7 @@ class Pane extends Model destroyItem: (item) -> index = @items.indexOf(item) if index isnt -1 - @emit 'before-item-destroyed', item + @emit 'before-item-destroyed', item if Grim.includeDeprecatedAPIs @emitter.emit 'will-destroy-item', {item, index} @container?.willDestroyPaneItem({item, index, pane: this}) if @promptToSaveItem(item) @@ -461,10 +424,12 @@ class Pane extends Model # Public: Destroy all items. destroyItems: -> @destroyItem(item) for item in @getItems() + return # Public: Destroy all items except for the active item. destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem + return promptToSaveItem: (item, options={}) -> return true unless item.shouldPromptToSave?(options) @@ -539,6 +504,7 @@ class Pane extends Model # Public: Save all items. saveItems: -> @saveItem(item) for item in @getItems() + return # Public: Return the first item that matches the given URI or undefined if # none exists. @@ -553,10 +519,6 @@ class Pane extends Model itemUri is uri - itemForUri: (uri) -> - Grim.deprecate("Use `::itemForURI` instead.") - @itemForURI(uri) - # Public: Activate the first item that matches the given URI. # # Returns a {Boolean} indicating whether an item matching the URI was found. @@ -567,10 +529,6 @@ class Pane extends Model else false - activateItemForUri: (uri) -> - Grim.deprecate("Use `::activateItemForURI` instead.") - @activateItemForURI(uri) - copyActiveItem: -> if @activeItem? @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) @@ -590,7 +548,7 @@ class Pane extends Model throw new Error("Pane has been destroyed") if @isDestroyed() @container?.setActivePane(this) - @emit 'activated' + @emit 'activated' if Grim.includeDeprecatedAPIs @emitter.emit 'did-activate' # Public: Close the pane and destroy all its items. @@ -705,7 +663,7 @@ class Pane extends Model true handleSaveError: (error) -> - if error.message.endsWith('is a directory') + if error.code is 'EISDIR' or error.message.endsWith('is a directory') atom.notifications.addWarning("Unable to save file: #{error.message}") else if error.code is 'EACCES' and error.path? atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'") @@ -715,8 +673,67 @@ class Pane extends Model atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'") else if error.code is 'ENOSPC' and error.path? atom.notifications.addWarning("Unable to save file: No space left on device '#{error.path}'") + else if error.code is 'ENXIO' and error.path? + atom.notifications.addWarning("Unable to save file: No such device or address '#{error.path}'") else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) fileName = errorMatch[1] atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") else throw error + +if Grim.includeDeprecatedAPIs + Pane.properties + container: undefined + activeItem: undefined + focused: false + + Pane.behavior 'active', -> + @$container + .switch((container) -> container?.$activePane) + .map((activePane) => activePane is this) + .distinctUntilChanged() + + Pane::on = (eventName) -> + switch eventName + when 'activated' + Grim.deprecate("Use Pane::onDidActivate instead") + when 'destroyed' + Grim.deprecate("Use Pane::onDidDestroy instead") + when 'item-added' + Grim.deprecate("Use Pane::onDidAddItem instead") + when 'item-removed' + Grim.deprecate("Use Pane::onDidRemoveItem instead") + when 'item-moved' + Grim.deprecate("Use Pane::onDidMoveItem instead") + when 'before-item-destroyed' + Grim.deprecate("Use Pane::onWillDestroyItem instead") + else + Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + super + + Pane::behavior = (behaviorName) -> + switch behaviorName + when 'active' + Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.") + when 'container' + Grim.deprecate("The $container behavior property is deprecated.") + when 'activeItem' + Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.") + when 'focused' + Grim.deprecate("The $focused behavior property is deprecated.") + else + Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.") + + super + + Pane::itemForUri = (uri) -> + Grim.deprecate("Use `::itemForURI` instead.") + @itemForURI(uri) + + Pane::activateItemForUri = (uri) -> + Grim.deprecate("Use `::activateItemForURI` instead.") + @activateItemForURI(uri) +else + Pane::container = undefined + Pane::activeItem = undefined + Pane::focused = undefined diff --git a/src/project.coffee b/src/project.coffee index e0d4bfedd..75cdb714f 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -4,15 +4,14 @@ url = require 'url' _ = require 'underscore-plus' fs = require 'fs-plus' Q = require 'q' -{deprecate} = require 'grim' -{Model} = require 'theorist' -{Subscriber} = require 'emissary' +{includeDeprecatedAPIs, deprecate} = require 'grim' {Emitter} = require 'event-kit' -DefaultDirectoryProvider = require './default-directory-provider' Serializable = require 'serializable' TextBuffer = require 'text-buffer' Grim = require 'grim' +DefaultDirectoryProvider = require './default-directory-provider' +Model = require './model' TextEditor = require './text-editor' Task = require './task' GitRepositoryProvider = require './git-repository-provider' @@ -25,12 +24,6 @@ class Project extends Model atom.deserializers.add(this) Serializable.includeInto(this) - @pathForRepositoryUrl: (repoUrl) -> - deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.' - [repoName] = url.parse(repoUrl).path.split('/')[-1..] - repoName = repoName.replace(/\.git$/, '') - path.join(atom.config.get('core.projectHome'), repoName) - ### Section: Construction and Destruction ### @@ -73,7 +66,9 @@ class Project extends Model @subscribeToBuffer(buffer) for buffer in @buffers - Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") if path? + if Grim.includeDeprecatedAPIs and path? + Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") + paths ?= _.compact([path]) @setPaths(paths) @@ -83,6 +78,7 @@ class Project extends Model destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() + return ### Section: Serialization @@ -118,10 +114,8 @@ class Project extends Model onDidChangePaths: (callback) -> @emitter.on 'did-change-paths', callback - on: (eventName) -> - if eventName is 'path-changed' - Grim.deprecate("Use Project::onDidChangePaths instead") - super + onDidAddBuffer: (callback) -> + @emitter.on 'did-add-buffer', callback ### Section: Accessing the git repository @@ -138,9 +132,6 @@ class Project extends Model # project.repositoryForDirectory.bind(project))) # ``` getRepositories: -> @repositories - getRepo: -> - Grim.deprecate("Use ::getRepositories instead") - @getRepositories()[0] # Public: Get the repository for a given directory asynchronously. # @@ -174,28 +165,23 @@ class Project extends Model # Public: Get an {Array} of {String}s containing the paths of the project's # directories. getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories - getPath: -> - Grim.deprecate("Use ::getPaths instead") - @getPaths()[0] # Public: Set the paths of the project's directories. # # * `projectPaths` {Array} of {String} paths. setPaths: (projectPaths) -> - rootDirectory.off() for rootDirectory in @rootDirectories + if includeDeprecatedAPIs + rootDirectory.off() for rootDirectory in @rootDirectories + repository?.destroy() for repository in @repositories @rootDirectories = [] @repositories = [] @addPath(projectPath, emitEvent: false) for projectPath in projectPaths - @emit "path-changed" + @emit "path-changed" if includeDeprecatedAPIs @emitter.emit 'did-change-paths', projectPaths - setPath: (path) -> - Grim.deprecate("Use ::setPaths instead") - @setPaths([path]) - # Public: Add a path to the project's list of root paths # # * `projectPath` {String} The path to the directory to add. @@ -220,7 +206,7 @@ class Project extends Model @repositories.push(repo ? null) unless options?.emitEvent is false - @emit "path-changed" + @emit "path-changed" if includeDeprecatedAPIs @emitter.emit 'did-change-paths', @getPaths() # Public: remove a path from the project's list of root paths. @@ -240,9 +226,9 @@ class Project extends Model if indexToRemove? [removedDirectory] = @rootDirectories.splice(indexToRemove, 1) [removedRepository] = @repositories.splice(indexToRemove, 1) - removedDirectory.off() + removedDirectory.off() if includeDeprecatedAPIs removedRepository?.destroy() unless removedRepository in @repositories - @emit "path-changed" + @emit "path-changed" if includeDeprecatedAPIs @emitter.emit "did-change-paths", @getPaths() true else @@ -251,13 +237,6 @@ class Project extends Model # Public: Get an {Array} of {Directory}s associated with this project. getDirectories: -> @rootDirectories - getRootDirectory: -> - Grim.deprecate("Use ::getDirectories instead") - @getDirectories()[0] - - resolve: (uri) -> - Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead") - @resolvePath(uri) resolvePath: (uri) -> return unless uri @@ -288,7 +267,6 @@ class Project extends Model # * `relativePath` {String} The relative path from the project directory to # the given path. relativizePath: (fullPath) -> - return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme for rootDirectory in @rootDirectories relativePath = rootDirectory.relativize(fullPath) return [rootDirectory.getPath(), relativePath] unless relativePath is fullPath @@ -324,18 +302,6 @@ class Project extends Model contains: (pathToCheck) -> @rootDirectories.some (dir) -> dir.contains(pathToCheck) - ### - Section: Searching and Replacing - ### - - scan: (regex, options={}, iterator) -> - Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan") - atom.workspace.scan(regex, options, iterator) - - replace: (regex, replacementText, filePaths, iterator) -> - Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace") - atom.workspace.replace(regex, replacementText, filePaths, iterator) - ### Section: Private ### @@ -360,12 +326,6 @@ class Project extends Model @bufferForPath(filePath).then (buffer) => @buildEditorForBuffer(buffer, options) - # Deprecated - openSync: (filePath, options={}) -> - deprecate("Use Project::open instead") - filePath = @resolvePath(filePath) - @buildEditorForBuffer(@bufferForPathSync(filePath), options) - # Retrieves all the {TextBuffer}s in the project; that is, the # buffers for all open files. # @@ -378,7 +338,7 @@ class Project extends Model @findBufferForPath(@resolvePath(filePath))?.isModified() findBufferForPath: (filePath) -> - _.find @buffers, (buffer) -> buffer.getPath() == filePath + _.find @buffers, (buffer) -> buffer.getPath() is filePath # Only to be used in specs bufferForPathSync: (filePath) -> @@ -434,7 +394,8 @@ class Project extends Model addBufferAtIndex: (buffer, index, options={}) -> @buffers.splice(index, 0, buffer) @subscribeToBuffer(buffer) - @emit 'buffer-created', buffer + @emit 'buffer-created', buffer if includeDeprecatedAPIs + @emitter.emit 'did-add-buffer', buffer buffer # Removes a {TextBuffer} association from the project. @@ -473,22 +434,65 @@ class Project extends Model detail: error.message dismissable: true - # Deprecated: delegate - registerOpener: (opener) -> - deprecate("Use Workspace::addOpener instead") - atom.workspace.registerOpener(opener) +if includeDeprecatedAPIs + Project.pathForRepositoryUrl = (repoUrl) -> + deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.' + [repoName] = url.parse(repoUrl).path.split('/')[-1..] + repoName = repoName.replace(/\.git$/, '') + path.join(atom.config.get('core.projectHome'), repoName) - # Deprecated: delegate - unregisterOpener: (opener) -> + Project::registerOpener = (opener) -> + deprecate("Use Workspace::addOpener instead") + atom.workspace.addOpener(opener) + + Project::unregisterOpener = (opener) -> deprecate("Call .dispose() on the Disposable returned from ::addOpener instead") atom.workspace.unregisterOpener(opener) - # Deprecated: delegate - eachEditor: (callback) -> - deprecate("Use Workspace::eachEditor instead") - atom.workspace.eachEditor(callback) + Project::eachEditor = (callback) -> + deprecate("Use Workspace::observeTextEditors instead") + atom.workspace.observeTextEditors(callback) - # Deprecated: delegate - getEditors: -> - deprecate("Use Workspace::getEditors instead") - atom.workspace.getEditors() + Project::getEditors = -> + deprecate("Use Workspace::getTextEditors instead") + atom.workspace.getTextEditors() + + Project::on = (eventName) -> + if eventName is 'path-changed' + Grim.deprecate("Use Project::onDidChangePaths instead") + else + Grim.deprecate("Project::on is deprecated. Use documented event subscription methods instead.") + super + + Project::getRepo = -> + Grim.deprecate("Use ::getRepositories instead") + @getRepositories()[0] + + Project::getPath = -> + Grim.deprecate("Use ::getPaths instead") + @getPaths()[0] + + Project::setPath = (path) -> + Grim.deprecate("Use ::setPaths instead") + @setPaths([path]) + + Project::getRootDirectory = -> + Grim.deprecate("Use ::getDirectories instead") + @getDirectories()[0] + + Project::resolve = (uri) -> + Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead") + @resolvePath(uri) + + Project::scan = (regex, options={}, iterator) -> + Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan") + atom.workspace.scan(regex, options, iterator) + + Project::replace = (regex, replacementText, filePaths, iterator) -> + Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace") + atom.workspace.replace(regex, replacementText, filePaths, iterator) + + Project::openSync = (filePath, options={}) -> + deprecate("Use Project::open instead") + filePath = @resolvePath(filePath) + @buildEditorForBuffer(@bufferForPathSync(filePath), options) diff --git a/src/row-map.coffee b/src/row-map.coffee index fd5dfb2ea..5510c1421 100644 --- a/src/row-map.coffee +++ b/src/row-map.coffee @@ -112,6 +112,7 @@ class RowMap @regions.splice index - 1, 2, bufferRows: leftRegion.bufferRows + rightRegion.bufferRows screenRows: leftRegion.screenRows + rightRegion.screenRows + return # Public: Returns an array of strings describing the map's regions. inspect: -> diff --git a/src/safe-clipboard.coffee b/src/safe-clipboard.coffee new file mode 100644 index 000000000..8301f9d54 --- /dev/null +++ b/src/safe-clipboard.coffee @@ -0,0 +1,6 @@ +# Using clipboard in renderer process is not safe on Linux. +module.exports = + if process.platform is 'linux' and process.type is 'renderer' + require('remote').require('clipboard') + else + require('clipboard') diff --git a/src/scan-handler.coffee b/src/scan-handler.coffee index 71917cc6e..74e15d930 100644 --- a/src/scan-handler.coffee +++ b/src/scan-handler.coffee @@ -34,7 +34,7 @@ module.exports = (rootPaths, regexSource, options) -> scanner.on 'path-found', -> pathsSearched++ - if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK == 0 + if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK is 0 emit('scan:paths-searched', pathsSearched) search regex, scanner, searcher, -> diff --git a/src/scope-descriptor.coffee b/src/scope-descriptor.coffee index 5035810d6..7940cc630 100644 --- a/src/scope-descriptor.coffee +++ b/src/scope-descriptor.coffee @@ -15,7 +15,7 @@ # specific position in the buffer. # * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position. # -# See the [scopes and scope descriptor guide](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors) +# See the [scopes and scope descriptor guide](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) # for more information. module.exports = class ScopeDescriptor @@ -44,3 +44,6 @@ class ScopeDescriptor scope = ".#{scope}" unless scope[0] is '.' scope .join(' ') + + toString: -> + @getScopeChain() diff --git a/src/selection.coffee b/src/selection.coffee index 1ecd35157..f806acf43 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -1,8 +1,8 @@ {Point, Range} = require 'text-buffer' -{Model} = require 'theorist' -{pick} = require 'underscore-plus' +{pick} = _ = require 'underscore-plus' {Emitter} = require 'event-kit' Grim = require 'grim' +Model = require './model' NonWhitespaceRegExp = /\S/ @@ -14,7 +14,6 @@ class Selection extends Model editor: null initialScreenRange: null wordwise: false - needsAutoscroll: null constructor: ({@cursor, @marker, @editor, id}) -> @emitter = new Emitter @@ -28,13 +27,16 @@ class Selection extends Model unless @editor.isDestroyed() @destroyed = true @editor.removeSelection(this) - @emit 'destroyed' + @emit 'destroyed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-destroy' @emitter.dispose() destroy: -> @marker.destroy() + isLastSelection: -> + this is @editor.getLastSelection() + ### Section: Event Subscription ### @@ -61,16 +63,6 @@ class Selection extends Model onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback - on: (eventName) -> - switch eventName - when 'screen-range-changed' - Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.") - when 'destroyed' - Grim.deprecate("Use Selection::onDidDestroy instead.") - - super - - ### Section: Managing the selection range ### @@ -94,19 +86,20 @@ class Selection extends Model # # * `screenRange` The new {Range} to select. # * `options` (optional) {Object} with the keys: - # * `preserveFolds` if `true`, the fold settings are preserved after the selection moves. - # * `autoscroll` if `true`, the {TextEditor} scrolls to the new selection. + # * `preserveFolds` if `true`, the fold settings are preserved after the + # selection moves. + # * `autoscroll` {Boolean} indicating whether to autoscroll to the new + # range. Defaults to `true` if this is the most recently added selection, + # `false` otherwise. setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) - @needsAutoscroll = options.autoscroll options.reversed ?= @isReversed() - @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds + @editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? - @cursor.needsAutoscroll = false if @needsAutoscroll? @marker.setBufferRange(bufferRange, options) - @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition + @autoscroll() if options?.autoscroll ? @isLastSelection() @decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash # Public: Returns the starting and ending buffer rows the selection is @@ -117,7 +110,7 @@ class Selection extends Model range = @getBufferRange() start = range.start.row end = range.end.row - end = Math.max(start, end - 1) if range.end.column == 0 + end = Math.max(start, end - 1) if range.end.column is 0 [start, end] getTailScreenPosition: -> @@ -182,9 +175,15 @@ class Selection extends Model ### # Public: Clears the selection, moving the marker to the head. - clear: -> - @marker.setProperties(goalBufferRange: null) + # + # * `options` (optional) {Object} with the following keys: + # * `autoscroll` {Boolean} indicating whether to autoscroll to the new + # range. Defaults to `true` if this is the most recently added selection, + # `false` otherwise. + clear: (options) -> + @marker.setProperties(goalScreenRange: null) @marker.clearTail() unless @retainSelection + @autoscroll() if options?.autoscroll ? @isLastSelection() @finalize() # Public: Selects the text from the current cursor position to a given screen @@ -251,8 +250,7 @@ class Selection extends Model # Public: Selects all the text in the buffer. selectAll: -> - @editor.unfoldAll() - @setBufferRange(@editor.buffer.getRange(), autoscroll: false, preserveFolds: true) + @setBufferRange(@editor.buffer.getRange(), autoscroll: false) # Public: Selects all the text from the current cursor position to the # beginning of the line. @@ -358,7 +356,6 @@ class Selection extends Model @editor.unfoldBufferRow(oldBufferRange.end.row) wasReversed = @isReversed() @clear() - @cursor.needsAutoscroll = @cursor.isLastCursor() autoIndentFirstLine = false precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) @@ -384,12 +381,12 @@ class Selection extends Model if options.select @setBufferRange(newBufferRange, reversed: wasReversed) else - @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed + @cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed if autoIndentFirstLine @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) - if options.autoIndentNewline and text == '\n' + if options.autoIndentNewline and text is '\n' currentIndentation = @editor.indentationForBufferRow(newBufferRange.start.row) @editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false) if @editor.indentationForBufferRow(newBufferRange.end.row) < currentIndentation @@ -397,6 +394,8 @@ class Selection extends Model else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text) @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) + @autoscroll() if @isLastSelection() + newBufferRange # Public: Removes the first character before the selection if the selection @@ -405,16 +404,6 @@ class Selection extends Model @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow()) @deleteSelectedText() - # Deprecated: Use {::deleteToBeginningOfWord} instead. - backspaceToBeginningOfWord: -> - deprecate("Use Selection::deleteToBeginningOfWord() instead") - @deleteToBeginningOfWord() - - # Deprecated: Use {::deleteToBeginningOfLine} instead. - backspaceToBeginningOfLine: -> - deprecate("Use Selection::deleteToBeginningOfLine() instead") - @deleteToBeginningOfLine() - # Public: Removes from the start of the selection to the beginning of the # current word if the selection is empty otherwise it deletes the selection. deleteToBeginningOfWord: -> @@ -537,6 +526,7 @@ class Selection extends Model for row in [start..end] if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length buffer.delete [[row, 0], [row, matchLength]] + return # Public: Sets the indentation level of all selected rows to values suggested # by the relevant grammars. @@ -611,7 +601,7 @@ class Selection extends Model # of levels. Leaves the first line unchanged. adjustIndent: (lines, indentAdjustment) -> for line, i in lines - if indentAdjustment == 0 or line is '' + if indentAdjustment is 0 or line is '' continue else if indentAdjustment > 0 lines[i] = @editor.buildIndentString(indentAdjustment) + line @@ -619,6 +609,7 @@ class Selection extends Model currentIndentLevel = @editor.indentLevelForLine(lines[i]) indentLevel = Math.max(0, currentIndentLevel + indentAdjustment) lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel)) + return # Indent the current line(s). # @@ -629,8 +620,8 @@ class Selection extends Model # * `options` (optional) {Object} with the keys: # * `autoIndent` If `true`, the line is indented to an automatically-inferred # level. Otherwise, {TextEditor::getTabText} is inserted. - indent: ({ autoIndent }={}) -> - { row, column } = @cursor.getBufferPosition() + indent: ({autoIndent}={}) -> + {row, column} = @cursor.getBufferPosition() if @isEmpty() @cursor.skipLeadingWhitespace() @@ -649,7 +640,8 @@ class Selection extends Model indentSelectedRows: -> [start, end] = @getBufferRowRange() for row in [start..end] - @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 + @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0 + return ### Section: Managing multiple selections @@ -657,53 +649,59 @@ class Selection extends Model # Public: Moves the selection down one row. addSelectionBelow: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() + range = (@getGoalScreenRange() ? @getScreenRange()).copy() nextRow = range.end.row + 1 - for row in [nextRow..@editor.getLastBufferRow()] + for row in [nextRow..@editor.getLastScreenRow()] range.start.row = row range.end.row = row - clippedRange = @editor.clipBufferRange(range) + clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) if range.isEmpty() continue if range.end.column > 0 and clippedRange.end.column is 0 else continue if clippedRange.isEmpty() - @editor.addSelectionForBufferRange(range, goalBufferRange: range) + @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range) break + return + # Public: Moves the selection up one row. addSelectionAbove: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() + range = (@getGoalScreenRange() ? @getScreenRange()).copy() previousRow = range.end.row - 1 for row in [previousRow..0] range.start.row = row range.end.row = row - clippedRange = @editor.clipBufferRange(range) + clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) if range.isEmpty() continue if range.end.column > 0 and clippedRange.end.column is 0 else continue if clippedRange.isEmpty() - @editor.addSelectionForBufferRange(range, goalBufferRange: range) + @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range) break + return + # Public: Combines the given selection into this selection and then destroys # the given selection. # # * `otherSelection` A {Selection} to merge with. # * `options` (optional) {Object} options matching those found in {::setBufferRange}. merge: (otherSelection, options) -> - myGoalBufferRange = @getGoalBufferRange() - otherGoalBufferRange = otherSelection.getGoalBufferRange() - if myGoalBufferRange? and otherGoalBufferRange? - options.goalBufferRange = myGoalBufferRange.union(otherGoalBufferRange) + myGoalScreenRange = @getGoalScreenRange() + otherGoalScreenRange = otherSelection.getGoalScreenRange() + + if myGoalScreenRange? and otherGoalScreenRange? + options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange) else - options.goalBufferRange = myGoalBufferRange ? otherGoalBufferRange - @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) + options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange + + @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), _.extend(autoscroll: false, options)) otherSelection.destroy() ### @@ -734,7 +732,7 @@ class Selection extends Model newScreenRange: @getScreenRange() selection: this - @emit 'screen-range-changed', @getScreenRange() # old event + @emit 'screen-range-changed', @getScreenRange() if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-range' @editor.selectionRangeChanged(eventObject) @@ -745,10 +743,12 @@ class Selection extends Model @linewise = false autoscroll: -> - @editor.scrollToScreenRange(@getScreenRange()) + if @marker.hasTail() + @editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed()) + else + @cursor.autoscroll() clearAutoscroll: -> - @needsAutoscroll = null modifySelection: (fn) -> @retainSelection = true @@ -764,6 +764,28 @@ class Selection extends Model plantTail: -> @marker.plantTail() - getGoalBufferRange: -> - if goalBufferRange = @marker.getProperties().goalBufferRange - Range.fromObject(goalBufferRange) + getGoalScreenRange: -> + if goalScreenRange = @marker.getProperties().goalScreenRange + Range.fromObject(goalScreenRange) + +if Grim.includeDeprecatedAPIs + Selection::on = (eventName) -> + switch eventName + when 'screen-range-changed' + Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.") + when 'destroyed' + Grim.deprecate("Use Selection::onDidDestroy instead.") + else + Grim.deprecate("Selection::on is deprecated. Use documented event subscription methods instead.") + + super + + # Deprecated: Use {::deleteToBeginningOfWord} instead. + Selection::backspaceToBeginningOfWord = -> + deprecate("Use Selection::deleteToBeginningOfWord() instead") + @deleteToBeginningOfWord() + + # Deprecated: Use {::deleteToBeginningOfLine} instead. + Selection::backspaceToBeginningOfLine = -> + deprecate("Use Selection::deleteToBeginningOfLine() instead") + @deleteToBeginningOfLine() diff --git a/src/selector-validator.coffee b/src/selector-validator.coffee deleted file mode 100644 index f8dcb240a..000000000 --- a/src/selector-validator.coffee +++ /dev/null @@ -1,28 +0,0 @@ -selectorCache = null -testElement = null - -# Parses CSS selectors and memoizes their validity so each selector will only -# be parsed once. -exports.isSelectorValid = (selector) -> - selectorCache ?= {} - cachedValue = selectorCache[selector] - return cachedValue if cachedValue? - - testElement ?= document.createElement('div') - try - # querySelector appears to be faster than webkitMatchesSelector - # http://jsperf.com/query-vs-matches - testElement.querySelector(selector) - selectorCache[selector] = true - true - catch selectorError - selectorCache[selector] = false - false - -# Parse the given CSS selector and throw an error if it is invalid. -exports.validateSelector = (selector) -> - return if exports.isSelectorValid(selector) - - error = new Error("'#{selector}' is not a valid selector") - error.code = 'EBADSELECTOR' - throw error diff --git a/src/style-manager.coffee b/src/style-manager.coffee index c891223b9..cfe86b3fe 100644 --- a/src/style-manager.coffee +++ b/src/style-manager.coffee @@ -152,6 +152,8 @@ class StyleManager for styleElement in styleElementsToRestore @addStyleElement(styleElement) unless styleElement in existingStyleElements + return + ### Section: Paths ### diff --git a/src/styles-element.coffee b/src/styles-element.coffee index d333a2e45..74ebd23ba 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -1,4 +1,5 @@ {Emitter, CompositeDisposable} = require 'event-kit' +{includeDeprecatedAPIs} = require 'grim' class StylesElement extends HTMLElement subscriptions: null @@ -18,7 +19,7 @@ class StylesElement extends HTMLElement @styleElementClonesByOriginalElement = new WeakMap attachedCallback: -> - if @context is 'atom-text-editor' + if includeDeprecatedAPIs and @context is 'atom-text-editor' for styleElement in @children @upgradeDeprecatedSelectors(styleElement) @initialize() @@ -46,6 +47,7 @@ class StylesElement extends HTMLElement @styleElementRemoved(child) for child in Array::slice.call(@children) @context = @getAttribute('context') @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() + return styleElementAdded: (styleElement) -> return unless @styleElementMatchesContext(styleElement) @@ -65,7 +67,7 @@ class StylesElement extends HTMLElement @insertBefore(styleElementClone, insertBefore) - if @context is 'atom-text-editor' + if includeDeprecatedAPIs and @context is 'atom-text-editor' @upgradeDeprecatedSelectors(styleElementClone) @emitter.emit 'did-add-style-element', styleElementClone diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 75a9a5136..4c8e567f0 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -11,6 +11,7 @@ InputComponent = require './input-component' LinesComponent = require './lines-component' ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' +OverlayManager = require './overlay-manager' module.exports = class TextEditorComponent @@ -39,7 +40,6 @@ class TextEditorComponent @lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin? @disposables = new CompositeDisposable - @editor.manageScrollPosition = true @observeConfig() @setScrollSensitivity(atom.config.get('editor.scrollSensitivity')) @@ -57,8 +57,14 @@ class TextEditorComponent @domNode = document.createElement('div') if @useShadowDOM @domNode.classList.add('editor-contents--private') + + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', 'atom-overlay') + @domNode.appendChild(insertionPoint) + @overlayManager = new OverlayManager(@presenter, @hostElement) else @domNode.classList.add('editor-contents') + @overlayManager = new OverlayManager(@presenter, @domNode) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -111,7 +117,7 @@ class TextEditorComponent @cursorMoved = false @selectionChanged = false - if @editor.getLastSelection()? and !@editor.getLastSelection().isEmpty() + if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty() @domNode.classList.add('has-selection') else @domNode.classList.remove('has-selection') @@ -141,15 +147,19 @@ class TextEditorComponent @verticalScrollbarComponent.updateSync(@newState) @scrollbarCornerComponent.updateSync(@newState) + @overlayManager?.render(@newState) + if @editor.isAlive() @updateParentViewFocusedClassIfNeeded() @updateParentViewMiniClass() - @hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved - @hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged - @hostElement.__spacePenView.trigger 'editor:display-updated' + if grim.includeDeprecatedAPIs + @hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved + @hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged + @hostElement.__spacePenView.trigger 'editor:display-updated' readAfterUpdateSync: => @linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically + @overlayManager?.measureOverlays() mountGutterComponent: -> @gutterComponent = new GutterComponent({@editor, onMouseDown: @onGutterMouseDown}) @@ -160,7 +170,8 @@ class TextEditorComponent @measureScrollbars() if @measureScrollbarsWhenShown @sampleFontStyling() @sampleBackgroundColors() - @measureHeightAndWidth() + @measureWindowSize() + @measureDimensions() @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown @editor.setVisible(true) @@ -545,7 +556,7 @@ class TextEditorComponent pasteSelectionClipboard = (event) => if event?.which is 2 and process.platform is 'linux' - if selection = require('clipboard').readText('selection') + if selection = require('./safe-clipboard').readText('selection') @editor.insertText(selection) window.addEventListener('mousemove', onMouseMove) @@ -557,8 +568,9 @@ class TextEditorComponent pollDOM: => unless @checkForVisibilityChange() @sampleBackgroundColors() - @measureHeightAndWidth() + @measureDimensions() @sampleFontStyling() + @overlayManager?.measureOverlays() checkForVisibilityChange: -> if @isVisible() @@ -576,13 +588,14 @@ class TextEditorComponent @heightAndWidthMeasurementRequested = true requestAnimationFrame => @heightAndWidthMeasurementRequested = false - @measureHeightAndWidth() + @measureDimensions() + @measureWindowSize() # Measure explicitly-styled height and width and relay them to the model. If # these values aren't explicitly styled, we assume the editor is unconstrained # and use the scrollHeight / scrollWidth as its height and width in # calculations. - measureHeightAndWidth: -> + measureDimensions: -> return unless @mounted {position} = getComputedStyle(@hostElement) @@ -603,6 +616,16 @@ class TextEditorComponent if clientWidth > 0 @presenter.setContentFrameWidth(clientWidth) + @presenter.setBoundingClientRect(@hostElement.getBoundingClientRect()) + + measureWindowSize: -> + return unless @mounted + + # FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value + # when window gets resized through `atom.setWindowDimensions({width: + # windowWidth, height: windowHeight})`. + @presenter.setWindowSize(window.innerWidth, window.innerHeight) + sampleFontStyling: => oldFontSize = @fontSize oldFontFamily = @fontFamily @@ -729,15 +752,6 @@ class TextEditorComponent setShowIndentGuide: (showIndentGuide) -> atom.config.set("editor.showIndentGuide", showIndentGuide) - # Deprecated - setInvisibles: (invisibles={}) -> - grim.deprecate "Use config.set('editor.invisibles', invisibles) instead" - atom.config.set('editor.invisibles', invisibles) - - # Deprecated - setShowInvisibles: (showInvisibles) -> - atom.config.set('editor.showInvisibles', showInvisibles) - setScrollSensitivity: (scrollSensitivity) => if scrollSensitivity = parseInt(scrollSensitivity) @scrollSensitivity = Math.abs(scrollSensitivity) / 100 @@ -770,3 +784,12 @@ class TextEditorComponent updateParentViewMiniClass: -> @hostElement.classList.toggle('mini', @editor.isMini()) @rootElement.classList.toggle('mini', @editor.isMini()) + +if grim.includeDeprecatedAPIs + TextEditorComponent::setInvisibles = (invisibles={}) -> + grim.deprecate "Use config.set('editor.invisibles', invisibles) instead" + atom.config.set('editor.invisibles', invisibles) + + TextEditorComponent::setShowInvisibles = (showInvisibles) -> + grim.deprecate "Use config.set('editor.showInvisibles', showInvisibles) instead" + atom.config.set('editor.showInvisibles', showInvisibles) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index ccd69dca8..4a7a11147 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -3,6 +3,7 @@ Path = require 'path' {defaults} = require 'underscore-plus' TextBuffer = require 'text-buffer' +Grim = require 'grim' TextEditor = require './text-editor' TextEditorComponent = require './text-editor-component' TextEditorView = null @@ -20,7 +21,7 @@ class TextEditorElement extends HTMLElement createdCallback: -> @emitter = new Emitter @initializeContent() - @createSpacePenShim() + @createSpacePenShim() if Grim.includeDeprecatedAPIs @addEventListener 'focus', @focused.bind(this) @addEventListener 'blur', @blurred.bind(this) @@ -86,7 +87,7 @@ class TextEditorElement extends HTMLElement @model.onDidChangeGrammar => @addGrammarScopeAttribute() @model.onDidChangeEncoding => @addEncodingAttribute() @model.onDidDestroy => @unmountComponent() - @__spacePenView.setModel(@model) + @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs @model getModel: -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 70eef98a9..5b30178d6 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -9,9 +9,10 @@ class TextEditorPresenter stoppedScrollingTimeoutId: null mouseWheelScreenRow: null scopedCharacterWidthsChangeCount: 0 + overlayDimensions: {} constructor: (params) -> - {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft} = params + {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @@ -61,6 +62,7 @@ class TextEditorPresenter @[flagName] = true else fn.apply(this) + @[flagName] = false @emitDidUpdateState() @@ -69,6 +71,11 @@ class TextEditorPresenter getState: -> @updating = true + @updateContentDimensions() + @updateScrollbarDimensions() + @updateStartRow() + @updateEndRow() + @updateFocusedState() if @shouldUpdateFocusedState @updateHeightState() if @shouldUpdateHeightState @updateVerticalScrollState() if @shouldUpdateVerticalScrollState @@ -83,20 +90,6 @@ class TextEditorPresenter @updateGutterState() if @shouldUpdateGutterState @updateLineNumbersState() if @shouldUpdateLineNumbersState - @shouldUpdateFocusedState = false - @shouldUpdateHeightState = false - @shouldUpdateVerticalScrollState = false - @shouldUpdateHorizontalScrollState = false - @shouldUpdateScrollbarsState = false - @shouldUpdateHiddenInputState = false - @shouldUpdateContentState = false - @shouldUpdateDecorations = false - @shouldUpdateLinesState = false - @shouldUpdateCursorsState = false - @shouldUpdateOverlaysState = false - @shouldUpdateGutterState = false - @shouldUpdateLineNumbersState = false - @updating = false @state @@ -132,6 +125,7 @@ class TextEditorPresenter @disposables.add @model.onDidChangeScrollLeft(@setScrollLeft.bind(this)) @observeDecoration(decoration) for decoration in @model.getDecorations() @observeCursor(cursor) for cursor in @model.getCursors() + return observeConfig: -> configParams = {scope: @model.getRootScopeDescriptor()} @@ -281,6 +275,7 @@ class TextEditorPresenter for id, line of @state.content.lines unless visibleLineIds.hasOwnProperty(id) delete @state.content.lines[id] + return updateLineState: (row, line) -> lineState = @state.content.lines[line.id] @@ -304,6 +299,7 @@ class TextEditorPresenter updateCursorsState: -> @batch "shouldUpdateCursorsState", -> @state.content.cursors = {} @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation + return updateCursorState: (cursor, destroyOnly = false) -> delete @state.content.cursors[cursor.id] @@ -319,7 +315,7 @@ class TextEditorPresenter @emitDidUpdateState() updateOverlaysState: -> @batch "shouldUpdateOverlaysState", -> - return unless @hasPixelRectRequirements() + return unless @hasOverlayPositionRequirements() visibleDecorationIds = {} @@ -332,13 +328,41 @@ class TextEditorPresenter else screenPosition = decoration.getMarker().getHeadScreenPosition() + pixelPosition = @pixelPositionForScreenPosition(screenPosition) + + {scrollTop, scrollLeft} = @state.content + gutterWidth = @boundingClientRect.width - @contentFrameWidth + + top = pixelPosition.top + @lineHeight - scrollTop + left = pixelPosition.left + gutterWidth - scrollLeft + + if overlayDimensions = @overlayDimensions[decoration.id] + {itemWidth, itemHeight, contentMargin} = overlayDimensions + + rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth + left -= rightDiff if rightDiff > 0 + + leftDiff = left + @boundingClientRect.left + contentMargin + left -= leftDiff if leftDiff < 0 + + if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0 + top -= itemHeight + @lineHeight + + pixelPosition.top = top + pixelPosition.left = left + @state.content.overlays[decoration.id] ?= {item} - @state.content.overlays[decoration.id].pixelPosition = @pixelPositionForScreenPosition(screenPosition) + @state.content.overlays[decoration.id].pixelPosition = pixelPosition visibleDecorationIds[decoration.id] = true for id of @state.content.overlays delete @state.content.overlays[id] unless visibleDecorationIds[id] + for id of @overlayDimensions + delete @overlayDimensions[id] unless visibleDecorationIds[id] + + return + updateGutterState: -> @batch "shouldUpdateGutterState", -> @state.gutter.visible = not @model.isMini() and (@model.isGutterVisible() ? true) and @showLineNumbers @state.gutter.maxLineNumberDigits = @model.getLineCount().toString().length @@ -390,6 +414,8 @@ class TextEditorPresenter for id of @state.gutter.lineNumbers delete @state.gutter.lineNumbers[id] unless visibleLineNumberIds[id] + return + updateStartRow: -> return unless @scrollTop? and @lineHeight? @@ -567,6 +593,7 @@ class TextEditorPresenter @updateLinesState() @updateCursorsState() @updateLineNumbersState() + @updateOverlaysState() didStartScrolling: -> if @stoppedScrollingTimeoutId? @@ -594,6 +621,7 @@ class TextEditorPresenter @updateHorizontalScrollState() @updateHiddenInputState() @updateCursorsState() unless oldScrollLeft? + @updateOverlaysState() setHorizontalScrollbarHeight: (horizontalScrollbarHeight) -> unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight @@ -658,6 +686,24 @@ class TextEditorPresenter @updateLinesState() @updateCursorsState() unless oldContentFrameWidth? + setBoundingClientRect: (boundingClientRect) -> + unless @clientRectsEqual(@boundingClientRect, boundingClientRect) + @boundingClientRect = boundingClientRect + @updateOverlaysState() + + clientRectsEqual: (clientRectA, clientRectB) -> + clientRectA? and clientRectB? and + clientRectA.top is clientRectB.top and + clientRectA.left is clientRectB.left and + clientRectA.width is clientRectB.width and + clientRectA.height is clientRectB.height + + setWindowSize: (width, height) -> + if @windowWidth isnt width or @windowHeight isnt height + @windowWidth = width + @windowHeight = height + @updateOverlaysState() + setBackgroundColor: (backgroundColor) -> unless @backgroundColor is backgroundColor @backgroundColor = backgroundColor @@ -778,6 +824,9 @@ class TextEditorPresenter hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? + hasOverlayPositionRequirements: -> + @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight + pixelRectForScreenRange: (screenRange) -> if screenRange.end.row > screenRange.start.row top = @pixelPositionForScreenPosition(screenRange.start).top @@ -881,11 +930,13 @@ class TextEditorPresenter unless visibleHighlights[id] delete @state.content.highlights[id] + return removeFromLineDecorationCaches: (decoration, range) -> for row in [range.start.row..range.end.row] by 1 delete @lineDecorationsByScreenRow[row]?[decoration.id] delete @lineNumberDecorationsByScreenRow[row]?[decoration.id] + return addToLineDecorationCaches: (decoration, range) -> marker = decoration.getMarker() @@ -911,6 +962,8 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow[row] ?= {} @lineNumberDecorationsByScreenRow[row][decoration.id] = decoration + return + updateHighlightState: (decoration) -> return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements() @@ -991,6 +1044,18 @@ class TextEditorPresenter regions + setOverlayDimensions: (decorationId, itemWidth, itemHeight, contentMargin) -> + @overlayDimensions[decorationId] ?= {} + overlayState = @overlayDimensions[decorationId] + dimensionsAreEqual = overlayState.itemWidth is itemWidth and + overlayState.itemHeight is itemHeight and + overlayState.contentMargin is contentMargin + unless dimensionsAreEqual + overlayState.itemWidth = itemWidth + overlayState.itemHeight = itemHeight + overlayState.contentMargin = contentMargin + @updateOverlaysState() + observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => @updateHiddenInputState() if cursor.isLastCursor() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b44b6e276..aa0cd8c26 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2,14 +2,13 @@ _ = require 'underscore-plus' path = require 'path' Serializable = require 'serializable' Delegator = require 'delegato' -{deprecate} = require 'grim' -{Model} = require 'theorist' -EmitterMixin = require('emissary').Emitter +{includeDeprecatedAPIs, deprecate} = require 'grim' {CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = TextBuffer = require 'text-buffer' LanguageMode = require './language-mode' DisplayBuffer = require './display-buffer' Cursor = require './cursor' +Model = require './model' Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector {Directory} = require "pathwatcher" @@ -74,14 +73,11 @@ class TextEditor extends Model 'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows', toProperty: 'languageMode' - @delegatesProperties '$lineHeightInPixels', '$defaultCharWidth', '$height', '$width', - '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft', - 'manageScrollPosition', toProperty: 'displayBuffer' - constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, @gutterVisible}) -> super @emitter = new Emitter + @disposables = new CompositeDisposable @cursors = [] @selections = [] @@ -108,11 +104,12 @@ class TextEditor extends Model @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) - @subscribe @$scrollTop, (scrollTop) => - @emit 'scroll-top-changed', scrollTop + @disposables.add @displayBuffer.onDidChangeScrollTop (scrollTop) => + @emit 'scroll-top-changed', scrollTop if includeDeprecatedAPIs @emitter.emit 'did-change-scroll-top', scrollTop - @subscribe @$scrollLeft, (scrollLeft) => - @emit 'scroll-left-changed', scrollLeft + + @disposables.add @displayBuffer.onDidChangeScrollLeft (scrollLeft) => + @emit 'scroll-left-changed', scrollLeft if includeDeprecatedAPIs @emitter.emit 'did-change-scroll-left', scrollLeft atom.workspace?.editorAdded(this) if registerEditor @@ -131,37 +128,39 @@ class TextEditor extends Model subscribeToBuffer: -> @buffer.retain() - @subscribe @buffer.onDidChangePath => + @disposables.add @buffer.onDidChangePath => unless atom.project.getPaths().length > 0 atom.project.setPaths([path.dirname(@getPath())]) - @emit "title-changed" + @emit "title-changed" if includeDeprecatedAPIs @emitter.emit 'did-change-title', @getTitle() - @emit "path-changed" + @emit "path-changed" if includeDeprecatedAPIs @emitter.emit 'did-change-path', @getPath() - @subscribe @buffer.onDidChangeEncoding => + @disposables.add @buffer.onDidChangeEncoding => @emitter.emit 'did-change-encoding', @getEncoding() - @subscribe @buffer.onDidDestroy => @destroy() + @disposables.add @buffer.onDidDestroy => @destroy() # TODO: remove these when we remove the deprecations. They are old events. - @subscribe @buffer.onDidStopChanging => @emit "contents-modified" - @subscribe @buffer.onDidConflict => @emit "contents-conflicted" - @subscribe @buffer.onDidChangeModified => @emit "modified-status-changed" + if includeDeprecatedAPIs + @subscribe @buffer.onDidStopChanging => @emit "contents-modified" + @subscribe @buffer.onDidConflict => @emit "contents-conflicted" + @subscribe @buffer.onDidChangeModified => @emit "modified-status-changed" @preserveCursorPositionOnBufferReload() subscribeToDisplayBuffer: -> - @subscribe @displayBuffer.onDidCreateMarker @handleMarkerCreated - @subscribe @displayBuffer.onDidUpdateMarkers => @mergeIntersectingSelections() - @subscribe @displayBuffer.onDidChangeGrammar => @handleGrammarChange() - @subscribe @displayBuffer.onDidTokenize => @handleTokenization() - @subscribe @displayBuffer.onDidChange (e) => - @emit 'screen-lines-changed', e + @disposables.add @displayBuffer.onDidCreateMarker @handleMarkerCreated + @disposables.add @displayBuffer.onDidUpdateMarkers => @mergeIntersectingSelections() + @disposables.add @displayBuffer.onDidChangeGrammar => @handleGrammarChange() + @disposables.add @displayBuffer.onDidTokenize => @handleTokenization() + @disposables.add @displayBuffer.onDidChange (e) => + @emit 'screen-lines-changed', e if includeDeprecatedAPIs @emitter.emit 'did-change', e # TODO: remove these when we remove the deprecations. Though, no one is likely using them - @subscribe @displayBuffer.onDidChangeSoftWrapped (softWrapped) => @emit 'soft-wrap-changed', softWrapped - @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration - @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration + if includeDeprecatedAPIs + @subscribe @displayBuffer.onDidChangeSoftWrapped (softWrapped) => @emit 'soft-wrap-changed', softWrapped + @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration + @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration @subscribeToScopedConfigSettings() @@ -174,11 +173,9 @@ class TextEditor extends Model subscriptions.add atom.config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, => @updateInvisibles() subscriptions.add atom.config.onDidChange 'editor.invisibles', scope: scopeDescriptor, => @updateInvisibles() - getViewClass: -> - require './text-editor-view' - destroyed: -> - @unsubscribe() + @unsubscribe() if includeDeprecatedAPIs + @disposables.dispose() @scopedConfigSubscriptions.dispose() selection.destroy() for selection in @getSelections() @buffer.release() @@ -457,73 +454,16 @@ class TextEditor extends Model onDidChangeScrollLeft: (callback) -> @emitter.on 'did-change-scroll-left', callback - on: (eventName) -> - switch eventName - when 'title-changed' - deprecate("Use TextEditor::onDidChangeTitle instead") - when 'path-changed' - deprecate("Use TextEditor::onDidChangePath instead") - when 'modified-status-changed' - deprecate("Use TextEditor::onDidChangeModified instead") - when 'soft-wrap-changed' - deprecate("Use TextEditor::onDidChangeSoftWrapped instead") - when 'grammar-changed' - deprecate("Use TextEditor::onDidChangeGrammar instead") - when 'character-widths-changed' - deprecate("Use TextEditor::onDidChangeCharacterWidths instead") - when 'contents-modified' - deprecate("Use TextEditor::onDidStopChanging instead") - when 'contents-conflicted' - deprecate("Use TextEditor::onDidConflict instead") + # TODO Remove once the tabs package no longer uses .on subscriptions + onDidChangeIcon: (callback) -> + @emitter.on 'did-change-icon', callback - when 'will-insert-text' - deprecate("Use TextEditor::onWillInsertText instead") - when 'did-insert-text' - deprecate("Use TextEditor::onDidInsertText instead") - - when 'cursor-added' - deprecate("Use TextEditor::onDidAddCursor instead") - when 'cursor-removed' - deprecate("Use TextEditor::onDidRemoveCursor instead") - when 'cursor-moved' - deprecate("Use TextEditor::onDidChangeCursorPosition instead") - - when 'selection-added' - deprecate("Use TextEditor::onDidAddSelection instead") - when 'selection-removed' - deprecate("Use TextEditor::onDidRemoveSelection instead") - when 'selection-screen-range-changed' - deprecate("Use TextEditor::onDidChangeSelectionRange instead") - - when 'decoration-added' - deprecate("Use TextEditor::onDidAddDecoration instead") - when 'decoration-removed' - deprecate("Use TextEditor::onDidRemoveDecoration instead") - when 'decoration-updated' - deprecate("Use Decoration::onDidChangeProperties instead. You will get the decoration back from `TextEditor::decorateMarker()`") - when 'decoration-changed' - deprecate("Use Marker::onDidChange instead. e.g. `editor::decorateMarker(...).getMarker().onDidChange()`") - - when 'screen-lines-changed' - deprecate("Use TextEditor::onDidChange instead") - - when 'scroll-top-changed' - deprecate("Use TextEditor::onDidChangeScrollTop instead") - when 'scroll-left-changed' - deprecate("Use TextEditor::onDidChangeScrollLeft instead") - - EmitterMixin::on.apply(this, arguments) - - # Retrieves the current {TextBuffer}. + # Public: Retrieves the current {TextBuffer}. getBuffer: -> @buffer # Retrieves the current buffer's URI. getURI: -> @buffer.getUri() - getUri: -> - deprecate("Use `::getURI` instead") - @getURI() - # Create an {TextEditor} with its initial state based on this object copy: -> displayBuffer = @displayBuffer.copy() @@ -691,9 +631,6 @@ class TextEditor extends Model # # * `bufferRow` A {Number} representing a zero-indexed buffer row. lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow) - lineForBufferRow: (bufferRow) -> - deprecate 'Use TextEditor::lineTextForBufferRow(bufferRow) instead' - @lineTextForBufferRow(bufferRow) # Essential: Returns a {String} representing the contents of the line at the # given screen row. @@ -707,23 +644,9 @@ class TextEditor extends Model # # Returns {TokenizedLine} tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow) - lineForScreenRow: (screenRow) -> - deprecate "TextEditor::tokenizedLineForScreenRow(bufferRow) is the new name. But it's private. Try to use TextEditor::lineTextForScreenRow instead" - @tokenizedLineForScreenRow(screenRow) # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows} tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end) - linesForScreenRows: (start, end) -> - deprecate "Use TextEditor::tokenizedLinesForScreenRows instead" - @tokenizedLinesForScreenRows(start, end) - - # Returns a {Number} representing the line length for the given - # buffer row, exclusive of its line-ending character(s). - # - # * `row` A {Number} indicating the buffer row. - lineLengthForBufferRow: (row) -> - deprecate "Use editor.lineTextForBufferRow(row).length instead" - @lineTextForBufferRow(row).length bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row) @@ -796,7 +719,7 @@ class TextEditor extends Model willInsert = true cancel = -> willInsert = false willInsertEvent = {cancel, text} - @emit('will-insert-text', willInsertEvent) + @emit('will-insert-text', willInsertEvent) if includeDeprecatedAPIs @emitter.emit 'will-insert-text', willInsertEvent if willInsert @@ -805,7 +728,7 @@ class TextEditor extends Model @mutateSelectedText (selection) => range = selection.insertText(text, options) didInsertEvent = {text, range} - @emit('did-insert-text', didInsertEvent) + @emit('did-insert-text', didInsertEvent) if includeDeprecatedAPIs @emitter.emit 'did-insert-text', didInsertEvent range else @@ -835,7 +758,8 @@ class TextEditor extends Model # {Number} index of that selection. mutateSelectedText: (fn) -> @mergeIntersectingSelections => - @transact => fn(selection, index) for selection, index in @getSelections() + @transact => + fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition() # Move lines intersection the most recent selection up by one row in screen # coordinates. @@ -971,11 +895,7 @@ class TextEditor extends Model selection.setBufferRange(selectedBufferRange.translate([delta, 0])) for [foldStartRow, foldEndRow] in foldedRowRanges @createFold(foldStartRow + delta, foldEndRow + delta) - - # Deprecated: Use {::duplicateLines} instead. - duplicateLine: -> - deprecate("Use TextEditor::duplicateLines() instead") - @duplicateLines() + return replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @@ -1006,6 +926,7 @@ class TextEditor extends Model while ++row < end.row @addSelectionForBufferRange([[row, 0], [row, Infinity]]) @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 + return # Extended: For each selection, transpose the selected text. # @@ -1027,14 +948,14 @@ class TextEditor extends Model # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. upperCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toUpperCase() + @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toUpperCase() # Extended: Convert the selected text to lower case. # # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. lowerCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toLowerCase() + @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase() # Extended: Toggle line comments for rows intersecting selections. # @@ -1104,31 +1025,22 @@ class TextEditor extends Model # Extended: Delete all lines intersecting selections. deleteLine: -> + @mergeSelectionsOnSameRows() @mutateSelectedText (selection) -> selection.deleteLine() - # Deprecated: Use {::deleteToBeginningOfWord} instead. - backspaceToBeginningOfWord: -> - deprecate("Use TextEditor::deleteToBeginningOfWord() instead") - @deleteToBeginningOfWord() - - # Deprecated: Use {::deleteToBeginningOfLine} instead. - backspaceToBeginningOfLine: -> - deprecate("Use TextEditor::deleteToBeginningOfLine() instead") - @deleteToBeginningOfLine() - ### Section: History ### # Essential: Undo the last change. undo: -> - @getLastCursor().needsAutoscroll = true - @buffer.undo(this) + @buffer.undo() + @getLastSelection().autoscroll() # Essential: Redo the last change. redo: -> - @getLastCursor().needsAutoscroll = true @buffer.redo(this) + @getLastSelection().autoscroll() # Extended: Batch multiple operations as a single undo/redo step. # @@ -1275,6 +1187,14 @@ class TextEditor extends Model # Returns a {Point}. clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) + # Extended: Clip the start and end of the given range to valid positions on screen. + # See {::clipScreenPosition} for more information. + # + # * `range` The {Range} to clip. + # * `options` (optional) See {::clipScreenPosition} `options`. + # Returns a {Range}. + clipScreenRange: (range, options) -> @displayBuffer.clipScreenRange(range, options) + ### Section: Decorations ### @@ -1333,7 +1253,7 @@ class TextEditor extends Model # # Returns a {Decoration} object decorateMarker: (marker, decorationParams) -> - if decorationParams.type is 'gutter' + if includeDeprecatedAPIs and decorationParams.type is 'gutter' deprecate("Decorations of `type: 'gutter'` have been renamed to `type: 'line-number'`.") decorationParams.type = 'line-number' @displayBuffer.decorateMarker(marker, decorationParams) @@ -1369,11 +1289,6 @@ class TextEditor extends Model getLineDecorations: (propertyFilter) -> @displayBuffer.getLineDecorations(propertyFilter) - # Soft-deprecated (forgot to deprecated this pre 1.0) - getGutterDecorations: (propertyFilter) -> - deprecate("Use ::getLineNumberDecorations instead") - @getLineNumberDecorations(propertyFilter) - # Extended: Get all decorations of type 'line-number'. # # * `propertyFilter` (optional) An {Object} containing key value pairs that @@ -1567,13 +1482,6 @@ class TextEditor extends Model getCursorScreenPositions: -> cursor.getScreenPosition() for cursor in @getCursors() - # Get the row of the most recently added cursor in screen coordinates. - # - # Returns the screen row {Number}. - getCursorScreenRow: -> - deprecate('Use `editor.getCursorScreenPosition().row` instead') - @getCursorScreenPosition().row - # Essential: Move the cursor to the given position in screen coordinates. # # If there are multiple cursors, they will be consolidated to a single cursor. @@ -1590,8 +1498,9 @@ class TextEditor extends Model # * `bufferPosition` A {Point} or {Array} of `[row, column]` # # Returns a {Cursor}. - addCursorAtBufferPosition: (bufferPosition) -> + addCursorAtBufferPosition: (bufferPosition, options) -> @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) + @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor # Essential: Add a cursor at the position in screen coordinates. @@ -1599,8 +1508,9 @@ class TextEditor extends Model # * `screenPosition` A {Point} or {Array} of `[row, column]` # # Returns a {Cursor}. - addCursorAtScreenPosition: (screenPosition) -> + addCursorAtScreenPosition: (screenPosition, options) -> @markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) + @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor # Essential: Returns {Boolean} indicating whether or not there are multiple cursors. @@ -1612,85 +1522,52 @@ class TextEditor extends Model # * `lineCount` (optional) {Number} number of lines to move moveUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - moveCursorUp: (lineCount) -> - deprecate("Use TextEditor::moveUp() instead") - @moveUp(lineCount) # Essential: Move every cursor down one row in screen coordinates. # # * `lineCount` (optional) {Number} number of lines to move moveDown: (lineCount) -> @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - moveCursorDown: (lineCount) -> - deprecate("Use TextEditor::moveDown() instead") - @moveDown(lineCount) # Essential: Move every cursor left one column. # # * `columnCount` (optional) {Number} number of columns to move (default: 1) moveLeft: (columnCount) -> @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) - moveCursorLeft: -> - deprecate("Use TextEditor::moveLeft() instead") - @moveLeft() # Essential: Move every cursor right one column. # # * `columnCount` (optional) {Number} number of columns to move (default: 1) moveRight: (columnCount) -> @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) - moveCursorRight: -> - deprecate("Use TextEditor::moveRight() instead") - @moveRight() # Essential: Move every cursor to the beginning of its line in buffer coordinates. moveToBeginningOfLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - moveCursorToBeginningOfLine: -> - deprecate("Use TextEditor::moveToBeginningOfLine() instead") - @moveToBeginningOfLine() # Essential: Move every cursor to the beginning of its line in screen coordinates. moveToBeginningOfScreenLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine() - moveCursorToBeginningOfScreenLine: -> - deprecate("Use TextEditor::moveToBeginningOfScreenLine() instead") - @moveToBeginningOfScreenLine() # Essential: Move every cursor to the first non-whitespace character of its line. moveToFirstCharacterOfLine: -> @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - moveCursorToFirstCharacterOfLine: -> - deprecate("Use TextEditor::moveToFirstCharacterOfLine() instead") - @moveToFirstCharacterOfLine() # Essential: Move every cursor to the end of its line in buffer coordinates. moveToEndOfLine: -> @moveCursors (cursor) -> cursor.moveToEndOfLine() - moveCursorToEndOfLine: -> - deprecate("Use TextEditor::moveToEndOfLine() instead") - @moveToEndOfLine() # Essential: Move every cursor to the end of its line in screen coordinates. moveToEndOfScreenLine: -> @moveCursors (cursor) -> cursor.moveToEndOfScreenLine() - moveCursorToEndOfScreenLine: -> - deprecate("Use TextEditor::moveToEndOfScreenLine() instead") - @moveToEndOfScreenLine() # Essential: Move every cursor to the beginning of its surrounding word. moveToBeginningOfWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - moveCursorToBeginningOfWord: -> - deprecate("Use TextEditor::moveToBeginningOfWord() instead") - @moveToBeginningOfWord() # Essential: Move every cursor to the end of its surrounding word. moveToEndOfWord: -> @moveCursors (cursor) -> cursor.moveToEndOfWord() - moveCursorToEndOfWord: -> - deprecate("Use TextEditor::moveToEndOfWord() instead") - @moveToEndOfWord() # Cursor Extended @@ -1699,63 +1576,37 @@ class TextEditor extends Model # If there are multiple cursors, they will be merged into a single cursor. moveToTop: -> @moveCursors (cursor) -> cursor.moveToTop() - moveCursorToTop: -> - deprecate("Use TextEditor::moveToTop() instead") - @moveToTop() # Extended: Move every cursor to the bottom of the buffer. # # If there are multiple cursors, they will be merged into a single cursor. moveToBottom: -> @moveCursors (cursor) -> cursor.moveToBottom() - moveCursorToBottom: -> - deprecate("Use TextEditor::moveToBottom() instead") - @moveToBottom() # Extended: Move every cursor to the beginning of the next word. moveToBeginningOfNextWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - moveCursorToBeginningOfNextWord: -> - deprecate("Use TextEditor::moveToBeginningOfNextWord() instead") - @moveToBeginningOfNextWord() # Extended: Move every cursor to the previous word boundary. moveToPreviousWordBoundary: -> @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - moveCursorToPreviousWordBoundary: -> - deprecate("Use TextEditor::moveToPreviousWordBoundary() instead") - @moveToPreviousWordBoundary() # Extended: Move every cursor to the next word boundary. moveToNextWordBoundary: -> @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - moveCursorToNextWordBoundary: -> - deprecate("Use TextEditor::moveToNextWordBoundary() instead") - @moveToNextWordBoundary() # Extended: Move every cursor to the beginning of the next paragraph. moveToBeginningOfNextParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() - moveCursorToBeginningOfNextParagraph: -> - deprecate("Use TextEditor::moveToBeginningOfNextParagraph() instead") - @moveToBeginningOfNextParagraph() # Extended: Move every cursor to the beginning of the previous paragraph. moveToBeginningOfPreviousParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() - moveCursorToBeginningOfPreviousParagraph: -> - deprecate("Use TextEditor::moveToBeginningOfPreviousParagraph() instead") - @moveToBeginningOfPreviousParagraph() # Extended: Returns the most recently added {Cursor} getLastCursor: -> _.last(@cursors) - # Deprecated: - getCursor: -> - deprecate("Use TextEditor::getLastCursor() instead") - @getLastCursor() - # Extended: Returns the word surrounding the most recently added cursor. # # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. @@ -1764,7 +1615,7 @@ class TextEditor extends Model # Extended: Get an Array of all {Cursor}s. getCursors: -> - cursor for cursor in @cursors + @cursors.slice() # Extended: Get all {Cursors}s, ordered by their position in the buffer # instead of the order in which they were added. @@ -1780,14 +1631,14 @@ class TextEditor extends Model @decorateMarker(marker, type: 'line-number', class: 'cursor-line') @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) - @emit 'cursor-added', cursor + @emit 'cursor-added', cursor if includeDeprecatedAPIs @emitter.emit 'did-add-cursor', cursor cursor # Remove the given cursor from this editor. removeCursor: (cursor) -> _.remove(@cursors, cursor) - @emit 'cursor-removed', cursor + @emit 'cursor-removed', cursor if includeDeprecatedAPIs @emitter.emit 'did-remove-cursor', cursor moveCursors: (fn) -> @@ -1795,7 +1646,7 @@ class TextEditor extends Model @mergeCursors() cursorMoved: (event) -> - @emit 'cursor-moved', event + @emit 'cursor-moved', event if includeDeprecatedAPIs @emitter.emit 'did-change-cursor-position', event # Merge cursors that have the same screen position @@ -1807,12 +1658,13 @@ class TextEditor extends Model cursor.destroy() else positions[position] = true + return preserveCursorPositionOnBufferReload: -> cursorPosition = null - @subscribe @buffer.onWillReload => + @disposables.add @buffer.onWillReload => cursorPosition = @getCursorBufferPosition() - @subscribe @buffer.onDidReload => + @disposables.add @buffer.onDidReload => @setCursorBufferPosition(cursorPosition) if cursorPosition cursorPosition = null @@ -1871,6 +1723,7 @@ class TextEditor extends Model selections[i].setBufferRange(bufferRange, options) else @addSelectionForBufferRange(bufferRange, options) + return # Essential: Get the {Range} of the most recently added selection in screen # coordinates. @@ -1917,6 +1770,7 @@ class TextEditor extends Model selections[i].setScreenRange(screenRange, options) else @addSelectionForScreenRange(screenRange, options) + return # Essential: Add a selection for the given range in buffer coordinates. # @@ -1928,9 +1782,8 @@ class TextEditor extends Model # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) - selection = @getLastSelection() - selection.autoscroll() if @manageScrollPosition - selection + @getLastSelection().autoscroll() unless options.autoscroll is false + @getLastSelection() # Essential: Add a selection for the given range in screen coordinates. # @@ -1942,9 +1795,8 @@ class TextEditor extends Model # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> @markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options)) - selection = @getLastSelection() - selection.autoscroll() if @manageScrollPosition - selection + @getLastSelection().autoscroll() unless options.autoscroll is false + @getLastSelection() # Essential: Select from the current cursor position to the given position in # buffer coordinates. @@ -2066,16 +1918,10 @@ class TextEditor extends Model # This method merges selections on successive lines. selectLinesContainingCursors: -> @expandSelectionsForward (selection) -> selection.selectLine() - selectLine: -> - deprecate('Use TextEditor::selectLinesContainingCursors instead') - @selectLinesContainingCursors() # Essential: Select the word surrounding each cursor. selectWordsContainingCursors: -> @expandSelectionsForward (selection) -> selection.selectWord() - selectWord: -> - deprecate('Use TextEditor::selectWordsContainingCursors instead') - @selectWordsContainingCursors() # Selection Extended @@ -2131,20 +1977,11 @@ class TextEditor extends Model getLastSelection: -> _.last(@selections) - # Deprecated: - getSelection: (index) -> - if index? - deprecate("Use TextEditor::getSelections()[index] instead when getting a specific selection") - @getSelections()[index] - else - deprecate("Use TextEditor::getLastSelection() instead") - @getLastSelection() - # Extended: Get current {Selection}s. # # Returns: An {Array} of {Selection}s. getSelections: -> - selection for selection in @selections + @selections.slice() # Extended: Get all {Selection}s, ordered by their position in the buffer # instead of the order in which they were added. @@ -2191,15 +2028,18 @@ class TextEditor extends Model expandSelectionsForward: (fn) -> @mergeIntersectingSelections => fn(selection) for selection in @getSelections() + return # Calls the given function with each selection, then merges selections in the # reversed orientation expandSelectionsBackward: (fn) -> @mergeIntersectingSelections reversed: true, => fn(selection) for selection in @getSelections() + return finalizeSelections: -> selection.finalize() for selection in @getSelections() + return selectionsForScreenRows: (startRow, endRow) -> @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) @@ -2208,6 +2048,19 @@ class TextEditor extends Model # the function with merging suppressed, then merges intersecting selections # afterward. mergeIntersectingSelections: (args...) -> + @mergeSelections args..., (previousSelection, currentSelection) -> + exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty() + + previousSelection.intersectsWith(currentSelection, exclusive) + + mergeSelectionsOnSameRows: (args...) -> + @mergeSelections args..., (previousSelection, currentSelection) -> + screenRange = currentSelection.getScreenRange() + + previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) + + mergeSelections: (args...) -> + mergePredicate = args.pop() fn = args.pop() if _.isFunction(_.last(args)) options = args.pop() ? {} @@ -2220,10 +2073,7 @@ class TextEditor extends Model reducer = (disjointSelections, selection) -> adjacentSelection = _.last(disjointSelections) - exclusive = not selection.isEmpty() and not adjacentSelection.isEmpty() - intersects = adjacentSelection.intersectsWith(selection, exclusive) - - if intersects + if mergePredicate(adjacentSelection, selection) adjacentSelection.merge(selection, options) disjointSelections else @@ -2241,32 +2091,33 @@ class TextEditor extends Model # Returns the new {Selection}. addSelection: (marker, options={}) -> unless marker.getProperties().preserveFolds - @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) + @destroyFoldsContainingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editor: this, marker, cursor}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds) + if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) return selection else - @emit 'selection-added', selection + @emit 'selection-added', selection if includeDeprecatedAPIs @emitter.emit 'did-add-selection', selection selection # Remove the given selection. removeSelection: (selection) -> _.remove(@selections, selection) - @emit 'selection-removed', selection + @emit 'selection-removed', selection if includeDeprecatedAPIs @emitter.emit 'did-remove-selection', selection # Reduce one or more selections to a single empty selection based on the most # recently added cursor. - clearSelections: -> + clearSelections: (options) -> @consolidateSelections() - @getLastSelection().clear() + @getLastSelection().clear(options) # Reduce multiple selections to the most recently added selection. consolidateSelections: -> @@ -2279,7 +2130,7 @@ class TextEditor extends Model # Called by the selection selectionRangeChanged: (event) -> - @emit 'selection-screen-range-changed', event + @emit 'selection-screen-range-changed', event if includeDeprecatedAPIs @emitter.emit 'did-change-selection-range', event ### @@ -2403,9 +2254,6 @@ class TextEditor extends Model # # Returns a {Boolean}. isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() - getSoftWrapped: -> - deprecate("Use TextEditor::isSoftWrapped instead") - @displayBuffer.isSoftWrapped() # Essential: Enable or disable soft wrapping for this editor. # @@ -2413,17 +2261,11 @@ class TextEditor extends Model # # Returns a {Boolean}. setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped) - setSoftWrap: (softWrapped) -> - deprecate("Use TextEditor::setSoftWrapped instead") - @setSoftWrapped(softWrapped) # Essential: Toggle soft wrapping for this editor # # Returns a {Boolean}. toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) - toggleSoftWrap: -> - deprecate("Use TextEditor::toggleSoftWrapped instead") - @toggleSoftWrapped() # Public: Gets the column at which column will soft wrap getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() @@ -2548,9 +2390,6 @@ class TextEditor extends Model # Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition: (bufferPosition) -> @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition) - scopesForBufferPosition: (bufferPosition) -> - deprecate 'Use ::scopeDescriptorForBufferPosition instead. The return value has changed! It now returns a `ScopeDescriptor`' - @scopeDescriptorForBufferPosition(bufferPosition).getScopesArray() # Extended: Get the range in buffer coordinates of all tokens surrounding the # cursor that match the given scope selector. @@ -2582,13 +2421,6 @@ class TextEditor extends Model # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) - scopesAtCursor: -> - deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead' - @getLastCursor().getScopeDescriptor().getScopesArray() - getCursorScopes: -> - deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead' - @scopesAtCursor() - ### Section: Clipboard Operations ### @@ -2596,7 +2428,7 @@ class TextEditor extends Model # Essential: For each selection, copy the selected text. copySelectedText: -> maintainClipboard = false - for selection in @getSelections() + for selection in @getSelectionsOrderedByBufferPosition() if selection.isEmpty() previousRange = selection.getBufferRange() selection.selectLine() @@ -2605,6 +2437,7 @@ class TextEditor extends Model else selection.copy(maintainClipboard, false) maintainClipboard = true + return # Essential: For each selection, cut the selected text. cutSelectedText: -> @@ -2641,7 +2474,7 @@ class TextEditor extends Model {cursor} = selection if indentBasis? containsNewlines = text.indexOf('\n') isnt -1 - if containsNewlines or !cursor.hasPrecedingCharactersOnLine() + if containsNewlines or not cursor.hasPrecedingCharactersOnLine() options.indentBasis ?= indentBasis if fullLine and selection.isEmpty() @@ -2699,6 +2532,7 @@ class TextEditor extends Model # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> selection.fold() for selection in @getSelections() + return # Extended: Fold all foldable lines. foldAll: -> @@ -2774,10 +2608,19 @@ class TextEditor extends Model destroyFoldWithId: (id) -> @displayBuffer.destroyFoldWithId(id) - # Remove any {Fold}s found that intersect the given buffer row. + # Remove any {Fold}s found that intersect the given buffer range. destroyFoldsIntersectingBufferRange: (bufferRange) -> - for row in [bufferRange.start.row..bufferRange.end.row] - @unfoldBufferRow(row) + @destroyFoldsContainingBufferRange(bufferRange) + + for row in [bufferRange.end.row..bufferRange.start.row] + fold.destroy() for fold in @displayBuffer.foldsStartingAtBufferRow(row) + + return + + # Remove any {Fold}s found that contain the given buffer range. + destroyFoldsContainingBufferRange: (bufferRange) -> + @unfoldBufferRow(bufferRange.start.row) + @unfoldBufferRow(bufferRange.end.row) # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} largestFoldContainingBufferRow: (bufferRow) -> @@ -2891,7 +2734,7 @@ class TextEditor extends Model @updateInvisibles() @subscribeToScopedConfigSettings() @unfoldAll() - @emit 'grammar-changed' + @emit 'grammar-changed' if includeDeprecatedAPIs @emitter.emit 'did-change-grammar', @getGrammar() handleMarkerCreated: (marker) => @@ -2994,11 +2837,6 @@ class TextEditor extends Model pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange) - # Deprecated: Call {::joinLines} instead. - joinLine: -> - deprecate("Use TextEditor::joinLines() instead") - @joinLines() - ### Section: Utility ### @@ -3007,3 +2845,232 @@ class TextEditor extends Model "" logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) + +if includeDeprecatedAPIs + TextEditor.delegatesProperties '$lineHeightInPixels', '$defaultCharWidth', '$height', '$width', + '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft', + toProperty: 'displayBuffer' + + TextEditor::getViewClass = -> + require './text-editor-view' + + TextEditor::joinLine = -> + deprecate("Use TextEditor::joinLines() instead") + @joinLines() + + TextEditor::scopesAtCursor = -> + deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead' + @getLastCursor().getScopeDescriptor().getScopesArray() + + TextEditor::getCursorScopes = -> + deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead' + @scopesAtCursor() + + TextEditor::getUri = -> + deprecate("Use `::getURI` instead") + @getURI() + + TextEditor::lineForBufferRow = (bufferRow) -> + deprecate 'Use TextEditor::lineTextForBufferRow(bufferRow) instead' + @lineTextForBufferRow(bufferRow) + + TextEditor::lineForScreenRow = (screenRow) -> + deprecate "TextEditor::tokenizedLineForScreenRow(bufferRow) is the new name. But it's private. Try to use TextEditor::lineTextForScreenRow instead" + @tokenizedLineForScreenRow(screenRow) + + TextEditor::linesForScreenRows = (start, end) -> + deprecate "Use TextEditor::tokenizedLinesForScreenRows instead" + @tokenizedLinesForScreenRows(start, end) + + TextEditor::lineLengthForBufferRow = (row) -> + deprecate "Use editor.lineTextForBufferRow(row).length instead" + @lineTextForBufferRow(row).length + + TextEditor::duplicateLine = -> + deprecate("Use TextEditor::duplicateLines() instead") + @duplicateLines() + + TextEditor::scopesForBufferPosition = (bufferPosition) -> + deprecate 'Use ::scopeDescriptorForBufferPosition instead. The return value has changed! It now returns a `ScopeDescriptor`' + @scopeDescriptorForBufferPosition(bufferPosition).getScopesArray() + + TextEditor::toggleSoftWrap = -> + deprecate("Use TextEditor::toggleSoftWrapped instead") + @toggleSoftWrapped() + + TextEditor::setSoftWrap = (softWrapped) -> + deprecate("Use TextEditor::setSoftWrapped instead") + @setSoftWrapped(softWrapped) + + TextEditor::backspaceToBeginningOfWord = -> + deprecate("Use TextEditor::deleteToBeginningOfWord() instead") + @deleteToBeginningOfWord() + + TextEditor::backspaceToBeginningOfLine = -> + deprecate("Use TextEditor::deleteToBeginningOfLine() instead") + @deleteToBeginningOfLine() + + TextEditor::getGutterDecorations = (propertyFilter) -> + deprecate("Use ::getLineNumberDecorations instead") + @getLineNumberDecorations(propertyFilter) + + TextEditor::getCursorScreenRow = -> + deprecate('Use `editor.getCursorScreenPosition().row` instead') + @getCursorScreenPosition().row + + TextEditor::moveCursorUp = (lineCount) -> + deprecate("Use TextEditor::moveUp() instead") + @moveUp(lineCount) + + TextEditor::moveCursorDown = (lineCount) -> + deprecate("Use TextEditor::moveDown() instead") + @moveDown(lineCount) + + TextEditor::moveCursorLeft = -> + deprecate("Use TextEditor::moveLeft() instead") + @moveLeft() + + TextEditor::moveCursorRight = -> + deprecate("Use TextEditor::moveRight() instead") + @moveRight() + + TextEditor::moveCursorToBeginningOfLine = -> + deprecate("Use TextEditor::moveToBeginningOfLine() instead") + @moveToBeginningOfLine() + + TextEditor::moveCursorToBeginningOfScreenLine = -> + deprecate("Use TextEditor::moveToBeginningOfScreenLine() instead") + @moveToBeginningOfScreenLine() + + TextEditor::moveCursorToFirstCharacterOfLine = -> + deprecate("Use TextEditor::moveToFirstCharacterOfLine() instead") + @moveToFirstCharacterOfLine() + + TextEditor::moveCursorToEndOfLine = -> + deprecate("Use TextEditor::moveToEndOfLine() instead") + @moveToEndOfLine() + + TextEditor::moveCursorToEndOfScreenLine = -> + deprecate("Use TextEditor::moveToEndOfScreenLine() instead") + @moveToEndOfScreenLine() + + TextEditor::moveCursorToBeginningOfWord = -> + deprecate("Use TextEditor::moveToBeginningOfWord() instead") + @moveToBeginningOfWord() + + TextEditor::moveCursorToEndOfWord = -> + deprecate("Use TextEditor::moveToEndOfWord() instead") + @moveToEndOfWord() + + TextEditor::moveCursorToTop = -> + deprecate("Use TextEditor::moveToTop() instead") + @moveToTop() + + TextEditor::moveCursorToBottom = -> + deprecate("Use TextEditor::moveToBottom() instead") + @moveToBottom() + + TextEditor::moveCursorToBeginningOfNextWord = -> + deprecate("Use TextEditor::moveToBeginningOfNextWord() instead") + @moveToBeginningOfNextWord() + + TextEditor::moveCursorToPreviousWordBoundary = -> + deprecate("Use TextEditor::moveToPreviousWordBoundary() instead") + @moveToPreviousWordBoundary() + + TextEditor::moveCursorToNextWordBoundary = -> + deprecate("Use TextEditor::moveToNextWordBoundary() instead") + @moveToNextWordBoundary() + + TextEditor::moveCursorToBeginningOfNextParagraph = -> + deprecate("Use TextEditor::moveToBeginningOfNextParagraph() instead") + @moveToBeginningOfNextParagraph() + + TextEditor::moveCursorToBeginningOfPreviousParagraph = -> + deprecate("Use TextEditor::moveToBeginningOfPreviousParagraph() instead") + @moveToBeginningOfPreviousParagraph() + + TextEditor::getCursor = -> + deprecate("Use TextEditor::getLastCursor() instead") + @getLastCursor() + + TextEditor::selectLine = -> + deprecate('Use TextEditor::selectLinesContainingCursors instead') + @selectLinesContainingCursors() + + TextEditor::selectWord = -> + deprecate('Use TextEditor::selectWordsContainingCursors instead') + @selectWordsContainingCursors() + + TextEditor::getSelection = (index) -> + if index? + deprecate("Use TextEditor::getSelections()[index] instead when getting a specific selection") + @getSelections()[index] + else + deprecate("Use TextEditor::getLastSelection() instead") + @getLastSelection() + + TextEditor::getSoftWrapped = -> + deprecate("Use TextEditor::isSoftWrapped instead") + @displayBuffer.isSoftWrapped() + + EmitterMixin = require('emissary').Emitter + TextEditor::on = (eventName) -> + switch eventName + when 'title-changed' + deprecate("Use TextEditor::onDidChangeTitle instead") + when 'path-changed' + deprecate("Use TextEditor::onDidChangePath instead") + when 'modified-status-changed' + deprecate("Use TextEditor::onDidChangeModified instead") + when 'soft-wrap-changed' + deprecate("Use TextEditor::onDidChangeSoftWrapped instead") + when 'grammar-changed' + deprecate("Use TextEditor::onDidChangeGrammar instead") + when 'character-widths-changed' + deprecate("Use TextEditor::onDidChangeCharacterWidths instead") + when 'contents-modified' + deprecate("Use TextEditor::onDidStopChanging instead") + when 'contents-conflicted' + deprecate("Use TextEditor::onDidConflict instead") + + when 'will-insert-text' + deprecate("Use TextEditor::onWillInsertText instead") + when 'did-insert-text' + deprecate("Use TextEditor::onDidInsertText instead") + + when 'cursor-added' + deprecate("Use TextEditor::onDidAddCursor instead") + when 'cursor-removed' + deprecate("Use TextEditor::onDidRemoveCursor instead") + when 'cursor-moved' + deprecate("Use TextEditor::onDidChangeCursorPosition instead") + + when 'selection-added' + deprecate("Use TextEditor::onDidAddSelection instead") + when 'selection-removed' + deprecate("Use TextEditor::onDidRemoveSelection instead") + when 'selection-screen-range-changed' + deprecate("Use TextEditor::onDidChangeSelectionRange instead") + + when 'decoration-added' + deprecate("Use TextEditor::onDidAddDecoration instead") + when 'decoration-removed' + deprecate("Use TextEditor::onDidRemoveDecoration instead") + when 'decoration-updated' + deprecate("Use Decoration::onDidChangeProperties instead. You will get the decoration back from `TextEditor::decorateMarker()`") + when 'decoration-changed' + deprecate("Use Marker::onDidChange instead. e.g. `editor::decorateMarker(...).getMarker().onDidChange()`") + + when 'screen-lines-changed' + deprecate("Use TextEditor::onDidChange instead") + + when 'scroll-top-changed' + deprecate("Use TextEditor::onDidChangeScrollTop instead") + when 'scroll-left-changed' + deprecate("Use TextEditor::onDidChangeScrollLeft instead") + + else + deprecate("TextEditor::on is deprecated. Use documented event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 650c1d168..01bb9652a 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -1,22 +1,16 @@ path = require 'path' - _ = require 'underscore-plus' -EmitterMixin = require('emissary').Emitter {Emitter, Disposable, CompositeDisposable} = require 'event-kit' {File} = require 'pathwatcher' fs = require 'fs-plus' Q = require 'q' Grim = require 'grim' -Package = require './package' - # Extended: Handles loading and activating available themes. # # An instance of this class is always available as the `atom.themes` global. module.exports = class ThemeManager - EmitterMixin.includeInto(this) - constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) -> @emitter = new Emitter @styleSheetDisposablesBySourcePath = {} @@ -33,24 +27,24 @@ class ThemeManager styleElementAdded: (styleElement) -> {sheet} = styleElement @sheetsByStyleElement.set(styleElement, sheet) - @emit 'stylesheet-added', sheet + @emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs @emitter.emit 'did-add-stylesheet', sheet - @emit 'stylesheets-changed' + @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-stylesheets' styleElementRemoved: (styleElement) -> sheet = @sheetsByStyleElement.get(styleElement) - @emit 'stylesheet-removed', sheet + @emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs @emitter.emit 'did-remove-stylesheet', sheet - @emit 'stylesheets-changed' + @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-stylesheets' styleElementUpdated: ({sheet}) -> - @emit 'stylesheet-removed', sheet + @emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs @emitter.emit 'did-remove-stylesheet', sheet - @emit 'stylesheet-added', sheet + @emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs @emitter.emit 'did-add-stylesheet', sheet - @emit 'stylesheets-changed' + @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-stylesheets' ### @@ -65,65 +59,6 @@ class ThemeManager @emitter.on 'did-change-active-themes', callback @emitter.on 'did-reload-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone - onDidReloadAll: (callback) -> - Grim.deprecate("Use `::onDidChangeActiveThemes` instead.") - @onDidChangeActiveThemes(callback) - - # Deprecated: Invoke `callback` when a stylesheet has been added to the dom. - # - # * `callback` {Function} - # * `stylesheet` {StyleSheet} the style node - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddStylesheet: (callback) -> - Grim.deprecate("Use atom.styles.onDidAddStyleElement instead") - @emitter.on 'did-add-stylesheet', callback - - # Deprecated: Invoke `callback` when a stylesheet has been removed from the dom. - # - # * `callback` {Function} - # * `stylesheet` {StyleSheet} the style node - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveStylesheet: (callback) -> - Grim.deprecate("Use atom.styles.onDidRemoveStyleElement instead") - @emitter.on 'did-remove-stylesheet', callback - - # Deprecated: Invoke `callback` when a stylesheet has been updated. - # - # * `callback` {Function} - # * `stylesheet` {StyleSheet} the style node - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidUpdateStylesheet: (callback) -> - Grim.deprecate("Use atom.styles.onDidUpdateStyleElement instead") - @emitter.on 'did-update-stylesheet', callback - - # Deprecated: Invoke `callback` when any stylesheet has been updated, added, or removed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStylesheets: (callback) -> - Grim.deprecate("Use atom.styles.onDidAdd/RemoveStyleElement instead") - @emitter.on 'did-change-stylesheets', callback - - on: (eventName) -> - switch eventName - when 'reloaded' - Grim.deprecate 'Use ThemeManager::onDidChangeActiveThemes instead' - when 'stylesheet-added' - Grim.deprecate 'Use ThemeManager::onDidAddStylesheet instead' - when 'stylesheet-removed' - Grim.deprecate 'Use ThemeManager::onDidRemoveStylesheet instead' - when 'stylesheet-updated' - Grim.deprecate 'Use ThemeManager::onDidUpdateStylesheet instead' - when 'stylesheets-changed' - Grim.deprecate 'Use ThemeManager::onDidChangeStylesheets instead' - else - Grim.deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.' - EmitterMixin::on.apply(this, arguments) - ### Section: Accessing Available Themes ### @@ -140,10 +75,6 @@ class ThemeManager getLoadedThemeNames: -> theme.name for theme in @getLoadedThemes() - getLoadedNames: -> - Grim.deprecate("Use `::getLoadedThemeNames` instead.") - @getLoadedThemeNames() - # Public: Get an array of all the loaded themes. getLoadedThemes: -> pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() @@ -156,10 +87,6 @@ class ThemeManager getActiveThemeNames: -> theme.name for theme in @getActiveThemes() - getActiveNames: -> - Grim.deprecate("Use `::getActiveThemeNames` instead.") - @getActiveThemeNames() - # Public: Get an array of all the active themes. getActiveThemes: -> pack for pack in @packageManager.getActivePackages() when pack.isTheme() @@ -208,22 +135,10 @@ class ThemeManager # the first/top theme to override later themes in the stack. themeNames.reverse() - # Set the list of enabled themes. - # - # * `enabledThemeNames` An {Array} of {String} theme names. - setEnabledThemes: (enabledThemeNames) -> - Grim.deprecate("Use `atom.config.set('core.themes', arrayOfThemeNames)` instead") - atom.config.set('core.themes', enabledThemeNames) - ### Section: Private ### - # Returns the {String} path to the user's stylesheet under ~/.atom - getUserStylesheetPath: -> - Grim.deprecate("Call atom.styles.getUserStyleSheetPath() instead") - atom.styles.getUserStyleSheetPath() - # Resolve and apply the stylesheet specified by the path. # # This supports both CSS and Less stylsheets. @@ -318,7 +233,11 @@ class ThemeManager else @lessCache.read(lessStylesheetPath) catch error + error.less = true if error.line? + # Adjust line numbers for import fallbacks + error.line -= 2 if importFallbackVariables + message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`" detail = """ Line number: #{error.line} @@ -362,7 +281,7 @@ class ThemeManager @loadUserStylesheet() @reloadBaseStylesheets() @initialLoadComplete = true - @emit 'reloaded' + @emit 'reloaded' if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-active-themes' deferred.resolve() @@ -406,3 +325,59 @@ class ThemeManager themePaths.push(path.join(themePath, 'styles')) themePaths.filter (themePath) -> fs.isDirectorySync(themePath) + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + EmitterMixin.includeInto(ThemeManager) + + ThemeManager::on = (eventName) -> + switch eventName + when 'reloaded' + Grim.deprecate 'Use ThemeManager::onDidChangeActiveThemes instead' + when 'stylesheet-added' + Grim.deprecate 'Use ThemeManager::onDidAddStylesheet instead' + when 'stylesheet-removed' + Grim.deprecate 'Use ThemeManager::onDidRemoveStylesheet instead' + when 'stylesheet-updated' + Grim.deprecate 'Use ThemeManager::onDidUpdateStylesheet instead' + when 'stylesheets-changed' + Grim.deprecate 'Use ThemeManager::onDidChangeStylesheets instead' + else + Grim.deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.' + EmitterMixin::on.apply(this, arguments) + + ThemeManager::onDidReloadAll = (callback) -> + Grim.deprecate("Use `::onDidChangeActiveThemes` instead.") + @onDidChangeActiveThemes(callback) + + ThemeManager::onDidAddStylesheet = (callback) -> + Grim.deprecate("Use atom.styles.onDidAddStyleElement instead") + @emitter.on 'did-add-stylesheet', callback + + ThemeManager::onDidRemoveStylesheet = (callback) -> + Grim.deprecate("Use atom.styles.onDidRemoveStyleElement instead") + @emitter.on 'did-remove-stylesheet', callback + + ThemeManager::onDidUpdateStylesheet = (callback) -> + Grim.deprecate("Use atom.styles.onDidUpdateStyleElement instead") + @emitter.on 'did-update-stylesheet', callback + + ThemeManager::onDidChangeStylesheets = (callback) -> + Grim.deprecate("Use atom.styles.onDidAdd/RemoveStyleElement instead") + @emitter.on 'did-change-stylesheets', callback + + ThemeManager::getUserStylesheetPath = -> + Grim.deprecate("Call atom.styles.getUserStyleSheetPath() instead") + atom.styles.getUserStyleSheetPath() + + ThemeManager::getLoadedNames = -> + Grim.deprecate("Use `::getLoadedThemeNames` instead.") + @getLoadedThemeNames() + + ThemeManager::getActiveNames = -> + Grim.deprecate("Use `::getActiveThemeNames` instead.") + @getActiveThemeNames() + + ThemeManager::setEnabledThemes = (enabledThemeNames) -> + Grim.deprecate("Use `atom.config.set('core.themes', arrayOfThemeNames)` instead") + atom.config.set('core.themes', enabledThemeNames) diff --git a/src/token.coffee b/src/token.coffee index 778ea16e6..8aa4a8706 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -1,5 +1,4 @@ _ = require 'underscore-plus' -{deprecate} = require 'grim' textUtils = require './text-utils' WhitespaceRegexesByTabLength = {} @@ -28,7 +27,7 @@ class Token isEqual: (other) -> # TODO: scopes is deprecated. This is here for the sake of lang package tests - @value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic + @value is other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic is !!other.isAtomic isBracket: -> /^meta\.brace\b/.test(_.last(@scopes)) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 67e4deb87..d9f1bcba7 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -1,9 +1,8 @@ _ = require 'underscore-plus' -{Model} = require 'theorist' -EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' +{CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' Serializable = require 'serializable' +Model = require './model' TokenizedLine = require './tokenized-line' Token = require './token' ScopeDescriptor = require './scope-descriptor' @@ -13,11 +12,10 @@ module.exports = class TokenizedBuffer extends Model Serializable.includeInto(this) - @property 'tabLength' - grammar: null currentGrammarScore: null buffer: null + tabLength: null tokenizedLines: null chunkSize: 50 invalidRows: null @@ -25,15 +23,19 @@ class TokenizedBuffer extends Model constructor: ({@buffer, @tabLength, @invisibles}) -> @emitter = new Emitter + @disposables = new CompositeDisposable - @subscribe atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated) - @subscribe atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated) + @disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated) + @disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated) - @subscribe @buffer.preemptDidChange (e) => @handleBufferChange(e) - @subscribe @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() + @disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e) + @disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() @reloadGrammar() + destroyed: -> + @disposables.dispose() + serializeParams: -> bufferPath: @buffer.getPath() tabLength: @tabLength @@ -56,19 +58,6 @@ class TokenizedBuffer extends Model onDidTokenize: (callback) -> @emitter.on 'did-tokenize', callback - on: (eventName) -> - switch eventName - when 'changed' - Grim.deprecate("Use TokenizedBuffer::onDidChange instead") - when 'grammar-changed' - Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead") - when 'tokenized' - Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead") - else - Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.") - - EmitterMixin::on.apply(this, arguments) - grammarAddedOrUpdated: (grammar) => if grammar.injectionSelector? @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector) @@ -78,11 +67,14 @@ class TokenizedBuffer extends Model setGrammar: (grammar, score) -> return if grammar is @grammar - @unsubscribe(@grammar) if @grammar + @grammar = grammar @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName]) @currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText()) - @subscribe @grammar.onDidUpdate => @retokenizeLines() + + @grammarUpdateDisposable?.dispose() + @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() + @disposables.add(@grammarUpdateDisposable) @configSettings = tabLength: atom.config.get('editor.tabLength', scope: @rootScopeDescriptor) @@ -90,11 +82,11 @@ class TokenizedBuffer extends Model @grammarTabLengthSubscription = atom.config.onDidChange 'editor.tabLength', scope: @rootScopeDescriptor, ({newValue}) => @configSettings.tabLength = newValue @retokenizeLines() - @subscribe @grammarTabLengthSubscription + @disposables.add(@grammarTabLengthSubscription) @retokenizeLines() - @emit 'grammar-changed', grammar + @emit 'grammar-changed', grammar if Grim.includeDeprecatedAPIs @emitter.emit 'did-change-grammar', grammar reloadGrammar: -> @@ -116,7 +108,7 @@ class TokenizedBuffer extends Model @invalidateRow(0) @fullyTokenized = false event = {start: 0, end: lastRow, delta: 0} - @emit 'changed', event + @emit 'changed', event if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', event setVisible: (@visible) -> @@ -138,12 +130,19 @@ class TokenizedBuffer extends Model tokenizeInBackground: -> return if not @visible or @pendingChunk or not @isAlive() + @pendingChunk = true _.defer => @pendingChunk = false @tokenizeNextChunk() if @isAlive() and @buffer.isAlive() tokenizeNextChunk: -> + # Short circuit null grammar which can just use the placeholder tokens + if @grammar is atom.grammars.nullGrammar and @firstInvalidRow()? + @invalidRows = [] + @markTokenizationComplete() + return + rowsRemaining = @chunkSize while @firstInvalidRow()? and rowsRemaining > 0 @@ -155,11 +154,11 @@ class TokenizedBuffer extends Model loop previousStack = @stackForRow(row) @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1)) - if --rowsRemaining == 0 + if --rowsRemaining is 0 filledRegion = false endRow = row break - if row == lastRow or _.isEqual(@stackForRow(row), previousStack) + if row is lastRow or _.isEqual(@stackForRow(row), previousStack) filledRegion = true endRow = row break @@ -171,22 +170,26 @@ class TokenizedBuffer extends Model [startRow, endRow] = @updateFoldableStatus(startRow, endRow) event = {start: startRow, end: endRow, delta: 0} - @emit 'changed', event + @emit 'changed', event if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', event if @firstInvalidRow()? @tokenizeInBackground() else - unless @fullyTokenized - @emit 'tokenized' - @emitter.emit 'did-tokenize' - @fullyTokenized = true + @markTokenizationComplete() + + markTokenizationComplete: -> + unless @fullyTokenized + @emit 'tokenized' if Grim.includeDeprecatedAPIs + @emitter.emit 'did-tokenize' + @fullyTokenized = true firstInvalidRow: -> @invalidRows[0] validateRow: (row) -> @invalidRows.shift() while @invalidRows[0] <= row + return invalidateRow: (row) -> @invalidRows.push(row) @@ -223,8 +226,8 @@ class TokenizedBuffer extends Model [start, end] = @updateFoldableStatus(start, end + delta) end -= delta - event = { start, end, delta, bufferChange: e } - @emit 'changed', event + event = {start, end, delta, bufferChange: e} + @emit 'changed', event if Grim.includeDeprecatedAPIs @emitter.emit 'did-change', event retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) -> @@ -277,7 +280,7 @@ class TokenizedBuffer extends Model ruleStack = startingStack stopTokenizingAt = startRow + @chunkSize tokenizedLines = for row in [startRow..endRow] - if (ruleStack or row == 0) and row < stopTokenizingAt + if (ruleStack or row is 0) and row < stopTokenizingAt screenLine = @buildTokenizedLineForRow(row, ruleStack) ruleStack = screenLine.ruleStack else @@ -387,7 +390,7 @@ class TokenizedBuffer extends Model iterateTokensInBufferRange: (bufferRange, iterator) -> bufferRange = Range.fromObject(bufferRange) - { start, end } = bufferRange + {start, end} = bufferRange keepLooping = true stop = -> keepLooping = false @@ -396,13 +399,13 @@ class TokenizedBuffer extends Model bufferColumn = 0 for token in @tokenizedLines[bufferRow].tokens startOfToken = new Point(bufferRow, bufferColumn) - iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken) + iterator(token, startOfToken, {stop}) if bufferRange.containsPoint(startOfToken) return unless keepLooping bufferColumn += token.bufferDelta backwardsIterateTokensInBufferRange: (bufferRange, iterator) -> bufferRange = Range.fromObject(bufferRange) - { start, end } = bufferRange + {start, end} = bufferRange keepLooping = true stop = -> keepLooping = false @@ -412,20 +415,20 @@ class TokenizedBuffer extends Model for token in new Array(@tokenizedLines[bufferRow].tokens...).reverse() bufferColumn -= token.bufferDelta startOfToken = new Point(bufferRow, bufferColumn) - iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken) + iterator(token, startOfToken, {stop}) if bufferRange.containsPoint(startOfToken) return unless keepLooping findOpeningBracket: (startBufferPosition) -> range = [[0,0], startBufferPosition] position = null depth = 0 - @backwardsIterateTokensInBufferRange range, (token, startPosition, { stop }) -> + @backwardsIterateTokensInBufferRange range, (token, startPosition, {stop}) -> if token.isBracket() - if token.value == '}' + if token.value is '}' depth++ - else if token.value == '{' + else if token.value is '{' depth-- - if depth == 0 + if depth is 0 position = startPosition stop() position @@ -434,13 +437,13 @@ class TokenizedBuffer extends Model range = [startBufferPosition, @buffer.getEndPosition()] position = null depth = 0 - @iterateTokensInBufferRange range, (token, startPosition, { stop }) -> + @iterateTokensInBufferRange range, (token, startPosition, {stop}) -> if token.isBracket() - if token.value == '{' + if token.value is '{' depth++ - else if token.value == '}' + else if token.value is '}' depth-- - if depth == 0 + if depth is 0 position = startPosition stop() position @@ -458,3 +461,20 @@ class TokenizedBuffer extends Model for row in [start..end] line = @tokenizedLineForRow(row).text console.log row, line, line.length + return + +if Grim.includeDeprecatedAPIs + EmitterMixin = require('emissary').Emitter + + TokenizedBuffer::on = (eventName) -> + switch eventName + when 'changed' + Grim.deprecate("Use TokenizedBuffer::onDidChange instead") + when 'grammar-changed' + Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead") + when 'tokenized' + Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead") + else + Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.") + + EmitterMixin::on.apply(this, arguments) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 3560c35a1..b81d972a0 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -11,6 +11,7 @@ module.exports = class TokenizedLine endOfLineInvisibles: null lineIsWhitespaceOnly: false + firstNonWhitespaceIndex: 0 foldable: false constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles}) -> @@ -40,10 +41,20 @@ class TokenizedLine copy: -> new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold}) + # This clips a given screen column to a valid column that's within the line + # and not in the middle of any atomic tokens. + # + # column - A {Number} representing the column to clip + # options - A hash with the key clip. Valid values for this key: + # 'closest' (default): clip to the closest edge of an atomic token. + # 'forward': clip to the forward edge. + # 'backward': clip to the backward edge. + # + # Returns a {Number} representing the clipped column. clipScreenColumn: (column, options={}) -> - return 0 if @tokens.length == 0 + return 0 if @tokens.length is 0 - { skipAtomicTokens } = options + {clip} = options column = Math.min(column, @getMaxScreenColumn()) tokenStartColumn = 0 @@ -54,10 +65,15 @@ class TokenizedLine if @isColumnInsideSoftWrapIndentation(tokenStartColumn) @softWrapIndentationDelta else if token.isAtomic and tokenStartColumn < column - if skipAtomicTokens + if clip is 'forward' tokenStartColumn + token.screenDelta - else + else if clip is 'backward' tokenStartColumn + else #'closest' + if column > tokenStartColumn + (token.screenDelta / 2) + tokenStartColumn + token.screenDelta + else + tokenStartColumn else column @@ -66,7 +82,7 @@ class TokenizedLine screenColumn = 0 currentBufferColumn = 0 for token in @tokens - break if currentBufferColumn > bufferColumn + break if currentBufferColumn + token.bufferDelta > bufferColumn screenColumn += token.screenDelta currentBufferColumn += token.bufferDelta @clipScreenColumn(screenColumn + (bufferColumn - currentBufferColumn)) @@ -96,6 +112,7 @@ class TokenizedLine # Returns a {Number} representing the `line` position where the wrap would take place. # Returns `null` if a wrap wouldn't occur. findWrapColumn: (maxColumn) -> + return unless maxColumn? return unless @text.length > maxColumn if /\s/.test(@text[maxColumn]) @@ -106,43 +123,37 @@ class TokenizedLine return @text.length else # search backward for the start of the word on the boundary - for column in [maxColumn..0] when @isColumnOutsideSoftWrapIndentation(column) + for column in [maxColumn..@firstNonWhitespaceIndex] return column + 1 if /\s/.test(@text[column]) return maxColumn - # Calculates how many trailing spaces in this line's indentation cannot fit in a single tab. - # - # Returns a {Number} representing the odd indentation spaces in this line. - getOddIndentationSpaces: -> - oddIndentLevel = @indentLevel - Math.floor(@indentLevel) - Math.round(@tabLength * oddIndentLevel) + buildSoftWrapIndentationTokens: (token, hangingIndent) -> + totalIndentSpaces = (@indentLevel * @tabLength) + hangingIndent + indentTokens = [] + while totalIndentSpaces > 0 + tokenLength = Math.min(@tabLength, totalIndentSpaces) + indentToken = token.buildSoftWrapIndentationToken(tokenLength) + indentTokens.push(indentToken) + totalIndentSpaces -= tokenLength - buildSoftWrapIndentationTokens: (token) -> - indentTokens = [0...Math.floor(@indentLevel)].map => - token.buildSoftWrapIndentationToken(@tabLength) + indentTokens - if @getOddIndentationSpaces() - indentTokens.concat( - token.buildSoftWrapIndentationToken @getOddIndentationSpaces() - ) - else - indentTokens - - softWrapAt: (column) -> - return [new TokenizedLine([], '', [0, 0], [0, 0]), this] if column == 0 + softWrapAt: (column, hangingIndent) -> + return [new TokenizedLine([], '', [0, 0], [0, 0]), this] if column is 0 rightTokens = new Array(@tokens...) leftTokens = [] - leftTextLength = 0 - while leftTextLength < column - if leftTextLength + rightTokens[0].value.length > column - rightTokens[0..0] = rightTokens[0].splitAt(column - leftTextLength) + leftScreenColumn = 0 + + while leftScreenColumn < column + if leftScreenColumn + rightTokens[0].screenDelta > column + rightTokens[0..0] = rightTokens[0].splitAt(column - leftScreenColumn) nextToken = rightTokens.shift() - leftTextLength += nextToken.value.length + leftScreenColumn += nextToken.screenDelta leftTokens.push nextToken - indentationTokens = @buildSoftWrapIndentationTokens(leftTokens[0]) + indentationTokens = @buildSoftWrapIndentationTokens(leftTokens[0], hangingIndent) leftFragment = new TokenizedLine( tokens: leftTokens @@ -167,13 +178,8 @@ class TokenizedLine isSoftWrapped: -> @lineEnding is null - isColumnOutsideSoftWrapIndentation: (column) -> - return true if @softWrapIndentationTokens.length == 0 - - column > @softWrapIndentationDelta - isColumnInsideSoftWrapIndentation: (column) -> - return false if @softWrapIndentationTokens.length == 0 + return false if @softWrapIndentationTokens.length is 0 column < @softWrapIndentationDelta @@ -184,7 +190,7 @@ class TokenizedLine _.reduce @softWrapIndentationTokens, ((acc, token) -> acc + token.screenDelta), 0 hasOnlySoftWrapIndentation: -> - @tokens.length == @softWrapIndentationTokens.length + @tokens.length is @softWrapIndentationTokens.length tokenAtBufferColumn: (bufferColumn) -> @tokens[@tokenIndexAtBufferColumn(bufferColumn)] @@ -216,19 +222,20 @@ class TokenizedLine outputTokens markLeadingAndTrailingWhitespaceTokens: -> - firstNonWhitespaceIndex = @text.search(NonWhitespaceRegex) - if firstNonWhitespaceIndex > 0 and isPairedCharacter(@text, firstNonWhitespaceIndex - 1) - firstNonWhitespaceIndex-- + @firstNonWhitespaceIndex = @text.search(NonWhitespaceRegex) + if @firstNonWhitespaceIndex > 0 and isPairedCharacter(@text, @firstNonWhitespaceIndex - 1) + @firstNonWhitespaceIndex-- firstTrailingWhitespaceIndex = @text.search(TrailingWhitespaceRegex) @lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0 index = 0 for token in @tokens - if index < firstNonWhitespaceIndex - token.firstNonWhitespaceIndex = Math.min(index + token.value.length, firstNonWhitespaceIndex - index) + if index < @firstNonWhitespaceIndex + token.firstNonWhitespaceIndex = Math.min(index + token.value.length, @firstNonWhitespaceIndex - index) # Only the *last* segment of a soft-wrapped line can have trailing whitespace if @lineEnding? and (index + token.value.length > firstTrailingWhitespaceIndex) token.firstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - index) index += token.value.length + return substituteInvisibleCharacters: -> invisibles = @invisibles @@ -316,6 +323,8 @@ class TokenizedLine for j in [i...desiredScopeDescriptor.length] scopeStack.push(new Scope(desiredScopeDescriptor[j])) + return + class Scope constructor: (@scope) -> @children = [] diff --git a/src/typescript.coffee b/src/typescript.coffee new file mode 100644 index 000000000..3a54941f3 --- /dev/null +++ b/src/typescript.coffee @@ -0,0 +1,106 @@ +### +Cache for source code transpiled by TypeScript. + +Inspired by https://github.com/atom/atom/blob/7a719d585db96ff7d2977db9067e1d9d4d0adf1a/src/babel.coffee +### + +crypto = require 'crypto' +fs = require 'fs-plus' +path = require 'path' +tss = null # Defer until used + +stats = + hits: 0 + misses: 0 + +defaultOptions = + target: 1 # ES5 + module: 'commonjs' + sourceMap: true + +createTypeScriptVersionAndOptionsDigest = (version, options) -> + shasum = crypto.createHash('sha1') + # Include the version of typescript in the hash. + shasum.update('typescript', 'utf8') + shasum.update('\0', 'utf8') + shasum.update(version, 'utf8') + shasum.update('\0', 'utf8') + shasum.update(JSON.stringify(options)) + shasum.digest('hex') + +cacheDir = null +jsCacheDir = null + +getCachePath = (sourceCode) -> + digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex') + + unless jsCacheDir? + tssVersion = require('typescript-simple/package.json').version + jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tssVersion, defaultOptions)) + + path.join(jsCacheDir, "#{digest}.js") + +getCachedJavaScript = (cachePath) -> + if fs.isFileSync(cachePath) + try + cachedJavaScript = fs.readFileSync(cachePath, 'utf8') + stats.hits++ + return cachedJavaScript + null + +# Returns the TypeScript options that should be used to transpile filePath. +createOptions = (filePath) -> + options = filename: filePath + for key, value of defaultOptions + options[key] = value + options + +transpile = (sourceCode, filePath, cachePath) -> + options = createOptions(filePath) + unless tss? + {TypeScriptSimple} = require 'typescript-simple' + tss = new TypeScriptSimple(options, false) + js = tss.compile(sourceCode, filePath) + stats.misses++ + + try + fs.writeFileSync(cachePath, js) + + js + +# Function that obeys the contract of an entry in the require.extensions map. +# Returns the transpiled version of the JavaScript code at filePath, which is +# either generated on the fly or pulled from cache. +loadFile = (module, filePath) -> + sourceCode = fs.readFileSync(filePath, 'utf8') + cachePath = getCachePath(sourceCode) + js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath) + module._compile(js, filePath) + +register = -> + Object.defineProperty(require.extensions, '.ts', { + enumerable: true + writable: false + value: loadFile + }) + +setCacheDirectory = (newCacheDir) -> + if cacheDir isnt newCacheDir + cacheDir = newCacheDir + jsCacheDir = null + +module.exports = + register: register + setCacheDirectory: setCacheDirectory + getCacheMisses: -> stats.misses + getCacheHits: -> stats.hits + + # Visible for testing. + createTypeScriptVersionAndOptionsDigest: createTypeScriptVersionAndOptionsDigest + + addPathToCache: (filePath) -> + return if path.extname(filePath) isnt '.ts' + + sourceCode = fs.readFileSync(filePath, 'utf8') + cachePath = getCachePath(sourceCode) + transpile(sourceCode, filePath, cachePath) diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 4dbb5594e..050444ff0 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -178,6 +178,9 @@ class ViewRegistry @documentPollers = @documentPollers.filter (poller) -> poller isnt fn @stopPollingDocument() if @documentPollers.length is 0 + pollAfterNextUpdate: -> + @performDocumentPollAfterUpdate = true + clearDocumentRequests: -> @documentReaders = [] @documentWriters = [] @@ -194,6 +197,7 @@ class ViewRegistry writer() while writer = @documentWriters.shift() reader() while reader = @documentReaders.shift() @performDocumentPoll() if @performDocumentPollAfterUpdate + @performDocumentPollAfterUpdate = false startPollingDocument: -> @pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval) @@ -205,6 +209,5 @@ class ViewRegistry if @documentUpdateRequested @performDocumentPollAfterUpdate = true else - @performDocumentPollAfterUpdate = false poller() for poller in @documentPollers return diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 506fc7d73..7d67e87ab 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -31,6 +31,8 @@ class WindowEventHandler unless fs.isDirectorySync(pathToOpen) atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) + return + when 'update-available' atom.updateAvailable(detail) @@ -82,7 +84,7 @@ class WindowEventHandler if process.platform in ['win32', 'linux'] @subscribeToCommand $(window), 'window:toggle-menu-bar', -> - atom.config.set('core.autoHideMenuBar', !atom.config.get('core.autoHideMenuBar')) + atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar')) @subscribeToCommand $(document), 'core:focus-next', @focusNext @@ -154,6 +156,7 @@ class WindowEventHandler continue unless tabIndex >= 0 callback(element, tabIndex) + return focusNext: => focusedTabIndex = parseInt($(':focus').attr('tabindex')) or -Infinity diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 425d920d8..85fb2f308 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -16,10 +16,10 @@ class WorkspaceElement extends HTMLElement @initializeContent() @observeScrollbarStyle() @observeTextEditorFontConfig() - @createSpacePenShim() + @createSpacePenShim() if Grim.includeDeprecatedAPIs attachedCallback: -> - callAttachHooks(this) + callAttachHooks(this) if Grim.includeDeprecatedAPIs @focus() detachedCallback: -> @@ -82,7 +82,7 @@ class WorkspaceElement extends HTMLElement @appendChild(@panelContainers.modal) - @__spacePenView.setModel(@model) + @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs this getModel: -> @model @@ -136,6 +136,9 @@ atom.commands.add 'atom-workspace', 'application:open-folder': -> ipc.send('command', 'application:open-folder') 'application:open-dev': -> ipc.send('command', 'application:open-dev') 'application:open-safe': -> ipc.send('command', 'application:open-safe') + 'application:open-api-preview': -> ipc.send('command', 'application:open-api-preview') + 'application:open-dev-api-preview': -> ipc.send('command', 'application:open-dev-api-preview') + 'application:add-project-folder': -> atom.addProjectFolder() 'application:minimize': -> ipc.send('command', 'application:minimize') 'application:zoom': -> ipc.send('command', 'application:zoom') 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front') @@ -153,9 +156,9 @@ atom.commands.add 'atom-workspace', 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() 'window:focus-pane-on-right': -> @focusPaneViewOnRight() 'window:save-all': -> @getModel().saveAll() - 'window:toggle-invisibles': -> atom.config.toggle("editor.showInvisibles") + 'window:toggle-invisibles': -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles")) 'window:log-deprecation-warnings': -> Grim.logDeprecations() - 'window:toggle-auto-indent': -> atom.config.toggle("editor.autoIndent") + 'window:toggle-auto-indent': -> atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent")) 'pane:reopen-closed-item': -> @getModel().reopenItem() 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane() 'core:save': -> @getModel().saveActivePaneItem() diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index addfe63c5..6a4a685d8 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -222,7 +222,6 @@ class WorkspaceView extends View for editorElement in @panes.element.querySelectorAll('atom-pane > .item-views > atom-text-editor') $(editorElement).view() - ### Section: Deprecated ### diff --git a/src/workspace.coffee b/src/workspace.coffee index 49f84f9b8..c094aaf92 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1,14 +1,13 @@ -{deprecate} = require 'grim' +{includeDeprecatedAPIs, deprecate} = require 'grim' _ = require 'underscore-plus' path = require 'path' {join} = path -{Model} = require 'theorist' Q = require 'q' Serializable = require 'serializable' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' Grim = require 'grim' fs = require 'fs-plus' -StackTraceParser = require 'stacktrace-parser' +Model = require './model' TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' @@ -33,24 +32,14 @@ class Workspace extends Model atom.deserializers.add(this) Serializable.includeInto(this) - Object.defineProperty @::, 'activePaneItem', - get: -> - Grim.deprecate "Use ::getActivePaneItem() instead of the ::activePaneItem property" - @getActivePaneItem() - - Object.defineProperty @::, 'activePane', - get: -> - Grim.deprecate "Use ::getActivePane() instead of the ::activePane property" - @getActivePane() - - @properties - paneContainer: null - fullScreen: false - destroyedItemURIs: -> [] - constructor: (params) -> super + unless Grim.includeDeprecatedAPIs + @paneContainer = params?.paneContainer + @fullScreen = params?.fullScreen ? false + @destroyedItemURIs = params?.destroyedItemURIs ? [] + @emitter = new Emitter @openers = [] @@ -110,6 +99,7 @@ class Workspace extends Model packageNames.push(packageName) for scopeName in includedGrammarScopes ? [] addGrammar(atom.grammars.grammarForScopeName(scopeName)) + return editors = @getTextEditors() addGrammar(editor.getGrammar()) for editor in editors @@ -121,7 +111,7 @@ class Workspace extends Model _.uniq(packageNames) editorAdded: (editor) -> - @emit 'editor-created', editor + @emit 'editor-created', editor if includeDeprecatedAPIs installShellCommands: -> require('./command-installer').installShellCommandsInteractively() @@ -341,39 +331,16 @@ class Workspace extends Model @onDidAddPaneItem ({item, pane, index}) -> callback({textEditor: item, pane, index}) if item instanceof TextEditor - eachEditor: (callback) -> - deprecate("Use Workspace::observeTextEditors instead") - - callback(editor) for editor in @getEditors() - @subscribe this, 'editor-created', (editor) -> callback(editor) - - getEditors: -> - deprecate("Use Workspace::getTextEditors instead") - - editors = [] - for pane in @paneContainer.getPanes() - editors.push(item) for item in pane.getItems() when item instanceof TextEditor - - editors - - on: (eventName) -> - switch eventName - when 'editor-created' - deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.") - when 'uri-opened' - deprecate("Use Workspace::onDidOpen or Workspace::onDidAddPaneItem instead. https://atom.io/docs/api/latest/Workspace#instance-onDidOpen") - else - deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") - - super - ### Section: Opening ### - # Essential: Open a given a URI in Atom asynchronously. + # Essential: Opens the given URI in Atom asynchronously. + # If the URI is already open, the existing item for that URI will be + # activated. If no URI is given, or no registered opener can open + # the URI, a new empty {TextEditor} will be created. # - # * `uri` A {String} containing a URI. + # * `uri` (optional) A {String} containing a URI. # * `options` (optional) {Object} # * `initialLine` A {Number} indicating which row to move the cursor to # initially. Defaults to `0`. @@ -424,7 +391,7 @@ class Workspace extends Model # the containing pane. Defaults to `true`. openSync: (uri='', options={}) -> # TODO: Remove deprecated changeFocus option - if options.changeFocus? + if includeDeprecatedAPIs and options.changeFocus? deprecate("The `changeFocus` option has been renamed to `activatePane`") options.activatePane = options.changeFocus delete options.changeFocus @@ -435,7 +402,7 @@ class Workspace extends Model uri = atom.project.resolvePath(uri) item = @getActivePane().itemForURI(uri) if uri - item ?= opener(uri, options) for opener in @getOpeners() when !item + item ?= opener(uri, options) for opener in @getOpeners() when not item item ?= atom.project.openSync(uri, {initialLine, initialColumn}) @getActivePane().activateItem(item) @@ -445,7 +412,7 @@ class Workspace extends Model openURIInPane: (uri, pane, options={}) -> # TODO: Remove deprecated changeFocus option - if options.changeFocus? + if includeDeprecatedAPIs and options.changeFocus? deprecate("The `changeFocus` option has been renamed to `activatePane`") options.activatePane = options.changeFocus delete options.changeFocus @@ -454,7 +421,7 @@ class Workspace extends Model if uri? item = pane.itemForURI(uri) - item ?= opener(uri, options) for opener in @getOpeners() when !item + item ?= opener(uri, options) for opener in @getOpeners() when not item try item ?= atom.project.open(uri, options) @@ -481,7 +448,7 @@ class Workspace extends Model if options.initialLine? or options.initialColumn? item.setCursorBufferPosition?([options.initialLine, options.initialColumn]) index = pane.getActiveItemIndex() - @emit "uri-opened" + @emit "uri-opened" if includeDeprecatedAPIs @emitter.emit 'did-open', {uri, pane, item, index} item @@ -495,12 +462,6 @@ class Workspace extends Model else Q() - # Deprecated - reopenItemSync: -> - deprecate("Use Workspace::reopenItem instead") - if uri = @destroyedItemURIs.pop() - @openSync(uri) - # Public: Register an opener for a uri. # # An {TextEditor} will be used if no openers return a value. @@ -518,56 +479,24 @@ class Workspace extends Model # Returns a {Disposable} on which `.dispose()` can be called to remove the # opener. addOpener: (opener) -> - packageName = @getCallingPackageName() + if includeDeprecatedAPIs + packageName = @getCallingPackageName() - wrappedOpener = (uri, options) -> - item = opener(uri, options) - if item? and typeof item.getUri is 'function' and typeof item.getURI isnt 'function' - Grim.deprecate("Pane item with class `#{item.constructor.name}` should implement `::getURI` instead of `::getUri`.", {packageName}) - item + wrappedOpener = (uri, options) -> + item = opener(uri, options) + if item? and typeof item.getUri is 'function' and typeof item.getURI isnt 'function' + Grim.deprecate("Pane item with class `#{item.constructor.name}` should implement `::getURI` instead of `::getUri`.", {packageName}) + item - @openers.push(wrappedOpener) - new Disposable => _.remove(@openers, wrappedOpener) - - registerOpener: (opener) -> - Grim.deprecate("Call Workspace::addOpener instead") - @addOpener(opener) - - unregisterOpener: (opener) -> - Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead") - _.remove(@openers, opener) + @openers.push(wrappedOpener) + new Disposable => _.remove(@openers, wrappedOpener) + else + @openers.push(opener) + new Disposable => _.remove(@openers, opener) getOpeners: -> @openers - getCallingPackageName: -> - error = new Error - Error.captureStackTrace(error) - stack = StackTraceParser.parse(error.stack) - - packagePaths = @getPackagePathsByPackageName() - - for i in [0...stack.length] - stackFramePath = stack[i].file - - # Empty when it was run from the dev console - return unless stackFramePath - - for packageName, packagePath of packagePaths - continue if stackFramePath is 'node.js' - relativePath = path.relative(packagePath, stackFramePath) - return packageName unless /^\.\./.test(relativePath) - return - - getPackagePathsByPackageName: -> - packagePathsByPackageName = {} - for pack in atom.packages.getLoadedPackages() - packagePath = pack.path - if packagePath.indexOf('.atom/dev/packages') > -1 or packagePath.indexOf('.atom/packages') > -1 - packagePath = fs.realpathSync(packagePath) - packagePathsByPackageName[pack.name] = packagePath - packagePathsByPackageName - ### Section: Pane Items ### @@ -598,11 +527,6 @@ class Workspace extends Model activeItem = @getActivePaneItem() activeItem if activeItem instanceof TextEditor - # Deprecated - getActiveEditor: -> - Grim.deprecate "Call ::getActiveTextEditor instead" - @getActivePane()?.getActiveEditor() - # Save all pane items. saveAll: -> @paneContainer.saveAll() @@ -666,10 +590,6 @@ class Workspace extends Model paneForURI: (uri) -> @paneContainer.paneForURI(uri) - paneForUri: (uri) -> - deprecate("Use ::paneForURI instead.") - @paneForURI(uri) - # Extended: Get the {Pane} containing the given item. # # * `item` Item the returned pane contains. @@ -919,8 +839,8 @@ class Workspace extends Model openPaths = (buffer.getPath() for buffer in atom.project.getBuffers()) outOfProcessPaths = _.difference(filePaths, openPaths) - inProcessFinished = !openPaths.length - outOfProcessFinished = !outOfProcessPaths.length + inProcessFinished = not openPaths.length + outOfProcessFinished = not outOfProcessPaths.length checkFinished = -> deferred.resolve() if outOfProcessFinished and inProcessFinished @@ -944,3 +864,96 @@ class Workspace extends Model checkFinished() deferred.promise + +if includeDeprecatedAPIs + Workspace.properties + paneContainer: null + fullScreen: false + destroyedItemURIs: -> [] + + Object.defineProperty Workspace::, 'activePaneItem', + get: -> + Grim.deprecate "Use ::getActivePaneItem() instead of the ::activePaneItem property" + @getActivePaneItem() + + Object.defineProperty Workspace::, 'activePane', + get: -> + Grim.deprecate "Use ::getActivePane() instead of the ::activePane property" + @getActivePane() + + StackTraceParser = require 'stacktrace-parser' + + Workspace::getCallingPackageName = -> + error = new Error + Error.captureStackTrace(error) + stack = StackTraceParser.parse(error.stack) + + packagePaths = @getPackagePathsByPackageName() + + for i in [0...stack.length] + stackFramePath = stack[i].file + + # Empty when it was run from the dev console + return unless stackFramePath + + for packageName, packagePath of packagePaths + continue if stackFramePath is 'node.js' + relativePath = path.relative(packagePath, stackFramePath) + return packageName unless /^\.\./.test(relativePath) + return + + Workspace::getPackagePathsByPackageName = -> + packagePathsByPackageName = {} + for pack in atom.packages.getLoadedPackages() + packagePath = pack.path + if packagePath.indexOf('.atom/dev/packages') > -1 or packagePath.indexOf('.atom/packages') > -1 + packagePath = fs.realpathSync(packagePath) + packagePathsByPackageName[pack.name] = packagePath + packagePathsByPackageName + + Workspace::eachEditor = (callback) -> + deprecate("Use Workspace::observeTextEditors instead") + + callback(editor) for editor in @getEditors() + @subscribe this, 'editor-created', (editor) -> callback(editor) + + Workspace::getEditors = -> + deprecate("Use Workspace::getTextEditors instead") + + editors = [] + for pane in @paneContainer.getPanes() + editors.push(item) for item in pane.getItems() when item instanceof TextEditor + + editors + + Workspace::on = (eventName) -> + switch eventName + when 'editor-created' + deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.") + when 'uri-opened' + deprecate("Use Workspace::onDidOpen or Workspace::onDidAddPaneItem instead. https://atom.io/docs/api/latest/Workspace#instance-onDidOpen") + else + deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") + + super + + Workspace::reopenItemSync = -> + deprecate("Use Workspace::reopenItem instead") + if uri = @destroyedItemURIs.pop() + @openSync(uri) + + Workspace::registerOpener = (opener) -> + Grim.deprecate("Call Workspace::addOpener instead") + @addOpener(opener) + + Workspace::unregisterOpener = (opener) -> + Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead") + _.remove(@openers, opener) + + Workspace::getActiveEditor = -> + Grim.deprecate "Call ::getActiveTextEditor instead" + @getActivePane()?.getActiveEditor() + + Workspace::paneForUri = (uri) -> + deprecate("Use ::paneForURI instead.") + @paneForURI(uri) diff --git a/static/index.js b/static/index.js index e8142a698..aa582f845 100644 --- a/static/index.js +++ b/static/index.js @@ -34,6 +34,8 @@ window.onload = function() { ModuleCache.register(loadSettings); ModuleCache.add(loadSettings.resourcePath); + require('grim').includeDeprecatedAPIs = !loadSettings.apiPreviewMode; + // Start the crash reporter before anything else. require('crash-reporter').start({ productName: 'Atom', @@ -47,6 +49,7 @@ window.onload = function() { setupCsonCache(cacheDir); setupSourceMapCache(cacheDir); setupBabel(cacheDir); + setupTypeScript(cacheDir); require(loadSettings.bootstrapScript); require('ipc').sendChannel('window-command', 'window:loaded'); @@ -95,6 +98,12 @@ var setupBabel = function(cacheDir) { babel.register(); } +var setupTypeScript = function(cacheDir) { + var typescript = require('../src/typescript'); + typescript.setCacheDirectory(path.join(cacheDir, 'typescript')); + typescript.register(); +} + var setupCsonCache = function(cacheDir) { require('season').setCacheDir(path.join(cacheDir, 'cson')); } diff --git a/static/panes.less b/static/panes.less index 672878b58..163721fcc 100644 --- a/static/panes.less +++ b/static/panes.less @@ -47,7 +47,7 @@ atom-pane-container { display: -webkit-flex; -webkit-flex: 1; -webkit-flex-direction: column; - overflow: hidden; + overflow: visible; .item-views { -webkit-flex: 1; diff --git a/static/text-editor-shadow.less b/static/text-editor-shadow.less index 63d27de7f..c67637290 100644 --- a/static/text-editor-shadow.less +++ b/static/text-editor-shadow.less @@ -9,7 +9,6 @@ .editor-contents--private { width: 100%; - overflow: hidden; cursor: text; display: -webkit-flex; -webkit-user-select: none; diff --git a/static/variables/syntax-variables.less b/static/variables/syntax-variables.less index 565341f7e..f569773e8 100644 --- a/static/variables/syntax-variables.less +++ b/static/variables/syntax-variables.less @@ -28,3 +28,16 @@ @syntax-color-modified: orange; @syntax-color-removed: red; @syntax-color-renamed: blue; + +// For language entity colors +@syntax-color-variable: #DF6A73; +@syntax-color-constant: #DF6A73; +@syntax-color-property: #DF6A73; +@syntax-color-value: #D29B67; +@syntax-color-function: #61AEEF; +@syntax-color-method: @syntax-color-function; +@syntax-color-class: #E5C17C; +@syntax-color-keyword: #555; +@syntax-color-tag: #555; +@syntax-color-import: #97C378; +@syntax-color-snippet: #97C378;