diff --git a/.node-version b/.node-version index b58b2ff99..87a1cf595 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v0.10.33 +v0.12.0 diff --git a/apm/package.json b/apm/package.json index 91e03bdca..e491d584e 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.138.0" + "atom-package-manager": "0.140.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index be093d93e..05efaeb38 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -220,7 +220,6 @@ module.exports = (grunt) -> grunt.registerTask('compile', ['coffee', 'prebuild-less', 'cson', 'peg']) grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint']) grunt.registerTask('test', ['shell:kill-atom', 'run-specs']) - grunt.registerTask('docs', ['markdown:guides', 'build-docs']) ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build'] ciTasks.push('dump-symbols') if process.platform isnt 'win32' diff --git a/build/package.json b/build/package.json index 4bfc940a2..ea5b8e6de 100644 --- a/build/package.json +++ b/build/package.json @@ -12,7 +12,7 @@ "fs-plus": "2.x", "github-releases": "~0.2.0", "grunt": "~0.4.1", - "grunt-atom-shell-installer": "^0.21.0", + "grunt-atom-shell-installer": "^0.23.0", "grunt-cli": "~0.1.9", "grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe", "grunt-contrib-coffee": "~0.12.0", @@ -26,7 +26,7 @@ "harmony-collections": "~0.3.8", "legal-eagle": "~0.9.0", "minidump": "~0.8", - "npm": "~1.4.5", + "npm": "2.5.1", "rcedit": "~0.3.0", "request": "~2.27.0", "rimraf": "~2.2.2", diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 6f9dbfde5..83815dd4a 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -7,7 +7,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * OS with 64-bit or 32-bit architecture * C++ toolchain * [Git](http://git-scm.com/) - * [Node.js](http://nodejs.org/download/) v0.10.x + * [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x) * [npm](https://www.npmjs.com/) v1.4.x (bundled with Node.js) * `npm -v` to check the version. * `npm config set python /usr/bin/python2 -g` to ensure that gyp uses python2. @@ -18,6 +18,9 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * `sudo apt-get install build-essential git libgnome-keyring-dev fakeroot` * Instructions for [Node.js](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#ubuntu-mint-elementary-os). + * Make sure the command `node` is available after Node.js installation (some sytems install it as `nodejs`). + * Use `which node` to check if it is available. + * Use `sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10` to update it. ### Fedora / CentOS / RHEL diff --git a/docs/build-instructions/os-x.md b/docs/build-instructions/os-x.md index b07e3bcf8..d8685cff8 100644 --- a/docs/build-instructions/os-x.md +++ b/docs/build-instructions/os-x.md @@ -3,7 +3,7 @@ ## Requirements * OS X 10.8 or later - * [node.js](http://nodejs.org/download/) v0.10.x + * [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x) * Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install) ## Instructions diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 02934ee33..fddb941c7 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -5,7 +5,7 @@ ### On Windows 7 * [Visual C++ 2010 Express](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_4) * [Visual Studio 2010 Service Pack 1](http://www.microsoft.com/en-us/download/details.aspx?id=23691) - * [node.js](http://nodejs.org/download/) v0.10.x + * [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x) * For 64-bit builds of node and native modules you **must** have the [Windows 7 64-bit SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279). You may also need the [compiler update for the Windows SDK 7.1](http://www.microsoft.com/en-us/download/details.aspx?id=4422) @@ -18,7 +18,7 @@ ### On Windows 8 * [Visual Studio Express 2013 for Windows Desktop](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_2) - * [node.js](http://nodejs.org/download/) v0.10.x + * [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x) * [Python](https://www.python.org/downloads/) v2.7.x (required by [node-gyp](https://github.com/TooTallNate/node-gyp)) * [GitHub for Windows](http://windows.github.com/) diff --git a/menus/darwin.cson b/menus/darwin.cson index 4c221c62c..7283ba16f 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -7,6 +7,7 @@ { label: 'VERSION', enabled: false } { label: 'Restart and Install Update', command: 'application:install-update', visible: false} { label: 'Check for Update', command: 'application:check-for-update', visible: false} + { label: 'Checking for Update', enabled: false, visible: false} { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: 'Preferences...', command: 'application:show-settings' } diff --git a/menus/win32.cson b/menus/win32.cson index 0067b9128..a3e5c8b8d 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -166,6 +166,7 @@ { label: 'VERSION', enabled: false } { label: 'Restart and Install Update', command: 'application:install-update', visible: false} { label: 'Check for Update', command: 'application:check-for-update', visible: false} + { label: 'Checking for Update', enabled: false, visible: false} { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: '&Documentation', command: 'application:open-documentation' } diff --git a/package.json b/package.json index 89477634a..39629d4a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.180.0", + "version": "0.183.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -19,10 +19,10 @@ ], "atomShellVersion": "0.21.0", "dependencies": { - "6to5-core": "^3.0.14", "async": "0.2.6", "atom-keymap": "^3.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", "coffee-cash": "0.7.0", @@ -30,25 +30,25 @@ "coffeestack": "^1.1.1", "color": "^0.7.3", "delegato": "^1", - "emissary": "^1.3.1", + "emissary": "^1.3.3", "event-kit": "^1.0.2", "first-mate": "^3.0.0", "fs-plus": "^2.5", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^3.0.0", - "grim": "1.1.2", + "grim": "1.2", "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.4", "jquery": "^2.1.1", - "less-cache": "0.21", + "less-cache": "0.22", "marked": "^0.3", "mixto": "^1", "nslog": "^2.0.0", "oniguruma": "^4.0.0", "optimist": "0.4.0", - "pathwatcher": "^3.1.1", - "property-accessors": "^1", + "pathwatcher": "^3.3.1", + "property-accessors": "^1.1.3", "q": "^1.1.2", "random-words": "0.0.1", "react-atom-fork": "^0.11.5", @@ -64,10 +64,9 @@ "space-pen": "3.8.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", - "text-buffer": "^4.1.4", + "text-buffer": "^4.1.5", "theorist": "^1.0.2", - "underscore-plus": "^1.6.6", - "vm-compatibility-layer": "0.1.0" + "underscore-plus": "^1.6.6" }, "packageDependencies": { "atom-dark-syntax": "0.26.0", @@ -86,54 +85,54 @@ "autocomplete": "0.44.0", "autoflow": "0.22.0", "autosave": "0.20.0", - "background-tips": "0.22.0", + "background-tips": "0.23.0", "bookmarks": "0.35.0", "bracket-matcher": "0.71.0", "command-palette": "0.34.0", - "deprecation-cop": "0.36.0", + "deprecation-cop": "0.37.0", "dev-live-reload": "0.41.0", "encoding-selector": "0.18.0", "exception-reporting": "0.24.0", "find-and-replace": "0.157.0", - "fuzzy-finder": "0.66.0", - "git-diff": "0.52.0", + "fuzzy-finder": "0.68.0", + "git-diff": "0.53.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.28.0", + "keybinding-resolver": "0.29.0", "link": "0.30.0", "markdown-preview": "0.137.0", - "metrics": "0.43.0", - "notifications": "0.27.0", + "metrics": "0.45.0", + "notifications": "0.28.0", "open-on-github": "0.32.0", "package-generator": "0.38.0", - "release-notes": "0.50.0", - "settings-view": "0.181.0", - "snippets": "0.74.0", - "spell-check": "0.54.0", + "release-notes": "0.51.0", + "settings-view": "0.183.0", + "snippets": "0.76.0", + "spell-check": "0.55.0", "status-bar": "0.60.0", "styleguide": "0.44.0", "symbols-view": "0.83.0", "tabs": "0.67.0", "timecop": "0.30.0", - "tree-view": "0.155.0", + "tree-view": "0.160.0", "update-package-dependencies": "0.8.0", - "welcome": "0.21.0", + "welcome": "0.24.0", "whitespace": "0.29.0", "wrap-guide": "0.31.0", - "language-c": "0.38.0", + "language-c": "0.40.0", "language-clojure": "0.12.0", "language-coffee-script": "0.39.0", "language-csharp": "0.5.0", "language-css": "0.28.0", - "language-gfm": "0.63.0", + "language-gfm": "0.64.0", "language-git": "0.10.0", "language-go": "0.21.0", "language-html": "0.29.0", "language-hyperlink": "0.12.2", "language-java": "0.14.0", - "language-javascript": "0.56.0", + "language-javascript": "0.57.0", "language-json": "0.12.0", "language-less": "0.24.0", "language-make": "0.13.0", @@ -144,7 +143,7 @@ "language-property-list": "0.8.0", "language-python": "0.32.0", "language-ruby": "0.48.0", - "language-ruby-on-rails": "0.18.0", + "language-ruby-on-rails": "0.19.0", "language-sass": "0.34.0", "language-shellscript": "0.12.0", "language-source": "0.9.0", diff --git a/resources/linux/debian/control.in b/resources/linux/debian/control.in index 13442a882..1578f544b 100644 --- a/resources/linux/debian/control.in +++ b/resources/linux/debian/control.in @@ -1,6 +1,6 @@ Package: <%= name %> Version: <%= version %> -Depends: git, gconf2, gconf-service, libgtk2.0-0, libudev0 | libudev1, libgcrypt11, libnotify4, libxtst6, libnss3, python, gvfs-bin, xdg-utils +Depends: git, gconf2, gconf-service, libgtk2.0-0, libudev0 | libudev1, libgcrypt11 | libgcrypt20, libnotify4, libxtst6, libnss3, python, gvfs-bin, xdg-utils Suggests: libgnome-keyring0, gir1.2-gnomekeyring-1.0 Section: <%= section %> Priority: optional diff --git a/script/bootstrap b/script/bootstrap index 0843730f8..270abff7c 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -64,18 +64,16 @@ function bootstrap() { var buildInstallCommand = initialNpmCommand + npmFlags + 'install'; var buildInstallOptions = {cwd: path.resolve(__dirname, '..', 'build')}; - var apmInstallCommand = npmPath + npmFlags + 'install'; + var apmInstallCommand = npmPath + npmFlags + '--target=0.10.35 ' + 'install'; var apmInstallOptions = {cwd: apmInstallPath}; var moduleInstallCommand = apmPath + ' install' + apmFlags; var dedupeApmCommand = apmPath + ' dedupe' + apmFlags; - var dedupeNpmCommand = npmPath + npmFlags + 'dedupe'; if (process.argv.indexOf('--no-quiet') === -1) { buildInstallCommand += ' --loglevel error'; apmInstallCommand += ' --loglevel error'; moduleInstallCommand += ' --loglevel error'; dedupeApmCommand += ' --quiet'; - dedupeNpmCommand += ' --quiet'; buildInstallOptions.ignoreStdout = true; apmInstallOptions.ignoreStdout = true; } @@ -99,12 +97,6 @@ function bootstrap() { apmPath + ' clean' + apmFlags, moduleInstallCommand, dedupeApmCommand + ' ' + packagesToDedupe.join(' '), - { - command: dedupeNpmCommand + ' request semver', - options: { - cwd: path.resolve(__dirname, '..', 'apm', 'node_modules', 'atom-package-manager') - } - }, ]; process.chdir(path.dirname(__dirname)); diff --git a/spec/6to5-spec.coffee b/spec/babel-spec.coffee similarity index 51% rename from spec/6to5-spec.coffee rename to spec/babel-spec.coffee index 2cb8d1ba6..f9abac930 100644 --- a/spec/6to5-spec.coffee +++ b/spec/babel-spec.coffee @@ -1,8 +1,8 @@ -to5 = require '../src/6to5' +babel = require '../src/babel' crypto = require 'crypto' -describe "6to5 transpiler support", -> - describe "::create6to5VersionAndOptionsDigest", -> +describe "Babel transpiler support", -> + describe "::createBabelVersionAndOptionsDigest", -> it "returns a digest for the library version and specified options", -> defaultOptions = blacklist: [ @@ -16,26 +16,36 @@ describe "6to5 transpiler support", -> sourceMap: 'inline' version = '3.0.14' shasum = crypto.createHash('sha1') - shasum.update('6to5-core', 'utf8') + shasum.update('babel-core', 'utf8') shasum.update('\0', 'utf8') shasum.update(version, 'utf8') shasum.update('\0', 'utf8') shasum.update('{"blacklist": ["useStrict",],"experimental": true,"optional": ["asyncToGenerator",],"reactCompat": true,"sourceMap": "inline",}') expectedDigest = shasum.digest('hex') - observedDigest = to5.create6to5VersionAndOptionsDigest(version, defaultOptions) + observedDigest = babel.createBabelVersionAndOptionsDigest(version, defaultOptions) expect(observedDigest).toEqual expectedDigest + describe "when a .js file starts with 'use babel';", -> + it "transpiles it using babel", -> + transpiled = require('./fixtures/babel/babel-single-quotes.js') + expect(transpiled(3)).toBe 4 + describe "when a .js file starts with 'use 6to5';", -> it "transpiles it using 6to5", -> - transpiled = require('./fixtures/6to5/single-quotes.js') + transpiled = require('./fixtures/babel/6to5-single-quotes.js') + expect(transpiled(3)).toBe 4 + + describe 'when a .js file starts with "use babel";', -> + it "transpiles it using babel", -> + transpiled = require('./fixtures/babel/babel-double-quotes.js') expect(transpiled(3)).toBe 4 describe 'when a .js file starts with "use 6to5";', -> - it "transpiles it using 6to5", -> - transpiled = require('./fixtures/6to5/double-quotes.js') + it "transpiles it using babel", -> + transpiled = require('./fixtures/babel/6to5-double-quotes.js') expect(transpiled(3)).toBe 4 describe "when a .js file does not start with 'use 6to6';", -> - it "does not transpile it using 6to5", -> - expect(-> require('./fixtures/6to5/invalid.js')).toThrow() + it "does not transpile it using babel", -> + expect(-> require('./fixtures/babel/invalid.js')).toThrow() diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index 0cae70434..534457157 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -2,27 +2,27 @@ path = require 'path' CSON = require 'season' CoffeeCache = require 'coffee-cash' -to5 = require '../src/6to5' +babel = require '../src/babel' CompileCache = require '../src/compile-cache' describe "Compile Cache", -> describe ".addPathToCache(filePath)", -> - it "adds the path to the correct CSON, CoffeeScript, or 6to5 cache", -> + it "adds the path to the correct CSON, CoffeeScript, or babel cache", -> spyOn(CSON, 'readFileSync').andCallThrough() spyOn(CoffeeCache, 'addPathToCache').andCallThrough() - spyOn(to5, 'addPathToCache').andCallThrough() + spyOn(babel, 'addPathToCache').andCallThrough() CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'cson.cson')) expect(CSON.readFileSync.callCount).toBe 1 expect(CoffeeCache.addPathToCache.callCount).toBe 0 - expect(to5.addPathToCache.callCount).toBe 0 + expect(babel.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(to5.addPathToCache.callCount).toBe 0 + expect(babel.addPathToCache.callCount).toBe 0 - CompileCache.addPathToCache(path.join(__dirname, 'fixtures', '6to5', 'double-quotes.js')) + CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'babel', 'babel-double-quotes.js')) expect(CSON.readFileSync.callCount).toBe 1 expect(CoffeeCache.addPathToCache.callCount).toBe 1 - expect(to5.addPathToCache.callCount).toBe 1 + expect(babel.addPathToCache.callCount).toBe 1 diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee new file mode 100644 index 000000000..780f2afd5 --- /dev/null +++ b/spec/default-directory-provider-spec.coffee @@ -0,0 +1,40 @@ +DefaultDirectoryProvider = require "../src/default-directory-provider" +path = require "path" +temp = require "temp" + +describe "DefaultDirectoryProvider", -> + describe ".directoryForURISync(uri)", -> + it "returns a Directory with a path that matches the uri", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + + directory = provider.directoryForURISync(tmp) + expect(directory.getPath()).toEqual tmp + + it "normalizes its input before creating a Directory for it", -> + 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 + + directory = provider.directoryForURISync(nonNormalizedPath) + expect(directory.getPath()).toEqual tmp + + it "creates a Directory for its parent dir when passed a file", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + file = path.join(tmp, "example.txt") + fs.writeFileSync(file, "data") + + directory = provider.directoryForURISync(file) + expect(directory.getPath()).toEqual tmp + + describe ".directoryForURI(uri)", -> + it "returns a Promise that resolves to a Directory with a path that matches the uri", -> + provider = new DefaultDirectoryProvider() + tmp = temp.mkdirSync() + + waitsForPromise -> + provider.directoryForURI(tmp).then (directory) -> + expect(directory.getPath()).toEqual tmp diff --git a/spec/fixtures/6to5/double-quotes.js b/spec/fixtures/babel/6to5-double-quotes.js similarity index 100% rename from spec/fixtures/6to5/double-quotes.js rename to spec/fixtures/babel/6to5-double-quotes.js diff --git a/spec/fixtures/6to5/single-quotes.js b/spec/fixtures/babel/6to5-single-quotes.js similarity index 100% rename from spec/fixtures/6to5/single-quotes.js rename to spec/fixtures/babel/6to5-single-quotes.js diff --git a/spec/fixtures/babel/babel-double-quotes.js b/spec/fixtures/babel/babel-double-quotes.js new file mode 100644 index 000000000..7d8ebfad7 --- /dev/null +++ b/spec/fixtures/babel/babel-double-quotes.js @@ -0,0 +1,3 @@ +"use babel"; + +module.exports = v => v + 1 diff --git a/spec/fixtures/babel/babel-single-quotes.js b/spec/fixtures/babel/babel-single-quotes.js new file mode 100644 index 000000000..980fe51be --- /dev/null +++ b/spec/fixtures/babel/babel-single-quotes.js @@ -0,0 +1,3 @@ +'use babel'; + +module.exports = v => v + 1 diff --git a/spec/fixtures/6to5/invalid.js b/spec/fixtures/babel/invalid.js similarity index 100% rename from spec/fixtures/6to5/invalid.js rename to spec/fixtures/babel/invalid.js diff --git a/spec/fixtures/task-handler-with-deprecations.coffee b/spec/fixtures/task-handler-with-deprecations.coffee new file mode 100644 index 000000000..6ba8e86e8 --- /dev/null +++ b/spec/fixtures/task-handler-with-deprecations.coffee @@ -0,0 +1,3 @@ +{Git} = require 'atom' + +module.exports = -> diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee index 2082bd50f..7d77adc36 100644 --- a/spec/git-repository-provider-spec.coffee +++ b/spec/git-repository-provider-spec.coffee @@ -1,11 +1,12 @@ path = require 'path' +fs = require 'fs-plus' +temp = require 'temp' {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' GitRepositoryProvider = require '../src/git-repository-provider' describe "GitRepositoryProvider", -> describe ".repositoryForDirectory(directory)", -> - describe "when specified a Directory with a Git repository", -> it "returns a Promise that resolves to a GitRepository", -> waitsForPromise -> @@ -37,6 +38,19 @@ describe "GitRepositoryProvider", -> it "returns a Promise that resolves to null", -> waitsForPromise -> provider = new GitRepositoryProvider atom.project - directory = new Directory '/tmp' + directory = new Directory temp.mkdirSync('dir') + provider.repositoryForDirectory(directory).then (result) -> + expect(result).toBe null + + describe "when specified a Directory with an invalid Git repository", -> + it "returns a Promise that resolves to null", -> + waitsForPromise -> + provider = new GitRepositoryProvider atom.project + dirPath = temp.mkdirSync('dir') + fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '') + + directory = new Directory dirPath provider.repositoryForDirectory(directory).then (result) -> expect(result).toBe null diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 8597dabdd..5e8265996 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -89,23 +89,26 @@ buildAtomClient = (args, env) -> done() module.exports = (args, env, fn) -> - chromedriver = spawn(ChromedriverPath, [ - "--verbose", - "--port=#{ChromedriverPort}", - "--url-base=/wd/hub" - ]) + [chromedriver, chromedriverLogs, chromedriverExit] = [] - waits(50) + runs -> + chromedriver = spawn(ChromedriverPath, [ + "--verbose", + "--port=#{ChromedriverPort}", + "--url-base=/wd/hub" + ]) - chromedriverLogs = [] - chromedriverExit = new Promise (resolve) -> - errorCode = null - chromedriver.on "exit", (code, signal) -> - errorCode = code unless signal? - chromedriver.stderr.on "data", (log) -> - chromedriverLogs.push(log.toString()) - chromedriver.stderr.on "close", -> - resolve(errorCode) + chromedriverLogs = [] + chromedriverExit = new Promise (resolve) -> + errorCode = null + chromedriver.on "exit", (code, signal) -> + errorCode = code unless signal? + chromedriver.stderr.on "data", (log) -> + chromedriverLogs.push(log.toString()) + chromedriver.stderr.on "close", -> + resolve(errorCode) + + waits(100) waitsFor("webdriver to finish", (done) -> finish = once -> diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 4cc5b1525..3161cd932 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -86,7 +86,7 @@ describe "Starting Atom", -> .waitForPaneItemCount(1, 5000) it "allows multiple project directories to be passed as separate arguments", -> - runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: AtomHome}, (client) -> + runAtom [tempDirPath, otherTempDirPath, "--multi-folder"], {ATOM_HOME: AtomHome}, (client) -> client .waitForExist("atom-workspace", 5000) .then((exists) -> expect(exists).toBe true) @@ -100,3 +100,51 @@ describe "Starting Atom", -> .waitForPaneItemCount(1, 5000) .execute(-> atom.project.getPaths()) .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) -> + projectPaths = [] + + client + .waitForWindowCount(2, 5000) + .windowHandles() + .then ({value: windowHandles}) -> + @window(windowHandles[0]) + .execute(-> atom.project.getPaths()) + .then ({value}) -> + expect(value).toHaveLength(1) + projectPaths.push(value[0]) + .window(windowHandles[1]) + .execute(-> atom.project.getPaths()) + .then ({value}) -> + expect(value).toHaveLength(1) + projectPaths.push(value[0]) + .then -> + expect(projectPaths.sort()).toEqual([tempDirPath, otherTempDirPath].sort()) + + it "opens the path in the current window if it doesn't have a project path yet", -> + runAtom [], {ATOM_HOME: AtomHome}, (client) -> + client + .waitForExist("atom-workspace") + .startAnotherAtom([tempDirPath], ATOM_HOME: AtomHome) + .waitUntil((-> + @title() + .then(({value}) -> value.indexOf(path.basename(tempDirPath)) >= 0)), 5000) + .waitForWindowCount(1, 5000) + + it "always opens with a single untitled buffer when launched w/ no path", -> + runAtom [], {ATOM_HOME: AtomHome}, (client) -> + client + .waitForExist("atom-workspace") + .waitForPaneItemCount(1, 5000) + + runAtom [], {ATOM_HOME: AtomHome}, (client) -> + client + .waitForExist("atom-workspace") + .waitForPaneItemCount(1, 5000) + + # Opening with no file paths always creates a new window, even if + # existing windows have no project paths. + .waitForNewWindow(-> + @startAnotherAtom([], ATOM_HOME: AtomHome) + , 5000) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index dbd0a0fbd..466431faf 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -13,6 +13,104 @@ describe "Project", -> beforeEach -> atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) + describe "constructor", -> + it "enables a custom DirectoryProvider to supersede the DefaultDirectoryProvider", -> + remotePath = "ssh://foreign-directory:8080/" + class DummyDirectory + constructor: (@path) -> + getPath: -> @path + getFile: -> existsSync: -> false + getSubdirectory: -> existsSync: -> false + isRoot: -> true + off: -> + contains: (filePath) -> filePath.startsWith(remotePath) + + directoryProvider = + directoryForURISync: (uri) -> + if uri.startsWith("ssh://") + new DummyDirectory(uri) + else + null + directoryForURI: (uri) -> throw new Error("This should not be called.") + atom.packages.serviceHub.provide( + "atom.directory-provider", "0.1.0", directoryProvider) + + tmp = temp.mkdirSync() + atom.project.setPaths([tmp, remotePath]) + directories = atom.project.getDirectories() + expect(directories.length).toBe 2 + + localDirectory = directories[0] + expect(localDirectory.getPath()).toBe tmp + expect(localDirectory instanceof Directory).toBe true + + dummyDirectory = directories[1] + expect(dummyDirectory.getPath()).toBe remotePath + expect(dummyDirectory instanceof DummyDirectory).toBe true + + expect(atom.project.getPaths()).toEqual([tmp, remotePath]) + + # Make sure that DummyDirectory.contains() is honored. + remotePathSubdirectory = remotePath + "a/subdirectory" + atom.project.addPath(remotePathSubdirectory) + expect(atom.project.getDirectories().length).toBe 2 + + # Make sure that a new DummyDirectory that is not contained by the first + # DummyDirectory can be added. + otherRemotePath = "ssh://other-foreign-directory:8080/" + atom.project.addPath(otherRemotePath) + newDirectories = atom.project.getDirectories() + expect(newDirectories.length).toBe 3 + otherDummyDirectory = newDirectories[2] + expect(otherDummyDirectory.getPath()).toBe otherRemotePath + expect(otherDummyDirectory instanceof DummyDirectory).toBe true + + it "uses the default directory provider if no custom provider can handle the URI", -> + directoryProvider = + directoryForURISync: (uri) -> null + directoryForURI: (uri) -> throw new Error("This should not be called.") + atom.packages.serviceHub.provide( + "atom.directory-provider", "0.1.0", directoryProvider) + + tmp = temp.mkdirSync() + atom.project.setPaths([tmp]) + directories = atom.project.getDirectories() + expect(directories.length).toBe 1 + expect(directories[0].getPath()).toBe tmp + + it "tries to update repositories when a new RepositoryProvider is registered", -> + tmp = temp.mkdirSync('atom-project') + atom.project.setPaths([tmp]) + expect(atom.project.getRepositories()).toEqual [null] + expect(atom.project.repositoryProviders.length).toEqual 1 + + # Register a new RepositoryProvider. + dummyRepository = destroy: -> + repositoryProvider = + repositoryForDirectory: (directory) -> Promise.resolve(dummyRepository) + repositoryForDirectorySync: (directory) -> dummyRepository + atom.packages.serviceHub.provide( + "atom.repository-provider", "0.1.0", repositoryProvider) + + expect(atom.project.repositoryProviders.length).toBe 2 + expect(atom.project.getRepositories()).toEqual [dummyRepository] + + it "does not update @repositories if every path has a Repository", -> + repositories = atom.project.getRepositories() + expect(repositories.length).toEqual 1 + [repository] = repositories + expect(repository).toBeTruthy() + + # Register a new RepositoryProvider. + dummyRepository = destroy: -> + repositoryProvider = + repositoryForDirectory: (directory) -> Promise.resolve(dummyRepository) + repositoryForDirectorySync: (directory) -> dummyRepository + atom.packages.serviceHub.provide( + "atom.repository-provider", "0.1.0", repositoryProvider) + + expect(atom.project.getRepositories()).toBe repositories + describe "serialization", -> deserializedProject = null @@ -296,12 +394,46 @@ describe "Project", -> expect(atom.project.getPaths()).toEqual([oldPath]) expect(onDidChangePathsSpy).not.toHaveBeenCalled() + describe ".removePath(path)", -> + onDidChangePathsSpy = null + + beforeEach -> + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + it "removes the directory and repository for the path", -> + result = atom.project.removePath(atom.project.getPaths()[0]) + expect(atom.project.getDirectories()).toEqual([]) + expect(atom.project.getRepositories()).toEqual([]) + expect(atom.project.getPaths()).toEqual([]) + expect(result).toBe true + expect(onDidChangePathsSpy).toHaveBeenCalled() + + it "does nothing if the path is not one of the project's root paths", -> + originalPaths = atom.project.getPaths() + result = atom.project.removePath(originalPaths[0] + "xyz") + expect(result).toBe false + expect(atom.project.getPaths()).toEqual(originalPaths) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + + it "doesn't destroy the repository if it is shared by another root directory", -> + atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")]) + atom.project.removePath(__dirname) + expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")]) + expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false + describe ".relativize(path)", -> it "returns the path, relative to whichever root directory it is inside of", -> + atom.project.addPath(temp.mkdirSync("another-path")) + rootPath = atom.project.getPaths()[0] childPath = path.join(rootPath, "some", "child", "directory") expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") + rootPath = atom.project.getPaths()[1] + childPath = path.join(rootPath, "some", "child", "directory") + expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") + it "returns the given path if it is not in any of the root directories", -> randomPath = path.join("some", "random", "path") expect(atom.project.relativize(randomPath)).toBe randomPath diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 484ade2d9..d34c72b1a 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -76,12 +76,13 @@ beforeEach -> $.fx.off = true documentTitle = null projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures') + atom.packages.serviceHub = new ServiceHub atom.project = new Project(paths: [projectPath]) atom.workspace = new Workspace() - atom.packages.serviceHub = new ServiceHub atom.keymaps.keyBindings = _.clone(keyBindingsToRestore) atom.commands.restoreSnapshot(commandsToRestore) atom.styles.restoreSnapshot(styleElementsToRestore) + atom.views.clearDocumentRequests() atom.workspaceViewParentSelector = '#jasmine-content' diff --git a/spec/task-spec.coffee b/spec/task-spec.coffee index 403e4650d..81a8713ad 100644 --- a/spec/task-spec.coffee +++ b/spec/task-spec.coffee @@ -1,4 +1,5 @@ Task = require '../src/task' +Grim = require 'grim' describe "Task", -> describe "@once(taskPath, args..., callback)", -> @@ -43,3 +44,16 @@ describe "Task", -> runs -> expect(eventSpy).not.toHaveBeenCalled() + + it "reports deprecations in tasks", -> + jasmine.snapshotDeprecations() + handlerPath = require.resolve('./fixtures/task-handler-with-deprecations') + task = new Task(handlerPath) + + waitsFor (done) -> task.start(done) + + runs -> + deprecations = Grim.getDeprecations() + expect(deprecations.length).toBe 1 + expect(deprecations[0].getStacks()[0][1].fileName).toBe handlerPath + jasmine.restoreDeprecationsSnapshot() diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index d4a64c300..5d6e7ed85 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -46,7 +46,7 @@ describe "TextEditorComponent", -> lineHeightInPixels = editor.getLineHeightInPixels() charWidth = editor.getDefaultCharWidth() - componentNode = component.getDOMNode() + componentNode = component.domNode verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') @@ -193,7 +193,8 @@ describe "TextEditorComponent", -> expect(linesNode.style.backgroundColor).toBe backgroundColor wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - advanceClock(component.domPollingInterval) + + advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' @@ -466,11 +467,6 @@ describe "TextEditorComponent", -> expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() describe "gutter rendering", -> - [gutter] = [] - - beforeEach -> - {gutter} = component.refs - it "renders the currently-visible line numbers", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureHeightAndWidth() @@ -567,32 +563,32 @@ describe "TextEditorComponent", -> # favor gutter color if it's assigned gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' - advanceClock(component.domPollingInterval) + advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' it "hides or shows the gutter based on the '::isGutterVisible' property on the model and the global 'editor.showLineNumbers' config setting", -> - expect(component.refs.gutter?).toBe true + expect(component.gutterComponent?).toBe true editor.setGutterVisible(false) nextAnimationFrame() - expect(component.refs.gutter?).toBe false + expect(componentNode.querySelector('.gutter')).toBeNull() atom.config.set("editor.showLineNumbers", false) - expect(nextAnimationFrame).toBe noAnimationFrame + nextAnimationFrame() - expect(component.refs.gutter?).toBe false + expect(componentNode.querySelector('.gutter')).toBeNull() editor.setGutterVisible(true) - expect(nextAnimationFrame).toBe noAnimationFrame + nextAnimationFrame() - expect(component.refs.gutter?).toBe false + expect(componentNode.querySelector('.gutter')).toBeNull() atom.config.set("editor.showLineNumbers", true) nextAnimationFrame() - expect(component.refs.gutter?).toBe true + expect(componentNode.querySelector('.gutter')).toBeDefined() expect(component.lineNumberNodeForScreenRow(3)?).toBe true describe "fold decorations", -> @@ -706,13 +702,13 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)" + 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') cursor3.setScreenPosition([4, 11], autoscroll: false) nextAnimationFrame() - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)" expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() @@ -800,11 +796,11 @@ describe "TextEditorComponent", -> expect(cursorsNode.classList.contains('blink-off')).toBe false - advanceClock(component.props.cursorBlinkPeriod / 2) + advanceClock(component.cursorBlinkPeriod / 2) nextAnimationFrame() expect(cursorsNode.classList.contains('blink-off')).toBe true - advanceClock(component.props.cursorBlinkPeriod / 2) + advanceClock(component.cursorBlinkPeriod / 2) nextAnimationFrame() expect(cursorsNode.classList.contains('blink-off')).toBe false @@ -813,8 +809,8 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(cursorsNode.classList.contains('blink-off')).toBe false - advanceClock(component.props.cursorBlinkResumeDelay) - advanceClock(component.props.cursorBlinkPeriod / 2) + advanceClock(component.cursorBlinkResumeDelay) + advanceClock(component.cursorBlinkPeriod / 2) nextAnimationFrame() expect(cursorsNode.classList.contains('blink-off')).toBe true @@ -1501,12 +1497,14 @@ describe "TextEditorComponent", -> expect(inputNode.offsetLeft).toBe 0 # In bounds and focused - inputNode.focus() # updates via state change + wrapperNode.focus() # updates via state change + nextAnimationFrame() expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - editor.getScrollTop() expect(inputNode.offsetLeft).toBe (4 * charWidth) - editor.getScrollLeft() # In bounds, not focused inputNode.blur() # updates via state change + nextAnimationFrame() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 @@ -1518,6 +1516,7 @@ describe "TextEditorComponent", -> # Out of bounds, focused inputNode.focus() # updates via state change + nextAnimationFrame() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 @@ -1839,9 +1838,11 @@ describe "TextEditorComponent", -> it "adds the 'is-focused' class to the editor when the hidden input is focused", -> expect(document.activeElement).toBe document.body inputNode.focus() + nextAnimationFrame() expect(componentNode.classList.contains('is-focused')).toBe true expect(wrapperView.hasClass('is-focused')).toBe true inputNode.blur() + nextAnimationFrame() expect(componentNode.classList.contains('is-focused')).toBe false expect(wrapperView.hasClass('is-focused')).toBe false @@ -2351,11 +2352,11 @@ describe "TextEditorComponent", -> wrapperView.appendTo(hiddenParent) {component} = wrapperView - componentNode = component.getDOMNode() + componentNode = component.domNode expect(componentNode.querySelectorAll('.line').length).toBe 0 hiddenParent.style.display = 'block' - advanceClock(component.domPollingInterval) + advanceClock(atom.views.documentPollingInterval) expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan 0 @@ -2465,14 +2466,15 @@ describe "TextEditorComponent", -> expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - advanceClock(component.domPollingInterval) + advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' - advanceClock(component.domPollingInterval) - nextAnimationFrame() + advanceClock(atom.views.documentPollingInterval) + nextAnimationFrame() # won't poll until cursor blinks + nextAnimationFrame() # handle update requested by poll expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " it "accounts for the scroll view's padding when determining the wrap location", -> @@ -2480,7 +2482,7 @@ describe "TextEditorComponent", -> scrollViewNode.style.paddingLeft = 20 + 'px' componentNode.style.width = 30 * charWidth + 'px' - advanceClock(component.domPollingInterval) + advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " @@ -2558,7 +2560,7 @@ describe "TextEditorComponent", -> expect(wrapperNode.classList.contains('mini')).toBe true it "does not have an opaque background on lines", -> - expect(component.refs.lines.getDOMNode().getAttribute('style')).not.toContain 'background-color' + expect(component.linesComponent.domNode.getAttribute('style')).not.toContain 'background-color' it "does not render invisible characters", -> atom.config.set('editor.invisibles', eol: 'E') diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index cbcc5fc16..f6082b571 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -46,12 +46,12 @@ describe "TextEditorElement", -> jasmine.attachToDOM(element) component = element.component - expect(component.isMounted()).toBe true + expect(component.mounted).toBe true element.remove() - expect(component.isMounted()).toBe false + expect(component.mounted).toBe false jasmine.attachToDOM(element) - expect(element.component.isMounted()).toBe true + expect(element.component.mounted).toBe true describe "when the editor.useShadowDOM config option is false", -> it "mounts the react component and unmounts when removed from the dom", -> @@ -61,9 +61,9 @@ describe "TextEditorElement", -> jasmine.attachToDOM(element) component = element.component - expect(component.isMounted()).toBe true + expect(component.mounted).toBe true element.getModel().destroy() - expect(component.isMounted()).toBe false + expect(component.mounted).toBe false describe "focus and blur handling", -> describe "when the editor.useShadowDOM config option is true", -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 18aa579ec..7691b663f 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -77,6 +77,18 @@ describe "TextEditorPresenter", -> presenter.setExplicitHeight((editor.getLineCount() * 10) - 1) expect(state.horizontalScrollbar.visible).toBe true + it "is false if the editor is mini", -> + presenter = buildPresenter + explicitHeight: editor.getLineCount() * 10 + contentFrameWidth: editor.getMaxScreenLineLength() * 10 - 10 + baseCharacterWidth: 10 + + expect(presenter.state.horizontalScrollbar.visible).toBe true + editor.setMini(true) + expect(presenter.state.horizontalScrollbar.visible).toBe false + editor.setMini(false) + expect(presenter.state.horizontalScrollbar.visible).toBe true + describe ".height", -> it "tracks the value of ::horizontalScrollbarHeight", -> presenter = buildPresenter(horizontalScrollbarHeight: 10) @@ -311,6 +323,69 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) expect(presenter.state.verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight + describe ".hiddenInput", -> + describe ".top/.left", -> + it "is positioned over the last cursor it is in view and the editor is focused", -> + editor.setCursorBufferPosition([3, 6]) + presenter = buildPresenter(focused: false, explicitHeight: 50, contentFrameWidth: 300, horizontalScrollbarHeight: 0, verticalScrollbarWidth: 0) + expectValues presenter.state.hiddenInput, {top: 0, left: 0} + + expectStateUpdate presenter, -> presenter.setFocused(true) + expectValues presenter.state.hiddenInput, {top: 3 * 10, left: 6 * 10} + + expectStateUpdate presenter, -> presenter.setScrollTop(15) + expectValues presenter.state.hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} + + expectStateUpdate presenter, -> presenter.setScrollLeft(35) + expectValues presenter.state.hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} + + expectStateUpdate presenter, -> presenter.setScrollTop(40) + expectValues presenter.state.hiddenInput, {top: 0, left: (6 * 10) - 35} + + expectStateUpdate presenter, -> presenter.setScrollLeft(70) + expectValues presenter.state.hiddenInput, {top: 0, left: 0} + + expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) + expectValues presenter.state.hiddenInput, {top: 50 - 10, left: 300 - 10} + + newCursor = null + expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) + expectValues presenter.state.hiddenInput, {top: (6 * 10) - 40, left: (10 * 10) - 70} + + expectStateUpdate presenter, -> newCursor.destroy() + expectValues presenter.state.hiddenInput, {top: 50 - 10, left: 300 - 10} + + expectStateUpdate presenter, -> presenter.setFocused(false) + expectValues presenter.state.hiddenInput, {top: 0, left: 0} + + describe ".height", -> + it "is assigned based on the line height", -> + presenter = buildPresenter() + expect(presenter.state.hiddenInput.height).toBe 10 + + expectStateUpdate presenter, -> presenter.setLineHeight(20) + expect(presenter.state.hiddenInput.height).toBe 20 + + describe ".width", -> + it "is assigned based on the width of the character following the cursor", -> + waitsForPromise -> atom.packages.activatePackage('language-javascript') + + runs -> + editor.setCursorBufferPosition([3, 6]) + presenter = buildPresenter() + expect(presenter.state.hiddenInput.width).toBe 10 + + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expect(presenter.state.hiddenInput.width).toBe 15 + + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expect(presenter.state.hiddenInput.width).toBe 20 + + it "is 2px at the end of lines", -> + presenter = buildPresenter() + editor.setCursorBufferPosition([3, Infinity]) + expect(presenter.state.hiddenInput.width).toBe 2 + describe ".content", -> describe ".scrollingVertically", -> it "is true for ::stoppedScrollingDelay milliseconds following a changes to ::scrollTop", -> @@ -1895,6 +1970,42 @@ describe "TextEditorPresenter", -> editor.undo() expect(lineNumberStateForScreenRow(presenter, 11).foldable).toBe false + describe ".visible", -> + it "is true iff the editor isn't mini, ::isGutterVisible is true on the editor, and 'editor.showLineNumbers' is enabled in config", -> + presenter = buildPresenter() + + expect(editor.isGutterVisible()).toBe true + expect(presenter.state.gutter.visible).toBe true + + expectStateUpdate presenter, -> editor.setMini(true) + expect(presenter.state.gutter.visible).toBe false + + expectStateUpdate presenter, -> editor.setMini(false) + expect(presenter.state.gutter.visible).toBe true + + expectStateUpdate presenter, -> editor.setGutterVisible(false) + expect(presenter.state.gutter.visible).toBe false + + expectStateUpdate presenter, -> editor.setGutterVisible(true) + expect(presenter.state.gutter.visible).toBe true + + expectStateUpdate presenter, -> atom.config.set('editor.showLineNumbers', false) + expect(presenter.state.gutter.visible).toBe false + + it "updates when the editor's grammar changes", -> + presenter = buildPresenter() + + atom.config.set('editor.showLineNumbers', false, scopeSelector: '.source.js') + expect(presenter.state.gutter.visible).toBe true + stateUpdated = false + presenter.onDidUpdateState -> stateUpdated = true + + waitsForPromise -> atom.packages.activatePackage('language-javascript') + + runs -> + expect(stateUpdated).toBe true + expect(presenter.state.gutter.visible).toBe false + describe ".height", -> it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", -> presenter = buildPresenter(explicitHeight: null, autoHeight: true) @@ -1912,16 +2023,30 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") expect(presenter.state.height).toBe editor.getScreenLineCount() * 20 - describe "when the model and view measurements are mutated randomly", -> + describe ".focused", -> + it "tracks the value of ::focused", -> + presenter = buildPresenter(focused: false) + expect(presenter.state.focused).toBe false + expectStateUpdate presenter, -> presenter.setFocused(true) + expect(presenter.state.focused).toBe true + expectStateUpdate presenter, -> presenter.setFocused(false) + expect(presenter.state.focused).toBe false + + # disabled until we fix an issue with display buffer markers not updating when + # they are moved on screen but not in the buffer + xdescribe "when the model and view measurements are mutated randomly", -> [editor, buffer, presenterParams, presenter, statements] = [] + recordStatement = (statement) -> statements.push(statement) + it "correctly maintains the presenter state", -> _.times 20, -> waits(0) runs -> performSetup() + performRandomInitialization(recordStatement) _.times 20, -> - performRandomAction (statement) -> statements.push(statement) + performRandomAction recordStatement expectValidState() performTeardown() @@ -1937,18 +2062,25 @@ describe "TextEditorPresenter", -> editor.setEditorWidthInChars(80) presenterParams = model: editor - explicitHeight: 50 - contentFrameWidth: 300 - scrollTop: 0 - scrollLeft: 0 - lineHeight: 10 - baseCharacterWidth: 10 lineOverdrawMargin: 1 - horizontalScrollbarHeight: 5 - verticalScrollbarWidth: 5 presenter = new TextEditorPresenter(presenterParams) statements = [] + performRandomInitialization = (log) -> + actions = _.shuffle([ + changeScrollLeft + changeScrollTop + changeExplicitHeight + changeContentFrameWidth + changeLineHeight + changeBaseCharacterWidth + changeHorizontalScrollbarHeight + changeVerticalScrollbarWidth + ]) + for action in actions + action(log) + expectValidState() + performTeardown = -> buffer.destroy() @@ -1989,14 +2121,16 @@ describe "TextEditorPresenter", -> ])(log) changeScrollTop = (log) -> - scrollHeight = presenterParams.lineHeight * editor.getScreenLineCount() - newScrollTop = Math.max(0, _.random(0, scrollHeight - presenterParams.explicitHeight)) + scrollHeight = (presenterParams.lineHeight ? 10) * editor.getScreenLineCount() + explicitHeight = (presenterParams.explicitHeight ? 500) + newScrollTop = Math.max(0, _.random(0, scrollHeight - explicitHeight)) log "presenter.setScrollTop(#{newScrollTop})" presenter.setScrollTop(newScrollTop) changeScrollLeft = (log) -> - scrollWidth = presenter.scrollWidth - newScrollLeft = Math.max(0, _.random(0, scrollWidth - presenterParams.contentFrameWidth)) + scrollWidth = presenter.scrollWidth ? 300 + contentFrameWidth = presenter.contentFrameWidth ? 200 + newScrollLeft = Math.max(0, _.random(0, scrollWidth - contentFrameWidth)) log """ presenterParams.scrollLeft = #{newScrollLeft} presenter.setScrollLeft(#{newScrollLeft}) @@ -2005,7 +2139,7 @@ describe "TextEditorPresenter", -> presenter.setScrollLeft(newScrollLeft) changeExplicitHeight = (log) -> - scrollHeight = presenterParams.lineHeight * editor.getScreenLineCount() + scrollHeight = (presenterParams.lineHeight ? 10) * editor.getScreenLineCount() newExplicitHeight = _.random(30, scrollHeight * 1.5) log """ presenterParams.explicitHeight = #{newExplicitHeight} @@ -2015,7 +2149,7 @@ describe "TextEditorPresenter", -> presenter.setExplicitHeight(newExplicitHeight) changeContentFrameWidth = (log) -> - scrollWidth = presenter.scrollWidth + scrollWidth = presenter.scrollWidth ? 300 newContentFrameWidth = _.random(100, scrollWidth * 1.5) log """ presenterParams.contentFrameWidth = #{newContentFrameWidth} @@ -2024,6 +2158,42 @@ describe "TextEditorPresenter", -> presenterParams.contentFrameWidth = newContentFrameWidth presenter.setContentFrameWidth(newContentFrameWidth) + changeLineHeight = (log) -> + newLineHeight = _.random(5, 15) + log """ + presenterParams.lineHeight = #{newLineHeight} + presenter.setLineHeight(#{newLineHeight}) + """ + presenterParams.lineHeight = newLineHeight + presenter.setLineHeight(newLineHeight) + + changeBaseCharacterWidth = (log) -> + newBaseCharacterWidth = _.random(5, 15) + log """ + presenterParams.baseCharacterWidth = #{newBaseCharacterWidth} + presenter.setBaseCharacterWidth(#{newBaseCharacterWidth}) + """ + presenterParams.baseCharacterWidth = newBaseCharacterWidth + presenter.setBaseCharacterWidth(newBaseCharacterWidth) + + changeHorizontalScrollbarHeight = (log) -> + newHorizontalScrollbarHeight = _.random(2, 15) + log """ + presenterParams.horizontalScrollbarHeight = #{newHorizontalScrollbarHeight} + presenter.setHorizontalScrollbarHeight(#{newHorizontalScrollbarHeight}) + """ + presenterParams.horizontalScrollbarHeight = newHorizontalScrollbarHeight + presenter.setHorizontalScrollbarHeight(newHorizontalScrollbarHeight) + + changeVerticalScrollbarWidth = (log) -> + newVerticalScrollbarWidth = _.random(2, 15) + log """ + presenterParams.verticalScrollbarWidth = #{newVerticalScrollbarWidth} + presenter.setVerticalScrollbarWidth(#{newVerticalScrollbarWidth}) + """ + presenterParams.verticalScrollbarWidth = newVerticalScrollbarWidth + presenter.setVerticalScrollbarWidth(newVerticalScrollbarWidth) + toggleSoftWrap = (log) -> softWrapped = not editor.isSoftWrapped() log "editor.setSoftWrapped(#{softWrapped})" diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index 851faf109..4d0d72abc 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -85,3 +85,78 @@ describe "ViewRegistry", -> expect(registry.getView(new TestModel) instanceof TestView).toBe true disposable.dispose() expect(-> registry.getView(new TestModel)).toThrow() + + describe "::updateDocument(fn) and ::readDocument(fn)", -> + frameRequests = null + + beforeEach -> + frameRequests = [] + spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn) + + it "performs all pending writes before all pending reads on the next animation frame", -> + events = [] + + registry.updateDocument -> events.push('write 1') + registry.readDocument -> events.push('read 1') + registry.readDocument -> events.push('read 2') + registry.updateDocument -> events.push('write 2') + + expect(events).toEqual [] + + expect(frameRequests.length).toBe 1 + frameRequests[0]() + expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2'] + + frameRequests = [] + events = [] + disposable = registry.updateDocument -> events.push('write 3') + registry.updateDocument -> events.push('write 4') + registry.readDocument -> events.push('read 3') + + disposable.dispose() + + expect(frameRequests.length).toBe 1 + frameRequests[0]() + expect(events).toEqual ['write 4', 'read 3'] + + it "pauses DOM polling when reads or writes are pending", -> + spyOn(window, 'setInterval').andCallFake(fakeSetInterval) + spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) + events = [] + + registry.pollDocument -> events.push('poll') + registry.updateDocument -> events.push('write') + registry.readDocument -> events.push('read') + + advanceClock(registry.documentPollingInterval) + expect(events).toEqual [] + + frameRequests[0]() + expect(events).toEqual ['write', 'read', 'poll'] + + advanceClock(registry.documentPollingInterval) + expect(events).toEqual ['write', 'read', 'poll', '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) + + events = [] + disposable1 = registry.pollDocument -> events.push('poll 1') + disposable2 = registry.pollDocument -> events.push('poll 2') + + expect(events).toEqual [] + + advanceClock(registry.documentPollingInterval) + expect(events).toEqual ['poll 1', 'poll 2'] + + advanceClock(registry.documentPollingInterval) + expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2'] + + disposable1.dispose() + advanceClock(registry.documentPollingInterval) + expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2'] + + disposable2.dispose() + advanceClock(registry.documentPollingInterval) + expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2'] diff --git a/src/atom.coffee b/src/atom.coffee index dcc8e0cfb..385180dfd 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -96,7 +96,7 @@ class Atom extends Model filename = 'spec' when 'editor' {initialPaths} = @getLoadSettings() - if initialPaths + if initialPaths?.length > 0 sha1 = crypto.createHash('sha1').update(initialPaths.join("\n")).digest('hex') filename = "editor-#{sha1}" @@ -803,7 +803,7 @@ class Atom extends Model # require completes. # # * `id` The {String} module name or path. - # * `globals` An optinal {Object} to set as globals during require. + # * `globals` An optional {Object} to set as globals during require. requireWithGlobals: (id, globals={}) -> existingGlobals = {} for key, value of globals diff --git a/src/6to5.coffee b/src/babel.coffee similarity index 79% rename from src/6to5.coffee rename to src/babel.coffee index 9d67efcf8..766324ce0 100644 --- a/src/6to5.coffee +++ b/src/babel.coffee @@ -1,5 +1,5 @@ ### -Cache for source code transpiled by 6to5. +Cache for source code transpiled by Babel. Inspired by https://github.com/atom/atom/blob/6b963a562f8d495fbebe6abdbafbc7caf705f2c3/src/coffee-cache.coffee. ### @@ -7,7 +7,7 @@ Inspired by https://github.com/atom/atom/blob/6b963a562f8d495fbebe6abdbafbc7caf7 crypto = require 'crypto' fs = require 'fs-plus' path = require 'path' -to5 = null # Defer until used +babel = null # Defer until used stats = hits: 0 @@ -18,11 +18,6 @@ defaultOptions = # when the source map is inlined. sourceMap: 'inline' - # Because Atom is currently packaged with a fork of React v0.11, - # it makes sense to use the --react-compat option so the React - # JSX transformer produces pre-v0.12 code. - reactCompat: true - # Blacklisted features do not get transpiled. Features that are # natively supported in the target environment should be listed # here. Because Atom uses a bleeding edge version of Node/io.js, @@ -33,13 +28,18 @@ defaultOptions = ] # Includes support for es7 features listed at: - # http://6to5.org/docs/usage/transformers/#es7-experimental-. + # http://babeljs.io/docs/usage/transformers/#es7-experimental-. experimental: true optional: [ # Target a version of the regenerator runtime that # supports yield so the transpiled code is cleaner/smaller. 'asyncToGenerator' + + # Because Atom is currently packaged with a fork of React v0.11, + # it makes sense to use the reactCompat transform so the React + # JSX transformer produces pre-v0.12 code. + 'reactCompat' ] ### @@ -79,10 +79,10 @@ updateDigestForJsonValue = (shasum, value) -> shasum.update(',', 'utf8') shasum.update('}', 'utf8') -create6to5VersionAndOptionsDigest = (version, options) -> +createBabelVersionAndOptionsDigest = (version, options) -> shasum = crypto.createHash('sha1') - # Include the version of 6to5 in the hash. - shasum.update('6to5-core', 'utf8') + # Include the version of babel in the hash. + shasum.update('babel-core', 'utf8') shasum.update('\0', 'utf8') shasum.update(version, 'utf8') shasum.update('\0', 'utf8') @@ -96,8 +96,8 @@ getCachePath = (sourceCode) -> digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex') unless jsCacheDir? - to5Version = require('6to5-core/package.json').version - jsCacheDir = path.join(cacheDir, create6to5VersionAndOptionsDigest(to5Version, defaultOptions)) + to5Version = require('babel-core/package.json').version + jsCacheDir = path.join(cacheDir, createBabelVersionAndOptionsDigest(to5Version, defaultOptions)) path.join(jsCacheDir, "#{digest}.js") @@ -109,7 +109,7 @@ getCachedJavaScript = (cachePath) -> return cachedJavaScript null -# Returns the 6to5 options that should be used to transpile filePath. +# Returns the babel options that should be used to transpile filePath. createOptions = (filePath) -> options = filename: filePath for key, value of defaultOptions @@ -118,8 +118,8 @@ createOptions = (filePath) -> transpile = (sourceCode, filePath, cachePath) -> options = createOptions(filePath) - to5 ?= require '6to5-core' - js = to5.transform(sourceCode, options).code + babel ?= require 'babel-core' + js = babel.transform(sourceCode, options).code stats.misses++ try @@ -132,8 +132,10 @@ transpile = (sourceCode, filePath, cachePath) -> # either generated on the fly or pulled from cache. loadFile = (module, filePath) -> sourceCode = fs.readFileSync(filePath, 'utf8') - unless sourceCode.startsWith('"use 6to5"') or sourceCode.startsWith("'use 6to5'") - return module._compile(sourceCode, filePath) + return module._compile(sourceCode, filePath) unless sourceCode.startsWith('"use 6to5"') or + sourceCode.startsWith("'use 6to5'") or + sourceCode.startsWith('"use babel"') or + sourceCode.startsWith("'use babel'") cachePath = getCachePath(sourceCode) js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath) @@ -158,7 +160,7 @@ module.exports = getCacheHits: -> stats.hits # Visible for testing. - create6to5VersionAndOptionsDigest: create6to5VersionAndOptionsDigest + createBabelVersionAndOptionsDigest: createBabelVersionAndOptionsDigest addPathToCache: (filePath) -> return if path.extname(filePath) isnt '.js' diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 56f7c3d8a..5218ff304 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -92,19 +92,23 @@ class ApplicationMenu # 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') - return unless checkForUpdateItem? and downloadingUpdateItem? and installUpdateItem? + return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem? checkForUpdateItem.visible = false + checkingForUpdateItem.visible = false downloadingUpdateItem.visible = false installUpdateItem.visible = false switch state when 'idle', 'error', 'no-update-available' checkForUpdateItem.visible = true - when 'checking', 'downloading' + when 'checking' + checkingForUpdateItem.visible = true + when 'downloading' downloadingUpdateItem.visible = true when 'update-available' installUpdateItem.visible = true diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 2f46ba6d2..7324aa460 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -60,7 +60,7 @@ class AtomApplication exit: (status) -> app.exit(status) constructor: (options) -> - {@resourcePath, @version, @devMode, @safeMode, @socketPath} = options + {@resourcePath, @version, @devMode, @safeMode, @socketPath, @enableMultiFolderProject} = options # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath @@ -348,15 +348,24 @@ class AtomApplication # :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 + pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) unless pidToKillWhenClosed or newWindow existingWindow = @windowForPaths(pathsToOpen, devMode) - # Default to using the specified window or the last focused window - if pathsToOpen.every((pathToOpen) -> fs.statSyncNoException(pathToOpen).isFile?()) - existingWindow ?= window ? @lastFocusedWindow + # Default to using the specified window or the last focused window + currentWindow = window ? @lastFocusedWindow + stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen) + existingWindow ?= currentWindow if ( + stats.every((stat) -> stat.isFile?()) or + stats.some((stat) -> stat.isDirectory?()) and not currentWindow?.hasProjectPath() + ) if existingWindow? openedWindow = existingWindow diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index bf43b0241..77888fed4 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -18,8 +18,9 @@ class AtomWindow isSpec: null constructor: (settings={}) -> - {@resourcePath, pathToOpen, @locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings - @locationsToOpen ?= [{pathToOpen}] if pathToOpen + {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings + locationsToOpen ?= [{pathToOpen}] if pathToOpen + locationsToOpen ?= [] # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath @@ -52,12 +53,15 @@ class AtomWindow @constructor.includeShellLoadTime = false loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime - loadSettings.initialPaths = for {pathToOpen} in (@locationsToOpen ? []) - if fs.statSyncNoException(pathToOpen).isFile?() - path.dirname(pathToOpen) - else - pathToOpen + loadSettings.initialPaths = + for {pathToOpen} in locationsToOpen when pathToOpen + if fs.statSyncNoException(pathToOpen).isFile?() + path.dirname(pathToOpen) + else + pathToOpen + loadSettings.initialPaths.sort() + @projectPaths = loadSettings.initialPaths @browserWindow.loadSettings = loadSettings @browserWindow.once 'window:loaded', => @@ -69,7 +73,7 @@ class AtomWindow @browserWindow.loadUrl @getUrl(loadSettings) @browserWindow.focusOnWebView() if @isSpec - @openLocations(@locationsToOpen) unless @isSpecWindow() + @openLocations(locationsToOpen) unless @isSpecWindow() getUrl: (loadSettingsObj) -> # Ignore the windowState when passing loadSettings via URL, since it could diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 3b6018d5b..b73556307 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -118,6 +118,7 @@ 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 @@ -139,6 +140,7 @@ 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 @@ -163,6 +165,7 @@ parseCommandLine = -> # explicitly pass it by command line, see http://git.io/YC8_Ew. process.env.PATH = args['path-environment'] if args['path-environment'] - {resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, socketPath} + {resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, + devMode, safeMode, newWindow, specDirectory, logFile, socketPath, enableMultiFolderProject} start() diff --git a/src/compile-cache.coffee b/src/compile-cache.coffee index 287916d94..c31f5bdd1 100644 --- a/src/compile-cache.coffee +++ b/src/compile-cache.coffee @@ -1,7 +1,7 @@ path = require 'path' CSON = require 'season' CoffeeCache = require 'coffee-cash' -to5 = require './6to5' +babel = require './babel' # 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 @@ -15,7 +15,7 @@ exports.addPathToCache = (filePath, atomHome) -> CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee')) CSON.setCacheDir(path.join(cacheDir, 'cson')) - to5.setCacheDirectory(path.join(cacheDir, 'js', '6to5')) + babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel')) switch path.extname(filePath) when '.coffee' @@ -23,4 +23,4 @@ exports.addPathToCache = (filePath, atomHome) -> when '.cson' CSON.readFileSync(filePath) when '.js' - to5.addPathToCache(filePath) + babel.addPathToCache(filePath) diff --git a/src/config-schema.coffee b/src/config-schema.coffee index f849e4909..1796bb64c 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -162,10 +162,6 @@ module.exports = default: 300 minimum: 0 description: 'Time interval in milliseconds within which operations will be grouped together in the undo history' - useHardwareAcceleration: - type: 'boolean' - default: true - description: 'Disabling will improve editor font rendering but reduce scrolling performance.' useShadowDOM: type: 'boolean' default: true diff --git a/src/cursor-component.coffee b/src/cursor-component.coffee deleted file mode 100644 index ec7e04c1c..000000000 --- a/src/cursor-component.coffee +++ /dev/null @@ -1,14 +0,0 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{isEqualForProperties} = require 'underscore-plus' - -module.exports = -CursorComponent = React.createClass - displayName: 'CursorComponent' - - render: -> - {pixelRect} = @props - {top, left, height, width} = pixelRect - WebkitTransform = "translate(#{left}px, #{top}px)" - - div className: 'cursor', style: {height, width, WebkitTransform} diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 646618e9b..e11019d6c 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -1,19 +1,54 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{debounce, toArray, isEqualForProperties, isEqual} = require 'underscore-plus' -SubscriberMixin = require './subscriber-mixin' -CursorComponent = require './cursor-component' - module.exports = -CursorsComponent = React.createClass - displayName: 'CursorsComponent' +class CursorsComponent + oldState: null - render: -> - {presenter} = @props + constructor: (@presenter) -> + @cursorNodesById = {} + @domNode = document.createElement('div') + @domNode.classList.add('cursors') + @updateSync() - className = 'cursors' - className += ' blink-off' if presenter.state.content.blinkCursorsOff + updateSync: -> + newState = @presenter.state.content + @oldState ?= {cursors: {}} - div {className}, - for key, pixelRect of presenter.state.content.cursors - CursorComponent({key, pixelRect}) + # update blink class + if newState.blinkCursorsOff isnt @oldState.blinkCursorsOff + if newState.blinkCursorsOff + @domNode.classList.add 'blink-off' + else + @domNode.classList.remove 'blink-off' + @oldState.blinkCursorsOff = newState.blinkCursorsOff + + # remove cursors + for id of @oldState.cursors + unless newState.cursors[id]? + @cursorNodesById[id].remove() + delete @cursorNodesById[id] + delete @oldState.cursors[id] + + # add or update cursors + for id, cursorState of newState.cursors + unless @oldState.cursors[id]? + cursorNode = document.createElement('div') + cursorNode.classList.add('cursor') + @cursorNodesById[id] = cursorNode + @domNode.appendChild(cursorNode) + @updateCursorNode(id, cursorState) + + updateCursorNode: (id, newCursorState) -> + cursorNode = @cursorNodesById[id] + oldCursorState = (@oldState.cursors[id] ?= {}) + + if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left + cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)" + oldCursorState.top = newCursorState.top + oldCursorState.left = newCursorState.left + + if newCursorState.height isnt oldCursorState.height + cursorNode.style.height = newCursorState.height + 'px' + oldCursorState.height = newCursorState.height + + if newCursorState.width isnt oldCursorState.width + cursorNode.style.width = newCursorState.width + 'px' + oldCursorState.width = newCursorState.width diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee new file mode 100644 index 000000000..7ecdbd354 --- /dev/null +++ b/src/default-directory-provider.coffee @@ -0,0 +1,35 @@ +{Directory} = require 'pathwatcher' +fs = require 'fs-plus' +path = require 'path' + +module.exports = +class DefaultDirectoryProvider + + # Public: Create a Directory that corresponds to the specified URI. + # + # * `uri` {String} The path to the directory to add. This is guaranteed not to + # be contained by a {Directory} in `atom.project`. + # + # Returns: + # * {Directory} if the given URI is compatible with this provider. + # * `null` if the given URI is not compatibile with this provider. + directoryForURISync: (uri) -> + projectPath = path.normalize(uri) + + directoryPath = if fs.isDirectorySync(projectPath) + projectPath + else + path.dirname(projectPath) + + new Directory(directoryPath) + + # Public: Create a Directory that corresponds to the specified URI. + # + # * `uri` {String} The path to the directory to add. This is guaranteed not to + # be contained by a {Directory} in `atom.project`. + # + # Returns a Promise that resolves to: + # * {Directory} if the given URI is compatible with this provider. + # * `null` if the given URI is not compatibile with this provider. + directoryForURI: (uri) -> + Promise.resolve(@directoryForURISync(uri)) diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee index 9772da416..210197599 100644 --- a/src/git-repository-provider.coffee +++ b/src/git-repository-provider.coffee @@ -11,7 +11,7 @@ findGitDirectorySync = (directory) -> # can return cached values rather than always returning new objects: # getParent(), getFile(), getSubdirectory(). gitDir = directory.getSubdirectory('.git') - if directoryExistsSync(gitDir) and isValidGitDirectorySync gitDir + if gitDir.existsSync() and isValidGitDirectorySync gitDir gitDir else if directory.isRoot() return null @@ -26,19 +26,9 @@ isValidGitDirectorySync = (directory) -> # To decide whether a directory has a valid .git folder, we use # the heuristic adopted by the valid_repository_path() function defined in # node_modules/git-utils/deps/libgit2/src/repository.c. - return directoryExistsSync(directory.getSubdirectory('objects')) and - directory.getFile('HEAD').exists() and - directoryExistsSync(directory.getSubdirectory('refs')) - -# Returns a boolean indicating whether the specified directory exists. -# -# * `directory` {Directory} to check for existence. -directoryExistsSync = (directory) -> - # TODO: Directory should have its own existsSync() method. Currently, File has - # an exists() method, which is synchronous, so it may be tricky to achieve - # consistency between the File and Directory APIs. Once Directory has its own - # method, this function should be replaced with direct calls to existsSync(). - return fs.existsSync(directory.getPath()) + return directory.getSubdirectory('objects').existsSync() and + directory.getFile('HEAD').existsSync() and + directory.getSubdirectory('refs').existsSync() # Provider that conforms to the atom.repository-provider@0.1.0 service. module.exports = @@ -72,6 +62,7 @@ class GitRepositoryProvider repo = @pathToRepository[gitDirPath] unless repo repo = GitRepository.open(gitDirPath, project: @project) + return null unless repo repo.onDidDestroy(=> delete @pathToRepository[gitDirPath]) @pathToRepository[gitDirPath] = repo repo.refreshIndex() diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 2ffb4e033..d523b562f 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -59,7 +59,7 @@ class GitRepository # Public: Creates a new GitRepository instance. # # * `path` The {String} path to the Git repository to open. - # * `options` An optinal {Object} with the following keys: + # * `options` An optional {Object} with the following keys: # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and # statuses when the window is focused. # diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 564c611ab..3209fe806 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -1,62 +1,47 @@ _ = require 'underscore-plus' -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{isEqual, isEqualForProperties, multiplyString, toArray} = _ -Decoration = require './decoration' -SubscriberMixin = require './subscriber-mixin' WrapperDiv = document.createElement('div') module.exports = -GutterComponent = React.createClass - displayName: 'GutterComponent' - mixins: [SubscriberMixin] - - maxLineNumberDigits: null +class GutterComponent dummyLineNumberNode: null - measuredWidth: null - render: -> - {presenter} = @props - @newState = presenter.state.gutter - @oldState ?= {lineNumbers: {}} - - {scrollHeight, backgroundColor} = @newState - - div className: 'gutter', - div className: 'line-numbers', ref: 'lineNumbers', style: - height: scrollHeight - WebkitTransform: @getTransform() - backgroundColor: backgroundColor - - getTransform: -> - {useHardwareAcceleration} = @props - {scrollTop} = @newState - - if useHardwareAcceleration - "translate3d(0px, #{-scrollTop}px, 0px)" - else - "translate(0px, #{-scrollTop}px)" - - componentWillMount: -> + constructor: ({@presenter, @onMouseDown, @editor}) -> @lineNumberNodesById = {} - componentDidMount: -> - {@maxLineNumberDigits} = @newState - @appendDummyLineNumber() - @updateLineNumbers() + @domNode = document.createElement('div') + @domNode.classList.add('gutter') + @lineNumbersNode = document.createElement('div') + @lineNumbersNode.classList.add('line-numbers') + @domNode.appendChild(@lineNumbersNode) - node = @getDOMNode() - node.addEventListener 'click', @onClick - node.addEventListener 'mousedown', @onMouseDown + @domNode.addEventListener 'click', @onClick + @domNode.addEventListener 'mousedown', @onMouseDown - componentDidUpdate: (oldProps) -> - {maxLineNumberDigits} = @newState - unless maxLineNumberDigits is @maxLineNumberDigits - @maxLineNumberDigits = maxLineNumberDigits + @updateSync() + + updateSync: -> + @newState = @presenter.state.gutter + @oldState ?= {lineNumbers: {}} + + @appendDummyLineNumber() unless @dummyLineNumberNode? + + if @newState.scrollHeight isnt @oldState.scrollHeight + @lineNumbersNode.style.height = @newState.scrollHeight + 'px' + @oldState.scrollHeight = @newState.scrollHeight + + if @newState.scrollTop isnt @oldState.scrollTop + @lineNumbersNode.style['-webkit-transform'] = "translate3d(0px, #{-@newState.scrollTop}px, 0px)" + @oldState.scrollTop = @newState.scrollTop + + if @newState.backgroundColor isnt @oldState.backgroundColor + @lineNumbersNode.style.backgroundColor = @newState.backgroundColor + @oldState.backgroundColor = @newState.backgroundColor + + if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits @updateDummyLineNumber() node.remove() for id, node of @lineNumberNodesById - @oldState = {lineNumbers: {}} + @oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}} @lineNumberNodesById = {} @updateLineNumbers() @@ -66,7 +51,7 @@ GutterComponent = React.createClass appendDummyLineNumber: -> WrapperDiv.innerHTML = @buildLineNumberHTML({bufferRow: -1}) @dummyLineNumberNode = WrapperDiv.children[0] - @refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode) + @lineNumbersNode.appendChild(@dummyLineNumberNode) updateDummyLineNumber: -> @dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false) @@ -87,9 +72,9 @@ GutterComponent = React.createClass if newLineNumberIds? WrapperDiv.innerHTML = newLineNumbersHTML - newLineNumberNodes = toArray(WrapperDiv.children) + newLineNumberNodes = _.toArray(WrapperDiv.children) - node = @refs.lineNumbers.getDOMNode() + node = @lineNumbersNode for id, i in newLineNumberIds lineNumberNode = newLineNumberNodes[i] @lineNumberNodesById[id] = lineNumberNode @@ -120,7 +105,7 @@ GutterComponent = React.createClass else lineNumber = (bufferRow + 1).toString() - padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length) + padding = _.multiplyString(' ', maxLineNumberDigits - lineNumber.length) iconHTML = '
' padding + lineNumber + iconHTML @@ -151,21 +136,20 @@ GutterComponent = React.createClass return @lineNumberNodesById[id] null - onMouseDown: (event) -> + onMouseDown: (event) => {target} = event lineNumber = target.parentNode unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') - @props.onMouseDown(event) + @onMouseDown(event) - onClick: (event) -> - {editor} = @props + onClick: (event) => {target} = event lineNumber = target.parentNode if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable') bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row')) if lineNumber.classList.contains('folded') - editor.unfoldBufferRow(bufferRow) + @editor.unfoldBufferRow(bufferRow) else - editor.foldBufferRow(bufferRow) + @editor.foldBufferRow(bufferRow) diff --git a/src/highlight-component.coffee b/src/highlight-component.coffee deleted file mode 100644 index ef090ea7b..000000000 --- a/src/highlight-component.coffee +++ /dev/null @@ -1,50 +0,0 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{isEqualForProperties} = require 'underscore-plus' - -module.exports = -HighlightComponent = React.createClass - displayName: 'HighlightComponent' - currentFlashCount: 0 - currentFlashClass: null - - render: -> - {state} = @props - - className = 'highlight' - className += " #{state.class}" if state.class? - - div {className}, - for region, i in state.regions - regionClassName = 'region' - regionClassName += " #{state.deprecatedRegionClass}" if state.deprecatedRegionClass? - div className: regionClassName, key: i, style: region - - componentDidMount: -> - @flashIfRequested() - - componentDidUpdate: -> - @flashIfRequested() - - flashIfRequested: -> - if @props.state.flashCount > @currentFlashCount - @currentFlashCount = @props.state.flashCount - - node = @getDOMNode() - {flashClass, flashDuration} = @props.state - - addFlashClass = => - node.classList.add(flashClass) - @currentFlashClass = flashClass - @flashTimeoutId = setTimeout(removeFlashClass, flashDuration) - - removeFlashClass = => - node.classList.remove(@currentFlashClass) - @currentFlashClass = null - clearTimeout(@flashTimeoutId) - - if @currentFlashClass? - removeFlashClass() - requestAnimationFrame(addFlashClass) - else - addFlashClass() diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 1d7ae4de3..488a2236f 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -1,25 +1,107 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{isEqualForProperties} = require 'underscore-plus' -HighlightComponent = require './highlight-component' +RegionStyleProperties = ['top', 'left', 'right', 'width', 'height'] module.exports = -HighlightsComponent = React.createClass - displayName: 'HighlightsComponent' +class HighlightsComponent + oldState: null - render: -> - div className: 'highlights', - @renderHighlights() + constructor: (@presenter) -> + @highlightNodesById = {} + @regionNodesByHighlightId = {} - renderHighlights: -> - {presenter} = @props - highlightComponents = [] - for key, state of presenter.state.content.highlights - highlightComponents.push(HighlightComponent({key, state})) - highlightComponents + @domNode = document.createElement('div') + @domNode.classList.add('highlights') - componentDidMount: -> if atom.config.get('editor.useShadowDOM') insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', '.underlayer') - @getDOMNode().appendChild(insertionPoint) + @domNode.appendChild(insertionPoint) + + updateSync: -> + newState = @presenter.state.content.highlights + @oldState ?= {} + + # remove highlights + for id of @oldState + unless newState[id]? + @highlightNodesById[id].remove() + delete @highlightNodesById[id] + delete @regionNodesByHighlightId[id] + delete @oldState[id] + + # add or update highlights + for id, highlightState of newState + unless @oldState[id]? + highlightNode = document.createElement('div') + highlightNode.classList.add('highlight') + @highlightNodesById[id] = highlightNode + @regionNodesByHighlightId[id] = {} + @domNode.appendChild(highlightNode) + @updateHighlightNode(id, highlightState) + + updateHighlightNode: (id, newHighlightState) -> + highlightNode = @highlightNodesById[id] + oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0}) + + # update class + if newHighlightState.class isnt oldHighlightState.class + highlightNode.classList.remove(oldHighlightState.class) if oldHighlightState.class? + highlightNode.classList.add(newHighlightState.class) + oldHighlightState.class = newHighlightState.class + + @updateHighlightRegions(id, newHighlightState) + @flashHighlightNodeIfRequested(id, newHighlightState) + + updateHighlightRegions: (id, newHighlightState) -> + oldHighlightState = @oldState[id] + highlightNode = @highlightNodesById[id] + + # remove regions + while oldHighlightState.regions.length > newHighlightState.regions.length + oldHighlightState.regions.pop() + @regionNodesByHighlightId[id][oldHighlightState.regions.length].remove() + delete @regionNodesByHighlightId[id][oldHighlightState.regions.length] + + # add or update regions + for newRegionState, i in newHighlightState.regions + unless oldHighlightState.regions[i]? + oldHighlightState.regions[i] = {} + regionNode = document.createElement('div') + regionNode.classList.add('region') + regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass? + @regionNodesByHighlightId[id][i] = regionNode + highlightNode.appendChild(regionNode) + + oldRegionState = oldHighlightState.regions[i] + regionNode = @regionNodesByHighlightId[id][i] + + for property in RegionStyleProperties + if newRegionState[property] isnt oldRegionState[property] + oldRegionState[property] = newRegionState[property] + if newRegionState[property]? + regionNode.style[property] = newRegionState[property] + 'px' + else + regionNode.style[property] = '' + + flashHighlightNodeIfRequested: (id, newHighlightState) -> + oldHighlightState = @oldState[id] + return unless newHighlightState.flashCount > oldHighlightState.flashCount + + highlightNode = @highlightNodesById[id] + + addFlashClass = => + highlightNode.classList.add(newHighlightState.flashClass) + oldHighlightState.flashClass = newHighlightState.flashClass + @flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration) + + removeFlashClass = => + highlightNode.classList.remove(oldHighlightState.flashClass) + oldHighlightState.flashClass = null + clearTimeout(@flashTimeoutId) + + if oldHighlightState.flashClass? + removeFlashClass() + requestAnimationFrame(addFlashClass) + else + addFlashClass() + + oldHighlightState.flashCount = newHighlightState.flashCount diff --git a/src/input-component.coffee b/src/input-component.coffee index 776be6c14..8a788927a 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -1,39 +1,29 @@ -{last, isEqual} = require 'underscore-plus' -React = require 'react-atom-fork' -{input} = require 'reactionary-atom-fork' - module.exports = -InputComponent = React.createClass - displayName: 'InputComponent' +class InputComponent + constructor: (@presenter) -> + @domNode = document.createElement('input') + @domNode.classList.add('hidden-input') + @domNode.setAttribute('data-react-skip-selection-restoration', true) + @domNode.style['-webkit-transform'] = 'translateZ(0)' + @domNode.addEventListener 'paste', (event) => event.preventDefault() + @updateSync() - render: -> - {className, style} = @props + updateSync: -> + @oldState ?= {} + newState = @presenter.state.hiddenInput - input {className, style, 'data-react-skip-selection-restoration': true} + if newState.top isnt @oldState.top + @domNode.style.top = newState.top + 'px' + @oldState.top = newState.top - getInitialState: -> - {lastChar: ''} + if newState.left isnt @oldState.left + @domNode.style.left = newState.left + 'px' + @oldState.left = newState.left - componentDidMount: -> - node = @getDOMNode() - node.addEventListener 'paste', @onPaste - node.addEventListener 'compositionupdate', @onCompositionUpdate + if newState.width isnt @oldState.width + @domNode.style.width = newState.width + 'px' + @oldState.width = newState.width - # Don't let text accumulate in the input forever, but avoid excessive reflows - componentDidUpdate: -> - if @lastValueLength > 500 and not @isPressAndHoldCharacter(@state.lastChar) - @getDOMNode().value = '' - @lastValueLength = 0 - - # This should actually consult the property lists in /System/Library/Input Methods/PressAndHold.app - isPressAndHoldCharacter: (char) -> - @state.lastChar.match /[aeiouAEIOU]/ - - shouldComponentUpdate: (newProps) -> - not isEqual(newProps.style, @props.style) - - onPaste: (e) -> - e.preventDefault() - - focus: -> - @getDOMNode().focus() + if newState.height isnt @oldState.height + @domNode.style.height = newState.height + 'px' + @oldState.height = newState.height diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1f830038f..d6498772d 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -1,7 +1,5 @@ _ = require 'underscore-plus' -React = require 'react-atom-fork' -{div, span} = require 'reactionary-atom-fork' -{debounce, isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus' +{toArray} = require 'underscore-plus' {$$} = require 'space-pen' CursorsComponent = require './cursors-component' @@ -12,73 +10,85 @@ DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibi AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} WrapperDiv = document.createElement('div') +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + module.exports = -LinesComponent = React.createClass - displayName: 'LinesComponent' +class LinesComponent + placeholderTextDiv: null - render: -> - {editor, presenter} = @props - @oldState ?= {lines: {}} - @newState = presenter.state.content - - {scrollHeight, scrollWidth, backgroundColor, placeholderText} = @newState - - style = - height: scrollHeight - width: scrollWidth - WebkitTransform: @getTransform() - backgroundColor: backgroundColor - - div {className: 'lines', style}, - div className: 'placeholder-text', placeholderText if placeholderText? - CursorsComponent {presenter} - HighlightsComponent {presenter} - - getTransform: -> - {scrollTop, scrollLeft} = @newState - {useHardwareAcceleration} = @props - - if useHardwareAcceleration - "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)" - else - "translate(#{-scrollLeft}px, #{-scrollTop}px)" - - componentWillMount: -> + constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} @renderedDecorationsByLineId = {} - componentDidMount: -> - if @props.useShadowDOM + @domNode = document.createElement('div') + @domNode.classList.add('lines') + + @cursorsComponent = new CursorsComponent(@presenter) + @domNode.appendChild(@cursorsComponent.domNode) + + @highlightsComponent = new HighlightsComponent(@presenter) + @domNode.appendChild(@highlightsComponent.domNode) + + if @useShadowDOM insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', '.overlayer') - @getDOMNode().appendChild(insertionPoint) + @domNode.appendChild(insertionPoint) insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', 'atom-overlay') - @overlayManager = new OverlayManager(@props.hostElement) - @getDOMNode().appendChild(insertionPoint) + @overlayManager = new OverlayManager(@hostElement) + @domNode.appendChild(insertionPoint) else - @overlayManager = new OverlayManager(@getDOMNode()) + @overlayManager = new OverlayManager(@domNode) - componentDidUpdate: -> - {visible, presenter} = @props + @updateSync(visible) + + updateSync: -> + @newState = @presenter.state.content + @oldState ?= {lines: {}} + + if @newState.scrollHeight isnt @oldState.scrollHeight + @domNode.style.height = @newState.scrollHeight + 'px' + @oldState.scrollHeight = @newState.scrollHeight + + if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft + @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)" + @oldState.scrollTop = @newState.scrollTop + @oldState.scrollLeft = @newState.scrollLeft + + if @newState.backgroundColor isnt @oldState.backgroundColor + @domNode.style.backgroundColor = @newState.backgroundColor + @oldState.backgroundColor = @newState.backgroundColor + + if @newState.placeholderText isnt @oldState.placeholderText + @placeholderTextDiv?.remove() + if @newState.placeholderText? + @placeholderTextDiv = document.createElement('div') + @placeholderTextDiv.classList.add('placeholder-text') + @placeholderTextDiv.textContent = @newState.placeholderText + @domNode.appendChild(@placeholderTextDiv) @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible @updateLineNodes() - @measureCharactersInNewLines() if visible and not @newState.scrollingVertically - @overlayManager?.render(@props) + if @newState.scrollWidth isnt @oldState.scrollWidth + @domNode.style.width = @newState.scrollWidth + 'px' + @oldState.scrollWidth = @newState.scrollWidth + + @cursorsComponent.updateSync() + @highlightsComponent.updateSync() + + @overlayManager?.render(@presenter) @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth - clearScreenRowCaches: -> - @screenRowsByLineId = {} - @lineIdsByScreenRow = {} - removeLineNodes: -> @removeLineNode(id) for id of @oldState.lines @@ -90,8 +100,6 @@ LinesComponent = React.createClass delete @oldState.lines[id] updateLineNodes: -> - {presenter} = @props - for id of @oldState.lines unless @newState.lines.hasOwnProperty(id) @removeLineNode(id) @@ -109,20 +117,18 @@ LinesComponent = React.createClass newLinesHTML += @buildLineHTML(id) @screenRowsByLineId[id] = lineState.screenRow @lineIdsByScreenRow[lineState.screenRow] = id - @oldState.lines[id] = _.clone(lineState) + @oldState.lines[id] = cloneObject(lineState) return unless newLineIds? WrapperDiv.innerHTML = newLinesHTML - newLineNodes = toArray(WrapperDiv.children) - node = @getDOMNode() + newLineNodes = _.toArray(WrapperDiv.children) for id, i in newLineIds lineNode = newLineNodes[i] @lineNodesByLineId[id] = lineNode - node.appendChild(lineNode) + @domNode.appendChild(lineNode) buildLineHTML: (id) -> - {presenter} = @props {scrollWidth} = @newState {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id] @@ -167,7 +173,6 @@ LinesComponent = React.createClass @buildEndOfLineHTML(id) or ' ' buildLineInnerHTML: (id) -> - {editor} = @props {indentGuidesVisible} = @newState {tokens, text, isOnlyWhitespace} = @newState.lines[id] innerHTML = "" @@ -217,13 +222,16 @@ LinesComponent = React.createClass "" updateLineNode: (id) -> - {scrollWidth} = @newState - {screenRow, top} = @newState.lines[id] + oldLineState = @oldState.lines[id] + newLineState = @newState.lines[id] lineNode = @lineNodesByLineId[id] - newDecorationClasses = @newState.lines[id].decorationClasses - oldDecorationClasses = @oldState.lines[id].decorationClasses + if @newState.scrollWidth isnt @oldState.scrollWidth + lineNode.style.width = @newState.scrollWidth + 'px' + + newDecorationClasses = newLineState.decorationClasses + oldDecorationClasses = oldLineState.decorationClasses if oldDecorationClasses? for decorationClass in oldDecorationClasses @@ -235,36 +243,37 @@ LinesComponent = React.createClass unless oldDecorationClasses? and decorationClass in oldDecorationClasses lineNode.classList.add(decorationClass) - lineNode.style.width = scrollWidth + 'px' - lineNode.style.top = top + 'px' - lineNode.dataset.screenRow = screenRow - @screenRowsByLineId[id] = screenRow - @lineIdsByScreenRow[screenRow] = id + oldLineState.decorationClasses = newLineState.decorationClasses + + if newLineState.top isnt oldLineState.top + lineNode.style.top = newLineState.top + 'px' + oldLineState.top = newLineState.cop + + if newLineState.screenRow isnt oldLineState.screenRow + lineNode.dataset.screenRow = newLineState.screenRow + oldLineState.screenRow = newLineState.screenRow + @lineIdsByScreenRow[newLineState.screenRow] = id lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureLineHeightAndDefaultCharWidth: -> - node = @getDOMNode() - node.appendChild(DummyLineNode) + @domNode.appendChild(DummyLineNode) lineHeightInPixels = DummyLineNode.getBoundingClientRect().height charWidth = DummyLineNode.firstChild.getBoundingClientRect().width - node.removeChild(DummyLineNode) + @domNode.removeChild(DummyLineNode) - {editor, presenter} = @props - presenter.setLineHeight(lineHeightInPixels) - presenter.setBaseCharacterWidth(charWidth) + @presenter.setLineHeight(lineHeightInPixels) + @presenter.setBaseCharacterWidth(charWidth) remeasureCharacterWidths: -> - return unless @props.presenter.baseCharacterWidth + return unless @presenter.baseCharacterWidth @clearScopedCharWidths() @measureCharactersInNewLines() measureCharactersInNewLines: -> - {presenter} = @props - - presenter.batchCharacterMeasurement => + @presenter.batchCharacterMeasurement => for id, lineState of @oldState.lines unless @measuredLines.has(id) lineNode = @lineNodesByLineId[id] @@ -272,13 +281,12 @@ LinesComponent = React.createClass return measureCharactersInLine: (tokenizedLine, lineNode) -> - {editor} = @props rangeForMeasurement = null iterator = null charIndex = 0 for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens - charWidths = editor.getScopedCharWidths(scopes) + charWidths = @presenter.getScopedCharacterWidths(scopes) valueIndex = 0 while valueIndex < value.length @@ -310,7 +318,7 @@ LinesComponent = React.createClass rangeForMeasurement.setStart(textNode, i) rangeForMeasurement.setEnd(textNode, i + charLength) charWidth = rangeForMeasurement.getBoundingClientRect().width - @props.presenter.setScopedCharacterWidth(scopes, char, charWidth) + @presenter.setScopedCharacterWidth(scopes, char, charWidth) charIndex += charLength @@ -318,5 +326,4 @@ LinesComponent = React.createClass clearScopedCharWidths: -> @measuredLines.clear() - @props.editor.clearScopedCharWidths() - @props.presenter.clearScopedCharacterWidths() + @presenter.clearScopedCharacterWidths() diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee index 82387fcf7..7d9c9d95d 100644 --- a/src/overlay-manager.coffee +++ b/src/overlay-manager.coffee @@ -3,16 +3,14 @@ class OverlayManager constructor: (@container) -> @overlayNodesById = {} - render: (props) -> - {presenter} = props - + render: (presenter) -> for decorationId, {pixelPosition, item} of presenter.state.content.overlays @renderOverlay(presenter, decorationId, item, pixelPosition) for id, overlayNode of @overlayNodesById unless presenter.state.content.overlays.hasOwnProperty(id) - overlayNode.remove() delete @overlayNodesById[id] + overlayNode.remove() return diff --git a/src/project.coffee b/src/project.coffee index 150e28d9a..d3fbd88eb 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -8,9 +8,9 @@ Q = require 'q' {Model} = require 'theorist' {Subscriber} = require 'emissary' {Emitter} = require 'event-kit' +DefaultDirectoryProvider = require './default-directory-provider' Serializable = require 'serializable' TextBuffer = require 'text-buffer' -{Directory} = require 'pathwatcher' Grim = require 'grim' TextEditor = require './text-editor' @@ -41,6 +41,14 @@ class Project extends Model @rootDirectories = [] @repositories = [] + @directoryProviders = [new DefaultDirectoryProvider()] + atom.packages.serviceHub.consume( + 'atom.directory-provider', + '^0.1.0', + # New providers are added to the front of @directoryProviders because + # DefaultDirectoryProvider is a catch-all that will always provide a Directory. + (provider) => @directoryProviders.unshift(provider)) + # Mapping from the real path of a {Directory} to a {Promise} that resolves # to either a {Repository} or null. Ideally, the {Directory} would be used # as the key; however, there can be multiple {Directory} objects created for @@ -53,7 +61,15 @@ class Project extends Model atom.packages.serviceHub.consume( 'atom.repository-provider', '^0.1.0', - (provider) => @repositoryProviders.push(provider)) + (provider) => + @repositoryProviders.push(provider) + + # If a path in getPaths() does not have a corresponding Repository, try + # to assign one by running through setPaths() again now that + # @repositoryProviders has been updated. + if null in @repositories + @setPaths(@getPaths()) + ) @subscribeToBuffer(buffer) for buffer in @buffers @@ -151,7 +167,7 @@ class Project extends Model # Public: Get an {Array} of {String}s containing the paths of the project's # directories. - getPaths: -> rootDirectory.path for rootDirectory in @rootDirectories + getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories getPath: -> Grim.deprecate("Use ::getPaths instead") @getPaths()[0] @@ -174,22 +190,22 @@ class Project extends Model Grim.deprecate("Use ::setPaths instead") @setPaths([path]) - # Public: Add a path the project's list of root paths + # Public: Add a path to the project's list of root paths # # * `projectPath` {String} The path to the directory to add. addPath: (projectPath, options) -> - projectPath = path.normalize(projectPath) + for directory in @getDirectories() + # Apparently a Directory does not believe it can contain itself, so we + # must also check whether the paths match. + return if directory.contains(projectPath) or directory.getPath() is projectPath - directoryPath = if fs.isDirectorySync(projectPath) - projectPath - else - path.dirname(projectPath) - - return if @getPaths().some (existingPath) -> - (directoryPath is existingPath) or - (directoryPath.indexOf(path.join(existingPath, path.sep)) is 0) - - directory = new Directory(directoryPath) + directory = null + for provider in @directoryProviders + break if directory = provider.directoryForURISync?(projectPath) + if directory is null + # This should never happen because DefaultDirectoryProvider should always + # return a Directory. + throw new Error(projectPath + ' could not be resolved to a directory') @rootDirectories.push(directory) repo = null @@ -201,6 +217,29 @@ class Project extends Model @emit "path-changed" @emitter.emit 'did-change-paths', @getPaths() + # Public: remove a path from the project's list of root paths. + # + # * `projectPath` {String} The path to remove. + removePath: (projectPath) -> + projectPath = path.normalize(projectPath) + + indexToRemove = null + for directory, i in @rootDirectories + if directory.getPath() is projectPath + indexToRemove = i + break + + if indexToRemove? + [removedDirectory] = @rootDirectories.splice(indexToRemove, 1) + [removedRepository] = @repositories.splice(indexToRemove, 1) + removedDirectory.off() + removedRepository?.destroy() unless removedRepository in @repositories + @emit "path-changed" + @emitter.emit "did-change-paths", @getPaths() + true + else + false + # Public: Get an {Array} of {Directory}s associated with this project. getDirectories: -> @rootDirectories @@ -233,8 +272,8 @@ class Project extends Model relativize: (fullPath) -> return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme for rootDirectory in @rootDirectories - if (relativePath = rootDirectory.relativize(fullPath))? - return relativePath + relativePath = rootDirectory.relativize(fullPath) + return relativePath if relativePath isnt fullPath fullPath # Public: Determines whether the given path (real or symbolic) is inside the diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee index eaf64b8bc..3e94a0708 100644 --- a/src/scrollbar-component.coffee +++ b/src/scrollbar-component.coffee @@ -1,69 +1,74 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{extend, isEqualForProperties} = require 'underscore-plus' - module.exports = -ScrollbarComponent = React.createClass - displayName: 'ScrollbarComponent' +class ScrollbarComponent + constructor: ({@presenter, @orientation, @onScroll}) -> + @domNode = document.createElement('div') + @domNode.classList.add "#{@orientation}-scrollbar" + @domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559 + @domNode.style.left = 0 if @orientation is 'horizontal' - render: -> - {presenter, orientation, className, useHardwareAcceleration} = @props + @contentNode = document.createElement('div') + @contentNode.classList.add "scrollbar-content" + @domNode.appendChild(@contentNode) - switch orientation + @domNode.addEventListener 'scroll', @onScrollCallback + + @updateSync() + + updateSync: -> + @oldState ?= {} + switch @orientation when 'vertical' - @newState = presenter.state.verticalScrollbar + @newState = @presenter.state.verticalScrollbar + @updateVertical() when 'horizontal' - @newState = presenter.state.horizontalScrollbar + @newState = @presenter.state.horizontalScrollbar + @updateHorizontal() - style = {} + if @newState.visible isnt @oldState.visible + if @newState.visible + @domNode.style.display = '' + else + @domNode.style.display = 'none' + @oldState.visible = @newState.visible - style.display = 'none' unless @newState.visible - style.transform = 'translateZ(0)' if useHardwareAcceleration # See atom/atom#3559 - switch orientation + updateVertical: -> + if @newState.width isnt @oldState.width + @domNode.style.width = @newState.width + 'px' + @oldState.width = @newState.width + + if @newState.bottom isnt @oldState.bottom + @domNode.style.bottom = @newState.bottom + 'px' + @oldState.bottom = @newState.bottom + + if @newState.scrollHeight isnt @oldState.scrollHeight + @contentNode.style.height = @newState.scrollHeight + 'px' + @oldState.scrollHeight = @newState.scrollHeight + + if @newState.scrollTop isnt @oldState.scrollTop + @domNode.scrollTop = @newState.scrollTop + @oldState.scrollTop = @newState.scrollTop + + updateHorizontal: -> + if @newState.height isnt @oldState.height + @domNode.style.height = @newState.height + 'px' + @oldState.height = @newState.height + + if @newState.right isnt @oldState.right + @domNode.style.right = @newState.right + 'px' + @oldState.right = @newState.right + + if @newState.scrollWidth isnt @oldState.scrollWidth + @contentNode.style.width = @newState.scrollWidth + 'px' + @oldState.scrollWidth = @newState.scrollWidth + + if @newState.scrollLeft isnt @oldState.scrollLeft + @domNode.scrollLeft = @newState.scrollLeft + @oldState.scrollLeft = @newState.scrollLeft + + + onScrollCallback: => + switch @orientation when 'vertical' - style.width = @newState.width - style.bottom = @newState.bottom + @onScroll(@domNode.scrollTop) when 'horizontal' - style.left = 0 - style.right = @newState.right - style.height = @newState.height - - div {className, style}, - switch orientation - when 'vertical' - div className: 'scrollbar-content', style: {height: @newState.scrollHeight} - when 'horizontal' - div className: 'scrollbar-content', style: {width: @newState.scrollWidth} - - componentDidMount: -> - {orientation} = @props - - unless orientation is 'vertical' or orientation is 'horizontal' - throw new Error("Must specify an orientation property of 'vertical' or 'horizontal'") - - @getDOMNode().addEventListener 'scroll', @onScroll - - componentWillUnmount: -> - @getDOMNode().removeEventListener 'scroll', @onScroll - - componentDidUpdate: -> - {orientation} = @props - node = @getDOMNode() - - switch orientation - when 'vertical' - node.scrollTop = @newState.scrollTop - when 'horizontal' - node.scrollLeft = @newState.scrollLeft - - onScroll: -> - {orientation, onScroll} = @props - node = @getDOMNode() - - switch orientation - when 'vertical' - scrollTop = node.scrollTop - onScroll(scrollTop) - when 'horizontal' - scrollLeft = node.scrollLeft - onScroll(scrollLeft) + @onScroll(@domNode.scrollLeft) diff --git a/src/scrollbar-corner-component.coffee b/src/scrollbar-corner-component.coffee index 706373ec2..1a266afc5 100644 --- a/src/scrollbar-corner-component.coffee +++ b/src/scrollbar-corner-component.coffee @@ -1,25 +1,37 @@ -React = require 'react-atom-fork' -{div} = require 'reactionary-atom-fork' -{isEqualForProperties} = require 'underscore-plus' - module.exports = -ScrollbarCornerComponent = React.createClass - displayName: 'ScrollbarCornerComponent' +class ScrollbarCornerComponent + constructor: (@presenter) -> + @domNode = document.createElement('div') + @domNode.classList.add('scrollbar-corner') - render: -> - {presenter, measuringScrollbars} = @props + @contentNode = document.createElement('div') + @domNode.appendChild(@contentNode) - visible = presenter.state.horizontalScrollbar.visible and presenter.state.verticalScrollbar.visible - width = presenter.state.verticalScrollbar.width - height = presenter.state.horizontalScrollbar.height + @updateSync() - if measuringScrollbars - height = 25 - width = 25 + updateSync: -> + @oldState ?= {} + @newState ?= {} - display = 'none' unless visible + newHorizontalState = @presenter.state.horizontalScrollbar + newVerticalState = @presenter.state.verticalScrollbar + @newState.visible = newHorizontalState.visible and newVerticalState.visible + @newState.height = newHorizontalState.height + @newState.width = newVerticalState.width - div className: 'scrollbar-corner', style: {display, width, height}, - div style: - height: height + 1 - width: width + 1 + if @newState.visible isnt @oldState.visible + if @newState.visible + @domNode.style.display = '' + else + @domNode.style.display = 'none' + @oldState.visible = @newState.visible + + if @newState.height isnt @oldState.height + @domNode.style.height = @newState.height + 'px' + @contentNode.style.height = @newState.height + 1 + 'px' + @oldState.height = @newState.height + + if @newState.width isnt @oldState.width + @domNode.style.width = @newState.width + 'px' + @contentNode.style.width = @newState.width + 1 + 'px' + @oldState.width = @newState.width diff --git a/src/task-bootstrap.coffee b/src/task-bootstrap.coffee index 4ed618d1a..ebb5cdc2b 100644 --- a/src/task-bootstrap.coffee +++ b/src/task-bootstrap.coffee @@ -41,6 +41,14 @@ handleEvents = -> result = handler.bind({async})(args...) emit('task:completed', result) unless isAsync +setupDeprecations = -> + Grim = require 'grim' + Grim.on 'updated', -> + deprecations = Grim.getDeprecations().map (deprecation) -> deprecation.serialize() + Grim.clearDeprecations() + emit('task:deprecations', deprecations) + setupGlobals() handleEvents() +setupDeprecations() handler = require(taskPath) diff --git a/src/task.coffee b/src/task.coffee index 6fe055ca6..9572494b8 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -1,6 +1,7 @@ _ = require 'underscore-plus' {fork} = require 'child_process' {Emitter} = require 'emissary' +Grim = require 'grim' # Extended: Run a node script in a separate process. # @@ -87,6 +88,9 @@ class Task @on "task:log", -> console.log(arguments...) @on "task:warn", -> console.warn(arguments...) @on "task:error", -> console.error(arguments...) + @on "task:deprecations", (deprecations) -> + Grim.addSerializedDeprecation(deprecation) for deprecation in deprecations + return @on "task:completed", (args...) => @callback?(args...) @handleEvents() diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 0f4695ca0..cea4cae3e 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -1,7 +1,4 @@ _ = require 'underscore-plus' -React = require 'react-atom-fork' -{div, span} = require 'reactionary-atom-fork' -{debounce, defaults, isEqualForProperties} = require 'underscore-plus' scrollbarStyle = require 'scrollbar-style' {Range, Point} = require 'text-buffer' grim = require 'grim' @@ -14,159 +11,149 @@ InputComponent = require './input-component' LinesComponent = require './lines-component' ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' -SubscriberMixin = require './subscriber-mixin' module.exports = -TextEditorComponent = React.createClass - displayName: 'TextEditorComponent' - mixins: [SubscriberMixin] +class TextEditorComponent + scrollSensitivity: 0.4 + cursorBlinkPeriod: 800 + cursorBlinkResumeDelay: 100 + lineOverdrawMargin: 15 - visible: false pendingScrollTop: null pendingScrollLeft: null - selectOnMouseMove: false updateRequested: false updatesPaused: false updateRequestedWhilePaused: false + heightAndWidthMeasurementRequested: false cursorMoved: false selectionChanged: false - scrollSensitivity: 0.4 - heightAndWidthMeasurementRequested: false inputEnabled: true - domPollingInterval: 100 - domPollingIntervalId: null - domPollingPaused: false measureScrollbarsWhenShown: true measureLineHeightAndDefaultCharWidthWhenShown: true remeasureCharacterWidthsWhenShown: false stylingChangeAnimationFrameRequested: false + gutterComponent: null + mounted: true - render: -> - {focused, showLineNumbers} = @state - {editor, cursorBlinkPeriod, cursorBlinkResumeDelay, hostElement, useShadowDOM} = @props - hasSelection = editor.getLastSelection()? and !editor.getLastSelection().isEmpty() - style = {} + constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) -> + @lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin? + @disposables = new CompositeDisposable - @performedInitialMeasurement = false if editor.isDestroyed() - - if @performedInitialMeasurement - visible = @isVisible() - - hiddenInputStyle = @getHiddenInputPosition() - hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @useHardwareAcceleration - style.height = @presenter.state.height if @presenter.state.height? - - if useShadowDOM - className = 'editor-contents--private' - else - className = 'editor-contents' - className += ' is-focused' if focused - className += ' has-selection' if hasSelection - - div {className, style}, - if @gutterVisible - GutterComponent { - ref: 'gutter', onMouseDown: @onGutterMouseDown, - @presenter, editor, @useHardwareAcceleration - } - - div ref: 'scrollView', className: 'scroll-view', - InputComponent - ref: 'input' - className: 'hidden-input' - style: hiddenInputStyle - - LinesComponent { - ref: 'lines', @presenter, editor, hostElement, @useHardwareAcceleration, useShadowDOM, visible - } - - ScrollbarComponent - ref: 'horizontalScrollbar' - className: 'horizontal-scrollbar' - orientation: 'horizontal' - presenter: @presenter - onScroll: @onHorizontalScroll - useHardwareAcceleration: @useHardwareAcceleration - - ScrollbarComponent - ref: 'verticalScrollbar' - className: 'vertical-scrollbar' - orientation: 'vertical' - presenter: @presenter - onScroll: @onVerticalScroll - useHardwareAcceleration: @useHardwareAcceleration - - # Also used to measure the height/width of scrollbars after the initial render - ScrollbarCornerComponent - ref: 'scrollbarCorner' - presenter: @presenter - measuringScrollbars: @measuringScrollbars - - getInitialState: -> {} - - getDefaultProps: -> - cursorBlinkPeriod: 800 - cursorBlinkResumeDelay: 100 - - componentWillMount: -> - @props.editor.manageScrollPosition = true + @editor.manageScrollPosition = true @observeConfig() @setScrollSensitivity(atom.config.get('editor.scrollSensitivity')) - {editor, lineOverdrawMargin, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props - lineOverdrawMargin ?= 15 - @presenter = new TextEditorPresenter - model: editor - scrollTop: editor.getScrollTop() - scrollLeft: editor.getScrollLeft() + model: @editor + scrollTop: @editor.getScrollTop() + scrollLeft: @editor.getScrollLeft() lineOverdrawMargin: lineOverdrawMargin - cursorBlinkPeriod: cursorBlinkPeriod - cursorBlinkResumeDelay: cursorBlinkResumeDelay + cursorBlinkPeriod: @cursorBlinkPeriod + cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 + @presenter.onDidUpdateState(@requestUpdate) + @domNode = document.createElement('div') + if @useShadowDOM + @domNode.classList.add('editor-contents--private') + else + @domNode.classList.add('editor-contents') - componentDidMount: -> - {editor, stylesElement} = @props + @scrollViewNode = document.createElement('div') + @scrollViewNode.classList.add('scroll-view') + @domNode.appendChild(@scrollViewNode) + + @mountGutterComponent() if @presenter.state.gutter.visible + + @hiddenInputComponent = new InputComponent(@presenter) + @scrollViewNode.appendChild(@hiddenInputComponent.domNode) + + @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM}) + @scrollViewNode.appendChild(@linesComponent.domNode) + + @horizontalScrollbarComponent = new ScrollbarComponent({@presenter, orientation: 'horizontal', onScroll: @onHorizontalScroll}) + @scrollViewNode.appendChild(@horizontalScrollbarComponent.domNode) + + @verticalScrollbarComponent = new ScrollbarComponent({@presenter, orientation: 'vertical', onScroll: @onVerticalScroll}) + @domNode.appendChild(@verticalScrollbarComponent.domNode) + + @scrollbarCornerComponent = new ScrollbarCornerComponent(@presenter) + @domNode.appendChild(@scrollbarCornerComponent.domNode) @observeEditor() @listenForDOMEvents() - @subscribe stylesElement.onDidAddStyleElement @onStylesheetsChanged - @subscribe stylesElement.onDidUpdateStyleElement @onStylesheetsChanged - @subscribe stylesElement.onDidRemoveStyleElement @onStylesheetsChanged + @disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged + @disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged + @disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged unless atom.themes.isInitialLoadComplete() - @subscribe atom.themes.onDidChangeActiveThemes @onAllThemesLoaded - @subscribe scrollbarStyle.changes, @refreshScrollbars + @disposables.add atom.themes.onDidChangeActiveThemes @onAllThemesLoaded + @disposables.add scrollbarStyle.changes.onValue @refreshScrollbars - @domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval) - @updateParentViewFocusedClassIfNeeded({}) - @updateParentViewMiniClass() + @disposables.add atom.views.pollDocument(@pollDOM) + + @updateSync() @checkForVisibilityChange() - componentWillUnmount: -> - {editor, hostElement} = @props - - @unsubscribe() + destroy: -> + @mounted = false + @disposables.dispose() @presenter.destroy() - @scopedConfigSubscriptions.dispose() window.removeEventListener 'resize', @requestHeightAndWidthMeasurement - clearInterval(@domPollingIntervalId) - @domPollingIntervalId = null - componentDidUpdate: (prevProps, prevState) -> + updateSync: -> + @oldState ?= {} + @newState = @presenter.state + cursorMoved = @cursorMoved selectionChanged = @selectionChanged @cursorMoved = false @selectionChanged = false - if @props.editor.isAlive() - @updateParentViewFocusedClassIfNeeded(prevState) + if @editor.getLastSelection()? and !@editor.getLastSelection().isEmpty() + @domNode.classList.add('has-selection') + else + @domNode.classList.remove('has-selection') + + if @newState.focused isnt @oldState.focused + @domNode.classList.toggle('is-focused', @newState.focused) + + @performedInitialMeasurement = false if @editor.isDestroyed() + + if @performedInitialMeasurement + if @newState.height isnt @oldState.height + if @newState.height? + @domNode.style.height = @newState.height + 'px' + else + @domNode.style.height = '' + + if @presenter.state.gutter.visible + @mountGutterComponent() unless @gutterComponent? + @gutterComponent.updateSync() + else + @gutterComponent?.domNode?.remove() + @gutterComponent = null + + @hiddenInputComponent.updateSync() + @linesComponent.updateSync() + @horizontalScrollbarComponent.updateSync() + @verticalScrollbarComponent.updateSync() + @scrollbarCornerComponent.updateSync() + + if @editor.isAlive() + @updateParentViewFocusedClassIfNeeded() @updateParentViewMiniClass() - @props.hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved - @props.hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged - @props.hostElement.__spacePenView.trigger 'editor:display-updated' + @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 + + mountGutterComponent: -> + @gutterComponent = new GutterComponent({@presenter, @editor, onMouseDown: @onGutterMouseDown}) + @domNode.insertBefore(@gutterComponent.domNode, @domNode.firstChild) becameVisible: -> @updatesPaused = true @@ -176,80 +163,58 @@ TextEditorComponent = React.createClass @measureHeightAndWidth() @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown - @props.editor.setVisible(true) + @editor.setVisible(true) @performedInitialMeasurement = true @updatesPaused = false - @forceUpdate() if @canUpdate() + @updateSync() if @canUpdate() - requestUpdate: -> + requestUpdate: => return unless @canUpdate() if @updatesPaused @updateRequestedWhilePaused = true return - if @props.hostElement.isUpdatedSynchronously() - @forceUpdate() + if @hostElement.isUpdatedSynchronously() + @updateSync() else unless @updateRequested @updateRequested = true - requestAnimationFrame => + atom.views.updateDocument => @updateRequested = false - @forceUpdate() if @canUpdate() + @updateSync() if @editor.isAlive() + atom.views.readDocument(@readAfterUpdateSync) canUpdate: -> - @isMounted() and @props.editor.isAlive() + @mounted and @editor.isAlive() requestAnimationFrame: (fn) -> @updatesPaused = true - @pauseDOMPolling() requestAnimationFrame => fn() @updatesPaused = false if @updateRequestedWhilePaused and @canUpdate() @updateRequestedWhilePaused = false - @forceUpdate() + @updateSync() getTopmostDOMNode: -> - @props.hostElement - - getHiddenInputPosition: -> - {editor} = @props - {focused} = @state - return {top: 0, left: 0} unless @isMounted() and focused and editor.getLastCursor()? - - {top, left, height, width} = editor.getLastCursor().getPixelRect() - width = 2 if width is 0 # Prevent autoscroll at the end of longest line - top -= editor.getScrollTop() - left -= editor.getScrollLeft() - top = Math.max(0, Math.min(editor.getHeight() - height, top)) - left = Math.max(0, Math.min(editor.getWidth() - width, left)) - {top, left} + @hostElement observeEditor: -> - {editor} = @props - @subscribe editor.onDidChangeGutterVisible(@updateGutterVisible) - @subscribe editor.onDidChangeMini(@setMini) - @subscribe editor.observeGrammar(@onGrammarChanged) - @subscribe editor.observeCursors(@onCursorAdded) - @subscribe editor.observeSelections(@onSelectionAdded) + @disposables.add @editor.observeGrammar(@onGrammarChanged) + @disposables.add @editor.observeCursors(@onCursorAdded) + @disposables.add @editor.observeSelections(@onSelectionAdded) listenForDOMEvents: -> - node = @getDOMNode() - node.addEventListener 'mousewheel', @onMouseWheel - node.addEventListener 'textInput', @onTextInput - @refs.scrollView.getDOMNode().addEventListener 'mousedown', @onMouseDown - - scrollViewNode = @refs.scrollView.getDOMNode() - scrollViewNode.addEventListener 'scroll', @onScrollViewScroll + @domNode.addEventListener 'mousewheel', @onMouseWheel + @domNode.addEventListener 'textInput', @onTextInput + @scrollViewNode.addEventListener 'mousedown', @onMouseDown + @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll window.addEventListener 'resize', @requestHeightAndWidthMeasurement @listenForIMEEvents() @trackSelectionClipboard() if process.platform is 'linux' listenForIMEEvents: -> - node = @getDOMNode() - {editor} = @props - # The IME composition events work like this: # # User types 's', chromium pops up the completion helper @@ -264,58 +229,55 @@ TextEditorComponent = React.createClass # 5. textInput fired; event.data == the completion string selectedText = null - node.addEventListener 'compositionstart', -> - selectedText = editor.getSelectedText() - node.addEventListener 'compositionupdate', (event) -> - editor.insertText(event.data, select: true, undo: 'skip') - node.addEventListener 'compositionend', (event) -> - editor.insertText(selectedText, select: true, undo: 'skip') + @domNode.addEventListener 'compositionstart', => + selectedText = @editor.getSelectedText() + @domNode.addEventListener 'compositionupdate', (event) => + @editor.insertText(event.data, select: true, undo: 'skip') + @domNode.addEventListener 'compositionend', (event) => + @editor.insertText(selectedText, select: true, undo: 'skip') event.target.value = '' # Listen for selection changes and store the currently selected text # in the selection clipboard. This is only applicable on Linux. trackSelectionClipboard: -> timeoutId = null - {editor} = @props - writeSelectedTextToSelectionClipboard = -> - return if editor.isDestroyed() - if selectedText = editor.getSelectedText() + writeSelectedTextToSelectionClipboard = => + return if @editor.isDestroyed() + if selectedText = @editor.getSelectedText() # This uses ipc.send instead of clipboard.writeText because # clipboard.writeText is a sync ipc call on Linux and that # will slow down selections. ipc.send('write-text-to-selection-clipboard', selectedText) - @subscribe editor.onDidChangeSelectionRange -> + @disposables.add @editor.onDidChangeSelectionRange -> clearTimeout(timeoutId) timeoutId = setTimeout(writeSelectedTextToSelectionClipboard) observeConfig: -> - @subscribe atom.config.observe 'editor.useHardwareAcceleration', @setUseHardwareAcceleration - @subscribe atom.config.onDidChange 'editor.fontSize', @sampleFontStyling - @subscribe atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling - @subscribe atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling + @disposables.add atom.config.onDidChange 'editor.fontSize', @sampleFontStyling + @disposables.add atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling + @disposables.add atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling - onGrammarChanged: -> - {editor} = @props + onGrammarChanged: => + if @scopedConfigDisposables? + @scopedConfigDisposables.dispose() + @disposables.remove(@scopedConfigDisposables) - @scopedConfigSubscriptions?.dispose() - @scopedConfigSubscriptions = subscriptions = new CompositeDisposable + @scopedConfigDisposables = new CompositeDisposable + @disposables.add(@scopedConfigDisposables) - scopeDescriptor = editor.getRootScopeDescriptor() - - subscriptions.add atom.config.observe 'editor.showIndentGuide', scope: scopeDescriptor, @requestUpdate - subscriptions.add atom.config.observe 'editor.showLineNumbers', scope: scopeDescriptor, @updateGutterVisible - subscriptions.add atom.config.observe 'editor.scrollSensitivity', scope: scopeDescriptor, @setScrollSensitivity + scope = @editor.getRootScopeDescriptor() + @scopedConfigDisposables.add atom.config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity focused: -> - if @isMounted() - @setState(focused: true) - @refs.input.focus() + if @mounted + @presenter.setFocused(true) + @hiddenInputComponent.domNode.focus() blurred: -> - if @isMounted() - @setState(focused: false) + if @mounted + @presenter.setFocused(false) - onTextInput: (event) -> + onTextInput: (event) => event.stopPropagation() # If we prevent the insertion of a space character, then the browser @@ -324,7 +286,6 @@ TextEditorComponent = React.createClass return unless @isInputEnabled() - {editor} = @props inputNode = event.target # Work around of the accented character suggestion feature in OS X. @@ -332,16 +293,14 @@ TextEditorComponent = React.createClass # replacing the previous un-accented character with an accented variant, it # will select backward over it. selectedLength = inputNode.selectionEnd - inputNode.selectionStart - editor.selectLeft() if selectedLength is 1 + @editor.selectLeft() if selectedLength is 1 - insertedRange = editor.transact atom.config.get('editor.undoGroupingInterval'), -> - editor.insertText(event.data) + insertedRange = @editor.transact atom.config.get('editor.undoGroupingInterval'), => + @editor.insertText(event.data) inputNode.value = event.data if insertedRange - onVerticalScroll: (scrollTop) -> - {editor} = @props - - return if @updateRequested or scrollTop is editor.getScrollTop() + onVerticalScroll: (scrollTop) => + return if @updateRequested or scrollTop is @editor.getScrollTop() animationFramePending = @pendingScrollTop? @pendingScrollTop = scrollTop @@ -351,10 +310,8 @@ TextEditorComponent = React.createClass @pendingScrollTop = null @presenter.setScrollTop(pendingScrollTop) - onHorizontalScroll: (scrollLeft) -> - {editor} = @props - - return if @updateRequested or scrollLeft is editor.getScrollLeft() + onHorizontalScroll: (scrollLeft) => + return if @updateRequested or scrollLeft is @editor.getScrollLeft() animationFramePending = @pendingScrollLeft? @pendingScrollLeft = scrollLeft @@ -363,9 +320,7 @@ TextEditorComponent = React.createClass @presenter.setScrollLeft(@pendingScrollLeft) @pendingScrollLeft = null - onMouseWheel: (event) -> - {editor} = @props - + onMouseWheel: (event) => # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event @@ -380,24 +335,23 @@ TextEditorComponent = React.createClass if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally - previousScrollLeft = editor.getScrollLeft() + previousScrollLeft = @editor.getScrollLeft() @presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)) - event.preventDefault() unless previousScrollLeft is editor.getScrollLeft() + event.preventDefault() unless previousScrollLeft is @editor.getScrollLeft() else # Scrolling vertically @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) previousScrollTop = @presenter.scrollTop @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) - event.preventDefault() unless previousScrollTop is editor.getScrollTop() + event.preventDefault() unless previousScrollTop is @editor.getScrollTop() - onScrollViewScroll: -> - if @isMounted() + onScrollViewScroll: => + if @mounted console.warn "TextEditorScrollView scrolled when it shouldn't have." - scrollViewNode = @refs.scrollView.getDOMNode() - scrollViewNode.scrollTop = 0 - scrollViewNode.scrollLeft = 0 + @scrollViewNode.scrollTop = 0 + @scrollViewNode.scrollLeft = 0 - onMouseDown: (event) -> + onMouseDown: (event) => unless event.button is 0 or (event.button is 1 and process.platform is 'linux') # Only handle mouse down events for left mouse button on all platforms # and middle mouse button on Linux since it pastes the selection clipboard @@ -405,39 +359,38 @@ TextEditorComponent = React.createClass return if event.target?.classList.contains('horizontal-scrollbar') - {editor} = @props {detail, shiftKey, metaKey, ctrlKey} = event # CTRL+click brings up the context menu on OSX, so don't handle those either return if ctrlKey and process.platform is 'darwin' # Prevent focusout event on hidden input if editor is already focused - event.preventDefault() if @state.focused + event.preventDefault() if @oldState.focused screenPosition = @screenPositionForMouseEvent(event) if event.target?.classList.contains('fold-marker') - bufferRow = editor.bufferRowForScreenRow(screenPosition.row) - editor.unfoldBufferRow(bufferRow) + bufferRow = @editor.bufferRowForScreenRow(screenPosition.row) + @editor.unfoldBufferRow(bufferRow) return switch detail when 1 if shiftKey - editor.selectToScreenPosition(screenPosition) + @editor.selectToScreenPosition(screenPosition) else if metaKey or (ctrlKey and process.platform isnt 'darwin') - editor.addCursorAtScreenPosition(screenPosition) + @editor.addCursorAtScreenPosition(screenPosition) else - editor.setCursorScreenPosition(screenPosition) + @editor.setCursorScreenPosition(screenPosition) when 2 - editor.getLastSelection().selectWord() + @editor.getLastSelection().selectWord() when 3 - editor.getLastSelection().selectLine() + @editor.getLastSelection().selectLine() - @handleDragUntilMouseUp event, (screenPosition) -> - editor.selectToScreenPosition(screenPosition) + @handleDragUntilMouseUp event, (screenPosition) => + @editor.selectToScreenPosition(screenPosition) - onGutterMouseDown: (event) -> + onGutterMouseDown: (event) => return unless event.button is 0 # only handle the left mouse button {shiftKey, metaKey, ctrlKey} = event @@ -449,27 +402,25 @@ TextEditorComponent = React.createClass else @onGutterClick(event) - onGutterClick: (event) -> - {editor} = @props + onGutterClick: (event) => clickedRow = @screenPositionForMouseEvent(event).row - editor.setSelectedScreenRange([[clickedRow, 0], [clickedRow + 1, 0]], preserveFolds: true) + @editor.setSelectedScreenRange([[clickedRow, 0], [clickedRow + 1, 0]], preserveFolds: true) - @handleDragUntilMouseUp event, (screenPosition) -> + @handleDragUntilMouseUp event, (screenPosition) => dragRow = screenPosition.row if dragRow < clickedRow # dragging up - editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true) + @editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true) else - editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true) + @editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true) - onGutterMetaClick: (event) -> - {editor} = @props + onGutterMetaClick: (event) => clickedRow = @screenPositionForMouseEvent(event).row - bufferRange = editor.bufferRangeForScreenRange([[clickedRow, 0], [clickedRow + 1, 0]]) - rowSelection = editor.addSelectionForBufferRange(bufferRange, preserveFolds: true) + bufferRange = @editor.bufferRangeForScreenRange([[clickedRow, 0], [clickedRow + 1, 0]]) + rowSelection = @editor.addSelectionForBufferRange(bufferRange, preserveFolds: true) - @handleDragUntilMouseUp event, (screenPosition) -> + @handleDragUntilMouseUp event, (screenPosition) => dragRow = screenPosition.row if dragRow < clickedRow # dragging up @@ -478,32 +429,31 @@ TextEditorComponent = React.createClass rowSelection.setScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true) # After updating the selected screen range, merge overlapping selections - editor.mergeIntersectingSelections(preserveFolds: true) + @editor.mergeIntersectingSelections(preserveFolds: true) # The merge process will possibly destroy the current selection because # it will be merged into another one. Therefore, we need to obtain a # reference to the new selection that contains the originally selected row - rowSelection = _.find editor.getSelections(), (selection) -> + rowSelection = _.find @editor.getSelections(), (selection) -> selection.intersectsBufferRange(bufferRange) - onGutterShiftClick: (event) -> - {editor} = @props + onGutterShiftClick: (event) => clickedRow = @screenPositionForMouseEvent(event).row - tailPosition = editor.getLastSelection().getTailScreenPosition() + tailPosition = @editor.getLastSelection().getTailScreenPosition() if clickedRow < tailPosition.row - editor.selectToScreenPosition([clickedRow, 0]) + @editor.selectToScreenPosition([clickedRow, 0]) else - editor.selectToScreenPosition([clickedRow + 1, 0]) + @editor.selectToScreenPosition([clickedRow + 1, 0]) - @handleDragUntilMouseUp event, (screenPosition) -> + @handleDragUntilMouseUp event, (screenPosition) => dragRow = screenPosition.row if dragRow < tailPosition.row # dragging up - editor.setSelectedScreenRange([[dragRow, 0], tailPosition], preserveFolds: true) + @editor.setSelectedScreenRange([[dragRow, 0], tailPosition], preserveFolds: true) else - editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true) + @editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true) - onStylesheetsChanged: (styleElement) -> + onStylesheetsChanged: (styleElement) => return unless @performedInitialMeasurement return unless atom.themes.isInitialLoadComplete() @@ -515,55 +465,52 @@ TextEditorComponent = React.createClass @stylingChangeAnimationFrameRequested = true requestAnimationFrame => @stylingChangeAnimationFrameRequested = false - if @isMounted() + if @mounted @refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet) @handleStylingChange() - onAllThemesLoaded: -> + onAllThemesLoaded: => @refreshScrollbars() @handleStylingChange() - handleStylingChange: -> + handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() @remeasureCharacterWidths() - onSelectionAdded: (selection) -> - {editor} = @props - - @subscribe selection.onDidChangeRange => @onSelectionChanged(selection) - @subscribe selection.onDidDestroy => + onSelectionAdded: (selection) => + selectionDisposables = new CompositeDisposable + selectionDisposables.add selection.onDidChangeRange => @onSelectionChanged(selection) + selectionDisposables.add selection.onDidDestroy => @onSelectionChanged(selection) - @unsubscribe(selection) + selectionDisposables.dispose() + @disposables.remove(selectionDisposables) - if editor.selectionIntersectsVisibleRowRange(selection) + @disposables.add(selectionDisposables) + + if @editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true - @requestUpdate() - onSelectionChanged: (selection) -> - {editor} = @props - if editor.selectionIntersectsVisibleRowRange(selection) + onSelectionChanged: (selection) => + if @editor.selectionIntersectsVisibleRowRange(selection) @selectionChanged = true - @requestUpdate() - onCursorAdded: (cursor) -> - @subscribe cursor.onDidChangePosition @onCursorMoved + onCursorAdded: (cursor) => + @disposables.add cursor.onDidChangePosition @onCursorMoved - onCursorMoved: -> + onCursorMoved: => @cursorMoved = true - @requestUpdate() - handleDragUntilMouseUp: (event, dragHandler) -> - {editor} = @props + handleDragUntilMouseUp: (event, dragHandler) => dragging = false lastMousePosition = {} animationLoop = => @requestAnimationFrame => - if dragging and @isMounted() + if dragging and @mounted screenPosition = @screenPositionForMouseEvent(lastMousePosition) dragHandler(screenPosition) animationLoop() - else if not @isMounted() + else if not @mounted stopDragging() onMouseMove = (event) -> @@ -578,9 +525,9 @@ TextEditorComponent = React.createClass # Stop dragging when cursor enters dev tools because we can't detect mouseup onMouseUp() if event.which is 0 - onMouseUp = (event) -> + onMouseUp = (event) => stopDragging() - editor.finalizeSelections() + @editor.finalizeSelections() pasteSelectionClipboard(event) stopDragging = -> @@ -588,31 +535,18 @@ TextEditorComponent = React.createClass window.removeEventListener('mousemove', onMouseMove) window.removeEventListener('mouseup', onMouseUp) - pasteSelectionClipboard = (event) -> + pasteSelectionClipboard = (event) => if event?.which is 2 and process.platform is 'linux' if selection = require('clipboard').readText('selection') - editor.insertText(selection) + @editor.insertText(selection) window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) isVisible: -> - node = @getDOMNode() - node.offsetHeight > 0 or node.offsetWidth > 0 - - pauseDOMPolling: -> - @domPollingPaused = true - @resumeDOMPollingAfterDelay ?= debounce(@resumeDOMPolling, 100) - @resumeDOMPollingAfterDelay() - - resumeDOMPolling: -> - @domPollingPaused = false - - resumeDOMPollingAfterDelay: null # created lazily - - pollDOM: -> - return if @domPollingPaused or @updateRequested or not @isMounted() + @domNode.offsetHeight > 0 or @domNode.offsetWidth > 0 + pollDOM: => unless @checkForVisibilityChange() @sampleBackgroundColors() @measureHeightAndWidth() @@ -628,7 +562,7 @@ TextEditorComponent = React.createClass else @wasVisible = false - requestHeightAndWidthMeasurement: -> + requestHeightAndWidthMeasurement: => return if @heightAndWidthMeasurementRequested @heightAndWidthMeasurementRequested = true @@ -641,29 +575,27 @@ TextEditorComponent = React.createClass # and use the scrollHeight / scrollWidth as its height and width in # calculations. measureHeightAndWidth: -> - return unless @isMounted() + return unless @mounted - {editor, hostElement} = @props - scrollViewNode = @refs.scrollView.getDOMNode() - {position} = getComputedStyle(hostElement) - {height} = hostElement.style + {position} = getComputedStyle(@hostElement) + {height} = @hostElement.style if position is 'absolute' or height @presenter.setAutoHeight(false) - height = hostElement.offsetHeight + height = @hostElement.offsetHeight if height > 0 @presenter.setExplicitHeight(height) else @presenter.setAutoHeight(true) @presenter.setExplicitHeight(null) - clientWidth = scrollViewNode.clientWidth - paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft) + clientWidth = @scrollViewNode.clientWidth + paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft) clientWidth -= paddingLeft if clientWidth > 0 @presenter.setContentFrameWidth(clientWidth) - sampleFontStyling: -> + sampleFontStyling: => oldFontSize = @fontSize oldFontFamily = @fontFamily oldLineHeight = @lineHeight @@ -677,34 +609,32 @@ TextEditorComponent = React.createClass @remeasureCharacterWidths() sampleBackgroundColors: (suppressUpdate) -> - {hostElement} = @props - {backgroundColor} = getComputedStyle(hostElement) + {backgroundColor} = getComputedStyle(@hostElement) @presenter.setBackgroundColor(backgroundColor) - if @refs.gutter? - gutterBackgroundColor = getComputedStyle(@refs.gutter.getDOMNode()).backgroundColor + if @gutterComponent? + gutterBackgroundColor = getComputedStyle(@gutterComponent.domNode).backgroundColor @presenter.setGutterBackgroundColor(gutterBackgroundColor) measureLineHeightAndDefaultCharWidth: -> if @isVisible() @measureLineHeightAndDefaultCharWidthWhenShown = false - @refs.lines.measureLineHeightAndDefaultCharWidth() + @linesComponent.measureLineHeightAndDefaultCharWidth() else @measureLineHeightAndDefaultCharWidthWhenShown = true remeasureCharacterWidths: -> if @isVisible() @remeasureCharacterWidthsWhenShown = false - @refs.lines.remeasureCharacterWidths() + @linesComponent.remeasureCharacterWidths() else @remeasureCharacterWidthsWhenShown = true measureScrollbars: -> @measureScrollbarsWhenShown = false - {editor} = @props - cornerNode = @refs.scrollbarCorner.getDOMNode() + cornerNode = @scrollbarCornerComponent.domNode originalDisplayValue = cornerNode.style.display cornerNode.style.display = 'block' @@ -723,18 +653,16 @@ TextEditorComponent = React.createClass return true false - refreshScrollbars: -> + refreshScrollbars: => if @isVisible() @measureScrollbarsWhenShown = false else @measureScrollbarsWhenShown = true return - {verticalScrollbar, horizontalScrollbar, scrollbarCorner} = @refs - - verticalNode = verticalScrollbar.getDOMNode() - horizontalNode = horizontalScrollbar.getDOMNode() - cornerNode = scrollbarCorner.getDOMNode() + verticalNode = @verticalScrollbarComponent.domNode + horizontalNode = @horizontalScrollbarComponent.domNode + cornerNode = @scrollbarCornerComponent.domNode originalVerticalDisplayValue = verticalNode.style.display originalHorizontalDisplayValue = horizontalNode.style.display @@ -759,11 +687,11 @@ TextEditorComponent = React.createClass cornerNode.style.display = originalCornerDisplayValue consolidateSelections: (e) -> - e.abortKeyBinding() unless @props.editor.consolidateSelections() + e.abortKeyBinding() unless @editor.consolidateSelections() - lineNodeForScreenRow: (screenRow) -> @refs.lines.lineNodeForScreenRow(screenRow) + lineNodeForScreenRow: (screenRow) -> @linesComponent.lineNodeForScreenRow(screenRow) - lineNumberNodeForScreenRow: (screenRow) -> @refs.gutter.lineNumberNodeForScreenRow(screenRow) + lineNumberNodeForScreenRow: (screenRow) -> @gutterComponent.lineNumberNodeForScreenRow(screenRow) screenRowForNode: (node) -> while node? @@ -793,16 +721,6 @@ TextEditorComponent = React.createClass setShowIndentGuide: (showIndentGuide) -> atom.config.set("editor.showIndentGuide", showIndentGuide) - setMini: -> - @updateGutterVisible() - @requestUpdate() - - updateGutterVisible: -> - gutterVisible = not @props.editor.isMini() and @props.editor.isGutterVisible() and atom.config.get('editor.showLineNumbers') - if gutterVisible isnt @gutterVisible - @gutterVisible = gutterVisible - @requestUpdate() - # Deprecated setInvisibles: (invisibles={}) -> grim.deprecate "Use config.set('editor.invisibles', invisibles) instead" @@ -812,75 +730,35 @@ TextEditorComponent = React.createClass setShowInvisibles: (showInvisibles) -> atom.config.set('editor.showInvisibles', showInvisibles) - setScrollSensitivity: (scrollSensitivity) -> + setScrollSensitivity: (scrollSensitivity) => if scrollSensitivity = parseInt(scrollSensitivity) @scrollSensitivity = Math.abs(scrollSensitivity) / 100 - setUseHardwareAcceleration: (useHardwareAcceleration=true) -> - unless @useHardwareAcceleration is useHardwareAcceleration - @useHardwareAcceleration = useHardwareAcceleration - @requestUpdate() - screenPositionForMouseEvent: (event) -> pixelPosition = @pixelPositionForMouseEvent(event) - @props.editor.screenPositionForPixelPosition(pixelPosition) + @editor.screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event) -> - {editor} = @props {clientX, clientY} = event - linesClientRect = @refs.lines.getDOMNode().getBoundingClientRect() + linesClientRect = @linesComponent.domNode.getBoundingClientRect() top = clientY - linesClientRect.top left = clientX - linesClientRect.left {top, left} getModel: -> - @props.editor + @editor isInputEnabled: -> @inputEnabled setInputEnabled: (@inputEnabled) -> @inputEnabled - updateParentViewFocusedClassIfNeeded: (prevState) -> - if prevState.focused isnt @state.focused - @props.hostElement.classList.toggle('is-focused', @state.focused) - @props.rootElement.classList.toggle('is-focused', @state.focused) + updateParentViewFocusedClassIfNeeded: -> + if @oldState.focused isnt @newState.focused + @hostElement.classList.toggle('is-focused', @newState.focused) + @rootElement.classList.toggle('is-focused', @newState.focused) + @oldState.focused = @newState.focused updateParentViewMiniClass: -> - @props.hostElement.classList.toggle('mini', @props.editor.isMini()) - @props.rootElement.classList.toggle('mini', @props.editor.isMini()) - - runScrollBenchmark: -> - unless process.env.NODE_ENV is 'production' - ReactPerf = require 'react-atom-fork/lib/ReactDefaultPerf' - ReactPerf.start() - - node = @getDOMNode() - - scroll = (delta, done) -> - dispatchMouseWheelEvent = -> - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -0, wheelDeltaY: -delta)) - - stopScrolling = -> - clearInterval(interval) - done?() - - interval = setInterval(dispatchMouseWheelEvent, 10) - setTimeout(stopScrolling, 500) - - console.timeline('scroll') - scroll 50, -> - scroll 100, -> - scroll 200, -> - scroll 400, -> - scroll 800, -> - scroll 1600, -> - console.timelineEnd('scroll') - unless process.env.NODE_ENV is 'production' - ReactPerf.stop() - console.log "Inclusive" - ReactPerf.printInclusive() - console.log "Exclusive" - ReactPerf.printExclusive() - console.log "Wasted" - ReactPerf.printWasted() + @hostElement.classList.toggle('mini', @editor.isMini()) + @rootElement.classList.toggle('mini', @editor.isMini()) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 67e62b408..443786793 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -1,6 +1,5 @@ {Emitter} = require 'event-kit' {View, $, callRemoveHooks} = require 'space-pen' -React = require 'react-atom-fork' Path = require 'path' {defaults} = require 'underscore-plus' TextBuffer = require 'text-buffer' @@ -61,7 +60,7 @@ class TextEditorElement extends HTMLElement attachedCallback: -> @buildModel() unless @getModel()? - @mountComponent() unless @component?.isMounted() + @mountComponent() unless @component? @component.checkForVisibilityChange() if this is document.activeElement @focused() @@ -105,7 +104,7 @@ class TextEditorElement extends HTMLElement )) mountComponent: -> - @componentDescriptor ?= TextEditorComponent( + @component = new TextEditorComponent( hostElement: this rootElement: @rootElement stylesElement: @stylesElement @@ -113,27 +112,28 @@ class TextEditorElement extends HTMLElement lineOverdrawMargin: @lineOverdrawMargin useShadowDOM: @useShadowDOM ) - @component = React.renderComponent(@componentDescriptor, @rootElement) + @rootElement.appendChild(@component.domNode) if @useShadowDOM @shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true) else - inputNode = @component.refs.input.getDOMNode() + inputNode = @component.hiddenInputComponent.domNode inputNode.addEventListener 'focus', @focused.bind(this) inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false)) unmountComponent: -> - return unless @component?.isMounted() callRemoveHooks(this) - React.unmountComponentAtNode(@rootElement) - @component = null + if @component? + @component.destroy() + @component.domNode.remove() + @component = null focused: -> @component?.focused() blurred: (event) -> unless @useShadowDOM - if event.relatedTarget is @component?.refs.input?.getDOMNode() + if event.relatedTarget is @component.hiddenInputComponent.domNode event.stopImmediatePropagation() return diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b8b9de943..215398c05 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -14,7 +14,7 @@ class TextEditorPresenter {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params - {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay} = params + {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @@ -56,13 +56,18 @@ class TextEditorPresenter @updateLinesState() @updateGutterState() @updateLineNumbersState() - @disposables.add @model.onDidChangeGrammar(@updateContentState.bind(this)) + @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this)) @disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this)) @disposables.add @model.onDidChangeMini => + @updateScrollbarDimensions() + @updateScrollbarsState() @updateContentState() @updateDecorations() @updateLinesState() + @updateGutterState() @updateLineNumbersState() + @disposables.add @model.onDidChangeGutterVisible => + @updateGutterState() @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this)) @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) @disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this)) @@ -71,19 +76,41 @@ class TextEditorPresenter @observeCursor(cursor) for cursor in @model.getCursors() observeConfig: -> - @scrollPastEnd = atom.config.get('editor.scrollPastEnd') + configParams = {scope: @model.getRootScopeDescriptor()} - @disposables.add atom.config.onDidChange 'editor.showIndentGuide', scope: @model.getRootScopeDescriptor(), @updateContentState.bind(this) - @disposables.add atom.config.onDidChange 'editor.scrollPastEnd', scope: @model.getRootScopeDescriptor(), ({newValue}) => + @scrollPastEnd = atom.config.get('editor.scrollPastEnd', configParams) + @showLineNumbers = atom.config.get('editor.showLineNumbers', configParams) + @showIndentGuide = atom.config.get('editor.showIndentGuide', configParams) + + if @configDisposables? + @configDisposables?.dispose() + @disposables.remove(@configDisposables) + + @configDisposables = new CompositeDisposable + @disposables.add(@configDisposables) + + @configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) => + @showIndentGuide = newValue + @updateContentState() + @configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) => @scrollPastEnd = newValue @updateScrollHeight() @updateVerticalScrollState() @updateScrollbarsState() + @configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) => + @showLineNumbers = newValue + @updateGutterState() + + didChangeGrammar: -> + @observeConfig() + @updateContentState() + @updateGutterState() buildState: -> @state = horizontalScrollbar: {} verticalScrollbar: {} + hiddenInput: {} content: scrollingVertically: false blinkCursorsOff: false @@ -100,10 +127,12 @@ class TextEditorPresenter @updateStartRow() @updateEndRow() + @updateFocusedState() @updateHeightState() @updateVerticalScrollState() @updateHorizontalScrollState() @updateScrollbarsState() + @updateHiddenInputState() @updateContentState() @updateDecorations() @updateLinesState() @@ -112,6 +141,9 @@ class TextEditorPresenter @updateGutterState() @updateLineNumbersState() + updateFocusedState: -> + @state.focused = @focused + updateHeightState: -> if @autoHeight @state.height = @contentHeight @@ -151,10 +183,29 @@ class TextEditorPresenter @emitter.emit 'did-update-state' + updateHiddenInputState: -> + return unless lastCursor = @model.getLastCursor() + + {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange()) + + if @focused + top -= @scrollTop + left -= @scrollLeft + @state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0) + @state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0) + else + @state.hiddenInput.top = 0 + @state.hiddenInput.left = 0 + + @state.hiddenInput.height = height + @state.hiddenInput.width = Math.max(width, 2) + + @emitter.emit 'did-update-state' + updateContentState: -> @state.content.scrollWidth = @scrollWidth @state.content.scrollLeft = @scrollLeft - @state.content.indentGuidesVisible = not @model.isMini() and atom.config.get('editor.showIndentGuide', scope: @model.getRootScopeDescriptor()) + @state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null @emitter.emit 'did-update-state' @@ -242,6 +293,7 @@ class TextEditorPresenter @emitter.emit "did-update-state" updateGutterState: -> + @state.gutter.visible = not @model.isMini() and (@model.isGutterVisible() ? true) and @showLineNumbers @state.gutter.maxLineNumberDigits = @model.getLineCount().toString().length @state.gutter.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)" @gutterBackgroundColor @@ -330,14 +382,14 @@ class TextEditorPresenter @updateScrollTop() updateContentDimensions: -> - return unless @lineHeight? and @baseCharacterWidth? + if @lineHeight? + oldContentHeight = @contentHeight + @contentHeight = @lineHeight * @model.getScreenLineCount() - oldContentHeight = @contentHeight - @contentHeight = @lineHeight * @model.getScreenLineCount() - - oldContentWidth = @contentWidth - @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), Infinity]).left - @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width + if @baseCharacterWidth? + oldContentWidth = @contentWidth + @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), Infinity]).left + @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width if @contentHeight isnt oldContentHeight @updateHeight() @@ -395,12 +447,14 @@ class TextEditorPresenter clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @measuredHorizontalScrollbarHeight horizontalScrollbarVisible = - @contentWidth > clientWidthWithoutVerticalScrollbar or - @contentWidth > clientWidthWithVerticalScrollbar and @contentHeight > clientHeightWithoutHorizontalScrollbar + not @model.isMini() and + (@contentWidth > clientWidthWithoutVerticalScrollbar or + @contentWidth > clientWidthWithVerticalScrollbar and @contentHeight > clientHeightWithoutHorizontalScrollbar) verticalScrollbarVisible = - @contentHeight > clientHeightWithoutHorizontalScrollbar or - @contentHeight > clientHeightWithHorizontalScrollbar and @contentWidth > clientWidthWithoutVerticalScrollbar + not @model.isMini() and + (@contentHeight > clientHeightWithoutHorizontalScrollbar or + @contentHeight > clientHeightWithHorizontalScrollbar and @contentWidth > clientWidthWithoutVerticalScrollbar) horizontalScrollbarHeight = if horizontalScrollbarVisible @@ -444,16 +498,23 @@ class TextEditorPresenter getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay + setFocused: (focused) -> + unless @focused is focused + @focused = focused + @updateFocusedState() + @updateHiddenInputState() + setScrollTop: (scrollTop) -> scrollTop = @constrainScrollTop(scrollTop) - unless @scrollTop is scrollTop + unless @scrollTop is scrollTop or Number.isNaN(scrollTop) @scrollTop = scrollTop @model.setScrollTop(scrollTop) @updateStartRow() @updateEndRow() @didStartScrolling() @updateVerticalScrollState() + @updateHiddenInputState() @updateDecorations() @updateLinesState() @updateCursorsState() @@ -478,11 +539,12 @@ class TextEditorPresenter setScrollLeft: (scrollLeft) -> scrollLeft = @constrainScrollLeft(scrollLeft) - unless @scrollLeft is scrollLeft + unless @scrollLeft is scrollLeft or Number.isNaN(scrollLeft) oldScrollLeft = @scrollLeft @scrollLeft = scrollLeft @model.setScrollLeft(scrollLeft) @updateHorizontalScrollState() + @updateHiddenInputState() @updateCursorsState() unless oldScrollLeft? setHorizontalScrollbarHeight: (horizontalScrollbarHeight) -> @@ -493,6 +555,7 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateScrollbarsState() @updateVerticalScrollState() + @updateHorizontalScrollState() @updateCursorsState() unless oldHorizontalScrollbarHeight? setVerticalScrollbarWidth: (verticalScrollbarWidth) -> @@ -502,6 +565,7 @@ class TextEditorPresenter @model.setVerticalScrollbarWidth(verticalScrollbarWidth) @updateScrollbarDimensions() @updateScrollbarsState() + @updateVerticalScrollState() @updateHorizontalScrollState() @updateCursorsState() unless oldVerticalScrollbarWidth? @@ -567,7 +631,10 @@ class TextEditorPresenter @updateStartRow() @updateEndRow() @updateHeightState() + @updateHorizontalScrollState() @updateVerticalScrollState() + @updateScrollbarsState() + @updateHiddenInputState() @updateDecorations() @updateLinesState() @updateCursorsState() @@ -613,6 +680,9 @@ class TextEditorPresenter @updateContentDimensions() @updateHorizontalScrollState() + @updateVerticalScrollState() + @updateScrollbarsState() + @updateHiddenInputState() @updateContentState() @updateDecorations() @updateLinesState() @@ -621,6 +691,7 @@ class TextEditorPresenter clearScopedCharacterWidths: -> @characterWidthsByScope = {} + @model.clearScopedCharWidths() hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? @@ -875,6 +946,7 @@ class TextEditorPresenter observeCursor: (cursor) -> didChangePositionDisposable = cursor.onDidChangePosition => + @updateHiddenInputState() if cursor.isLastCursor() @pauseCursorBlinking() @updateCursorsState() @@ -884,6 +956,7 @@ class TextEditorPresenter @disposables.remove(didChangePositionDisposable) @disposables.remove(didChangeVisibilityDisposable) @disposables.remove(didDestroyDisposable) + @updateHiddenInputState() @updateCursorsState() @disposables.add(didChangePositionDisposable) @@ -892,6 +965,7 @@ class TextEditorPresenter didAddCursor: (cursor) -> @observeCursor(cursor) + @updateHiddenInputState() @pauseCursorBlinking() @updateCursorsState() diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 7fceb42a8..5db4e41ce 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -126,7 +126,7 @@ class TextEditorView extends View Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView) - Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.refs.input.getDOMNode() + Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.hiddenInputComponent?.domNode Object.defineProperty @::, 'mini', get: -> @model?.isMini() Object.defineProperty @::, 'component', get: -> @element?.component diff --git a/src/view-registry.coffee b/src/view-registry.coffee index a9104af5f..4dbb5594e 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -42,9 +42,17 @@ Grim = require 'grim' # ``` module.exports = class ViewRegistry + documentPollingInterval: 200 + documentUpdateRequested: false + performDocumentPollAfterUpdate: false + pollIntervalHandle: null + constructor: -> @views = new WeakMap @providers = [] + @documentWriters = [] + @documentReaders = [] + @documentPollers = [] # Essential: Add a provider that will be used to construct views in the # workspace's view layer based on model objects in its model layer. @@ -150,3 +158,53 @@ class ViewRegistry findProvider: (object) -> find @providers, ({modelConstructor}) -> object instanceof modelConstructor + + updateDocument: (fn) -> + @documentWriters.push(fn) + @requestDocumentUpdate() + new Disposable => + @documentWriters = @documentWriters.filter (writer) -> writer isnt fn + + readDocument: (fn) -> + @documentReaders.push(fn) + @requestDocumentUpdate() + new Disposable => + @documentReaders = @documentReaders.filter (reader) -> reader isnt fn + + pollDocument: (fn) -> + @startPollingDocument() if @documentPollers.length is 0 + @documentPollers.push(fn) + new Disposable => + @documentPollers = @documentPollers.filter (poller) -> poller isnt fn + @stopPollingDocument() if @documentPollers.length is 0 + + clearDocumentRequests: -> + @documentReaders = [] + @documentWriters = [] + @documentPollers = [] + @documentUpdateRequested = false + + requestDocumentUpdate: -> + unless @documentUpdateRequested + @documentUpdateRequested = true + requestAnimationFrame(@performDocumentUpdate) + + performDocumentUpdate: => + @documentUpdateRequested = false + writer() while writer = @documentWriters.shift() + reader() while reader = @documentReaders.shift() + @performDocumentPoll() if @performDocumentPollAfterUpdate + + startPollingDocument: -> + @pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval) + + stopPollingDocument: -> + window.clearInterval(@pollIntervalHandle) + + performDocumentPoll: => + if @documentUpdateRequested + @performDocumentPollAfterUpdate = true + else + @performDocumentPollAfterUpdate = false + poller() for poller in @documentPollers + return diff --git a/static/icons.less b/static/icons.less index d42d32e79..4fe56a8c1 100644 --- a/static/icons.less +++ b/static/icons.less @@ -1,6 +1,6 @@ @import "ui-variables"; -.icon:before { +.icon::before { margin-right: @component-icon-padding; } diff --git a/static/index.js b/static/index.js index fb56a84bd..b502c1b42 100644 --- a/static/index.js +++ b/static/index.js @@ -44,11 +44,10 @@ window.onload = function() { extra: {_version: loadSettings.appVersion} }); - require('vm-compatibility-layer'); - + setupVmCompatibility(); setupCsonCache(cacheDir); setupSourceMapCache(cacheDir); - setup6to5(cacheDir); + setupBabel(cacheDir); require(loadSettings.bootstrapScript); require('ipc').sendChannel('window-command', 'window:loaded'); @@ -91,10 +90,10 @@ var setupAtomHome = function() { } } -var setup6to5 = function(cacheDir) { - var to5 = require('../src/6to5'); - to5.setCacheDirectory(path.join(cacheDir, 'js', '6to5')); - to5.register(); +var setupBabel = function(cacheDir) { + var babel = require('../src/babel'); + babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel')); + babel.register(); } var setupCsonCache = function(cacheDir) { @@ -104,3 +103,9 @@ var setupCsonCache = function(cacheDir) { var setupSourceMapCache = function(cacheDir) { require('coffeestack').setCacheDirectory(path.join(cacheDir, 'coffee', 'source-maps')); } + +var setupVmCompatibility = function() { + var vm = require('vm'); + if (!vm.Script.createContext) + vm.Script.createContext = vm.createContext; +} diff --git a/static/jasmine.less b/static/jasmine.less index e0a920302..2b2659a9d 100644 --- a/static/jasmine.less +++ b/static/jasmine.less @@ -68,7 +68,7 @@ body { color: #eee; } - &:before { + &::before { content: "\02022"; } } diff --git a/static/lists.less b/static/lists.less index d1613cda9..2e325e150 100644 --- a/static/lists.less +++ b/static/lists.less @@ -25,10 +25,10 @@ white-space: nowrap; } - // The background highlight uses :before rather than the item background so + // The background highlight uses ::before rather than the item background so // it can span the entire width of the parent container rather than the size // of the list item. - .selected:before { + .selected::before { content: ''; background-color: @background-color-selected; position: absolute; @@ -42,7 +42,7 @@ position: relative; } - .icon:before { + .icon::before { margin-right: @component-icon-padding; position: relative; top: 1px; @@ -73,7 +73,7 @@ // Nested items always get disclosure arrows .list-nested-item > .list-item { .octicon(chevron-down, @disclosure-arrow-size); - &:before{ + &::before{ position: relative; top: -1px; margin-right: @component-icon-padding; @@ -81,7 +81,7 @@ } .list-nested-item.collapsed > .list-item { .octicon(chevron-right, @disclosure-arrow-size); - &:before{ + &::before{ left: 1px; } } diff --git a/static/select-list.less b/static/select-list.less index a9c43b8fc..df95d4add 100644 --- a/static/select-list.less +++ b/static/select-list.less @@ -6,7 +6,7 @@ .loading-message { .octicon(hourglass); - &:before { + &::before { font-size: 1.1em; width: 1.1em; height: 1.1em; diff --git a/static/text-editor-light.less b/static/text-editor-light.less index 43902c97b..3ba4e8313 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -62,7 +62,7 @@ atom-text-editor { opacity: .6; padding: 0 .4em; - &:before { + &::before { text-align: center; } } @@ -84,7 +84,7 @@ atom-text-editor { visibility: visible; - &:before { + &::before { position: relative; left: -.1em; } @@ -129,7 +129,7 @@ atom-text-editor { .line { white-space: pre; - &.cursor-line .fold-marker:after { + &.cursor-line .fold-marker::after { opacity: 1; } } @@ -137,7 +137,7 @@ atom-text-editor { .fold-marker { cursor: default; - &:after { + &::after { .icon(0.8em, inline); content: @ellipsis; diff --git a/static/text-editor-shadow.less b/static/text-editor-shadow.less index 62be81739..63d27de7f 100644 --- a/static/text-editor-shadow.less +++ b/static/text-editor-shadow.less @@ -44,7 +44,7 @@ opacity: .6; padding: 0 .4em; - &:before { + &::before { text-align: center; } } @@ -66,7 +66,7 @@ visibility: visible; - &:before { + &::before { position: relative; left: -.1em; } @@ -111,7 +111,7 @@ .line { white-space: pre; - &.cursor-line .fold-marker:after { + &.cursor-line .fold-marker::after { opacity: 1; } } @@ -119,7 +119,7 @@ .fold-marker { cursor: default; - &:after { + &::after { .icon(0.8em, inline); content: @ellipsis; diff --git a/static/variables/octicon-mixins.less b/static/variables/octicon-mixins.less index e1fbe3e5f..0cb614814 100644 --- a/static/variables/octicon-mixins.less +++ b/static/variables/octicon-mixins.less @@ -18,7 +18,7 @@ .octicon(@name, @size: 16px) { @import "octicon-utf-codes.less"; - &:before { + &::before { .icon(@size); content: @@name } @@ -26,7 +26,7 @@ .mega-octicon(@name, @size: 32px) { @import "octicon-utf-codes.less"; - &:before { + &::before { .icon(@size); content: @@name }