diff --git a/CHANGELOG.md b/CHANGELOG.md index 057b8bcf3..e36b3f59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ See https://atom.io/releases +## 1.4.0 + +* Switching encoding is now fast also with large files. +* Fixed an issue where disabling and re-enabling a package caused custom keymaps to be overridden. +* Fixed restoring untitled editors on restart. The new behavior never prompts to save new/changed files when closing a window or quitting Atom. + ## 1.3.0 * The tree-view now sorts directory entries more naturally, in a locale-sensitive way. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..444ce0b4c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,24 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery +- Personal attacks +- Trolling or insulting/derogatory comments +- Public or private harassment +- Publishing other's private information, such as physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from http://contributor-covenant.org/version/1/3/0/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36de7b46c..ada420a40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ These are just guidelines, not rules, use your best judgment and feel free to pr ### Code of Conduct -This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0). +This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [atom@github.com](mailto:atom@github.com). diff --git a/README.md b/README.md index 168124ac5..7f7acb5fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https:/ Follow [@AtomEditor](https://twitter.com/atomeditor) on Twitter for important announcements. -This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0). +This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to atom@github.com. ## Documentation diff --git a/build/package.json b/build/package.json index de9053006..fd7d29d80 100644 --- a/build/package.json +++ b/build/package.json @@ -8,10 +8,11 @@ "dependencies": { "asar": "^0.8.0", "async": "~0.2.9", + "aws-sdk": "^2.2.18", "donna": "^1.0.13", "formidable": "~1.0.14", "fs-plus": "2.x", - "github-releases": "~0.3.0", + "github-releases": "~0.3.1", "glob": "^5.0.14", "grunt": "~0.4.1", "grunt-babel": "^5.0.1", diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index 54fd06022..2d9054385 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -20,9 +20,6 @@ module.exports = (grunt) -> copyFolder = path.resolve 'script', 'copy-folder.cmd' if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0 grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") - - createShortcut = path.resolve 'script', 'create-shortcut.cmd' - runas('cmd', ['/c', createShortcut, path.join(installDir, 'atom.exe'), appName]) else if process.platform is 'darwin' rm installDir mkdir path.dirname(installDir) diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index fc96121ae..4f8df6336 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -6,6 +6,7 @@ async = require 'async' fs = require 'fs-plus' GitHub = require 'github-releases' request = require 'request' +AWS = require 'aws-sdk' grunt = null @@ -210,7 +211,7 @@ deleteExistingAssets = (release, assetNames, callback) -> async.parallel(tasks, callback) uploadAssets = (release, buildDir, assets, callback) -> - upload = (release, assetName, assetPath, callback) -> + uploadToReleases = (release, assetName, assetPath, callback) -> options = uri: release.upload_url.replace(/\{.*$/, "?name=#{assetName}") method: 'POST' @@ -221,15 +222,43 @@ uploadAssets = (release, buildDir, assets, callback) -> assetRequest = request options, (error, response, body='') -> if error? or response.statusCode >= 400 - logError("Upload release asset #{assetName} failed", error, body) + logError("Upload release asset #{assetName} to Releases failed", error, body) callback(error ? new Error(response.statusCode)) else callback(null, release) fs.createReadStream(assetPath).pipe(assetRequest) + uploadToS3 = (release, assetName, assetPath, callback) -> + s3Key = process.env.BUILD_ATOM_RELEASES_S3_KEY + s3Secret = process.env.BUILD_ATOM_RELEASES_S3_SECRET + s3Bucket = process.env.BUILD_ATOM_RELEASES_S3_BUCKET + + unless s3Key and s3Secret and s3Bucket + callback(new Error('BUILD_ATOM_RELEASES_S3_KEY, BUILD_ATOM_RELEASES_S3_SECRET, and BUILD_ATOM_RELEASES_S3_BUCKET environment variables must be set.')) + return + + s3Info = + accessKeyId: s3Key + secretAccessKey: s3Secret + s3 = new AWS.S3 s3Info + + key = "releases/#{release.tag_name}/#{assetName}" + uploadParams = + Bucket: s3Bucket + ACL: 'public-read' + Key: key + Body: fs.createReadStream(assetPath) + s3.upload uploadParams, (error, data) -> + if error? + logError("Upload release asset #{assetName} to S3 failed", error) + callback(error) + else + callback(null, release) + tasks = [] for {assetName} in assets assetPath = path.join(buildDir, assetName) - tasks.push(upload.bind(this, release, assetName, assetPath)) + tasks.push(uploadToReleases.bind(this, release, assetName, assetPath)) + tasks.push(uploadToS3.bind(this, release, assetName, assetPath)) async.parallel(tasks, callback) diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee index 28abb6493..fc2382476 100644 --- a/build/tasks/set-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -29,6 +29,7 @@ module.exports = (grunt) -> return appDir = grunt.config.get('atom.appDir') + shellAppDir = grunt.config.get('atom.shellAppDir') # Replace version field of package.json. packageJsonPath = path.join(appDir, 'package.json') @@ -39,7 +40,7 @@ module.exports = (grunt) -> if process.platform is 'darwin' cmd = 'script/set-version' - args = [grunt.config.get('atom.buildDir'), version] + args = [shellAppDir, version] spawn {cmd, args}, (error, result, code) -> done(error) else if process.platform is 'win32' shellAppDir = grunt.config.get('atom.shellAppDir') diff --git a/package.json b/package.json index 79cdd4bee..82211c710 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.4.0-dev", + "version": "1.5.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -15,7 +15,7 @@ "electronVersion": "0.35.2", "dependencies": { "async": "0.2.6", - "atom-keymap": "^6.1.1", + "atom-keymap": "^6.2.0", "babel-core": "^5.8.21", "bootstrap": "^3.3.4", "cached-run-in-this-context": "0.4.0", @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "8.0.9", + "text-buffer": "8.1.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -64,10 +64,10 @@ "atom-light-ui": "0.43.0", "base16-tomorrow-dark-theme": "1.0.0", "base16-tomorrow-light-theme": "1.0.0", - "one-dark-ui": "1.1.7", + "one-dark-ui": "1.1.9", "one-dark-syntax": "1.1.1", "one-light-syntax": "1.1.1", - "one-light-ui": "1.1.7", + "one-light-ui": "1.1.9", "solarized-dark-syntax": "0.39.0", "solarized-light-syntax": "0.23.0", "about": "1.1.0", @@ -75,8 +75,8 @@ "autocomplete-atom-api": "0.9.2", "autocomplete-css": "0.11.0", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.23.1", - "autocomplete-snippets": "1.8.0", + "autocomplete-plus": "2.24.0", + "autocomplete-snippets": "1.9.0", "autoflow": "0.26.0", "autosave": "0.23.0", "background-tips": "0.26.0", @@ -87,7 +87,7 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.37.0", - "find-and-replace": "0.191.0", + "find-and-replace": "0.194.0", "fuzzy-finder": "0.93.0", "git-diff": "0.57.0", "go-to-line": "0.30.0", @@ -97,7 +97,7 @@ "keybinding-resolver": "0.33.0", "line-ending-selector": "0.3.0", "link": "0.31.0", - "markdown-preview": "0.156.2", + "markdown-preview": "0.157.0", "metrics": "0.53.1", "notifications": "0.62.1", "open-on-github": "0.40.0", @@ -116,38 +116,38 @@ "welcome": "0.33.0", "whitespace": "0.32.1", "wrap-guide": "0.38.1", - "language-c": "0.50.1", - "language-clojure": "0.18.0", - "language-coffee-script": "0.45.0", + "language-c": "0.51.1", + "language-clojure": "0.19.0", + "language-coffee-script": "0.46.0", "language-csharp": "0.11.0", - "language-css": "0.35.1", - "language-gfm": "0.81.0", - "language-git": "0.10.0", - "language-go": "0.40.0", - "language-html": "0.42.0", - "language-hyperlink": "0.15.0", + "language-css": "0.36.0", + "language-gfm": "0.82.0", + "language-git": "0.11.0", + "language-go": "0.41.0", + "language-html": "0.43.1", + "language-hyperlink": "0.16.0", "language-java": "0.17.0", - "language-javascript": "0.102.2", - "language-json": "0.17.1", + "language-javascript": "0.104.0", + "language-json": "0.17.2", "language-less": "0.29.0", - "language-make": "0.20.0", + "language-make": "0.21.0", "language-mustache": "0.13.0", - "language-objective-c": "0.15.0", - "language-perl": "0.31.0", + "language-objective-c": "0.15.1", + "language-perl": "0.32.0", "language-php": "0.34.0", "language-property-list": "0.8.0", "language-python": "0.42.1", - "language-ruby": "0.64.1", + "language-ruby": "0.65.0", "language-ruby-on-rails": "0.24.0", - "language-sass": "0.44.1", - "language-shellscript": "0.20.0", + "language-sass": "0.45.0", + "language-shellscript": "0.21.0", "language-source": "0.9.0", - "language-sql": "0.19.0", + "language-sql": "0.20.0", "language-text": "0.7.0", "language-todo": "0.27.0", - "language-toml": "0.17.0", - "language-xml": "0.34.1", - "language-yaml": "0.24.0" + "language-toml": "0.18.0", + "language-xml": "0.34.2", + "language-yaml": "0.25.0" }, "private": true, "scripts": { diff --git a/script/cibuild b/script/cibuild index b3f0b3f83..860e0a938 100755 --- a/script/cibuild +++ b/script/cibuild @@ -40,6 +40,10 @@ function setEnvironmentVariables() { process.env.CC = 'clang'; process.env.CXX = 'clang++'; process.env.npm_config_clang = '1'; + } else if (process.platform === 'win32') { + process.env.BUILD_ATOM_RELEASES_S3_KEY = process.env.BUILD_ATOM_WIN_RELEASES_S3_KEY + process.env.BUILD_ATOM_RELEASES_S3_SECRET = process.env.BUILD_ATOM_WIN_RELEASES_S3_SECRET + process.env.BUILD_ATOM_RELEASES_S3_BUCKET = process.env.BUILD_ATOM_WIN_RELEASES_S3_BUCKET } } diff --git a/script/cibuild-atom-linux b/script/cibuild-atom-linux index c4e957189..2c3395608 100755 --- a/script/cibuild-atom-linux +++ b/script/cibuild-atom-linux @@ -3,6 +3,9 @@ set -e export ATOM_ACCESS_TOKEN=$BUILD_ATOM_LINUX_ACCESS_TOKEN +export BUILD_ATOM_RELEASES_S3_KEY=$BUILD_ATOM_LINUX_RELEASES_S3_KEY +export BUILD_ATOM_RELEASES_S3_SECRET=$BUILD_ATOM_LINUX_RELEASES_S3_SECRET +export BUILD_ATOM_RELEASES_S3_BUCKET=$BUILD_ATOM_LINUX_RELEASES_S3_BUCKET if [ -d /usr/local/share/nodenv ]; then export NODENV_ROOT=/usr/local/share/nodenv diff --git a/script/cibuild-atom-rpm b/script/cibuild-atom-rpm index a861a068b..2faa89347 100755 --- a/script/cibuild-atom-rpm +++ b/script/cibuild-atom-rpm @@ -8,5 +8,8 @@ docker run \ --env JANKY_SHA1="$JANKY_SHA1" \ --env JANKY_BRANCH="$JANKY_BRANCH" \ --env ATOM_ACCESS_TOKEN="$BUILD_ATOM_RPM_ACCESS_TOKEN" \ + --env BUILD_ATOM_RELEASES_S3_KEY="$BUILD_ATOM_RPM_RELEASES_S3_KEY" \ + --env BUILD_ATOM_RELEASES_S3_SECRET="$BUILD_ATOM_RPM_RELEASES_S3_SECRET" \ + --env BUILD_ATOM_RELEASES_S3_BUCKET="$BUILD_ATOM_RPM_RELEASES_S3_BUCKET" \ atom-rpm /atom/script/rpmbuild docker rmi atom-rpm diff --git a/script/create-shortcut.cmd b/script/create-shortcut.cmd deleted file mode 100644 index ca295d434..000000000 --- a/script/create-shortcut.cmd +++ /dev/null @@ -1,23 +0,0 @@ -@echo off - -set USAGE=Usage: %0 source name-on-desktop - -if [%1] == [] ( - echo %USAGE% - exit 1 -) -if [%2] == [] ( - echo %USAGE% - exit 2 -) - -set SCRIPT="%TEMP%\%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.vbs" - -echo Set oWS = WScript.CreateObject("WScript.Shell") >> %SCRIPT% -echo sLinkFile = "%USERPROFILE%\Desktop\%2.lnk" >> %SCRIPT% -echo Set oLink = oWS.CreateShortcut(sLinkFile) >> %SCRIPT% -echo oLink.TargetPath = %1 >> %SCRIPT% -echo oLink.Save >> %SCRIPT% - -cscript /nologo %SCRIPT% -del %SCRIPT% diff --git a/script/set-version b/script/set-version index 7cad26799..33dec9d77 100755 --- a/script/set-version +++ b/script/set-version @@ -2,10 +2,11 @@ set -e -BUILT_PRODUCTS_DIR=$1 +SHELL_APP_DIR=$1 VERSION=$2 -PLIST_PATH="$BUILT_PRODUCTS_DIR/Atom.app/Contents/Info.plist" -HELPER_PLIST_PATH="$BUILT_PRODUCTS_DIR/Atom.app/Contents/Frameworks/Atom Helper.app/Contents/Info.plist" + +PLIST_PATH="$SHELL_APP_DIR/Contents/Info.plist" +HELPER_PLIST_PATH="$SHELL_APP_DIR/Contents/Frameworks/Atom Helper.app/Contents/Info.plist" # Update version /usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $VERSION" "$PLIST_PATH" diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index 8a6cc214e..848da6b49 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -81,6 +81,27 @@ describe 'CompileCache', -> waits(1) runs -> error = new Error("Oops again") - console.log error.stack expect(error.stack).toContain('compile-cache-spec.coffee') expect(Array.isArray(error.getRawStack())).toBe true + + it 'does not infinitely loop when the original prepareStackTrace value is reassigned', -> + originalPrepareStackTrace = Error.prepareStackTrace + + Error.prepareStackTrace = -> 'a-stack-trace' + Error.prepareStackTrace = originalPrepareStackTrace + + error = new Error('Oops') + expect(error.stack).toContain('compile-cache-spec.coffee') + expect(Array.isArray(error.getRawStack())).toBe true + + it 'does not infinitely loop when the assigned prepareStackTrace calls the original prepareStackTrace', -> + originalPrepareStackTrace = Error.prepareStackTrace + + Error.prepareStackTrace = (error, stack) -> + error.foo = 'bar' + originalPrepareStackTrace(error, stack) + + error = new Error('Oops') + expect(error.stack).toContain('compile-cache-spec.coffee') + expect(error.foo).toBe('bar') + expect(Array.isArray(error.getRawStack())).toBe true diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index e00cee789..eab2f6f04 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -679,6 +679,26 @@ describe "Config", -> writtenConfig = CSON.writeFileSync.argsForCall[0][1] expect(writtenConfig).toEqual '*': atom.config.settings + it 'writes properties in alphabetical order', -> + atom.config.set('foo', 1) + atom.config.set('bar', 2) + atom.config.set('baz.foo', 3) + atom.config.set('baz.bar', 4) + + CSON.writeFileSync.reset() + atom.config.save() + + expect(CSON.writeFileSync.argsForCall[0][0]).toBe atom.config.configFilePath + writtenConfig = CSON.writeFileSync.argsForCall[0][1] + expect(writtenConfig).toEqual '*': atom.config.settings + + expectedKeys = ['bar', 'baz', 'foo'] + foundKeys = (key for key of writtenConfig['*'] when key in expectedKeys) + expect(foundKeys).toEqual expectedKeys + expectedKeys = ['bar', 'foo'] + foundKeys = (key for key of writtenConfig['*']['baz'] when key in expectedKeys) + expect(foundKeys).toEqual expectedKeys + describe "when ~/.atom/config.json doesn't exist", -> it "writes any non-default properties to ~/.atom/config.cson", -> atom.config.set("a.b.c", 1) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 1872b8c65..da5f8327e 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -2,13 +2,9 @@ module.exports = class FakeLinesYardstick - constructor: (@model, @presenter) -> + constructor: (@model) -> @characterWidthsByScope = {} - prepareScreenRowsForMeasurement: -> - @presenter.getPreMeasurementState() - @screenRows = new Set(@presenter.getScreenRows()) - getScopedCharacterWidth: (scopeNames, char) -> @getScopedCharacterWidths(scopeNames)[char] @@ -35,8 +31,6 @@ class FakeLinesYardstick left = 0 column = 0 - return {top, left: 0} unless @screenRows.has(screenPosition.row) - iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() while iterator.next() characterWidths = @getScopedCharacterWidths(iterator.getScopes()) @@ -59,18 +53,3 @@ class FakeLinesYardstick column += charLength {top, left} - - pixelRectForScreenRange: (screenRange) -> - lineHeight = @model.getLineHeightInPixels() - - if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start).top - left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight - width = @presenter.getScrollWidth() - else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = lineHeight - width = @pixelPositionForScreenPosition(screenRange.end, false).left - left - - {top, left, width, height} diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-1.js b/spec/fixtures/packages/package-with-deserializers/deserializer-1.js new file mode 100644 index 000000000..f4d7a1488 --- /dev/null +++ b/spec/fixtures/packages/package-with-deserializers/deserializer-1.js @@ -0,0 +1,6 @@ +module.exports = function (state) { + return { + wasDeserializedBy: 'Deserializer1', + state: state + } +} diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-2.js b/spec/fixtures/packages/package-with-deserializers/deserializer-2.js new file mode 100644 index 000000000..3099d2b15 --- /dev/null +++ b/spec/fixtures/packages/package-with-deserializers/deserializer-2.js @@ -0,0 +1,6 @@ +module.exports = function (state) { + return { + wasDeserializedBy: 'Deserializer2', + state: state + } +} diff --git a/spec/fixtures/packages/package-with-deserializers/index.js b/spec/fixtures/packages/package-with-deserializers/index.js new file mode 100644 index 000000000..19bba5ecb --- /dev/null +++ b/spec/fixtures/packages/package-with-deserializers/index.js @@ -0,0 +1,3 @@ +module.exports = { + activate: function() {} +} diff --git a/spec/fixtures/packages/package-with-deserializers/package.json b/spec/fixtures/packages/package-with-deserializers/package.json new file mode 100644 index 000000000..daa5776bf --- /dev/null +++ b/spec/fixtures/packages/package-with-deserializers/package.json @@ -0,0 +1,9 @@ +{ + "name": "package-with-deserializers", + "version": "1.0.0", + "main": "./index", + "deserializers": { + "Deserializer1": "./deserializer-1.js", + "Deserializer2": "./deserializer-2.js" + } +} diff --git a/spec/fixtures/packages/package-with-eval-time-api-calls/index.js b/spec/fixtures/packages/package-with-eval-time-api-calls/index.js new file mode 100644 index 000000000..e16db0743 --- /dev/null +++ b/spec/fixtures/packages/package-with-eval-time-api-calls/index.js @@ -0,0 +1,5 @@ +atom.deserializers.add('MyDeserializer', function (state) { + return {state: state, a: 'b'} +}) + +exports.activate = function () {} diff --git a/spec/fixtures/packages/package-with-eval-time-api-calls/package.json b/spec/fixtures/packages/package-with-eval-time-api-calls/package.json new file mode 100644 index 000000000..7a76abb5f --- /dev/null +++ b/spec/fixtures/packages/package-with-eval-time-api-calls/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-with-eval-time-api-calls", + "version": "1.2.3", + "main": "./index" +} diff --git a/spec/fixtures/packages/package-with-json-config-schema/package.json b/spec/fixtures/packages/package-with-json-config-schema/package.json new file mode 100644 index 000000000..960d87fc1 --- /dev/null +++ b/spec/fixtures/packages/package-with-json-config-schema/package.json @@ -0,0 +1,13 @@ +{ + "name": "package-with-json-config-schema", + "configSchema": { + "a": { + "type": "number", + "default": 5 + }, + "b": { + "type": "string", + "default": "five" + } + } +} diff --git a/spec/fixtures/packages/package-with-main/package.cson b/spec/fixtures/packages/package-with-main/package.cson index e799f6ca8..75910bbcc 100644 --- a/spec/fixtures/packages/package-with-main/package.cson +++ b/spec/fixtures/packages/package-with-main/package.cson @@ -1 +1,2 @@ 'main': 'main-module.coffee' +'version': '2.3.4' diff --git a/spec/fixtures/packages/package-with-view-providers/deserializer.js b/spec/fixtures/packages/package-with-view-providers/deserializer.js new file mode 100644 index 000000000..334e7b2ab --- /dev/null +++ b/spec/fixtures/packages/package-with-view-providers/deserializer.js @@ -0,0 +1,3 @@ +module.exports = function (state) { + return {state: state} +} diff --git a/spec/fixtures/packages/package-with-view-providers/index.js b/spec/fixtures/packages/package-with-view-providers/index.js new file mode 100644 index 000000000..19bba5ecb --- /dev/null +++ b/spec/fixtures/packages/package-with-view-providers/index.js @@ -0,0 +1,3 @@ +module.exports = { + activate: function() {} +} diff --git a/spec/fixtures/packages/package-with-view-providers/package.json b/spec/fixtures/packages/package-with-view-providers/package.json new file mode 100644 index 000000000..f67477280 --- /dev/null +++ b/spec/fixtures/packages/package-with-view-providers/package.json @@ -0,0 +1,12 @@ +{ + "name": "package-with-view-providers", + "main": "./index", + "version": "1.0.0", + "deserializers": { + "DeserializerFromPackageWithViewProviders": "./deserializer" + }, + "viewProviders": [ + "./view-provider-1", + "./view-provider-2" + ] +} diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-1.js b/spec/fixtures/packages/package-with-view-providers/view-provider-1.js new file mode 100644 index 000000000..e4f0dcc0b --- /dev/null +++ b/spec/fixtures/packages/package-with-view-providers/view-provider-1.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = function (model) { + if (model.worksWithViewProvider1) { + let element = document.createElement('div') + element.dataset['createdBy'] = 'view-provider-1' + return element + } +} diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-2.js b/spec/fixtures/packages/package-with-view-providers/view-provider-2.js new file mode 100644 index 000000000..a3b58a3aa --- /dev/null +++ b/spec/fixtures/packages/package-with-view-providers/view-provider-2.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = function (model) { + if (model.worksWithViewProvider2) { + let element = document.createElement('div') + element.dataset['createdBy'] = 'view-provider-2' + return element + } +} diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index ae85a0e9d..74f5fca6a 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -2,7 +2,7 @@ LinesYardstick = require "../src/lines-yardstick" {toArray} = require 'underscore-plus' describe "LinesYardstick", -> - [editor, mockPresenter, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = [] + [editor, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = [] beforeEach -> waitsForPromise -> @@ -31,22 +31,10 @@ describe "LinesYardstick", -> createdLineNodes.push(lineNode) lineNode - mockPresenter = - setScreenRowsToMeasure: (screenRows) -> screenRowsToMeasure = screenRows - clearScreenRowsToMeasure: -> setScreenRowsToMeasure = [] - getPreMeasurementState: -> - state = {} - for screenRow in screenRowsToMeasure - tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) - state[tokenizedLine.id] = screenRow - state - mockLineNodesProvider = - updateSync: (state) -> availableScreenRows = state lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> - return if availableScreenRows[lineId] isnt screenRow - buildLineNode(screenRow) + textNodesForLineIdAndScreenRow: (lineId, screenRow) -> lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow) iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) @@ -56,7 +44,7 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, atom.grammars) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, atom.grammars) afterEach -> lineNode.remove() for lineNode in createdLineNodes @@ -153,18 +141,6 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition([0, 36]).left).toBe 237.5 expect(linesYardstick.pixelPositionForScreenPosition([0, 37]).left).toBe 244.09375 - it "doesn't measure invisible lines if it is explicitly told so", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; - font-family: monospace; - } - """ - - expect(linesYardstick.pixelPositionForScreenPosition([0, 0], true, true)).toEqual({left: 0, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([0, 1], true, true)).toEqual({left: 0, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([0, 5], true, true)).toEqual({left: 0, top: 0}) - describe "::screenPositionForPixelPosition(pixelPosition)", -> it "converts pixel positions to screen positions", -> atom.styles.addStyleSheet """ @@ -197,15 +173,3 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] - - it "doesn't measure invisible lines if it is explicitly told so", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; - font-family: monospace; - } - """ - - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 13}, true)).toEqual([0, 0]) - expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 20}, true)).toEqual([1, 0]) - expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100}, true)).toEqual([2, 0]) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 4b5f3c26d..e6848ef03 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -1,6 +1,10 @@ path = require 'path' Package = require '../src/package' +temp = require 'temp' +fs = require 'fs-plus' {Disposable} = require 'atom' +{buildKeydownEvent} = require '../src/keymap-extensions' +{mockLocalStorage} = require './spec-helper' describe "PackageManager", -> workspaceElement = null @@ -79,6 +83,121 @@ describe "PackageManager", -> expect(loadedPackage.name).toBe "package-with-main" + it "registers any deserializers specified in the package's package.json", -> + pack = atom.packages.loadPackage("package-with-deserializers") + + state1 = {deserializer: 'Deserializer1', a: 'b'} + expect(atom.deserializers.deserialize(state1)).toEqual { + wasDeserializedBy: 'Deserializer1' + state: state1 + } + + state2 = {deserializer: 'Deserializer2', c: 'd'} + expect(atom.deserializers.deserialize(state2)).toEqual { + wasDeserializedBy: 'Deserializer2' + state: state2 + } + + expect(pack.mainModule).toBeNull() + + describe "when there are view providers specified in the package's package.json", -> + model1 = {worksWithViewProvider1: true} + model2 = {worksWithViewProvider2: true} + + afterEach -> + atom.packages.deactivatePackage('package-with-view-providers') + atom.packages.unloadPackage('package-with-view-providers') + + it "does not load the view providers immediately", -> + pack = atom.packages.loadPackage("package-with-view-providers") + expect(pack.mainModule).toBeNull() + + expect(-> atom.views.getView(model1)).toThrow() + expect(-> atom.views.getView(model2)).toThrow() + + it "registers the view providers when the package is activated", -> + pack = atom.packages.loadPackage("package-with-view-providers") + + waitsForPromise -> + atom.packages.activatePackage("package-with-view-providers").then -> + element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe true + expect(element1.dataset.createdBy).toBe 'view-provider-1' + + element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe true + expect(element2.dataset.createdBy).toBe 'view-provider-2' + + it "registers the view providers when any of the package's deserializers are used", -> + pack = atom.packages.loadPackage("package-with-view-providers") + + spyOn(atom.views, 'addViewProvider').andCallThrough() + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe 2 + + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe 2 + + element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe true + expect(element1.dataset.createdBy).toBe 'view-provider-1' + + element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe true + expect(element2.dataset.createdBy).toBe 'view-provider-2' + + it "registers the config schema in the package's metadata, if present", -> + pack = atom.packages.loadPackage("package-with-json-config-schema") + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { + type: 'object' + properties: { + a: {type: 'number', default: 5} + b: {type: 'string', default: 'five'} + } + } + + expect(pack.mainModule).toBeNull() + + atom.packages.unloadPackage('package-with-json-config-schema') + atom.config.clear() + + pack = atom.packages.loadPackage("package-with-json-config-schema") + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { + type: 'object' + properties: { + a: {type: 'number', default: 5} + b: {type: 'string', default: 'five'} + } + } + + describe "when a package does not have deserializers, view providers or a config schema in its package.json", -> + beforeEach -> + mockLocalStorage() + + it "defers loading the package's main module if the package previously used no Atom APIs when its main module was required", -> + pack1 = atom.packages.loadPackage('package-with-main') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-main') + + pack2 = atom.packages.loadPackage('package-with-main') + expect(pack2.mainModule).toBeNull() + + it "does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", -> + pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-eval-time-api-calls') + + pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack2.mainModule).not.toBeNull() + describe "::unloadPackage(name)", -> describe "when the package is active", -> it "throws an error", -> @@ -456,6 +575,54 @@ describe "PackageManager", -> atom.config.set("core.packagesWithKeymapsDisabled", []) expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1' + describe "when the package is de-activated and re-activated", -> + [element, events, userKeymapPath] = [] + + beforeEach -> + userKeymapPath = path.join(temp.path(), "user-keymaps.cson") + spyOn(atom.keymaps, "getUserKeymapPath").andReturn(userKeymapPath) + + element = createTestElement('test-1') + jasmine.attachToDOM(element) + + events = [] + element.addEventListener 'user-command', (e) -> events.push(e) + element.addEventListener 'test-1', (e) -> events.push(e) + + afterEach -> + element.remove() + + # Avoid leaking user keymap subscription + atom.keymaps.watchSubscriptions[userKeymapPath].dispose() + delete atom.keymaps.watchSubscriptions[userKeymapPath] + + it "doesn't override user-defined keymaps", -> + fs.writeFileSync userKeymapPath, """ + ".test-1": + "ctrl-z": "user-command" + """ + atom.keymaps.loadUserKeymap() + + waitsForPromise -> + atom.packages.activatePackage("package-with-keymaps") + + runs -> + atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) + + expect(events.length).toBe(1) + expect(events[0].type).toBe("user-command") + + atom.packages.deactivatePackage("package-with-keymaps") + + waitsForPromise -> + atom.packages.activatePackage("package-with-keymaps") + + runs -> + atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) + + expect(events.length).toBe(2) + expect(events[1].type).toBe("user-command") + describe "menu loading", -> beforeEach -> atom.contextMenu.definitions = [] diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 63a80a7db..92218e749 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -1,6 +1,7 @@ path = require 'path' Package = require '../src/package' ThemePackage = require '../src/theme-package' +{mockLocalStorage} = require './spec-helper' describe "Package", -> build = (constructor, path) -> @@ -10,6 +11,7 @@ describe "Package", -> keymapManager: atom.keymaps, commandRegistry: atom.command, grammarRegistry: atom.grammars, themeManager: atom.themes, menuManager: atom.menu, contextMenuManager: atom.contextMenu, + deserializerManager: atom.deserializers, viewRegistry: atom.views, devMode: false ) @@ -19,10 +21,7 @@ describe "Package", -> describe "when the package contains incompatible native modules", -> beforeEach -> - items = {} - spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item; undefined - spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null - spyOn(global.localStorage, 'removeItem').andCallFake (key) -> delete items[key]; undefined + mockLocalStorage() it "does not activate it", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') @@ -54,10 +53,7 @@ describe "Package", -> describe "::rebuild()", -> beforeEach -> - items = {} - spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item; undefined - spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null - spyOn(global.localStorage, 'removeItem').andCallFake (key) -> delete items[key]; undefined + mockLocalStorage() it "returns a promise resolving to the results of `apm rebuild`", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-index') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index f31c67298..67883511b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -265,3 +265,9 @@ window.advanceClock = (delta=1) -> true callback() for callback in callbacks + +exports.mockLocalStorage = -> + items = {} + spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item.toString(); undefined + spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null + spyOn(global.localStorage, 'removeItem').andCallFake (key) -> delete items[key]; undefined diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index c6062d0ec..19f193917 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -484,7 +484,7 @@ describe('TextEditorComponent', function () { it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () { editor.setText('let\n') await nextViewUpdatePromise() - expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') + expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') }) it('displays trailing carriage returns using a visible, non-empty value', async function () { diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 8dd34fde8..effa579b1 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -9,7 +9,7 @@ FakeLinesYardstick = require './fake-lines-yardstick' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. - describe "::getState()", -> + describe "::get(Pre|Post)MeasurementState()", -> [buffer, editor] = [] beforeEach -> @@ -25,6 +25,10 @@ describe "TextEditorPresenter", -> editor.destroy() buffer.destroy() + getState = (presenter) -> + presenter.getPreMeasurementState() + presenter.getPostMeasurementState() + buildPresenterWithoutMeasurements = (params={}) -> _.defaults params, model: editor @@ -279,7 +283,7 @@ describe "TextEditorPresenter", -> describe "during state retrieval", -> it "does not trigger onDidUpdateState events", -> presenter = buildPresenter() - expectNoStateUpdate presenter, -> presenter.getState() + expectNoStateUpdate presenter, -> getState(presenter) describe ".horizontalScrollbar", -> describe ".visible", -> @@ -292,19 +296,19 @@ describe "TextEditorPresenter", -> horizontalScrollbarHeight: 10 verticalScrollbarWidth: 10 - expect(presenter.getState().horizontalScrollbar.visible).toBe false + expect(getState(presenter).horizontalScrollbar.visible).toBe false # ::contentFrameWidth itself is smaller than scrollWidth presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(presenter.getState().horizontalScrollbar.visible).toBe true + expect(getState(presenter).horizontalScrollbar.visible).toBe true # restore... presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10 + 1) - expect(presenter.getState().horizontalScrollbar.visible).toBe false + expect(getState(presenter).horizontalScrollbar.visible).toBe false # visible vertical scrollbar makes the clientWidth smaller than the scrollWidth presenter.setExplicitHeight((editor.getLineCount() * 10) - 1) - expect(presenter.getState().horizontalScrollbar.visible).toBe true + expect(getState(presenter).horizontalScrollbar.visible).toBe true it "is false if the editor is mini", -> presenter = buildPresenter @@ -312,18 +316,18 @@ describe "TextEditorPresenter", -> contentFrameWidth: editor.getMaxScreenLineLength() * 10 - 10 baseCharacterWidth: 10 - expect(presenter.getState().horizontalScrollbar.visible).toBe true + expect(getState(presenter).horizontalScrollbar.visible).toBe true editor.setMini(true) - expect(presenter.getState().horizontalScrollbar.visible).toBe false + expect(getState(presenter).horizontalScrollbar.visible).toBe false editor.setMini(false) - expect(presenter.getState().horizontalScrollbar.visible).toBe true + expect(getState(presenter).horizontalScrollbar.visible).toBe true describe ".height", -> it "tracks the value of ::horizontalScrollbarHeight", -> presenter = buildPresenter(horizontalScrollbarHeight: 10) - expect(presenter.getState().horizontalScrollbar.height).toBe 10 + expect(getState(presenter).horizontalScrollbar.height).toBe 10 expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(20) - expect(presenter.getState().horizontalScrollbar.height).toBe 20 + expect(getState(presenter).horizontalScrollbar.height).toBe 20 describe ".right", -> it "is ::verticalScrollbarWidth if the vertical scrollbar is visible and 0 otherwise", -> @@ -335,27 +339,27 @@ describe "TextEditorPresenter", -> horizontalScrollbarHeight: 10 verticalScrollbarWidth: 10 - expect(presenter.getState().horizontalScrollbar.right).toBe 0 + expect(getState(presenter).horizontalScrollbar.right).toBe 0 presenter.setExplicitHeight((editor.getLineCount() * 10) - 1) - expect(presenter.getState().horizontalScrollbar.right).toBe 10 + expect(getState(presenter).horizontalScrollbar.right).toBe 10 describe ".scrollWidth", -> it "is initialized as the max of the ::contentFrameWidth and the width of the longest line", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 presenter = buildPresenter(contentFrameWidth: 10 * maxLineLength + 20, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 it "updates when the ::contentFrameWidth changes", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 it "updates when character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -364,72 +368,72 @@ describe "TextEditorPresenter", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) presenter.characterWidthsChanged() - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 expectStateUpdate presenter, -> editor.setSoftWrapped(true) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe presenter.clientWidth expectStateUpdate presenter, -> editor.setSoftWrapped(false) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 it "updates when the longest line changes", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 expectStateUpdate presenter, -> editor.setCursorBufferPosition([editor.getLongestScreenRow(), 0]) expectStateUpdate presenter, -> editor.insertText('xyz') - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 describe ".scrollLeft", -> it "tracks the value of ::scrollLeft", -> presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe 10 + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 10 expectStateUpdate presenter, -> presenter.setScrollLeft(50) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe 50 + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 50 it "never exceeds the computed scrollWidth minus the computed clientWidth", -> presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, explicitHeight: 100, contentFrameWidth: 500) expectStateUpdate presenter, -> presenter.setScrollLeft(300) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> presenter.setContentFrameWidth(600) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(5) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> editor.getBuffer().delete([[6, 0], [6, Infinity]]) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollLeftBefore = presenter.getState().horizontalScrollbar.scrollLeft + scrollLeftBefore = getState(presenter).horizontalScrollbar.scrollLeft expectStateUpdate presenter, -> editor.getBuffer().insert([6, 0], new Array(100).join('x')) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe scrollLeftBefore + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe scrollLeftBefore it "never goes negative", -> presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expectStateUpdate presenter, -> presenter.setScrollLeft(-300) - expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe 0 + expect(getState(presenter).horizontalScrollbar.scrollLeft).toBe 0 it "is always 0 when soft wrapping is enabled", -> presenter = buildPresenter(scrollLeft: 0, verticalScrollbarWidth: 0, contentFrameWidth: 85, baseCharacterWidth: 10) editor.setSoftWrapped(false) presenter.setScrollLeft(Infinity) - expect(presenter.getState().content.scrollLeft).toBeGreaterThan 0 + expect(getState(presenter).content.scrollLeft).toBeGreaterThan 0 editor.setSoftWrapped(true) - expect(presenter.getState().content.scrollLeft).toBe 0 + expect(getState(presenter).content.scrollLeft).toBe 0 presenter.setScrollLeft(10) - expect(presenter.getState().content.scrollLeft).toBe 0 + expect(getState(presenter).content.scrollLeft).toBe 0 describe ".verticalScrollbar", -> describe ".visible", -> @@ -443,26 +447,26 @@ describe "TextEditorPresenter", -> horizontalScrollbarHeight: 10 verticalScrollbarWidth: 10 - expect(presenter.getState().verticalScrollbar.visible).toBe false + expect(getState(presenter).verticalScrollbar.visible).toBe false # ::explicitHeight itself is smaller than scrollWidth presenter.setExplicitHeight(editor.getLineCount() * 10 - 1) - expect(presenter.getState().verticalScrollbar.visible).toBe true + expect(getState(presenter).verticalScrollbar.visible).toBe true # restore... presenter.setExplicitHeight(editor.getLineCount() * 10) - expect(presenter.getState().verticalScrollbar.visible).toBe false + expect(getState(presenter).verticalScrollbar.visible).toBe false # visible horizontal scrollbar makes the clientHeight smaller than the scrollHeight presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(presenter.getState().verticalScrollbar.visible).toBe true + expect(getState(presenter).verticalScrollbar.visible).toBe true describe ".width", -> it "is assigned based on ::verticalScrollbarWidth", -> presenter = buildPresenter(verticalScrollbarWidth: 10) - expect(presenter.getState().verticalScrollbar.width).toBe 10 + expect(getState(presenter).verticalScrollbar.width).toBe 10 expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(20) - expect(presenter.getState().verticalScrollbar.width).toBe 20 + expect(getState(presenter).verticalScrollbar.width).toBe 20 describe ".bottom", -> it "is ::horizontalScrollbarHeight if the horizontal scrollbar is visible and 0 otherwise", -> @@ -474,129 +478,129 @@ describe "TextEditorPresenter", -> horizontalScrollbarHeight: 10 verticalScrollbarWidth: 10 - expect(presenter.getState().verticalScrollbar.bottom).toBe 0 + expect(getState(presenter).verticalScrollbar.bottom).toBe 0 presenter.setContentFrameWidth(editor.getMaxScreenLineLength() * 10) - expect(presenter.getState().verticalScrollbar.bottom).toBe 10 + expect(getState(presenter).verticalScrollbar.bottom).toBe 10 describe ".scrollHeight", -> it "is initialized based on the lineHeight, the number of lines, and the height", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe 500 + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500 it "updates when the ::lineHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 20 + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 20 it "updates when the line count changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe editor.getScreenLineCount() * 10 it "updates when ::explicitHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe 500 + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500 it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe presenter.contentHeight + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().verticalScrollbar.scrollHeight).toBe presenter.contentHeight + expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight describe ".scrollTop", -> it "tracks the value of ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 20, horizontalScrollbarHeight: 10) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe 10 + expect(getState(presenter).verticalScrollbar.scrollTop).toBe 10 expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe 50 + expect(getState(presenter).verticalScrollbar.scrollTop).toBe 50 it "never exceeds the computed scrollHeight minus the computed clientHeight", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop + scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(presenter.getState().verticalScrollbar.scrollTop).toBe scrollTopBefore + expect(getState(presenter).verticalScrollbar.scrollTop).toBe scrollTopBefore it "never goes negative", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe 0 + expect(getState(presenter).verticalScrollbar.scrollTop).toBe 0 it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight atom.config.set("editor.scrollPastEnd", true) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) + expect(getState(presenter).verticalScrollbar.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().verticalScrollbar.scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getState(presenter).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.getState().hiddenInput, {top: 0, left: 0} + expectValues getState(presenter).hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> presenter.setFocused(true) - expectValues presenter.getState().hiddenInput, {top: 3 * 10, left: 6 * 10} + expectValues getState(presenter).hiddenInput, {top: 3 * 10, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollTop(15) - expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} + expectValues getState(presenter).hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollLeft(35) - expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} + expectValues getState(presenter).hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} expectStateUpdate presenter, -> presenter.setScrollTop(40) - expectValues presenter.getState().hiddenInput, {top: 0, left: (6 * 10) - 35} + expectValues getState(presenter).hiddenInput, {top: 0, left: (6 * 10) - 35} expectStateUpdate presenter, -> presenter.setScrollLeft(70) - expectValues presenter.getState().hiddenInput, {top: 0, left: 0} + expectValues getState(presenter).hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues presenter.getState().hiddenInput, {top: 11 * 10 - presenter.getScrollTop(), left: 43 * 10 - presenter.getScrollLeft()} + expectValues getState(presenter).hiddenInput, {top: 11 * 10 - presenter.getScrollTop(), left: 43 * 10 - presenter.getScrollLeft()} newCursor = null expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues presenter.getState().hiddenInput, {top: (6 * 10) - presenter.getScrollTop(), left: (10 * 10) - presenter.getScrollLeft()} + expectValues getState(presenter).hiddenInput, {top: (6 * 10) - presenter.getScrollTop(), left: (10 * 10) - presenter.getScrollLeft()} expectStateUpdate presenter, -> newCursor.destroy() - expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} + expectValues getState(presenter).hiddenInput, {top: 50 - 10, left: 300 - 10} expectStateUpdate presenter, -> presenter.setFocused(false) - expectValues presenter.getState().hiddenInput, {top: 0, left: 0} + expectValues getState(presenter).hiddenInput, {top: 0, left: 0} describe ".height", -> it "is assigned based on the line height", -> presenter = buildPresenter() - expect(presenter.getState().hiddenInput.height).toBe 10 + expect(getState(presenter).hiddenInput.height).toBe 10 expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(presenter.getState().hiddenInput.height).toBe 20 + expect(getState(presenter).hiddenInput.height).toBe 20 describe ".width", -> it "is assigned based on the width of the character following the cursor", -> @@ -605,38 +609,38 @@ describe "TextEditorPresenter", -> runs -> editor.setCursorBufferPosition([3, 6]) presenter = buildPresenter() - expect(presenter.getState().hiddenInput.width).toBe 10 + expect(getState(presenter).hiddenInput.width).toBe 10 expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().hiddenInput.width).toBe 15 + expect(getState(presenter).hiddenInput.width).toBe 15 expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) presenter.characterWidthsChanged() - expect(presenter.getState().hiddenInput.width).toBe 20 + expect(getState(presenter).hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> presenter = buildPresenter() editor.setCursorBufferPosition([3, Infinity]) - expect(presenter.getState().hiddenInput.width).toBe 2 + expect(getState(presenter).hiddenInput.width).toBe 2 describe ".content", -> describe ".scrollingVertically", -> it "is true for ::stoppedScrollingDelay milliseconds following a changes to ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, stoppedScrollingDelay: 200, explicitHeight: 100) - expect(presenter.getState().content.scrollingVertically).toBe true + expect(getState(presenter).content.scrollingVertically).toBe true advanceClock(300) - expect(presenter.getState().content.scrollingVertically).toBe false + expect(getState(presenter).content.scrollingVertically).toBe false expectStateUpdate presenter, -> presenter.setScrollTop(0) - expect(presenter.getState().content.scrollingVertically).toBe true + expect(getState(presenter).content.scrollingVertically).toBe true advanceClock(100) - expect(presenter.getState().content.scrollingVertically).toBe true + expect(getState(presenter).content.scrollingVertically).toBe true presenter.setScrollTop(10) - presenter.getState() # commits scroll position + getState(presenter) # commits scroll position advanceClock(100) - expect(presenter.getState().content.scrollingVertically).toBe true + expect(getState(presenter).content.scrollingVertically).toBe true expectStateUpdate presenter, -> advanceClock(100) - expect(presenter.getState().content.scrollingVertically).toBe false + expect(getState(presenter).content.scrollingVertically).toBe false describe ".maxHeight", -> it "changes based on boundingClientRect", -> @@ -644,55 +648,55 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setBoundingClientRect(left: 0, top: 0, height: 20, width: 0) - expect(presenter.getState().content.maxHeight).toBe(20) + expect(getState(presenter).content.maxHeight).toBe(20) expectStateUpdate presenter, -> presenter.setBoundingClientRect(left: 0, top: 0, height: 50, width: 0) - expect(presenter.getState().content.maxHeight).toBe(50) + expect(getState(presenter).content.maxHeight).toBe(50) describe ".scrollHeight", -> it "is initialized based on the lineHeight, the number of lines, and the height", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) - expect(presenter.getState().content.scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 10 presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 500) - expect(presenter.getState().content.scrollHeight).toBe 500 + expect(getState(presenter).content.scrollHeight).toBe 500 it "updates when the ::lineHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(presenter.getState().content.scrollHeight).toBe editor.getScreenLineCount() * 20 + expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 20 it "updates when the line count changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(presenter.getState().content.scrollHeight).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).content.scrollHeight).toBe editor.getScreenLineCount() * 10 it "updates when ::explicitHeight changes", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) expectStateUpdate presenter, -> presenter.setExplicitHeight(500) - expect(presenter.getState().content.scrollHeight).toBe 500 + expect(getState(presenter).content.scrollHeight).toBe 500 it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().content.scrollHeight).toBe presenter.contentHeight + expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true) - expect(presenter.getState().content.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) + expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3) expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().content.scrollHeight).toBe presenter.contentHeight + expect(getState(presenter).content.scrollHeight).toBe presenter.contentHeight describe ".scrollWidth", -> it "is initialized as the max of the computed clientWidth and the width of the longest line", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(explicitHeight: 100, contentFrameWidth: 50, baseCharacterWidth: 10, verticalScrollbarWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 presenter = buildPresenter(explicitHeight: 100, contentFrameWidth: 10 * maxLineLength + 20, baseCharacterWidth: 10, verticalScrollbarWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 - 10 # subtract vertical scrollbar width + expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 20 - 10 # subtract vertical scrollbar width describe "when the longest screen row is the first one and it's hidden", -> it "doesn't compute an invalid value (regression)", -> @@ -706,15 +710,15 @@ describe "TextEditorPresenter", -> """ expectStateUpdate presenter, -> presenter.setScrollTop(40) - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 it "updates when the ::contentFrameWidth changes", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 + expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 20 it "updates when character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -723,109 +727,109 @@ describe "TextEditorPresenter", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) presenter.characterWidthsChanged() - expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expect(getState(presenter).content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 expectStateUpdate presenter, -> editor.setSoftWrapped(true) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe presenter.clientWidth + expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe presenter.clientWidth expectStateUpdate presenter, -> editor.setSoftWrapped(false) - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 it "updates when the longest line changes", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 expectStateUpdate presenter, -> editor.setCursorBufferPosition([editor.getLongestScreenRow(), 0]) expectStateUpdate presenter, -> editor.insertText('xyz') - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 it "isn't clipped to 0 when the longest line is folded (regression)", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) editor.foldBufferRow(0) - expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 + expect(getState(presenter).content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 describe ".scrollTop", -> it "doesn't get stuck when repeatedly setting the same non-integer position in a scroll event listener", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) - expect(presenter.getState().content.scrollTop).toBe(0) + expect(getState(presenter).content.scrollTop).toBe(0) presenter.onDidChangeScrollTop -> presenter.setScrollTop(1.5) - presenter.getState() # trigger scroll update + getState(presenter) # trigger scroll update presenter.setScrollTop(1.5) - presenter.getState() # trigger scroll update + getState(presenter) # trigger scroll update expect(presenter.getScrollTop()).toBe(2) expect(presenter.getRealScrollTop()).toBe(1.5) it "changes based on the scroll operation that was performed last", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) - expect(presenter.getState().content.scrollTop).toBe(0) + expect(getState(presenter).content.scrollTop).toBe(0) presenter.setScrollTop(20) editor.setCursorBufferPosition([5, 0]) - expect(presenter.getState().content.scrollTop).toBe(50) + expect(getState(presenter).content.scrollTop).toBe(50) editor.setCursorBufferPosition([8, 0]) presenter.setScrollTop(10) - expect(presenter.getState().content.scrollTop).toBe(10) + expect(getState(presenter).content.scrollTop).toBe(10) it "corresponds to the passed logical coordinates when building the presenter", -> editor.setFirstVisibleScreenRow(4) presenter = buildPresenter(lineHeight: 10, explicitHeight: 20) - expect(presenter.getState().content.scrollTop).toBe(40) + expect(getState(presenter).content.scrollTop).toBe(40) it "tracks the value of ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) - expect(presenter.getState().content.scrollTop).toBe 10 + expect(getState(presenter).content.scrollTop).toBe 10 expectStateUpdate presenter, -> presenter.setScrollTop(50) - expect(presenter.getState().content.scrollTop).toBe 50 + expect(getState(presenter).content.scrollTop).toBe 50 it "keeps the model up to date with the corresponding logical coordinates", -> presenter = buildPresenter(scrollTop: 0, explicitHeight: 20, horizontalScrollbarHeight: 10, lineHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(50) - presenter.getState() # commits scroll position + getState(presenter) # commits scroll position expect(editor.getFirstVisibleScreenRow()).toBe 5 expectStateUpdate presenter, -> presenter.setScrollTop(57) - presenter.getState() # commits scroll position + getState(presenter) # commits scroll position expect(editor.getFirstVisibleScreenRow()).toBe 6 it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> presenter = buildPresenter(scrollTop: 80, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 0) - expect(presenter.getState().content.scrollTop).toBe(80) + expect(getState(presenter).content.scrollTop).toBe(80) buffer.deleteRows(10, 9, 8) - expect(presenter.getState().content.scrollTop).toBe(60) + expect(getState(presenter).content.scrollTop).toBe(60) it "is always rounded to the nearest integer", -> presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) - expect(presenter.getState().content.scrollTop).toBe 10 + expect(getState(presenter).content.scrollTop).toBe 10 expectStateUpdate presenter, -> presenter.setScrollTop(11.4) - expect(presenter.getState().content.scrollTop).toBe 11 + expect(getState(presenter).content.scrollTop).toBe 11 expectStateUpdate presenter, -> presenter.setScrollTop(12.6) - expect(presenter.getState().content.scrollTop).toBe 13 + expect(getState(presenter).content.scrollTop).toBe 13 it "scrolls down automatically when the model is changed", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) editor.setText("") editor.insertNewline() - expect(presenter.getState().content.scrollTop).toBe(0) + expect(getState(presenter).content.scrollTop).toBe(0) editor.insertNewline() - expect(presenter.getState().content.scrollTop).toBe(10) + expect(getState(presenter).content.scrollTop).toBe(10) it "never exceeds the computed scroll height minus the computed client height", -> didChangeScrollTopSpy = jasmine.createSpy() @@ -833,111 +837,111 @@ describe "TextEditorPresenter", -> presenter.onDidChangeScrollTop(didChangeScrollTopSpy) expectStateUpdate presenter, -> presenter.setScrollTop(100) - expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight didChangeScrollTopSpy.reset() expectStateUpdate presenter, -> presenter.setExplicitHeight(60) - expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight didChangeScrollTopSpy.reset() expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) - expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight didChangeScrollTopSpy.reset() expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) - expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expect(didChangeScrollTopSpy).toHaveBeenCalledWith presenter.scrollHeight - presenter.clientHeight # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop + scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop didChangeScrollTopSpy.reset() expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') - expect(presenter.getState().content.scrollTop).toBe scrollTopBefore + expect(getState(presenter).content.scrollTop).toBe scrollTopBefore expect(presenter.getRealScrollTop()).toBe scrollTopBefore expect(didChangeScrollTopSpy).not.toHaveBeenCalled() it "never goes negative", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(-100) - expect(presenter.getState().content.scrollTop).toBe 0 + expect(getState(presenter).content.scrollTop).toBe 0 it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight atom.config.set("editor.scrollPastEnd", true) expectStateUpdate presenter, -> presenter.setScrollTop(300) - expect(presenter.getState().content.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) + expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - (presenter.lineHeight * 3) expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false) - expect(presenter.getState().content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight + expect(getState(presenter).content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight describe ".scrollLeft", -> it "doesn't get stuck when repeatedly setting the same non-integer position in a scroll event listener", -> presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 10) - expect(presenter.getState().content.scrollLeft).toBe(0) + expect(getState(presenter).content.scrollLeft).toBe(0) presenter.onDidChangeScrollLeft -> presenter.setScrollLeft(1.5) - presenter.getState() # trigger scroll update + getState(presenter) # trigger scroll update presenter.setScrollLeft(1.5) - presenter.getState() # trigger scroll update + getState(presenter) # trigger scroll update expect(presenter.getScrollLeft()).toBe(2) expect(presenter.getRealScrollLeft()).toBe(1.5) it "changes based on the scroll operation that was performed last", -> presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 10) - expect(presenter.getState().content.scrollLeft).toBe(0) + expect(getState(presenter).content.scrollLeft).toBe(0) presenter.setScrollLeft(20) editor.setCursorBufferPosition([0, 9]) - expect(presenter.getState().content.scrollLeft).toBe(90) + expect(getState(presenter).content.scrollLeft).toBe(90) editor.setCursorBufferPosition([0, 18]) presenter.setScrollLeft(50) - expect(presenter.getState().content.scrollLeft).toBe(50) + expect(getState(presenter).content.scrollLeft).toBe(50) it "corresponds to the passed logical coordinates when building the presenter", -> editor.setFirstVisibleScreenColumn(3) presenter = buildPresenter(lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(presenter.getState().content.scrollLeft).toBe(30) + expect(getState(presenter).content.scrollLeft).toBe(30) it "tracks the value of ::scrollLeft", -> presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(presenter.getState().content.scrollLeft).toBe 10 + expect(getState(presenter).content.scrollLeft).toBe 10 expectStateUpdate presenter, -> presenter.setScrollLeft(50) - expect(presenter.getState().content.scrollLeft).toBe 50 + expect(getState(presenter).content.scrollLeft).toBe 50 it "keeps the model up to date with the corresponding logical coordinates", -> presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expectStateUpdate presenter, -> presenter.setScrollLeft(50) - presenter.getState() # commits scroll position + getState(presenter) # commits scroll position expect(editor.getFirstVisibleScreenColumn()).toBe 5 expectStateUpdate presenter, -> presenter.setScrollLeft(57) - presenter.getState() # commits scroll position + getState(presenter) # commits scroll position expect(editor.getFirstVisibleScreenColumn()).toBe 6 it "is always rounded to the nearest integer", -> presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) - expect(presenter.getState().content.scrollLeft).toBe 10 + expect(getState(presenter).content.scrollLeft).toBe 10 expectStateUpdate presenter, -> presenter.setScrollLeft(11.4) - expect(presenter.getState().content.scrollLeft).toBe 11 + expect(getState(presenter).content.scrollLeft).toBe 11 expectStateUpdate presenter, -> presenter.setScrollLeft(12.6) - expect(presenter.getState().content.scrollLeft).toBe 13 + expect(getState(presenter).content.scrollLeft).toBe 13 it "never exceeds the computed scrollWidth minus the computed clientWidth", -> didChangeScrollLeftSpy = jasmine.createSpy() @@ -945,65 +949,65 @@ describe "TextEditorPresenter", -> presenter.onDidChangeScrollLeft(didChangeScrollLeftSpy) expectStateUpdate presenter, -> presenter.setScrollLeft(300) - expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth didChangeScrollLeftSpy.reset() expectStateUpdate presenter, -> presenter.setContentFrameWidth(600) - expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth didChangeScrollLeftSpy.reset() expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(5) - expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth didChangeScrollLeftSpy.reset() expectStateUpdate presenter, -> editor.getBuffer().delete([[6, 0], [6, Infinity]]) - expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(getState(presenter).content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expect(didChangeScrollLeftSpy).toHaveBeenCalledWith presenter.scrollWidth - presenter.clientWidth # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollLeftBefore = presenter.getState().content.scrollLeft + scrollLeftBefore = getState(presenter).content.scrollLeft didChangeScrollLeftSpy.reset() expectStateUpdate presenter, -> editor.getBuffer().insert([6, 0], new Array(100).join('x')) - expect(presenter.getState().content.scrollLeft).toBe scrollLeftBefore + expect(getState(presenter).content.scrollLeft).toBe scrollLeftBefore expect(presenter.getRealScrollLeft()).toBe scrollLeftBefore expect(didChangeScrollLeftSpy).not.toHaveBeenCalled() it "never goes negative", -> presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expectStateUpdate presenter, -> presenter.setScrollLeft(-300) - expect(presenter.getState().content.scrollLeft).toBe 0 + expect(getState(presenter).content.scrollLeft).toBe 0 describe ".indentGuidesVisible", -> it "is initialized based on the editor.showIndentGuide config setting", -> presenter = buildPresenter() - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false atom.config.set('editor.showIndentGuide', true) presenter = buildPresenter() - expect(presenter.getState().content.indentGuidesVisible).toBe true + expect(getState(presenter).content.indentGuidesVisible).toBe true it "updates when the editor.showIndentGuide config setting changes", -> presenter = buildPresenter() - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true) - expect(presenter.getState().content.indentGuidesVisible).toBe true + expect(getState(presenter).content.indentGuidesVisible).toBe true expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false) - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false it "updates when the editor's grammar changes", -> atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js") presenter = buildPresenter() - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false stateUpdated = false presenter.onDidUpdateState -> stateUpdated = true @@ -1012,65 +1016,65 @@ describe "TextEditorPresenter", -> runs -> expect(stateUpdated).toBe true - expect(presenter.getState().content.indentGuidesVisible).toBe true + expect(getState(presenter).content.indentGuidesVisible).toBe true expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt')) - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false it "is always false when the editor is mini", -> atom.config.set('editor.showIndentGuide', true) editor.setMini(true) presenter = buildPresenter() - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false editor.setMini(false) - expect(presenter.getState().content.indentGuidesVisible).toBe true + expect(getState(presenter).content.indentGuidesVisible).toBe true editor.setMini(true) - expect(presenter.getState().content.indentGuidesVisible).toBe false + expect(getState(presenter).content.indentGuidesVisible).toBe false describe ".backgroundColor", -> it "is assigned to ::backgroundColor unless the editor is mini", -> presenter = buildPresenter() presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' + expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' editor.setMini(true) presenter = buildPresenter() presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(presenter.getState().content.backgroundColor).toBeNull() + expect(getState(presenter).content.backgroundColor).toBeNull() it "updates when ::backgroundColor changes", -> presenter = buildPresenter() presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' + expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' expectStateUpdate presenter, -> presenter.setBackgroundColor('rgba(0, 0, 255, 0)') - expect(presenter.getState().content.backgroundColor).toBe 'rgba(0, 0, 255, 0)' + expect(getState(presenter).content.backgroundColor).toBe 'rgba(0, 0, 255, 0)' it "updates when ::mini changes", -> presenter = buildPresenter() presenter.setBackgroundColor('rgba(255, 0, 0, 0)') - expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' + expect(getState(presenter).content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' expectStateUpdate presenter, -> editor.setMini(true) - expect(presenter.getState().content.backgroundColor).toBeNull() + expect(getState(presenter).content.backgroundColor).toBeNull() describe ".placeholderText", -> it "is present when the editor has no text", -> editor.setPlaceholderText("the-placeholder-text") presenter = buildPresenter() - expect(presenter.getState().content.placeholderText).toBeNull() + expect(getState(presenter).content.placeholderText).toBeNull() expectStateUpdate presenter, -> editor.setText("") - expect(presenter.getState().content.placeholderText).toBe "the-placeholder-text" + expect(getState(presenter).content.placeholderText).toBe "the-placeholder-text" expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text") - expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" + expect(getState(presenter).content.placeholderText).toBe "new-placeholder-text" describe ".tiles", -> lineStateForScreenRow = (presenter, row) -> lineId = presenter.model.tokenizedLineForScreenRow(row).id tileRow = presenter.tileForRow(row) - presenter.getState().content.tiles[tileRow]?.lines[lineId] + getState(presenter).content.tiles[tileRow]?.lines[lineId] - tiledContentContract (presenter) -> presenter.getState().content + tiledContentContract (presenter) -> getState(presenter).content describe "[tileId].lines[lineId]", -> # line state objects it "includes the state for visible lines in a tile", -> @@ -1330,7 +1334,7 @@ describe "TextEditorPresenter", -> describe ".cursors", -> stateForCursor = (presenter, cursorIndex) -> - presenter.getState().content.cursors[presenter.model.getCursors()[cursorIndex].id] + getState(presenter).content.cursors[presenter.model.getCursors()[cursorIndex].id] it "contains pixelRects for empty selections that are visible on screen", -> editor.setSelectedBufferRanges([ @@ -1350,31 +1354,31 @@ describe "TextEditorPresenter", -> it "is empty until all of the required measurements are assigned", -> presenter = buildPresenterWithoutMeasurements() - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setExplicitHeight(25) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setLineHeight(10) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setScrollTop(0) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setBaseCharacterWidth(8) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setBoundingClientRect(top: 0, left: 0, width: 500, height: 130) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setWindowSize(500, 130) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setVerticalScrollbarWidth(10) - expect(presenter.getState().content.cursors).toEqual({}) + expect(getState(presenter).content.cursors).toEqual({}) presenter.setHorizontalScrollbarHeight(10) - expect(presenter.getState().content.cursors).not.toEqual({}) + expect(getState(presenter).content.cursors).not.toEqual({}) it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ @@ -1449,12 +1453,12 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 20) expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'v', 20) presenter.characterWidthsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) presenter.characterWidthsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} @@ -1493,7 +1497,7 @@ describe "TextEditorPresenter", -> # destroying destroyedCursor = editor.getCursors()[2] expectStateUpdate presenter, -> destroyedCursor.destroy() - expect(presenter.getState().content.cursors[destroyedCursor.id]).toBeUndefined() + expect(getState(presenter).content.cursors[destroyedCursor.id]).toBeUndefined() it "makes cursors as wide as the ::baseCharacterWidth if they're at the end of a line", -> editor.setCursorBufferPosition([1, Infinity]) @@ -1507,27 +1511,27 @@ describe "TextEditorPresenter", -> presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) presenter.setFocused(true) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> presenter.setFocused(false) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> presenter.setFocused(true) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false it "stops alternating for ::cursorBlinkResumeDelay when a cursor moves or a cursor is added", -> cursorBlinkPeriod = 100 @@ -1535,46 +1539,46 @@ describe "TextEditorPresenter", -> presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) presenter.setFocused(true) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> editor.moveRight() - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkResumeDelay) advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([1, 0]) - expect(presenter.getState().content.cursorsVisible).toBe true + expect(getState(presenter).content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkResumeDelay) advanceClock(cursorBlinkPeriod / 2) - expect(presenter.getState().content.cursorsVisible).toBe false + expect(getState(presenter).content.cursorsVisible).toBe false describe ".highlights", -> expectUndefinedStateForHighlight = (presenter, decoration) -> - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles state = stateForHighlightInTile(presenter, decoration, tileId) expect(state).toBeUndefined() stateForHighlightInTile = (presenter, decoration, tile) -> - presenter.getState().content.tiles[tile]?.highlights[decoration.id] + getState(presenter).content.tiles[tile]?.highlights[decoration.id] stateForSelectionInTile = (presenter, selectionIndex, tile) -> selection = presenter.model.getSelections()[selectionIndex] stateForHighlightInTile(presenter, selection.decoration, tile) expectUndefinedStateForSelection = (presenter, selectionIndex) -> - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles state = stateForSelectionInTile(presenter, selectionIndex, tileId) expect(state).toBeUndefined() @@ -1684,24 +1688,24 @@ describe "TextEditorPresenter", -> ]) presenter = buildPresenterWithoutMeasurements(tileSize: 2) - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles expect(tileState.highlights).toEqual({}) presenter.setExplicitHeight(25) - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles expect(tileState.highlights).toEqual({}) presenter.setLineHeight(10) - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles expect(tileState.highlights).toEqual({}) presenter.setScrollTop(0) - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles expect(tileState.highlights).toEqual({}) presenter.setBaseCharacterWidth(8) assignedAnyHighlight = false - for tileId, tileState of presenter.getState().content.tiles + for tileId, tileState of getState(presenter).content.tiles assignedAnyHighlight ||= _.isEqual(tileState.highlights, {}) expect(assignedAnyHighlight).toBe(true) @@ -1916,7 +1920,7 @@ describe "TextEditorPresenter", -> describe ".overlays", -> [item] = [] stateForOverlay = (presenter, decoration) -> - presenter.getState().content.overlays[decoration.id] + getState(presenter).content.overlays[decoration.id] it "contains state for overlay decorations both initially and when their markers move", -> marker = editor.addMarkerLayer(maintainHistory: true).markBufferPosition([2, 13], invalidate: 'touch') @@ -2023,25 +2027,25 @@ describe "TextEditorPresenter", -> decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) presenter = buildPresenterWithoutMeasurements() - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setBaseCharacterWidth(10) - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setLineHeight(10) - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setWindowSize(500, 100) - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setVerticalScrollbarWidth(10) - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setHorizontalScrollbarHeight(10) - expect(presenter.getState().content.overlays).toEqual({}) + expect(getState(presenter).content.overlays).toEqual({}) presenter.setBoundingClientRect({top: 0, left: 0, height: 100, width: 500}) - expect(presenter.getState().content.overlays).not.toEqual({}) + expect(getState(presenter).content.overlays).not.toEqual({}) describe "when the overlay has been measured", -> [gutterWidth, windowWidth, windowHeight, itemWidth, itemHeight, contentMargin, boundingClientRect, contentFrameWidth] = [] @@ -2210,52 +2214,52 @@ describe "TextEditorPresenter", -> it "updates model's rows per page when it changes", -> presenter = buildPresenter(explicitHeight: 50, lineHeightInPixels: 10, horizontalScrollbarHeight: 10) - presenter.getState() # trigger state update + getState(presenter) # trigger state update expect(editor.getRowsPerPage()).toBe(4) presenter.setExplicitHeight(100) - presenter.getState() # trigger state update + getState(presenter) # trigger state update expect(editor.getRowsPerPage()).toBe(9) presenter.setHorizontalScrollbarHeight(0) - presenter.getState() # trigger state update + getState(presenter) # trigger state update expect(editor.getRowsPerPage()).toBe(10) presenter.setLineHeight(5) - presenter.getState() # trigger state update + getState(presenter) # trigger state update expect(editor.getRowsPerPage()).toBe(20) it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", -> presenter = buildPresenter(explicitHeight: null) presenter.setAutoHeight(true) - expect(presenter.getState().height).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).height).toBe editor.getScreenLineCount() * 10 expectStateUpdate presenter, -> presenter.setAutoHeight(false) - expect(presenter.getState().height).toBe null + expect(getState(presenter).height).toBe null expectStateUpdate presenter, -> presenter.setAutoHeight(true) - expect(presenter.getState().height).toBe editor.getScreenLineCount() * 10 + expect(getState(presenter).height).toBe editor.getScreenLineCount() * 10 expectStateUpdate presenter, -> presenter.setLineHeight(20) - expect(presenter.getState().height).toBe editor.getScreenLineCount() * 20 + expect(getState(presenter).height).toBe editor.getScreenLineCount() * 20 expectStateUpdate presenter, -> editor.getBuffer().append("\n\n\n") - expect(presenter.getState().height).toBe editor.getScreenLineCount() * 20 + expect(getState(presenter).height).toBe editor.getScreenLineCount() * 20 describe ".focused", -> it "tracks the value of ::focused", -> presenter = buildPresenter() presenter.setFocused(false) - expect(presenter.getState().focused).toBe false + expect(getState(presenter).focused).toBe false expectStateUpdate presenter, -> presenter.setFocused(true) - expect(presenter.getState().focused).toBe true + expect(getState(presenter).focused).toBe true expectStateUpdate presenter, -> presenter.setFocused(false) - expect(presenter.getState().focused).toBe false + expect(getState(presenter).focused).toBe false describe ".gutters", -> getStateForGutterWithName = (presenter, gutterName) -> - gutterDescriptions = presenter.getState().gutters + gutterDescriptions = getState(presenter).gutters for description in gutterDescriptions gutter = description.gutter return description if gutter.name is gutterName @@ -2267,7 +2271,7 @@ describe "TextEditorPresenter", -> gutter2 = editor.addGutter({name: 'test-gutter-2', priority: 100, visible: false}) expectedGutterOrder = [gutter1, editor.gutterWithName('line-number'), gutter2] - for gutterDescription, index in presenter.getState().gutters + for gutterDescription, index in getState(presenter).gutters expect(gutterDescription.gutter).toEqual expectedGutterOrder[index] it "updates when the visibility of a gutter changes", -> @@ -2286,7 +2290,7 @@ describe "TextEditorPresenter", -> describe "for a gutter description that corresponds to the line-number gutter", -> getLineNumberGutterState = (presenter) -> - gutterDescriptions = presenter.getState().gutters + gutterDescriptions = getState(presenter).gutters for description in gutterDescriptions gutter = description.gutter return description if gutter.name is 'line-number' @@ -2614,7 +2618,7 @@ describe "TextEditorPresenter", -> decoration3 = editor.decorateMarker(marker3, decorationParams) # Clear any batched state updates. - presenter.getState() + getState(presenter) it "contains all decorations within the visible buffer range", -> decorationState = getContentForGutterWithName(presenter, 'test-gutter') @@ -2915,7 +2919,7 @@ describe "TextEditorPresenter", -> expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe presenter.scrollHeight - presenter.clientHeight # Scroll top only gets smaller when needed as dimensions change, never bigger - scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop + scrollTopBefore = getState(presenter).verticalScrollbar.scrollTop expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') expect(getStylesForGutterWithName(presenter, 'line-number').scrollTop).toBe scrollTopBefore expect(getStylesForGutterWithName(presenter, 'test-gutter').scrollTop).toBe scrollTopBefore @@ -3016,7 +3020,7 @@ describe "TextEditorPresenter", -> expectValidState = -> presenterParams.scrollTop = presenter.scrollTop presenterParams.scrollLeft = presenter.scrollLeft - actualState = presenter.getState() + actualState = getState(presenter) expectedState = new TextEditorPresenter(presenterParams).state delete actualState.content.scrollingVertically delete expectedState.content.scrollingVertically diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 0cee8215a..02d2e4a96 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -22,17 +22,6 @@ describe "TextEditor", -> atom.packages.activatePackage('language-javascript') describe "when the editor is deserialized", -> - it "returns undefined when the path cannot be read", -> - pathToOpen = path.join(temp.mkdirSync(), 'file.txt') - editor1 = null - - waitsForPromise -> - atom.workspace.open(pathToOpen).then (o) -> editor1 = o - - runs -> - fs.mkdirSync(pathToOpen) - expect(TextEditor.deserialize(editor1.serialize(), atom)).toBeUndefined() - it "restores selections and folds based on markers in the buffer", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true) @@ -762,11 +751,24 @@ describe "TextEditor", -> editor.moveToBeginningOfWord() expect(editor.getCursorBufferPosition()).toEqual [10, 0] + it "treats lines with only whitespace as a word (CRLF line ending)", -> + editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + it "works when the current line is blank", -> editor.setCursorBufferPosition([10, 0]) editor.moveToBeginningOfWord() expect(editor.getCursorBufferPosition()).toEqual [9, 2] + it "works when the current line is blank (CRLF line ending)", -> + editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual [9, 2] + editor.buffer.setText(buffer.getText().replace(/\r\n/g, "\n")) + describe ".moveToPreviousWordBoundary()", -> it "moves the cursor to the previous word boundary", -> editor.setCursorBufferPosition [0, 8] @@ -821,11 +823,23 @@ describe "TextEditor", -> editor.moveToEndOfWord() expect(editor.getCursorBufferPosition()).toEqual [10, 0] + it "treats lines with only whitespace as a word (CRLF line ending)", -> + editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + it "works when the current line is blank", -> editor.setCursorBufferPosition([10, 0]) editor.moveToEndOfWord() expect(editor.getCursorBufferPosition()).toEqual [11, 8] + it "works when the current line is blank (CRLF line ending)", -> + editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual [11, 8] + describe ".moveToBeginningOfNextWord()", -> it "moves the cursor before the first character of the next word", -> editor.setCursorBufferPosition [0, 6] @@ -1055,8 +1069,36 @@ describe "TextEditor", -> editor.moveToBeginningOfNextParagraph() expect(editor.getCursorBufferPosition()).toEqual [0, 0] + it "moves the cursor before the first line of the next paragraph (CRLF line endings)", -> + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition [0, 6] + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + + editor.setText("") + editor.setCursorBufferPosition [0, 0] + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + describe ".moveToBeginningOfPreviousParagraph()", -> - it "moves the cursor before the first line of the pevious paragraph", -> + it "moves the cursor before the first line of the previous paragraph", -> + editor.setCursorBufferPosition [10, 0] + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + editor.setText("") + editor.setCursorBufferPosition [0, 0] + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + + it "moves the cursor before the first line of the previous paragraph (CRLF line endings)", -> + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition [10, 0] editor.foldBufferRow(4) @@ -5337,7 +5379,7 @@ describe "TextEditor", -> tokens = atom.grammars.decodeTokens(line, tags) expect(tokens[0].value).toBe "var" - expect(tokens[0].scopes).toEqual ["source.js", "storage.modifier.js"] + expect(tokens[0].scopes).toEqual ["source.js", "storage.type.var.js"] expect(tokens[6].value).toBe "http://github.com" expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 5d6d3cfdc..76314681c 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -24,6 +24,34 @@ describe "TokenizedBuffer", -> advanceClock() while tokenizedBuffer.firstInvalidRow()? changeHandler?.reset() + describe "serialization", -> + describe "when the underlying buffer has a path", -> + it "deserializes it searching among the buffers in the current project", -> + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBufferA = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) + tokenizedBufferB = TokenizedBuffer.deserialize( + JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), + atom + ) + + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + + describe "when the underlying buffer has no path", -> + it "deserializes it searching among the buffers in the current project", -> + buffer = atom.project.bufferForPathSync(null) + + tokenizedBufferA = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) + tokenizedBufferB = TokenizedBuffer.deserialize( + JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), + atom + ) + + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + describe "when the buffer is destroyed", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') @@ -198,7 +226,7 @@ describe "TokenizedBuffer", -> buffer.setTextInRange([[1, 0], [3, 0]], "foo()") # previous line 0 remains - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js']) + expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.type.var.js']) # previous line 3 should be combined with input to form line 1 expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) @@ -242,7 +270,7 @@ describe "TokenizedBuffer", -> buffer.setTextInRange([[1, 0], [2, 0]], "foo()\nbar()\nbaz()\nquux()") # previous line 0 remains - expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.modifier.js']) + expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.type.var.js']) # 3 new lines inserted expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) @@ -582,7 +610,7 @@ describe "TokenizedBuffer", -> fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"] expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"] - expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.modifier.js"] + expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.type.var.js"] describe ".bufferRangeForScopeAtPosition(selector, position)", -> beforeEach -> @@ -599,8 +627,8 @@ describe "TokenizedBuffer", -> describe "when the selector matches a single token at the position", -> it "returns the range covered by the token", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.modifier.js', [0, 1])).toEqual [[0, 0], [0, 3]] - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.modifier.js', [0, 3])).toEqual [[0, 0], [0, 3]] + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual [[0, 0], [0, 3]] + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]] describe "when the selector matches a run of multiple tokens at the position", -> it "returns the range covered by all contigous tokens (within a single line)", -> diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index a2b4965a5..16672b25d 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -47,6 +47,21 @@ describe "ViewRegistry", -> expect(view2 instanceof TestView).toBe true expect(view2.model).toBe subclassModel + describe "when a view provider is registered generically, and works with the object", -> + it "constructs a view element and assigns the model on it", -> + model = {a: 'b'} + + registry.addViewProvider (model) -> + if model.a is 'b' + element = document.createElement('div') + element.className = 'test-element' + element + + view = registry.getView({a: 'b'}) + expect(view.className).toBe 'test-element' + + expect(-> registry.getView({a: 'c'})).toThrow() + describe "when no view provider is registered for the object's constructor", -> it "throws an exception", -> expect(-> registry.getView(new Object)).toThrow() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index e150761af..35585479b 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -43,6 +43,9 @@ describe "Workspace", -> pane3 = pane2.splitRight(copyActiveItem: true) pane4 = null + waitsForPromise -> + atom.workspace.open(null).then (editor) -> editor.setText("An untitled editor.") + waitsForPromise -> atom.workspace.open('b').then (editor) -> pane2.activateItem(editor.copy()) @@ -65,18 +68,19 @@ describe "Workspace", -> simulateReload() - expect(atom.workspace.getTextEditors().length).toBe 4 - [editor1, editor2, editor3, editor4] = atom.workspace.getTextEditors() - + expect(atom.workspace.getTextEditors().length).toBe 5 + [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() expect(editor1.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') expect(editor2.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.txt') expect(editor2.getCursorScreenPosition()).toEqual [0, 2] expect(editor3.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') expect(editor4.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.js') expect(editor4.getCursorScreenPosition()).toEqual [2, 4] + expect(untitledEditor.getPath()).toBeUndefined() + expect(untitledEditor.getText()).toBe("An untitled editor.") expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - expect(document.title).toBe "#{path.basename(editor3.getLongTitle())} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}/// describe "where there are no open panes or editors", -> it "constructs the view with no open editors", -> @@ -661,7 +665,7 @@ describe "Workspace", -> describe "::isTextEditor(obj)", -> it "returns true when the passed object is an instance of `TextEditor`", -> expect(workspace.isTextEditor(atom.workspace.buildTextEditor())).toBe(true) - expect(workspace.isTextEditor({getText: ->})).toBe(false) + expect(workspace.isTextEditor({getText: -> null})).toBe(false) expect(workspace.isTextEditor(null)).toBe(false) expect(workspace.isTextEditor(undefined)).toBe(false) @@ -732,7 +736,7 @@ describe "Workspace", -> describe "when the project has no path", -> it "sets the title to 'untitled'", -> atom.project.setPaths([]) - expect(document.title).toBe 'untitled - Atom' + expect(document.title).toMatch ///^untitled/// describe "when the project has a path", -> beforeEach -> @@ -742,25 +746,25 @@ describe "Workspace", -> describe "when there is an active pane item", -> it "sets the title to the pane item's title plus the project path", -> item = atom.workspace.getActivePaneItem() - expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) - expect(document.title).toBe "#{editor.getTitle()} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// describe "when the active pane's item changes", -> it "updates the title to the new item's title plus the project path", -> atom.workspace.getActivePane().activateNextItem() item = atom.workspace.getActivePaneItem() - expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// describe "when the last pane item is removed", -> it "updates the title to contain the project's path", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toBe "#{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}/// describe "when an inactive pane's item changes", -> it "does not update the title", -> @@ -784,7 +788,7 @@ describe "Workspace", -> }) workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) item = workspace2.getActivePaneItem() - expect(document.title).toBe "#{item.getLongTitle()} - #{atom.project.getPaths()[0]} - Atom" + expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}/// workspace2.destroy() describe "document edited status", -> diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 5e82ebd15..60627e5ce 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -106,6 +106,9 @@ class ApplicationDelegate setRepresentedFilename: (filename) -> ipcRenderer.send("call-window-method", "setRepresentedFilename", filename) + addRecentDocument: (filename) -> + ipc.send("add-recent-document", filename) + setRepresentedDirectoryPaths: (paths) -> loadSettings = getWindowLoadSettings() loadSettings['initialPaths'] = paths diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7c1f81bba..f46df14ab 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -151,7 +151,7 @@ class AtomEnvironment extends Model @packages = new PackageManager({ devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles, commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, - grammarRegistry: @grammars + grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views }) @themes = new ThemeManager({ @@ -885,6 +885,8 @@ class AtomEnvironment extends Model else @project.addPath(pathToOpen) + @applicationDelegate.addRecentDocument(pathToOpen) + unless fs.isDirectorySync(pathToOpen) @workspace?.open(pathToOpen, {initialLine, initialColumn}) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index f721596d8..f997c6f86 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -77,6 +77,7 @@ class AtomApplication @listenForArgumentsFromNewProcess() @setupJavaScriptArguments() @handleEvents() + @setupDockMenu() @storageFolder = new StorageFolder(process.env.ATOM_HOME) if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test @@ -275,6 +276,16 @@ class AtomApplication ipcMain.on 'write-to-stderr', (event, output) -> process.stderr.write(output) + ipc.on 'add-recent-document', (event, filename) -> + app.addRecentDocument(filename) + + setupDockMenu: -> + if process.platform is 'darwin' + dockMenu = Menu.buildFromTemplate [ + {label: 'New Window', click: => @emit('application:new-window')} + ] + app.dock.setMenu dockMenu + # Public: Executes the given command. # # If it isn't handled globally, delegate to the currently focused window. diff --git a/src/compile-cache.js b/src/compile-cache.js index bedbd2549..79984ccee 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -163,10 +163,16 @@ var prepareStackTraceWithSourceMapping = Error.prepareStackTrace let prepareStackTrace = prepareStackTraceWithSourceMapping function prepareStackTraceWithRawStackAssignment (error, frames) { - error.rawStack = frames - return prepareStackTrace(error, frames) + if (error.rawStack) { // avoid infinite recursion + return prepareStackTraceWithSourceMapping(error, frames) + } else { + error.rawStack = frames + return prepareStackTrace(error, frames) + } } +Error.stackTraceLimit = 30 + Object.defineProperty(Error, 'prepareStackTrace', { get: function () { return prepareStackTraceWithRawStackAssignment diff --git a/src/config.coffee b/src/config.coffee index 489e16016..2e4387732 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -381,8 +381,8 @@ class Config # ``` # # * `keyPath` {String} name of the key to observe - # * `options` {Object} - # * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from + # * `options` (optional) {Object} + # * `scope` (optional) {ScopeDescriptor} describing a path from # the root of the syntax tree to a token. Get one by calling # {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) @@ -412,8 +412,8 @@ class Config # # * `keyPath` (optional) {String} name of the key to observe. Must be # specified if `scopeDescriptor` is specified. - # * `optional` (optional) {Object} - # * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from + # * `options` (optional) {Object} + # * `scope` (optional) {ScopeDescriptor} describing a path from # the root of the syntax tree to a token. Get one by calling # {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. # See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors) @@ -827,6 +827,7 @@ class Config allSettings = {'*': @settings} allSettings = _.extend allSettings, @scopedSettingsStore.propertiesForSource(@getUserConfigPath()) + allSettings = sortObject(allSettings) try CSON.writeFileSync(@configFilePath, allSettings) catch error @@ -1190,6 +1191,13 @@ Config.addSchemaEnforcers isPlainObject = (value) -> _.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) and not (value instanceof Color) +sortObject = (value) -> + return value unless isPlainObject(value) + result = {} + for key in Object.keys(value).sort() + result[key] = sortObject(value[key]) + result + withoutEmptyObjects = (object) -> resultObject = undefined if isPlainObject(object) diff --git a/src/cursor.coffee b/src/cursor.coffee index 0f87c2760..5b3b23b73 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -3,6 +3,8 @@ _ = require 'underscore-plus' Model = require './model' +EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g + # Extended: The `Cursor` class represents the little blinking line identifying # where text can be inserted. # @@ -467,10 +469,13 @@ class Cursor extends Model scanRange = [[previousNonBlankRow, 0], currentBufferPosition] beginningOfWordPosition = null - @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) -> - if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious - beginningOfWordPosition = range.start - if not beginningOfWordPosition?.isEqual(currentBufferPosition) + @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> + # Ignore 'empty line' matches between '\r' and '\n' + return if matchText is '' and range.start.column isnt 0 + + if range.start.isLessThan(currentBufferPosition) + if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious + beginningOfWordPosition = range.start stop() if beginningOfWordPosition? @@ -496,13 +501,12 @@ class Cursor extends Model scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] endOfWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) -> - if allowNext - if range.end.isGreaterThan(currentBufferPosition) - endOfWordPosition = range.end - stop() - else - if range.start.isLessThanOrEqual(currentBufferPosition) + @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> + # Ignore 'empty line' matches between '\r' and '\n' + return if matchText is '' and range.start.column isnt 0 + + if range.end.isGreaterThan(currentBufferPosition) + if allowNext or range.start.isLessThanOrEqual(currentBufferPosition) endOfWordPosition = range.end stop() @@ -603,14 +607,14 @@ class Cursor extends Model # non-word characters in the regex. (default: true) # # Returns a {RegExp}. - wordRegExp: ({includeNonWordCharacters}={}) -> - includeNonWordCharacters ?= true - nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) - segments = ["^[\t ]*$"] - segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") - if includeNonWordCharacters - segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") - new RegExp(segments.join("|"), "g") + wordRegExp: (options) -> + scope = @getScopeDescriptor() + nonWordCharacters = _.escapeRegExp(@config.get('editor.nonWordCharacters', {scope})) + + source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+" + if options?.includeNonWordCharacters ? true + source += "|" + "[#{nonWordCharacters}]+" + new RegExp(source, "g") # Public: Get the RegExp used by the cursor to determine what a "subword" is. # @@ -666,10 +670,9 @@ class Cursor extends Model {row, column} = eof position = new Point(row, column - 1) - @editor.scanInBufferRange /^\n*$/g, scanRange, ({range, stop}) -> - unless range.start.isEqual(start) - position = range.start - stop() + @editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> + position = range.start.traverse(Point(1, 0)) + stop() unless position.isEqual(start) position getBeginningOfPreviousParagraphBufferPosition: -> @@ -679,8 +682,7 @@ class Cursor extends Model scanRange = [[row-1, column], [0, 0]] position = new Point(0, 0) zero = new Point(0, 0) - @editor.backwardsScanInBufferRange /^\n*$/g, scanRange, ({range, stop}) -> - unless range.start.isEqual(zero) - position = range.start - stop() + @editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> + position = range.start.traverse(Point(1, 0)) + stop() unless position.isEqual(start) position diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 7f6cb0f65..3c73a0b02 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -39,6 +39,9 @@ class DeserializerManager delete @deserializers[deserializer.name] for deserializer in deserializers return + getDeserializerCount: -> + Object.keys(@deserializers).length + # Public: Deserialize the state and params. # # * `state` The state {Object} to deserialize. diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index 82f2e8b99..b5c3964f9 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -32,7 +32,7 @@ KeymapManager::loadUserKeymap = -> return unless fs.isFileSync(userKeymapPath) try - @loadKeymap(userKeymapPath, watch: true, suppressErrors: true) + @loadKeymap(userKeymapPath, watch: true, suppressErrors: true, priority: 100) catch error if error.message.indexOf('Unable to watch path') > -1 message = """ diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 54ba6cf57..bd8219e81 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -3,7 +3,7 @@ TokenIterator = require './token-iterator' module.exports = class LinesYardstick - constructor: (@model, @presenter, @lineNodesProvider, grammarRegistry) -> + constructor: (@model, @lineNodesProvider, grammarRegistry) -> @tokenIterator = new TokenIterator({grammarRegistry}) @rangeForMeasurement = document.createRange() @invalidateCache() @@ -11,14 +11,12 @@ class LinesYardstick invalidateCache: -> @pixelPositionsByLineIdAndColumn = {} - prepareScreenRowsForMeasurement: (screenRows) -> - @presenter.setScreenRowsToMeasure(screenRows) - @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) + measuredRowForPixelPosition: (pixelPosition) -> + targetTop = pixelPosition.top + row = Math.floor(targetTop / @model.getLineHeightInPixels()) + row if 0 <= row <= @model.getLastScreenRow() - clearScreenRowsForMeasurement: -> - @presenter.clearScreenRowsToMeasure() - - screenPositionForPixelPosition: (pixelPosition, measureVisibleLinesOnly) -> + screenPositionForPixelPosition: (pixelPosition) -> targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() @@ -28,12 +26,10 @@ class LinesYardstick row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) - @prepareScreenRowsForMeasurement([row]) unless measureVisibleLinesOnly - line = @model.tokenizedLineForScreenRow(row) lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) - return new Point(row, 0) unless lineNode? and line? + return Point(row, 0) unless lineNode? and line? textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row) column = 0 @@ -70,33 +66,27 @@ class LinesYardstick left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) charWidth = left - previousLeft - return new Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2) + return Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2) previousLeft = left previousColumn = column column += charLength - @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly - if targetLeft <= previousLeft + (charWidth / 2) - new Point(row, previousColumn) + Point(row, previousColumn) else - new Point(row, column) + Point(row, column) - pixelPositionForScreenPosition: (screenPosition, clip=true, measureVisibleLinesOnly) -> + pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip targetRow = screenPosition.row targetColumn = screenPosition.column - @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly - top = targetRow * @model.getLineHeightInPixels() left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) - @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly - {top, left} leftPixelPositionForScreenPosition: (row, column) -> @@ -173,18 +163,3 @@ class LinesYardstick offset = lineNode.getBoundingClientRect().left left + width - offset - - pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> - lineHeight = @model.getLineHeightInPixels() - - if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top - left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight - width = @presenter.getScrollWidth() - else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) - height = lineHeight - width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left - - {top, left, width, height} diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 789b2eae5..6772178af 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -31,7 +31,8 @@ class PackageManager constructor: (params) -> { configDirPath, @devMode, safeMode, @resourcePath, @config, @styleManager, - @notificationManager, @keymapManager, @commandRegistry, @grammarRegistry + @notificationManager, @keymapManager, @commandRegistry, @grammarRegistry, + @deserializerManager, @viewRegistry } = params @emitter = new Emitter @@ -46,6 +47,7 @@ class PackageManager @packagesCache = require('../package.json')?._atomPackages ? {} @loadedPackages = {} @activePackages = {} + @activatingPackages = {} @packageStates = {} @serviceHub = new ServiceHub @@ -61,6 +63,7 @@ class PackageManager reset: -> @serviceHub.clear() @deactivatePackages() + @loadedPackages = {} @packageStates = {} ### @@ -375,7 +378,8 @@ class PackageManager options = { path: packagePath, metadata, packageManager: this, @config, @styleManager, @commandRegistry, @keymapManager, @devMode, @notificationManager, - @grammarRegistry, @themeManager, @menuManager, @contextMenuManager + @grammarRegistry, @themeManager, @menuManager, @contextMenuManager, + @deserializerManager, @viewRegistry } if metadata.theme pack = new ThemePackage(options) @@ -434,9 +438,12 @@ class PackageManager if pack = @getActivePackage(name) Promise.resolve(pack) else if pack = @loadPackage(name) + @activatingPackages[pack.name] = pack pack.activate().then => - @activePackages[pack.name] = pack - @emitter.emit 'did-activate-package', pack + if @activatingPackages[pack.name]? + delete @activatingPackages[pack.name] + @activePackages[pack.name] = pack + @emitter.emit 'did-activate-package', pack pack else Promise.reject(new Error("Failed to load package '#{name}'")) @@ -472,6 +479,7 @@ class PackageManager @setPackageState(pack.name, state) if state = pack.serialize?() pack.deactivate() delete @activePackages[pack.name] + delete @activatingPackages[pack.name] @emitter.emit 'did-deactivate-package', pack handleMetadataError: (error, packagePath) -> diff --git a/src/package.coffee b/src/package.coffee index 4cd6a18fd..b831b3c55 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -33,7 +33,7 @@ class Package { @path, @metadata, @packageManager, @config, @styleManager, @commandRegistry, @keymapManager, @devMode, @notificationManager, @grammarRegistry, @themeManager, - @menuManager, @contextMenuManager + @menuManager, @contextMenuManager, @deserializerManager, @viewRegistry } = params @emitter = new Emitter @@ -84,12 +84,24 @@ class Package @loadKeymaps() @loadMenus() @loadStylesheets() + @loadDeserializers() + @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() - @requireMainModule() unless @mainModule? or @activationShouldBeDeferred() + if @shouldRequireMainModuleOnLoad() and not @mainModule? + @requireMainModule() catch error @handleError("Failed to load the #{@name} package", error) this + shouldRequireMainModuleOnLoad: -> + not ( + @metadata.deserializers? or + @metadata.viewProviders? or + @metadata.configSchema? or + @activationShouldBeDeferred() or + localStorage.getItem(@getCanDeferMainModuleRequireStorageKey()) is 'true' + ) + reset: -> @stylesheets = [] @keymaps = [] @@ -117,9 +129,12 @@ class Package activateNow: -> try - @activateConfig() + @requireMainModule() unless @mainModule? + @configSchemaRegisteredOnActivate = @registerConfigSchemaFromMainModule() + @registerViewProviders() @activateStylesheets() if @mainModule? and not @mainActivated + @mainModule.activateConfig?() @mainModule.activate?(@packageManager.getPackageState(@name) ? {}) @mainActivated = true @activateServices() @@ -128,15 +143,22 @@ class Package @resolveActivationPromise?() - activateConfig: -> - return if @configActivated + registerConfigSchemaFromMetadata: -> + if configSchema = @metadata.configSchema + @config.setSchema @name, {type: 'object', properties: configSchema} + true + else + false - @requireMainModule() unless @mainModule? - if @mainModule? + registerConfigSchemaFromMainModule: -> + if @mainModule? and not @configSchemaRegisteredOnLoad if @mainModule.config? and typeof @mainModule.config is 'object' @config.setSchema @name, {type: 'object', properties: @mainModule.config} - @mainModule.activateConfig?() - @configActivated = true + return true + false + + # TODO: Remove. Settings view calls this method currently. + activateConfig: -> @registerConfigSchemaFromMainModule() activateStylesheets: -> return if @stylesheetsActivated @@ -253,6 +275,26 @@ class Package @stylesheets = @getStylesheetPaths().map (stylesheetPath) => [stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)] + loadDeserializers: -> + if @metadata.deserializers? + for name, implementationPath of @metadata.deserializers + do => + deserializePath = path.join(@path, implementationPath) + deserializeFunction = null + atom.deserializers.add + name: name, + deserialize: => + @registerViewProviders() + deserializeFunction ?= require(deserializePath) + deserializeFunction.apply(this, arguments) + return + + registerViewProviders: -> + if @metadata.viewProviders? and not @registeredViewProviders + for implementationPath in @metadata.viewProviders + @viewRegistry.addViewProvider(require(path.join(@path, implementationPath))) + @registeredViewProviders = true + getStylesheetsPath: -> path.join(@path, 'styles') @@ -343,21 +385,18 @@ class Package @activationPromise = null @resolveActivationPromise = null @activationCommandSubscriptions?.dispose() + @configSchemaRegisteredOnActivate = false @deactivateResources() - @deactivateConfig() @deactivateKeymaps() if @mainActivated try @mainModule?.deactivate?() + @mainModule?.deactivateConfig?() @mainActivated = false catch e console.error "Error deactivating package '#{@name}'", e.stack @emitter.emit 'did-deactivate' - deactivateConfig: -> - @mainModule?.deactivateConfig?() - @configActivated = false - deactivateResources: -> grammar.deactivate() for grammar in @grammars settings.deactivate() for settings in @settings @@ -392,7 +431,13 @@ class Package mainModulePath = @getMainModulePath() if fs.isFileSync(mainModulePath) @mainModuleRequired = true + + previousViewProviderCount = @viewRegistry.getViewProviderCount() + previousDeserializerCount = @deserializerManager.getDeserializerCount() @mainModule = require(mainModulePath) + if (@viewRegistry.getViewProviderCount() is previousViewProviderCount and + @deserializerManager.getDeserializerCount() is previousDeserializerCount) + localStorage.setItem(@getCanDeferMainModuleRequireStorageKey(), 'true') getMainModulePath: -> return @mainModulePath if @resolvedMainModulePath @@ -586,6 +631,9 @@ class Package electronVersion = process.versions['electron'] ? process.versions['atom-shell'] "installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules" + getCanDeferMainModuleRequireStorageKey: -> + "installed-packages:#{@name}:#{@metadata.version}:can-defer-main-module-require" + # Get the incompatible native modules that this package depends on. # This recurses through all dependencies and requires all modules that # contain a `.node` file. diff --git a/src/project.coffee b/src/project.coffee index bb9c8be80..d59c041cb 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -308,12 +308,20 @@ class Project extends Model findBufferForPath: (filePath) -> _.find @buffers, (buffer) -> buffer.getPath() is filePath + findBufferForId: (id) -> + _.find @buffers, (buffer) -> buffer.getId() is id + # Only to be used in specs bufferForPathSync: (filePath) -> absoluteFilePath = @resolvePath(filePath) existingBuffer = @findBufferForPath(absoluteFilePath) if filePath existingBuffer ? @buildBufferSync(absoluteFilePath) + # Only to be used when deserializing + bufferForIdSync: (id) -> + existingBuffer = @findBufferForId(id) if id + existingBuffer ? @buildBufferSync() + # Given a file path, this retrieves or creates a new {TextBuffer}. # # If the `filePath` already has a `buffer`, that value is used instead. Otherwise, @@ -329,9 +337,6 @@ class Project extends Model else @buildBuffer(absoluteFilePath) - bufferForId: (id) -> - _.find @buffers, (buffer) -> buffer.id is id - # Still needed when deserializing a tokenized buffer buildBufferSync: (absoluteFilePath) -> buffer = new TextBuffer({filePath: absoluteFilePath}) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index c6ed4e053..af55a3c32 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -82,7 +82,7 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, @grammars) + @linesYardstick = new LinesYardstick(@editor, @linesComponent, @grammars) @presenter.setLinesYardstick(@linesYardstick) @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @@ -127,8 +127,10 @@ class TextEditorComponent @domNode updateSync: -> + @updateSyncPreMeasurement() + @oldState ?= {} - @newState = @presenter.getState() + @newState = @presenter.getPostMeasurementState() if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty() @domNode.classList.add('has-selection') @@ -170,6 +172,9 @@ class TextEditorComponent @updateParentViewFocusedClassIfNeeded() @updateParentViewMiniClass() + updateSyncPreMeasurement: -> + @linesComponent.updateSync(@presenter.getPreMeasurementState()) + readAfterUpdateSync: => @overlayManager?.measureOverlays() @@ -429,14 +434,42 @@ class TextEditorComponent getVisibleRowRange: -> @presenter.getVisibleRowRange() - pixelPositionForScreenPosition: -> - @linesYardstick.pixelPositionForScreenPosition(arguments...) + pixelPositionForScreenPosition: (screenPosition, clip) -> + unless @presenter.isRowVisible(screenPosition.row) + @presenter.setScreenRowsToMeasure([screenPosition.row]) + @updateSyncPreMeasurement() - screenPositionForPixelPosition: -> - @linesYardstick.screenPositionForPixelPosition(arguments...) + pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + @presenter.clearScreenRowsToMeasure() + pixelPosition - pixelRectForScreenRange: -> - @linesYardstick.pixelRectForScreenRange(arguments...) + screenPositionForPixelPosition: (pixelPosition) -> + row = @linesYardstick.measuredRowForPixelPosition(pixelPosition) + if row? and not @presenter.isRowVisible(row) + @presenter.setScreenRowsToMeasure([row]) + @updateSyncPreMeasurement() + + position = @linesYardstick.screenPositionForPixelPosition(pixelPosition) + @presenter.clearScreenRowsToMeasure() + position + + pixelRectForScreenRange: (screenRange) -> + rowsToMeasure = [] + unless @presenter.isRowVisible(screenRange.start.row) + rowsToMeasure.push(screenRange.start.row) + unless @presenter.isRowVisible(screenRange.end.row) + rowsToMeasure.push(screenRange.end.row) + + if rowsToMeasure.length > 0 + @presenter.setScreenRowsToMeasure(rowsToMeasure) + @updateSyncPreMeasurement() + + rect = @presenter.absolutePixelRectForScreenRange(screenRange) + + if rowsToMeasure.length > 0 + @presenter.clearScreenRowsToMeasure() + + rect pixelRangeForScreenRange: (screenRange, clip=true) -> {start, end} = Range.fromObject(screenRange) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 405e34548..5cfaeebcf 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -121,14 +121,6 @@ class TextEditorPresenter @updating = false @resetTrackedUpdates() - - # Public: Gets this presenter's state, updating it just in time before returning from this function. - # Returns a state {Object}, useful for rendering to screen. - getState: -> - @linesYardstick.prepareScreenRowsForMeasurement() - - @getPostMeasurementState() - @state resetTrackedUpdates: -> @@ -1155,16 +1147,29 @@ class TextEditorPresenter hasOverlayPositionRequirements: -> @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight + absolutePixelRectForScreenRange: (screenRange) -> + lineHeight = @model.getLineHeightInPixels() + + if screenRange.end.row > screenRange.start.row + top = @linesYardstick.pixelPositionForScreenPosition(screenRange.start, true).top + left = 0 + height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight + width = @getScrollWidth() + else + {top, left} = @linesYardstick.pixelPositionForScreenPosition(screenRange.start, false) + height = lineHeight + width = @linesYardstick.pixelPositionForScreenPosition(screenRange.end, false).left - left + + {top, left, width, height} + pixelRectForScreenRange: (screenRange) -> - rect = @linesYardstick.pixelRectForScreenRange(screenRange, true) + rect = @absolutePixelRectForScreenRange(screenRange) rect.top -= @getScrollTop() rect.left -= @getScrollLeft() - rect.top = Math.round(rect.top) rect.left = Math.round(rect.left) rect.width = Math.round(rect.width) rect.height = Math.round(rect.height) - rect fetchDecorations: -> @@ -1550,3 +1555,6 @@ class TextEditorPresenter getVisibleRowRange: -> [@startRow, @endRow] + + isRowVisible: (row) -> + @startRow <= row < @endRow diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a151c9dba..1435aef19 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -677,7 +677,7 @@ class TextEditor extends Model # this editor. shouldPromptToSave: ({windowCloseRequested}={}) -> if windowCloseRequested - @isModified() + false else @isModified() and not @buffer.hasMultipleEditors() diff --git a/src/theme-package.coffee b/src/theme-package.coffee index 084728869..502fbd52b 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -14,6 +14,7 @@ class ThemePackage extends Package load: -> @loadTime = 0 + @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() this activate: -> diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 2df29a31c..cdafc2869 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -22,7 +22,11 @@ class TokenizedBuffer extends Model changeCount: 0 @deserialize: (state, atomEnvironment) -> - state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) + if state.bufferId + state.buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) + else + # TODO: remove this fallback after everyone transitions to the latest version. + state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) state.config = atomEnvironment.config state.grammarRegistry = atomEnvironment.grammars state.packageManager = atomEnvironment.packages @@ -53,6 +57,7 @@ class TokenizedBuffer extends Model serialize: -> deserializer: 'TokenizedBuffer' bufferPath: @buffer.getPath() + bufferId: @buffer.getId() tabLength: @tabLength ignoreInvisibles: @ignoreInvisibles largeFileMode: @largeFileMode diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 0f07600ae..ef7151353 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -3,6 +3,8 @@ Grim = require 'grim' {Disposable} = require 'event-kit' _ = require 'underscore-plus' +AnyConstructor = Symbol('any-constructor') + # Essential: `ViewRegistry` handles the association between model and view # types in Atom. We call this association a View Provider. As in, for a given # model, this class can provide a view via {::getView}, as long as the @@ -76,16 +78,27 @@ class ViewRegistry # textEditorElement # ``` # - # * `modelConstructor` Constructor {Function} for your model. + # * `modelConstructor` (optional) Constructor {Function} for your model. If + # a constructor is given, the `createView` function will only be used + # for model objects inheriting from that constructor. Otherwise, it will + # will be called for any object. # * `createView` Factory {Function} that is passed an instance of your model - # and must return a subclass of `HTMLElement` or `undefined`. + # and must return a subclass of `HTMLElement` or `undefined`. If it returns + # `undefined`, then the registry will continue to search for other view + # providers. # # Returns a {Disposable} on which `.dispose()` can be called to remove the # added provider. addViewProvider: (modelConstructor, createView) -> if arguments.length is 1 - Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.") - provider = modelConstructor + switch typeof modelConstructor + when 'function' + provider = {createView: modelConstructor, modelConstructor: AnyConstructor} + when 'object' + Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.") + provider = modelConstructor + else + throw new TypeError("Arguments to addViewProvider must be functions") else provider = {modelConstructor, createView} @@ -93,6 +106,9 @@ class ViewRegistry new Disposable => @providers = @providers.filter (p) -> p isnt provider + getViewProviderCount: -> + @providers.length + # Essential: Get the view associated with an object in the workspace. # # If you're just *using* the workspace, you shouldn't need to access the view @@ -153,25 +169,34 @@ class ViewRegistry createView: (object) -> if object instanceof HTMLElement - object - else if object?.element instanceof HTMLElement - object.element - else if object?.jquery - object[0] - else if provider = @findProvider(object) - element = provider.createView?(object, @atomEnvironment) - unless element? - element = new provider.viewConstructor - element.initialize?(object) ? element.setModel?(object) - element - else if viewConstructor = object?.getViewClass?() - view = new viewConstructor(object) - view[0] - else - throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.") + return object - findProvider: (object) -> - find @providers, ({modelConstructor}) -> object instanceof modelConstructor + if object?.element instanceof HTMLElement + return object.element + + if object?.jquery + return object[0] + + for provider in @providers + if provider.modelConstructor is AnyConstructor + if element = provider.createView(object, @atomEnvironment) + return element + continue + + if object instanceof provider.modelConstructor + if element = provider.createView?(object, @atomEnvironment) + return element + + if viewConstructor = provider.viewConstructor + element = new viewConstructor + element.initialize?(object) ? element.setModel?(object) + return element + + if viewConstructor = object?.getViewClass?() + view = new viewConstructor(object) + return view[0] + + throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.") updateDocument: (fn) -> @documentWriters.push(fn) diff --git a/src/workspace.coffee b/src/workspace.coffee index 04feef61e..f64f58ee0 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -161,15 +161,22 @@ class Workspace extends Model itemTitle ?= "untitled" projectPath ?= projectPaths[0] + titleParts = [] if item? and projectPath? - document.title = "#{itemTitle} - #{projectPath} - #{appName}" - @applicationDelegate.setRepresentedFilename(itemPath ? projectPath) + titleParts.push itemTitle, projectPath + representedPath = itemPath ? projectPath else if projectPath? - document.title = "#{projectPath} - #{appName}" - @applicationDelegate.setRepresentedFilename(projectPath) + titleParts.push projectPath + representedPath = projectPath else - document.title = "#{itemTitle} - #{appName}" - @applicationDelegate.setRepresentedFilename("") + titleParts.push itemTitle + representedPath = "" + + unless process.platform is 'darwin' + titleParts.push appName + + document.title = titleParts.join(" \u2014 ") + @applicationDelegate.setRepresentedFilename(representedPath) # On OS X, fades the application window's proxy icon when the current file # has been modified.