diff --git a/apm/package.json b/apm/package.json index 9d788a5ee..a47620ea2 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.91.0" + "atom-package-manager": "0.92.0" } } diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 6b1dae4a8..876964a20 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -60,7 +60,7 @@ module.exports = (grunt) -> contentsDir = shellAppDir appDir = path.join(shellAppDir, 'resources', 'app') installDir ?= process.env.INSTALL_PREFIX ? '/usr/local' - killCommand ='pkill -9 Atom' + killCommand ='pkill -9 atom' coffeeConfig = glob_to_multiple: @@ -130,7 +130,7 @@ module.exports = (grunt) -> atom: {appDir, appName, symbolsDir, buildDir, contentsDir, installDir, shellAppDir} - docsOutputDir: 'docs/output/api' + docsOutputDir: 'docs/output' coffee: coffeeConfig diff --git a/build/package.json b/build/package.json index cbdc58736..ea8514acb 100644 --- a/build/package.json +++ b/build/package.json @@ -7,7 +7,8 @@ }, "dependencies": { "async": "~0.2.9", - "biscotto": ">=2.1.1 <3.0", + "donna": "~1.0", + "tello": "~0.2", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.2.0", diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee index 093162337..605269933 100644 --- a/build/tasks/docs-task.coffee +++ b/build/tasks/docs-task.coffee @@ -1,162 +1,39 @@ path = require 'path' -async = require 'async' fs = require 'fs-plus' -request = require 'request' +_ = require 'underscore-plus' + +donna = require 'donna' +tello = require 'tello' module.exports = (grunt) -> - {rm} = require('./task-helpers')(grunt) + getClassesToInclude = -> + modulesPath = path.resolve(__dirname, '..', '..', 'node_modules') + classes = {} + fs.traverseTreeSync modulesPath, (modulePath) -> + return true unless path.basename(modulePath) is 'package.json' + return true unless fs.isFileSync(modulePath) - cmd = path.join('node_modules', '.bin', 'coffee') - commonArgs = [path.join('build', 'node_modules', '.bin', 'biscotto'), '--'] - opts = - stdio: 'inherit' + apiPath = path.join(path.dirname(modulePath), 'api.json') + if fs.isFileSync(apiPath) + _.extend(classes, grunt.file.readJSON(apiPath).classes) + true + classes + + sortClasses = (classes) -> + sortedClasses = {} + for className in Object.keys(classes).sort() + sortedClasses[className] = classes[className] + sortedClasses grunt.registerTask 'build-docs', 'Builds the API docs in src', -> - done = @async() - docsOutputDir = grunt.config.get('docsOutputDir') - downloadIncludes (error, includePaths) -> - if error? - done(error) - else - rm(docsOutputDir) - args = [ - commonArgs... - '--title', 'Atom API Documentation' - '-o', docsOutputDir - '-r', 'docs/README.md' - '--stability', '1' - 'src/' - includePaths... - ] - grunt.util.spawn({cmd, args, opts}, done) + metadata = donna.generateMetadata(['.']) + api = tello.digest(metadata) + _.extend(api.classes, getClassesToInclude()) + api.classes = sortClasses(api.classes) - grunt.registerTask 'lint-docs', 'Generate stats about the doc coverage', -> - done = @async() - downloadIncludes (error, includePaths) -> - if error? - done(error) - else - args = [ - commonArgs... - '--noOutput' - 'src/' - includePaths... - ] - grunt.util.spawn({cmd, args, opts}, done) - - grunt.registerTask 'missing-docs', 'Generate stats about the doc coverage', -> - done = @async() - downloadIncludes (error, includePaths) -> - if error? - done(error) - else - args = [ - commonArgs... - '--noOutput' - '--missing' - 'src/' - includePaths... - ] - grunt.util.spawn({cmd, args, opts}, done) - - grunt.registerTask 'copy-docs', 'Copies over latest API docs to atom-docs', -> - done = @async() - - fetchTag = (args..., callback) -> - cmd = 'git' - args = ['describe', '--abbrev=0', '--tags'] - grunt.util.spawn {cmd, args}, (error, result) -> - if error? - callback(error) - else - callback(null, String(result).trim()) - - copyDocs = (tag, callback) -> - cmd = 'cp' - args = ['-r', 'docs/output/', "../atom.io/public/docs/api/#{tag}/"] - - fs.exists "../atom.io/public/docs/api/", (exists) -> - if exists - grunt.util.spawn {cmd, args}, (error, result) -> - if error? - callback(error) - else - callback(null, tag) - else - grunt.log.error "../atom.io/public/docs/api/ doesn't exist" - return false - - grunt.util.async.waterfall [fetchTag, copyDocs], done - - grunt.registerTask 'deploy-docs', 'Publishes latest API docs to atom-docs.githubapp.com', -> - done = @async() - docsRepoArgs = ['--work-tree=../atom-docs/', '--git-dir=../atom-docs/.git/'] - - fetchTag = (args..., callback) -> - cmd = 'git' - args = ['describe', '--abbrev=0', '--tags'] - grunt.util.spawn {cmd, args}, (error, result) -> - if error? - callback(error) - else - callback(null, String(result).trim().split('.')[0..1].join('.')) - - stageDocs = (tag, callback) -> - cmd = 'git' - args = [docsRepoArgs..., 'add', "public/#{tag}"] - grunt.util.spawn({cmd, args, opts}, callback) - - fetchSha = (args..., callback) -> - cmd = 'git' - args = ['rev-parse', 'HEAD'] - grunt.util.spawn {cmd, args}, (error, result) -> - if error? - callback(error) - else - callback(null, String(result).trim()) - - commitChanges = (sha, callback) -> - cmd = 'git' - args = [docsRepoArgs..., 'commit', "-m Update API docs to #{sha}"] - grunt.util.spawn({cmd, args, opts}, callback) - - pushOrigin = (args..., callback) -> - cmd = 'git' - args = [docsRepoArgs..., 'push', 'origin', 'master'] - grunt.util.spawn({cmd, args, opts}, callback) - - pushHeroku = (args..., callback) -> - cmd = 'git' - args = [docsRepoArgs..., 'push', 'heroku', 'master'] - grunt.util.spawn({cmd, args, opts}, callback) - - grunt.util.async.waterfall [fetchTag, stageDocs, fetchSha, commitChanges, pushOrigin, pushHeroku], done - -downloadFileFromRepo = ({repo, file}, callback) -> - uri = "https://raw.github.com/atom/#{repo}/master/#{file}" - request uri, (error, response, contents) -> - return callback(error) if error? - downloadPath = path.join('docs', 'includes', repo, file) - fs.writeFile downloadPath, contents, (error) -> - callback(error, downloadPath) - -downloadIncludes = (callback) -> - includes = [ - {repo: 'atom-keymap', file: 'src/keymap-manager.coffee'} - {repo: 'atom-keymap', file: 'src/key-binding.coffee'} - {repo: 'first-mate', file: 'src/grammar.coffee'} - {repo: 'first-mate', file: 'src/grammar-registry.coffee'} - {repo: 'node-pathwatcher', file: 'src/directory.coffee'} - {repo: 'node-pathwatcher', file: 'src/file.coffee'} - {repo: 'space-pen', file: 'src/space-pen.coffee'} - {repo: 'text-buffer', file: 'src/marker.coffee'} - {repo: 'text-buffer', file: 'src/point.coffee'} - {repo: 'text-buffer', file: 'src/range.coffee'} - {repo: 'text-buffer', file: 'src/text-buffer.coffee'} - {repo: 'theorist', file: 'src/model.coffee'} - ] - - async.map(includes, downloadFileFromRepo, callback) + apiJson = JSON.stringify(api, null, 2) + apiJsonPath = path.join(docsOutputDir, 'api.json') + grunt.file.write(apiJsonPath, apiJson) diff --git a/build/tasks/mkdeb-task.coffee b/build/tasks/mkdeb-task.coffee index ccf20b790..bf563582d 100644 --- a/build/tasks/mkdeb-task.coffee +++ b/build/tasks/mkdeb-task.coffee @@ -13,8 +13,16 @@ module.exports = (grunt) -> grunt.file.write(outputPath, filled) outputPath + getInstalledSize = (buildDir, callback) -> + cmd = 'du' + args = ['-sk', path.join(buildDir, 'Atom')] + spawn {cmd, args}, (error, {stdout}) -> + installedSize = stdout.split(/\s+/)?[0] or '200000' # default to 200MB + callback(null, installedSize) + grunt.registerTask 'mkdeb', 'Create debian package', -> done = @async() + buildDir = grunt.config.get('atom.buildDir') if process.arch is 'ia32' arch = 'i386' @@ -28,13 +36,17 @@ module.exports = (grunt) -> maintainer = 'GitHub ' installDir = '/usr' iconName = 'atom' - data = {name, version, description, section, arch, maintainer, installDir, iconName} + getInstalledSize buildDir, (error, installedSize) -> + data = {name, version, description, section, arch, maintainer, installDir, iconName, installedSize} + controlFilePath = fillTemplate(path.join('resources', 'linux', 'debian', 'control'), data) + desktopFilePath = fillTemplate(path.join('resources', 'linux', 'Atom.desktop'), data) + icon = path.join('resources', 'atom.png') - controlFilePath = fillTemplate(path.join('resources', 'linux', 'debian', 'control'), data) - desktopFilePath = fillTemplate(path.join('resources', 'linux', 'Atom.desktop'), data) - icon = path.join('resources', 'atom.png') - buildDir = grunt.config.get('atom.buildDir') - - cmd = path.join('script', 'mkdeb') - args = [version, arch, controlFilePath, desktopFilePath, icon, buildDir] - spawn({cmd, args}, done) + cmd = path.join('script', 'mkdeb') + args = [version, arch, controlFilePath, desktopFilePath, icon, buildDir] + spawn {cmd, args}, (error) -> + if error? + done(error) + else + grunt.log.ok "Created #{buildDir}/atom-#{version}-#{arch}.deb" + done() diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 61738b2f6..69ba1b20b 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -17,6 +17,7 @@ defaultHeaders = module.exports = (gruntObject) -> grunt = gruntObject + {cp} = require('./task-helpers')(grunt) grunt.registerTask 'publish-build', 'Publish the built app', -> return if process.env.JANKY_SHA1 and process.env.JANKY_BRANCH isnt 'master' @@ -24,8 +25,10 @@ module.exports = (gruntObject) -> tasks.unshift('build-docs', 'prepare-docs') if process.platform is 'darwin' grunt.task.run(tasks) - grunt.registerTask 'prepare-docs', 'Move the build docs to the build dir', -> - fs.copySync(grunt.config.get('docsOutputDir'), path.join(grunt.config.get('atom.buildDir'), 'atom-docs')) + grunt.registerTask 'prepare-docs', 'Move api.json to atom-api.json', -> + docsOutputDir = grunt.config.get('docsOutputDir') + buildDir = grunt.config.get('atom.buildDir') + cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json') grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', -> done = @async() @@ -46,7 +49,7 @@ getAssets = -> [ {assetName: 'atom-mac.zip', sourcePath: 'Atom.app'} {assetName: 'atom-mac-symbols.zip', sourcePath: 'Atom.breakpad.syms'} - {assetName: 'atom-docs.zip', sourcePath: 'atom-docs'} + {assetName: 'atom-api.json', sourcePath: 'atom-api.json'} ] else [ @@ -70,7 +73,7 @@ zipAssets = (buildDir, assets, callback) -> callback(error) tasks = [] - for {assetName, sourcePath} in assets + for {assetName, sourcePath} in assets when path.extname(assetName) is '.zip' fs.removeSync(path.join(buildDir, assetName)) tasks.push(zip.bind(this, buildDir, sourcePath, assetName)) async.parallel(tasks, callback) diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 1d2355f2d..5ddd0275a 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -2,7 +2,6 @@ fs = require 'fs' path = require 'path' _ = require 'underscore-plus' - async = require 'async' module.exports = (grunt) -> @@ -10,18 +9,27 @@ module.exports = (grunt) -> packageSpecQueue = null + getAppPath = -> + contentsDir = grunt.config.get('atom.contentsDir') + switch process.platform + when 'darwin' + path.join(contentsDir, 'MacOS', 'Atom') + when 'linux' + path.join(contentsDir, 'atom') + when 'win32' + path.join(contentsDir, 'atom.exe') + runPackageSpecs = (callback) -> failedPackages = [] rootDir = grunt.config.get('atom.shellAppDir') - contentsDir = grunt.config.get('atom.contentsDir') resourcePath = process.cwd() - if process.platform is 'darwin' - appPath = path.join(contentsDir, 'MacOS', 'Atom') - else if process.platform is 'win32' - appPath = path.join(contentsDir, 'atom.exe') + appPath = getAppPath() + + # Ensure application is executable on Linux + fs.chmodSync(appPath, '755') if process.platform is 'linux' packageSpecQueue = async.queue (packagePath, callback) -> - if process.platform is 'darwin' + if process.platform in ['darwin', 'linux'] options = cmd: appPath args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}"] @@ -57,15 +65,11 @@ module.exports = (grunt) -> packageSpecQueue.drain = -> callback(null, failedPackages) runCoreSpecs = (callback) -> - contentsDir = grunt.config.get('atom.contentsDir') - if process.platform is 'darwin' - appPath = path.join(contentsDir, 'MacOS', 'Atom') - else if process.platform is 'win32' - appPath = path.join(contentsDir, 'atom.exe') + appPath = getAppPath() resourcePath = process.cwd() coreSpecsPath = path.resolve('spec') - if process.platform is 'darwin' + if process.platform in ['darwin', 'linux'] options = cmd: appPath args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}"] @@ -90,7 +94,7 @@ module.exports = (grunt) -> # TODO: This should really be parallel on both platforms, however our # fixtures step on each others toes currently. - if process.platform is 'darwin' + if process.platform in ['darwin', 'linux'] method = async.parallel else if process.platform is 'win32' method = async.series diff --git a/exports/atom.coffee b/exports/atom.coffee index a536de48d..985fd4782 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -15,10 +15,7 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.$ = $ module.exports.$$ = $$ module.exports.$$$ = $$$ - if atom.config.get('core.useReactMiniEditors') - module.exports.EditorView = require '../src/react-editor-view' - else - module.exports.EditorView = require '../src/editor-view' + module.exports.EditorView = require '../src/editor-view' module.exports.ScrollView = require '../src/scroll-view' module.exports.SelectListView = require '../src/select-list-view' module.exports.Task = require '../src/task' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index ed0e49eb6..b6ca5b1f1 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -14,6 +14,7 @@ 'F11': 'window:toggle-full-screen' # Sublime Parity + 'ctrl-,': 'application:show-settings' 'ctrl-N': 'application:new-window' 'ctrl-W': 'window:close' 'ctrl-o': 'application:open-file' diff --git a/menus/linux.cson b/menus/linux.cson index 1b736fea3..6c6a14c89 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -8,8 +8,6 @@ { label: 'Open Folder...', command: 'application:open-folder' } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } - { label: '&Preferences...', command: 'application:show-settings' } - { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As...', command: 'core:save-as' } { label: 'Save A&ll', command: 'window:save-all' } @@ -80,6 +78,13 @@ { label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' } ] } + { type: 'separator' } + { label: '&Preferences', command: 'application:show-settings' } + { label: 'Open Your Config', command: 'application:open-your-config' } + { label: 'Open Your Init Script', command: 'application:open-your-init-script' } + { label: 'Open Your Keymap', command: 'application:open-your-keymap' } + { label: 'Open Your Snippets', command: 'application:open-your-snippets' } + { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } ] } diff --git a/package.json b/package.json index 9fc983ea1..28bbf5e6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.124.0", + "version": "0.125.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -20,14 +20,14 @@ "atomShellVersion": "0.15.9", "dependencies": { "async": "0.2.6", - "atom-keymap": "^2.0.2", + "atom-keymap": "^2.0.5", "bootstrap": "git+https://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372", "clear-cut": "0.4.0", "coffee-script": "1.7.0", "coffeestack": "0.7.0", "delegato": "^1", "emissary": "^1.2.2", - "first-mate": "^2.0.1", + "first-mate": "^2.0.5", "fs-plus": "^2.2.6", "fstream": "0.1.24", "fuzzaldrin": "^1.1", @@ -39,9 +39,9 @@ "mixto": "^1", "mkdirp": "0.3.5", "nslog": "^1.0.1", - "oniguruma": "^3.0.3", + "oniguruma": "^3.0.4", "optimist": "0.4.0", - "pathwatcher": "^2.0.7", + "pathwatcher": "^2.0.10", "property-accessors": "^1", "q": "^1.0.1", "random-words": "0.0.1", @@ -54,10 +54,10 @@ "season": "^1.0.2", "semver": "1.1.4", "serializable": "^1", - "space-pen": "3.4.1", + "space-pen": "3.4.6", "temp": "0.7.0", - "text-buffer": "^3.0.1", - "theorist": "^1", + "text-buffer": "^3.0.4", + "theorist": "^1.0.2", "underscore-plus": "^1.5.1", "vm-compatibility-layer": "0.1.0" }, @@ -74,39 +74,39 @@ "autocomplete": "0.31.0", "autoflow": "0.18.0", "autosave": "0.15.0", - "background-tips": "0.15.0", - "bookmarks": "0.27.0", + "background-tips": "0.16.0", + "bookmarks": "0.28.0", "bracket-matcher": "0.54.0", "command-palette": "0.24.0", "deprecation-cop": "0.9.0", "dev-live-reload": "0.34.0", "exception-reporting": "0.20.0", "feedback": "0.33.0", - "find-and-replace": "0.129.0", + "find-and-replace": "0.131.0", "fuzzy-finder": "0.57.0", - "git-diff": "0.38.0", - "go-to-line": "0.24.0", + "git-diff": "0.39.0", + "go-to-line": "0.25.0", "grammar-selector": "0.29.0", "image-view": "0.36.0", "incompatible-packages": "0.9.0", "keybinding-resolver": "0.19.0", "link": "0.25.0", - "markdown-preview": "0.99.0", + "markdown-preview": "0.100.0", "metrics": "0.33.0", "open-on-github": "0.30.0", "package-generator": "0.31.0", "release-notes": "0.36.0", - "settings-view": "0.140.0", + "settings-view": "0.141.0", "snippets": "0.51.0", "spell-check": "0.42.0", - "status-bar": "0.43.0", + "status-bar": "0.44.0", "styleguide": "0.30.0", "symbols-view": "0.63.0", - "tabs": "0.49.0", + "tabs": "0.50.0", "timecop": "0.22.0", "tree-view": "0.112.0", "update-package-dependencies": "0.6.0", - "welcome": "0.17.0", + "welcome": "0.18.0", "whitespace": "0.25.0", "wrap-guide": "0.21.0", @@ -123,7 +123,7 @@ "language-json": "0.8.0", "language-less": "0.14.0", "language-make": "0.12.0", - "language-mustache": "0.9.0", + "language-mustache": "0.10.0", "language-objective-c": "0.11.0", "language-perl": "0.9.0", "language-php": "0.15.0", @@ -131,7 +131,7 @@ "language-python": "0.18.0", "language-ruby": "0.35.0", "language-ruby-on-rails": "0.18.0", - "language-sass": "0.19.0", + "language-sass": "0.20.0", "language-shellscript": "0.8.0", "language-source": "0.8.0", "language-sql": "0.10.0", @@ -139,7 +139,7 @@ "language-todo": "0.10.0", "language-toml": "0.12.0", "language-xml": "0.18.0", - "language-yaml": "0.16.0" + "language-yaml": "0.17.0" }, "private": true, "scripts": { diff --git a/resources/linux/debian/control.in b/resources/linux/debian/control.in index cf670e62b..5d8812124 100644 --- a/resources/linux/debian/control.in +++ b/resources/linux/debian/control.in @@ -3,6 +3,6 @@ Version: <%= version %> Section: <%= section %> Priority: optional Architecture: <%= arch %> -Installed-Size: `du -ks usr|cut -f 1` +Installed-Size: <%= installedSize %> Maintainer: <%= maintainer %> Description: <%= description %> diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index ce85951d9..4dc934587 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -493,13 +493,14 @@ describe "the `atom` global", -> expect(atom.config.get('core.disabledPackages')).toContain packageName describe "with themes", -> + reloadedHandler = null + beforeEach -> waitsForPromise -> atom.themes.activateThemes() afterEach -> atom.themes.deactivateThemes() - atom.config.unobserve('core.themes') it ".enablePackage() and .disablePackage() enables and disables a theme", -> packageName = 'theme-with-package-file' @@ -517,13 +518,17 @@ describe "the `atom` global", -> expect(atom.config.get('core.themes')).toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName - # disabling of theme + reloadedHandler = jasmine.createSpy('reloadedHandler') + reloadedHandler.reset() + atom.themes.on('reloaded', reloadedHandler) + pack = atom.packages.disablePackage(packageName) waitsFor -> - not (pack in atom.packages.getActivePackages()) + reloadedHandler.callCount is 1 runs -> + expect(atom.packages.getActivePackages()).not.toContain pack expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.themes')).not.toContain packageName expect(atom.config.get('core.disabledPackages')).not.toContain packageName diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 1fbbbe873..34b75f54f 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1037,6 +1037,19 @@ describe "DisplayBuffer", -> expect(start.top).toBe 5 * 20 expect(start.left).toBe (4 * 10) + (6 * 11) + describe 'when there are multiple DisplayBuffers for a buffer', -> + describe 'when a marker is created', -> + it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', -> + displayBuffer2 = new DisplayBuffer({buffer, tabLength}) + displayBuffer.on 'marker-created', markerCreated1 = jasmine.createSpy().andCallFake (marker) -> + marker.destroy() + displayBuffer2.on 'marker-created', markerCreated2 = jasmine.createSpy() + + displayBuffer.markBufferRange([[0, 0], [1, 5]], {}) + + expect(markerCreated1).toHaveBeenCalled() + expect(markerCreated2).not.toHaveBeenCalled() + describe "decorations", -> [marker, decoration, decorationParams] = [] beforeEach -> diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index e93afa445..d80396d23 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1,7 +1,7 @@ _ = require 'underscore-plus' {extend, flatten, toArray, last} = _ -ReactEditorView = require '../src/react-editor-view' +EditorView = require '../src/editor-view' EditorComponent = require '../src/editor-component' nbsp = String.fromCharCode(160) @@ -34,7 +34,7 @@ describe "EditorComponent", -> contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' - wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) + wrapperView = new EditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() wrapperNode = wrapperView.element @@ -1911,7 +1911,7 @@ describe "EditorComponent", -> hiddenParent.style.display = 'none' contentNode.appendChild(hiddenParent) - wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) + wrapperView = new EditorView(editor, {lineOverdrawMargin}) wrapperNode = wrapperView.element wrapperView.appendTo(hiddenParent) @@ -1953,6 +1953,7 @@ describe "EditorComponent", -> wrapperView.hide() component.setFontSize(22) + editor.getBuffer().insert([0, 0], 'a') # regression test against atom/atom#3318 wrapperView.show() editor.setCursorBufferPosition([0, Infinity]) @@ -2164,6 +2165,12 @@ describe "EditorComponent", -> setEditorWidthInChars(wrapperView, 10) expect(componentNode.querySelector('.scroll-view').offsetWidth).toBe charWidth * 10 + describe "grammar data attributes", -> + it "adds and updates the grammar data attribute based on the current grammar", -> + expect(wrapperNode.dataset.grammar).toBe 'source js' + editor.setGrammar(atom.syntax.nullGrammar) + expect(wrapperNode.dataset.grammar).toBe 'text plain null-grammar' + buildMouseEvent = (type, properties...) -> properties = extend({bubbles: true, cancelable: true}, properties...) properties.detail ?= 1 diff --git a/spec/editor-view-spec.coffee b/spec/editor-view-spec.coffee deleted file mode 100644 index 467266023..000000000 --- a/spec/editor-view-spec.coffee +++ /dev/null @@ -1,3047 +0,0 @@ -WorkspaceView = require '../src/workspace-view' -EditorView = require '../src/editor-view' -{$, $$} = require '../src/space-pen-extensions' -_ = require 'underscore-plus' -fs = require 'fs-plus' -path = require 'path' -temp = require 'temp' - -describe "EditorView", -> - [buffer, editorView, editor, cachedEditor, cachedLineHeight, cachedCharWidth, fart] = [] - - beforeEach -> - atom.config.set 'core.useReactEditor', false - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - waitsForPromise -> - atom.workspace.open('sample.less').then (o) -> cachedEditor = o - - runs -> - buffer = editor.buffer - editorView = new EditorView(editor) - editorView.lineOverdraw = 2 - editorView.isFocused = true - editorView.enableKeymap() - - editorView.calculateHeightInLines = -> - Math.ceil(@height() / @lineHeight) - - editorView.attachToDom = ({ heightInLines, widthInChars } = {}) -> - heightInLines ?= @getEditor().getBuffer().getLineCount() - @height(getLineHeight() * heightInLines) - @width(getCharWidth() * widthInChars) if widthInChars - $('#jasmine-content').append(this) - - waitsForPromise -> - atom.packages.activatePackage('language-text') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - getLineHeight = -> - return cachedLineHeight if cachedLineHeight? - calcDimensions() - cachedLineHeight - - getCharWidth = -> - return cachedCharWidth if cachedCharWidth? - calcDimensions() - cachedCharWidth - - calcDimensions = -> - editorForMeasurement = new EditorView({editor: cachedEditor}) - editorForMeasurement.attachToDom() - cachedLineHeight = editorForMeasurement.lineHeight - cachedCharWidth = editorForMeasurement.charWidth - editorForMeasurement.remove() - - describe "construction", -> - it "throws an error if no edit session is given", -> - expect(-> new EditorView).toThrow() - - describe "when the editor view view is attached to the dom", -> - it "calculates line height and char width and updates the pixel position of the cursor", -> - expect(editorView.lineHeight).toBeNull() - expect(editorView.charWidth).toBeNull() - editor.setCursorScreenPosition(row: 2, column: 2) - - editorView.attachToDom() - - expect(editorView.lineHeight).not.toBeNull() - expect(editorView.charWidth).not.toBeNull() - expect(editorView.find('.cursor').offset()).toEqual pagePixelPositionForPoint(editorView, [2, 2]) - - it "is focused", -> - editorView.attachToDom() - expect(editorView).toMatchSelector ":has(:focus)" - - describe "when the editor view view receives focus", -> - it "focuses the hidden input", -> - editorView.attachToDom() - editorView.focus() - expect(editorView).not.toMatchSelector ':focus' - expect(editorView.hiddenInput).toMatchSelector ':focus' - - it "does not scroll the editor view (regression)", -> - editorView.attachToDom(heightInLines: 2) - editor.selectAll() - editorView.hiddenInput.blur() - editorView.focus() - - expect(editorView.hiddenInput).toMatchSelector ':focus' - expect($(editorView[0]).scrollTop()).toBe 0 - expect($(editorView.scrollView[0]).scrollTop()).toBe 0 - - editor.moveCursorToBottom() - editorView.hiddenInput.blur() - editorView.scrollTop(0) - editorView.focus() - - expect(editorView.hiddenInput).toMatchSelector ':focus' - expect($(editorView[0]).scrollTop()).toBe 0 - expect($(editorView.scrollView[0]).scrollTop()).toBe 0 - - describe "when the hidden input is focused / unfocused", -> - it "assigns the isFocused flag on the editor view view and also adds/removes the .focused css class", -> - editorView.attachToDom() - editorView.isFocused = false - editorView.hiddenInput.focus() - expect(editorView.isFocused).toBeTruthy() - - editorView.hiddenInput.focusout() - expect(editorView.isFocused).toBeFalsy() - - describe "when the editor's file is modified on disk", -> - it "triggers an alert", -> - fileChangeHandler = null - filePath = path.join(temp.dir, 'atom-changed-file.txt') - fs.writeFileSync(filePath, "") - - waitsForPromise -> - atom.workspace.open(filePath).then (o) -> editor = o - - runs -> - editorView.edit(editor) - editor.insertText("now the buffer is modified") - - fileChangeHandler = jasmine.createSpy('fileChange') - editor.buffer.file.on 'contents-changed', fileChangeHandler - - spyOn(atom, "confirm") - - fs.writeFileSync(filePath, "a file change") - - waitsFor "file to trigger contents-changed event", -> - fileChangeHandler.callCount > 0 - - runs -> - expect(atom.confirm).toHaveBeenCalled() - - describe ".remove()", -> - it "destroys the edit session", -> - editorView.remove() - expect(editorView.editor.isDestroyed()).toBe true - - describe ".edit(editor)", -> - [newEditor, newBuffer] = [] - - beforeEach -> - waitsForPromise -> - atom.workspace.open('two-hundred.txt').then (o) -> newEditor = o - - runs -> - newBuffer = newEditor.buffer - - it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", -> - editorView.attachToDom(heightInLines: 5, widthInChars: 30) - editor.setCursorBufferPosition([6, 13]) - editorView.scrollToBottom() - editorView.scrollLeft(150) - previousScrollHeight = editorView.verticalScrollbar.prop('scrollHeight') - previousScrollTop = editorView.scrollTop() - previousScrollLeft = editorView.scrollLeft() - - newEditor.setScrollTop(900) - newEditor.setSelectedBufferRange([[40, 0], [43, 1]]) - - editorView.edit(newEditor) - { firstRenderedScreenRow, lastRenderedScreenRow } = editorView - expect(editorView.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow) - expect(editorView.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editorView.lastRenderedScreenRow) - expect(editorView.scrollTop()).toBe 900 - expect(editorView.scrollLeft()).toBe 0 - expect(editorView.getSelectionView().regions[0].position().top).toBe 40 * editorView.lineHeight - newEditor.insertText("hello") - expect(editorView.lineElementForScreenRow(40).text()).toBe "hello3" - - editorView.edit(editor) - { firstRenderedScreenRow, lastRenderedScreenRow } = editorView - expect(editorView.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) - expect(editorView.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editorView.lastRenderedScreenRow) - expect(editorView.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight - expect(editorView.scrollTop()).toBe previousScrollTop - expect(editorView.scrollLeft()).toBe previousScrollLeft - expect(editorView.getCursorView().position()).toEqual { top: 6 * editorView.lineHeight, left: 13 * editorView.charWidth } - editor.insertText("goodbye") - expect(editorView.lineElementForScreenRow(6).text()).toMatch /^ currentgoodbye/ - - it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> - contentsConflictedHandler = null - filePath = path.join(temp.dir, 'atom-changed-file.txt') - fs.writeFileSync(filePath, "") - tempEditor = null - - waitsForPromise -> - atom.workspace.open(filePath).then (o) -> tempEditor = o - - runs -> - editorView.edit(tempEditor) - tempEditor.insertText("a buffer change") - - spyOn(atom, "confirm") - - contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler") - tempEditor.on 'contents-conflicted', contentsConflictedHandler - fs.writeFileSync(filePath, "a file change") - - waitsFor -> - contentsConflictedHandler.callCount > 0 - - runs -> - expect(atom.confirm).toHaveBeenCalled() - - describe ".scrollTop(n)", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5) - expect(editorView.verticalScrollbar.scrollTop()).toBe 0 - - describe "when called with a scroll top argument", -> - it "sets the scrollTop of the vertical scrollbar and sets scrollTop on the line numbers and lines", -> - editorView.scrollTop(100) - expect(editorView.verticalScrollbar.scrollTop()).toBe 100 - expect(editorView.scrollView.scrollTop()).toBe 0 - expect(editorView.renderedLines.css('top')).toBe "-100px" - expect(editorView.gutter.lineNumbers.css('top')).toBe "-100px" - - editorView.scrollTop(120) - expect(editorView.verticalScrollbar.scrollTop()).toBe 120 - expect(editorView.scrollView.scrollTop()).toBe 0 - expect(editorView.renderedLines.css('top')).toBe "-120px" - expect(editorView.gutter.lineNumbers.css('top')).toBe "-120px" - - it "does not allow negative scrollTops to be assigned", -> - editorView.scrollTop(-100) - expect(editorView.scrollTop()).toBe 0 - - it "doesn't do anything if the scrollTop hasn't changed", -> - editorView.scrollTop(100) - spyOn(editorView.verticalScrollbar, 'scrollTop') - spyOn(editorView.renderedLines, 'css') - spyOn(editorView.gutter.lineNumbers, 'css') - - editorView.scrollTop(100) - expect(editorView.verticalScrollbar.scrollTop).not.toHaveBeenCalled() - expect(editorView.renderedLines.css).not.toHaveBeenCalled() - expect(editorView.gutter.lineNumbers.css).not.toHaveBeenCalled() - - describe "when the 'adjustVerticalScrollbar' option is false (defaults to true)", -> - it "doesn't adjust the scrollTop of the vertical scrollbar", -> - editorView.scrollTop(100, adjustVerticalScrollbar: false) - expect(editorView.verticalScrollbar.scrollTop()).toBe 0 - expect(editorView.renderedLines.css('top')).toBe "-100px" - expect(editorView.gutter.lineNumbers.css('top')).toBe "-100px" - - describe "when called with no argument", -> - it "returns the last assigned value or 0 if none has been assigned", -> - expect(editorView.scrollTop()).toBe 0 - editorView.scrollTop(50) - expect(editorView.scrollTop()).toBe 50 - - it "sets the new scroll top position on the active edit session", -> - expect(editorView.editor.getScrollTop()).toBe 0 - editorView.scrollTop(123) - expect(editorView.editor.getScrollTop()).toBe 123 - - describe ".scrollHorizontally(pixelPosition)", -> - it "sets the new scroll left position on the active edit session", -> - editorView.attachToDom(heightInLines: 5) - setEditorWidthInChars(editorView, 5) - expect(editorView.editor.getScrollLeft()).toBe 0 - editorView.scrollHorizontally(left: 50) - expect(editorView.editor.getScrollLeft()).toBeGreaterThan 0 - expect(editorView.editor.getScrollLeft()).toBe editorView.scrollLeft() - - describe "editor:attached event", -> - it 'only triggers an editor:attached event when it is first added to the DOM', -> - openHandler = jasmine.createSpy('openHandler') - editorView.on 'editor:attached', openHandler - - editorView.attachToDom() - expect(openHandler).toHaveBeenCalled() - [event, eventEditor] = openHandler.argsForCall[0] - expect(eventEditor).toBe editorView - - openHandler.reset() - editorView.attachToDom() - expect(openHandler).not.toHaveBeenCalled() - - describe "editor:path-changed event", -> - filePath = null - - beforeEach -> - filePath = path.join(temp.dir, 'something.txt') - fs.writeFileSync(filePath, filePath) - - afterEach -> - fs.removeSync(filePath) if fs.existsSync(filePath) - - it "emits event when buffer's path is changed", -> - eventHandler = jasmine.createSpy('eventHandler') - editorView.on 'editor:path-changed', eventHandler - editor.saveAs(filePath) - expect(eventHandler).toHaveBeenCalled() - - it "emits event when editor view view receives a new buffer", -> - eventHandler = jasmine.createSpy('eventHandler') - editorView.on 'editor:path-changed', eventHandler - waitsForPromise -> - atom.workspace.open(filePath).then (editor) -> - editorView.edit(editor) - - runs -> - expect(eventHandler).toHaveBeenCalled() - - it "stops listening to events on previously set buffers", -> - eventHandler = jasmine.createSpy('eventHandler') - oldBuffer = editor.getBuffer() - newEditor = null - - waitsForPromise -> - atom.workspace.open(filePath).then (o) -> newEditor = o - - runs -> - editorView.on 'editor:path-changed', eventHandler - - editorView.edit(newEditor) - expect(eventHandler).toHaveBeenCalled() - - eventHandler.reset() - oldBuffer.saveAs(path.join(temp.dir, 'atom-bad.txt')) - expect(eventHandler).not.toHaveBeenCalled() - - eventHandler.reset() - newEditor.getBuffer().saveAs(path.join(temp.dir, 'atom-new.txt')) - expect(eventHandler).toHaveBeenCalled() - - it "loads the grammar for the new path", -> - expect(editor.getGrammar().name).toBe 'JavaScript' - editor.getBuffer().saveAs(filePath) - expect(editor.getGrammar().name).toBe 'Plain Text' - - describe "font family", -> - beforeEach -> - expect(editorView.css('font-family')).toBe 'Courier' - - it "when there is no config in fontFamily don't set it", -> - atom.config.set('editor.fontFamily', null) - expect(editorView.css('font-family')).toBe '' - - describe "when the font family changes", -> - [fontFamily] = [] - - beforeEach -> - if process.platform is 'darwin' - fontFamily = "PCMyungjo" - else - fontFamily = "Consolas" - - it "updates the font family of editors and recalculates dimensions critical to cursor positioning", -> - editorView.attachToDom(12) - lineHeightBefore = editorView.lineHeight - charWidthBefore = editorView.charWidth - editor.setCursorScreenPosition [5, 6] - - atom.config.set("editor.fontFamily", fontFamily) - expect(editorView.css('font-family')).toBe fontFamily - expect(editorView.charWidth).not.toBe charWidthBefore - expect(editorView.getCursorView().position()).toEqual { top: 5 * editorView.lineHeight, left: 6 * editorView.charWidth } - - newEditor = new EditorView(editorView.editor.copy()) - newEditor.attachToDom() - expect(newEditor.css('font-family')).toBe fontFamily - - describe "font size", -> - beforeEach -> - expect(editorView.css('font-size')).not.toBe "20px" - expect(editorView.css('font-size')).not.toBe "10px" - - it "sets the initial font size based on the value from config", -> - expect(editorView.css('font-size')).toBe "#{atom.config.get('editor.fontSize')}px" - - describe "when the font size changes", -> - it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", -> - atom.config.set("editor.fontSize", 10) - editorView.attachToDom() - lineHeightBefore = editorView.lineHeight - charWidthBefore = editorView.charWidth - editor.setCursorScreenPosition [5, 6] - - atom.config.set("editor.fontSize", 30) - expect(editorView.css('font-size')).toBe '30px' - expect(editorView.lineHeight).toBeGreaterThan lineHeightBefore - expect(editorView.charWidth).toBeGreaterThan charWidthBefore - expect(editorView.getCursorView().position()).toEqual { top: 5 * editorView.lineHeight, left: 6 * editorView.charWidth } - expect(editorView.renderedLines.outerHeight()).toBe buffer.getLineCount() * editorView.lineHeight - expect(editorView.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editorView.lineHeight - - newEditor = new EditorView(editorView.editor.copy()) - editorView.remove() - newEditor.attachToDom() - expect(newEditor.css('font-size')).toBe '30px' - - it "updates the position and size of selection regions", -> - atom.config.set("editor.fontSize", 10) - editor.setSelectedBufferRange([[5, 2], [5, 7]]) - editorView.attachToDom() - - atom.config.set("editor.fontSize", 30) - selectionRegion = editorView.find('.region') - expect(selectionRegion.position().top).toBe 5 * editorView.lineHeight - expect(selectionRegion.position().left).toBe 2 * editorView.charWidth - expect(selectionRegion.height()).toBe editorView.lineHeight - expect(selectionRegion.width()).toBe 5 * editorView.charWidth - - it "updates lines if there are unrendered lines", -> - editorView.attachToDom(heightInLines: 5) - originalLineCount = editorView.renderedLines.find(".line").length - expect(originalLineCount).toBeGreaterThan 0 - - atom.config.set("editor.fontSize", 10) - expect(editorView.renderedLines.find(".line").length).toBeGreaterThan originalLineCount - - describe "when the font size changes while editor view view is detached", -> - it "redraws the editor view view according to the new font size when it is reattached", -> - editor.setCursorScreenPosition([4, 2]) - editorView.attachToDom() - initialLineHeight = editorView.lineHeight - initialCharWidth = editorView.charWidth - initialCursorPosition = editorView.getCursorView().position() - initialScrollbarHeight = editorView.verticalScrollbarContent.height() - editorView.detach() - - atom.config.set("editor.fontSize", 10) - expect(editorView.lineHeight).toBe initialLineHeight - expect(editorView.charWidth).toBe initialCharWidth - - editorView.attachToDom() - expect(editorView.lineHeight).not.toBe initialLineHeight - expect(editorView.charWidth).not.toBe initialCharWidth - expect(editorView.getCursorView().position()).not.toEqual initialCursorPosition - expect(editorView.verticalScrollbarContent.height()).not.toBe initialScrollbarHeight - - describe "mouse events", -> - beforeEach -> - editorView.attachToDom() - editorView.css(position: 'absolute', top: 10, left: 10, width: 400) - - describe "single-click", -> - it "re-positions the cursor to the clicked row / column", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 10]) - expect(editor.getCursorScreenPosition()).toEqual(row: 3, column: 10) - - describe "when the lines are scrolled to the right", -> - it "re-positions the cursor on the clicked location", -> - setEditorWidthInChars(editorView, 30) - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 30]) # scrolls lines to the right - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 50]) - expect(editor.getCursorBufferPosition()).toEqual(row: 3, column: 50) - - describe "when the editor view view is using a variable-width font", -> - beforeEach -> - editorView.setFontFamily('sans-serif') - - it "positions the cursor to the clicked row and column", -> - {top, left} = editorView.pixelOffsetForScreenPosition([3, 30]) - editorView.renderedLines.trigger mousedownEvent(pageX: left, pageY: top) - expect(editor.getCursorScreenPosition()).toEqual [3, 30] - - describe "double-click", -> - it "selects the word under the cursor, and expands the selection wordwise in either direction on a subsequent shift-click", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 24], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 24], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "concat" - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [8, 7], shiftKey: true) - editorView.renderedLines.trigger 'mouseup' - - expect(editor.getSelectedText()).toBe "return sort(left).concat" - - it "stops selecting by word when the selection is emptied", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "quicksort" - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 10]) - editorView.renderedLines.trigger 'mouseup' - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 12], originalEvent: {detail: 1}, shiftKey: true) - expect(editor.getSelectedBufferRange()).toEqual [[3, 10], [3, 12]] - - it "stops selecting by word when another selection is made", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "quicksort" - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 10]) - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [3, 12], which: 1) - editorView.renderedLines.trigger 'mouseup' - - expect(editor.getSelectedBufferRange()).toEqual [[3, 10], [3, 12]] - - describe "when double-clicking between a word and a non-word", -> - it "selects the word", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 21], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 21], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "function" - - editor.setCursorBufferPosition([0, 0]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 22], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 22], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "items" - - editor.setCursorBufferPosition([0, 0]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 28], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 28], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe "{" - - describe "when double-clicking on whitespace", -> - it "selects all adjacent whitespace", -> - editor.setText(" some text ") - editor.setCursorBufferPosition([0, 2]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 2], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 2], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - editor.setCursorBufferPosition([0, 8]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 7], [0, 9]] - - editor.setCursorBufferPosition([0, 14]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 14], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 14], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 13], [0, 17]] - - describe "triple/quardruple/etc-click", -> - it "selects the line under the cursor", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - # Triple click - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 3}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" - - # Quad click - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 3}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 3], originalEvent: {detail: 4}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedText()).toBe " if (items.length <= 1) return items;\n" - - it "expands the selection linewise in either direction on a subsequent shift-click, but stops selecting linewise once the selection is emptied", -> - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 2}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 8], originalEvent: {detail: 3}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [1, 8], originalEvent: {detail: 1}, shiftKey: true) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [5, 0]] - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [2, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelection().isEmpty()).toBeTruthy() - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [3, 8], originalEvent: {detail: 1}, shiftKey: true) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[2, 8], [3, 8]] - - describe "shift-click", -> - it "selects from the cursor's current location to the clicked location", -> - editor.setCursorScreenPosition([4, 7]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true) - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "shift-double-click", -> - it "expands the selection on the first click and ignores the second click", -> - editor.setCursorScreenPosition([4, 7]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "shift-triple-click", -> - it "expands the selection on the first click and ignores the second click", -> - editor.setCursorScreenPosition([4, 7]) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 1 }) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 2 }) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 24], shiftKey: true, originalEvent: { detail: 3 }) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelection().getScreenRange()).toEqual [[4, 7], [5, 24]] - - describe "meta-click", -> - it "places an additional cursor", -> - editorView.attachToDom() - setEditorHeightInLines(editorView, 5) - editor.setCursorBufferPosition([3, 0]) - editorView.scrollTop(editorView.lineHeight * 6) - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [6, 0], metaKey: true) - expect(editorView.scrollTop()).toBe editorView.lineHeight * (6 - editorView.vScrollMargin) - - [cursor1, cursor2] = editorView.getCursorViews() - expect(cursor1.position()).toEqual(top: 3 * editorView.lineHeight, left: 0) - expect(cursor1.getBufferPosition()).toEqual [3, 0] - expect(cursor2.position()).toEqual(top: 6 * editorView.lineHeight, left: 0) - expect(cursor2.getBufferPosition()).toEqual [6, 0] - - describe "click and drag", -> - it "creates a selection from the initial click to mouse cursor's location ", -> - editorView.attachToDom() - editorView.css(position: 'absolute', top: 10, left: 10) - - # start - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 10]) - - # moving changes selection - $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27], which: 1) - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 5, column: 27}) - expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27) - - # mouse up may occur outside of editorView, but still need to halt selection - $(document).trigger 'mouseup' - - # moving after mouse up should not change selection - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [8, 8]) - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 5, column: 27}) - expect(editor.getCursorScreenPosition()).toEqual(row: 5, column: 27) - - it "selects and scrolls if the mouse is dragged outside of the editor view view itself", -> - editorView.vScrollMargin = 0 - editorView.attachToDom(heightInLines: 5) - editorView.scrollToBottom() - - spyOn(window, 'setInterval').andCallFake -> - - # start - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [12, 0]) - originalScrollTop = editorView.scrollTop() - - # moving changes selection - $(document).trigger mousemoveEvent(editorView: editorView, pageX: 0, pageY: -1, which: 1) - expect(editorView.scrollTop()).toBe originalScrollTop - editorView.lineHeight - - # every mouse move selects more text - for x in [0..10] - $(document).trigger mousemoveEvent(editorView: editorView, pageX: 0, pageY: -1, which: 1) - - expect(editorView.scrollTop()).toBe 0 - - it "ignores non left-click and drags", -> - editorView.attachToDom() - editorView.css(position: 'absolute', top: 10, left: 10) - - event = mousedownEvent(editorView: editorView, point: [4, 10]) - event.originalEvent.which = 2 - editorView.renderedLines.trigger(event) - $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27], which: 1) - $(document).trigger 'mouseup' - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 4, column: 10}) - - it "ignores ctrl-click and drags", -> - editorView.attachToDom() - editorView.css(position: 'absolute', top: 10, left: 10) - - event = mousedownEvent(editorView: editorView, point: [4, 10]) - event.ctrlKey = true - editorView.renderedLines.trigger(event) - $(document).trigger mousemoveEvent(editorView: editorView, point: [5, 27]) - $(document).trigger 'mouseup' - - range = editor.getSelection().getScreenRange() - expect(range.start).toEqual({row: 4, column: 10}) - expect(range.end).toEqual({row: 4, column: 10}) - - describe "when the editor is hidden", -> - it "stops scrolling the editor", -> - editorView.vScrollMargin = 0 - editorView.attachToDom(heightInLines: 5) - editorView.scrollToBottom() - - spyOn(window, 'setInterval').andCallFake -> - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [12, 0]) - originalScrollTop = editorView.scrollTop() - - $(document).trigger mousemoveEvent(editorView: editorView, pageX: 0, pageY: -1, which: 1) - expect(editorView.scrollTop()).toBe originalScrollTop - editorView.lineHeight - - editorView.hide() - - $(document).trigger mousemoveEvent(editorView: editorView, pageX: 100000, pageY: -1, which: 1) - expect(editorView.scrollTop()).toBe originalScrollTop - editorView.lineHeight - - describe "double-click and drag", -> - it "selects the word under the cursor, then continues to select by word in either direction as the mouse is dragged", -> - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 1}) - editorView.renderedLines.trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [0, 8], originalEvent: {detail: 2}) - expect(editor.getSelectedText()).toBe "quicksort" - - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [1, 8], which: 1) - expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [1, 10]] - expect(editor.getCursorBufferPosition()).toEqual [1, 10] - - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [0, 1], which: 1) - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 13]] - - # shift-clicking still selects by word, but does not preserve the initial range - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [5, 25], originalEvent: {detail: 1}, shiftKey: true) - editorView.renderedLines.trigger 'mouseup' - expect(editor.getSelectedBufferRange()).toEqual [[0, 13], [5, 27]] - - describe "triple-click and drag", -> - it "expands the initial selection linewise in either direction", -> - editorView.attachToDom() - - # triple click - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 1}) - $(document).trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 2}) - $(document).trigger 'mouseup' - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 7], originalEvent: {detail: 3}) - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 0]] - - # moving changes selection linewise - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [5, 27], which: 1) - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [6, 0]] - expect(editor.getCursorBufferPosition()).toEqual [6, 0] - - # moving changes selection linewise - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [2, 27], which: 1) - expect(editor.getSelectedBufferRange()).toEqual [[2, 0], [5, 0]] - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - - # mouse up may occur outside of editorView, but still need to halt selection - $(document).trigger 'mouseup' - - describe "meta-click and drag", -> - it "adds an additional selection", -> - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [4, 10]) - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [5, 27], which: 1) - editorView.renderedLines.trigger 'mouseup' - - editorView.renderedLines.trigger mousedownEvent(editorView: editorView, point: [6, 10], metaKey: true) - editorView.renderedLines.trigger mousemoveEvent(editorView: editorView, point: [8, 27], metaKey: true, which: 1) - editorView.renderedLines.trigger 'mouseup' - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[4, 10], [5, 27]] - expect(selection2.getScreenRange()).toEqual [[6, 10], [8, 27]] - - describe "mousedown on the fold icon of a foldable line number", -> - it "toggles folding on the clicked buffer row", -> - expect(editor.isFoldedAtScreenRow(1)).toBe false - editorView.gutter.find('.line-number:eq(1) .icon-right').mousedown() - expect(editor.isFoldedAtScreenRow(1)).toBe true - editorView.gutter.find('.line-number:eq(1) .icon-right').mousedown() - expect(editor.isFoldedAtScreenRow(1)).toBe false - - describe "when text input events are triggered on the hidden input element", -> - it "inserts the typed character at the cursor position, both in the buffer and the pre element", -> - editorView.attachToDom() - editor.setCursorScreenPosition(row: 1, column: 6) - - expect(buffer.lineForRow(1).charAt(6)).not.toBe 'q' - - editorView.hiddenInput.textInput 'q' - - expect(buffer.lineForRow(1).charAt(6)).toBe 'q' - expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 7) - expect(editorView.renderedLines.find('.line:eq(1)')).toHaveText buffer.lineForRow(1) - - describe "selection rendering", -> - [charWidth, lineHeight, selection, selectionView] = [] - - beforeEach -> - editorView.attachToDom() - editorView.width(500) - { charWidth, lineHeight } = editorView - selection = editor.getSelection() - selectionView = editorView.getSelectionView() - - describe "when a selection is added", -> - it "adds a selection view for it with the proper regions", -> - editorView.editor.addSelectionForBufferRange([[2, 7], [2, 25]]) - selectionViews = editorView.getSelectionViews() - expect(selectionViews.length).toBe 2 - expect(selectionViews[1].regions.length).toBe 1 - region = selectionViews[1].regions[0] - expect(region.position().top).toBeCloseTo(2 * lineHeight) - expect(region.position().left).toBeCloseTo(7 * charWidth) - expect(region.height()).toBeCloseTo lineHeight - expect(region.width()).toBeCloseTo((25 - 7) * charWidth) - - describe "when a selection changes", -> - describe "when the selection is within a single line", -> - it "covers the selection's range with a single region", -> - selection.setBufferRange([[2, 7], [2, 25]]) - - expect(selectionView.regions.length).toBe 1 - region = selectionView.regions[0] - expect(region.position().top).toBeCloseTo(2 * lineHeight) - expect(region.position().left).toBeCloseTo(7 * charWidth) - expect(region.height()).toBeCloseTo lineHeight - expect(region.width()).toBeCloseTo((25 - 7) * charWidth) - - describe "when the selection spans 2 lines", -> - it "covers the selection's range with 2 regions", -> - selection.setBufferRange([[2,7],[3,25]]) - - expect(selectionView.regions.length).toBe 2 - - region1 = selectionView.regions[0] - expect(region1.position().top).toBeCloseTo(2 * lineHeight) - expect(region1.position().left).toBeCloseTo(7 * charWidth) - expect(region1.height()).toBeCloseTo lineHeight - - expect(region1.width()).toBeCloseTo(editorView.renderedLines.outerWidth() - region1.position().left) - region2 = selectionView.regions[1] - expect(region2.position().top).toBeCloseTo(3 * lineHeight) - expect(region2.position().left).toBeCloseTo(0) - expect(region2.height()).toBeCloseTo lineHeight - expect(region2.width()).toBeCloseTo(25 * charWidth) - - describe "when the selection spans more than 2 lines", -> - it "covers the selection's range with 3 regions", -> - selection.setBufferRange([[2,7],[6,25]]) - - expect(selectionView.regions.length).toBe 3 - - region1 = selectionView.regions[0] - expect(region1.position().top).toBeCloseTo(2 * lineHeight) - expect(region1.position().left).toBeCloseTo(7 * charWidth) - expect(region1.height()).toBeCloseTo lineHeight - - expect(region1.width()).toBeCloseTo(editorView.renderedLines.outerWidth() - region1.position().left) - region2 = selectionView.regions[1] - expect(region2.position().top).toBeCloseTo(3 * lineHeight) - expect(region2.position().left).toBeCloseTo(0) - expect(region2.height()).toBeCloseTo(3 * lineHeight) - expect(region2.width()).toBeCloseTo(editorView.renderedLines.outerWidth()) - - # resizes with the editorView - expect(editorView.width()).toBeLessThan(800) - editorView.width(800) - editorView.resize() # call to trigger the resize event. - - region2 = selectionView.regions[1] - expect(region2.width()).toBe(editorView.renderedLines.outerWidth()) - - region3 = selectionView.regions[2] - expect(region3.position().top).toBeCloseTo(6 * lineHeight) - expect(region3.position().left).toBeCloseTo(0) - expect(region3.height()).toBeCloseTo lineHeight - expect(region3.width()).toBeCloseTo(25 * charWidth) - - it "clears previously drawn regions before creating new ones", -> - selection.setBufferRange([[2,7],[4,25]]) - expect(selectionView.regions.length).toBe 3 - expect(selectionView.find('.region').length).toBe 3 - - selectionView.updateDisplay() - expect(selectionView.regions.length).toBe 3 - expect(selectionView.find('.region').length).toBe 3 - - describe "when a selection merges with another selection", -> - it "removes the merged selection view", -> - editor = editorView.editor - editor.setCursorScreenPosition([4, 10]) - editor.selectToScreenPosition([5, 27]) - editor.addCursorAtScreenPosition([3, 10]) - editor.selectToScreenPosition([6, 27]) - - expect(editorView.getSelectionViews().length).toBe 1 - expect(editorView.find('.region').length).toBe 3 - - describe "when a selection is added and removed before the display is updated", -> - it "does not attempt to render the selection", -> - # don't update display until we request it - jasmine.unspy(editorView, 'requestDisplayUpdate') - spyOn(editorView, 'requestDisplayUpdate') - - editor = editorView.editor - selection = editor.addSelectionForBufferRange([[3, 0], [3, 4]]) - selection.destroy() - editorView.updateDisplay() - expect(editorView.getSelectionViews().length).toBe 1 - - describe "when the selection is created with the selectAll event", -> - it "does not scroll to the end of the buffer", -> - editorView.height(150) - editor.selectAll() - expect(editorView.scrollTop()).toBe 0 - - # regression: does not scroll the scroll view when the editorView is refocused - editorView.hiddenInput.blur() - editorView.hiddenInput.focus() - expect(editorView.scrollTop()).toBe 0 - expect(editorView.scrollView.scrollTop()).toBe 0 - - # does autoscroll when the selection is cleared - editor.moveCursorDown() - expect(editorView.scrollTop()).toBeGreaterThan(0) - - describe "selection autoscrolling and highlighting when setting selected buffer range", -> - beforeEach -> - setEditorHeightInLines(editorView, 4) - - describe "if autoscroll is true", -> - it "centers the viewport on the selection if its vertical center is currently offscreen", -> - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editorView.scrollTop()).toBe 0 - - editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true) - expect(editorView.scrollTop()).toBe 5 * editorView.lineHeight - - it "highlights the selection if autoscroll is true", -> - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editorView.getSelectionView()).toHaveClass 'highlighted' - advanceClock(1000) - expect(editorView.getSelectionView()).not.toHaveClass 'highlighted' - - editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true) - expect(editorView.getSelectionView()).toHaveClass 'highlighted' - - advanceClock(500) - spyOn(editorView.getSelectionView(), 'removeClass').andCallThrough() - editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true) - expect(editorView.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted') - expect(editorView.getSelectionView()).toHaveClass 'highlighted' - - advanceClock(500) - expect(editorView.getSelectionView()).toHaveClass 'highlighted' - - describe "if autoscroll is false", -> - it "does not scroll to the selection or the cursor", -> - editorView.scrollToBottom() - scrollTopBefore = editorView.scrollTop() - editor.setSelectedBufferRange([[0, 0], [1, 0]], autoscroll: false) - expect(editorView.scrollTop()).toBe scrollTopBefore - - describe "if autoscroll is not specified", -> - it "autoscrolls to the cursor as normal", -> - editorView.scrollToBottom() - editor.setSelectedBufferRange([[0, 0], [1, 0]]) - expect(editorView.scrollTop()).toBe 0 - - describe "cursor rendering", -> - describe "when the cursor moves", -> - charWidth = null - - beforeEach -> - editorView.attachToDom() - editorView.vScrollMargin = 3 - editorView.hScrollMargin = 5 - {charWidth} = editorView - - it "repositions the cursor's view on screen", -> - editor.setCursorScreenPosition(row: 2, column: 2) - expect(editorView.getCursorView().position()).toEqual(top: 2 * editorView.lineHeight, left: 2 * editorView.charWidth) - - it "hides the cursor when the selection is non-empty, and shows it otherwise", -> - cursorView = editorView.getCursorView() - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(cursorView).toBeVisible() - - editor.setSelectedBufferRange([[0, 0], [3, 0]]) - expect(editor.getSelection().isEmpty()).toBeFalsy() - expect(cursorView).toBeHidden() - - editor.setCursorBufferPosition([1, 3]) - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(cursorView).toBeVisible() - - it "moves the hiddenInput to the same position with cursor's view", -> - editor.setCursorScreenPosition(row: 2, column: 2) - expect(editorView.getCursorView().offset()).toEqual(editorView.hiddenInput.offset()) - - describe "when the editor view is using a variable-width font", -> - beforeEach -> - editorView.setFontFamily('sans-serif') - - describe "on #darwin or #linux", -> - it "correctly positions the cursor", -> - editor.setCursorBufferPosition([3, 30]) - expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 178} - editor.setCursorBufferPosition([3, Infinity]) - expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 353} - - describe "on #win32", -> - it "correctly positions the cursor", -> - editor.setCursorBufferPosition([3, 30]) - expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 175} - editor.setCursorBufferPosition([3, Infinity]) - expect(editorView.getCursorView().position()).toEqual {top: 3 * editorView.lineHeight, left: 346} - - describe "autoscrolling", -> - it "only autoscrolls when the last cursor is moved", -> - editor.setCursorBufferPosition([11,0]) - editor.addCursorAtBufferPosition([6,50]) - [cursor1, cursor2] = editor.getCursors() - - spyOn(editorView, 'scrollToPixelPosition') - cursor1.setScreenPosition([10, 10]) - expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() - - cursor2.setScreenPosition([11, 11]) - expect(editorView.scrollToPixelPosition).toHaveBeenCalled() - - it "does not autoscroll if the 'autoscroll' option is false", -> - editor.setCursorBufferPosition([11,0]) - spyOn(editorView, 'scrollToPixelPosition') - editor.setCursorScreenPosition([10, 10], autoscroll: false) - expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() - - it "autoscrolls to cursor if autoscroll is true, even if the position does not change", -> - spyOn(editorView, 'scrollToPixelPosition') - editor.setCursorScreenPosition([4, 10], autoscroll: false) - editor.setCursorScreenPosition([4, 10]) - expect(editorView.scrollToPixelPosition).toHaveBeenCalled() - editorView.scrollToPixelPosition.reset() - - editor.setCursorBufferPosition([4, 10]) - expect(editorView.scrollToPixelPosition).toHaveBeenCalled() - - it "does not autoscroll the cursor based on a buffer change, unless the buffer change was initiated by the cursor", -> - lastVisibleRow = editorView.getLastVisibleScreenRow() - editor.addCursorAtBufferPosition([lastVisibleRow, 0]) - spyOn(editorView, 'scrollToPixelPosition') - buffer.insert([lastVisibleRow, 0], "\n\n") - expect(editorView.scrollToPixelPosition).not.toHaveBeenCalled() - editor.insertText('\n\n') - expect(editorView.scrollToPixelPosition.callCount).toBe 1 - - it "autoscrolls on undo/redo", -> - spyOn(editorView, 'scrollToPixelPosition') - editor.insertText('\n\n') - expect(editorView.scrollToPixelPosition.callCount).toBe 1 - editor.undo() - expect(editorView.scrollToPixelPosition.callCount).toBe 2 - editor.redo() - expect(editorView.scrollToPixelPosition.callCount).toBe 3 - - describe "when the last cursor exceeds the upper or lower scroll margins", -> - describe "when the editor view is taller than twice the vertical scroll margin", -> - it "sets the scrollTop so the cursor remains within the scroll margin", -> - setEditorHeightInLines(editorView, 10) - - _.times 6, -> editor.moveCursorDown() - expect(editorView.scrollTop()).toBe(0) - - editor.moveCursorDown() - expect(editorView.scrollTop()).toBe(editorView.lineHeight) - - editor.moveCursorDown() - expect(editorView.scrollTop()).toBe(editorView.lineHeight * 2) - - _.times 3, -> editor.moveCursorUp() - - editor.moveCursorUp() - expect(editorView.scrollTop()).toBe(editorView.lineHeight) - - editor.moveCursorUp() - expect(editorView.scrollTop()).toBe(0) - - describe "when the editor view is shorter than twice the vertical scroll margin", -> - it "sets the scrollTop based on a reduced scroll margin, which prevents a jerky tug-of-war between upper and lower scroll margins", -> - setEditorHeightInLines(editorView, 5) - - _.times 3, -> editor.moveCursorDown() - - expect(editorView.scrollTop()).toBe(editorView.lineHeight) - - editor.moveCursorUp() - expect(editorView.renderedLines.css('top')).toBe "0px" - - describe "when the last cursor exceeds the right or left scroll margins", -> - describe "when soft-wrap is disabled", -> - describe "when the editor view is wider than twice the horizontal scroll margin", -> - it "sets the scrollView's scrollLeft so the cursor remains within the scroll margin", -> - setEditorWidthInChars(editorView, 30) - - # moving right - editor.setCursorScreenPosition([2, 24]) - expect(editorView.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 25]) - expect(editorView.scrollLeft()).toBe charWidth - - editor.setCursorScreenPosition([2, 28]) - expect(editorView.scrollLeft()).toBe charWidth * 4 - - # moving left - editor.setCursorScreenPosition([2, 9]) - expect(editorView.scrollLeft()).toBe charWidth * 4 - - editor.setCursorScreenPosition([2, 8]) - expect(editorView.scrollLeft()).toBe charWidth * 3 - - editor.setCursorScreenPosition([2, 5]) - expect(editorView.scrollLeft()).toBe 0 - - describe "when the editor view is narrower than twice the horizontal scroll margin", -> - it "sets the scrollView's scrollLeft based on a reduced horizontal scroll margin, to prevent a jerky tug-of-war between right and left scroll margins", -> - editorView.hScrollMargin = 6 - setEditorWidthInChars(editorView, 7) - - editor.setCursorScreenPosition([2, 3]) - window.advanceClock() - expect(editorView.scrollLeft()).toBe(0) - - editor.setCursorScreenPosition([2, 4]) - window.advanceClock() - expect(editorView.scrollLeft()).toBe(charWidth) - - editor.setCursorScreenPosition([2, 3]) - window.advanceClock() - expect(editorView.scrollLeft()).toBe(0) - - describe "when soft-wrap is enabled", -> - beforeEach -> - editor.setSoftWrap(true) - - it "does not scroll the buffer horizontally", -> - editorView.width(charWidth * 30) - - # moving right - editor.setCursorScreenPosition([2, 24]) - expect(editorView.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 25]) - expect(editorView.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 28]) - expect(editorView.scrollLeft()).toBe 0 - - # moving left - editor.setCursorScreenPosition([2, 9]) - expect(editorView.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 8]) - expect(editorView.scrollLeft()).toBe 0 - - editor.setCursorScreenPosition([2, 5]) - expect(editorView.scrollLeft()).toBe 0 - - describe "when editor:toggle-soft-wrap is toggled", -> - describe "when the text exceeds the editor view width and the scroll-view is horizontally scrolled", -> - it "wraps the text and renders properly", -> - editorView.attachToDom(heightInLines: 30, widthInChars: 30) - editorView.setWidthInChars(100) - editor.setText("Fashion axe umami jean shorts retro hashtag carles mumblecore. Photo booth skateboard Austin gentrify occupy ethical. Food truck gastropub keffiyeh, squid deep v pinterest literally sustainable salvia scenester messenger bag. Neutra messenger bag flexitarian four loko, shoreditch VHS pop-up tumblr seitan synth master cleanse. Marfa selvage ugh, raw denim authentic try-hard mcsweeney's trust fund fashion axe actually polaroid viral sriracha. Banh mi marfa plaid single-origin coffee. Pickled mumblecore lomo ugh bespoke.") - editorView.scrollLeft(editorView.charWidth * 30) - editorView.trigger "editor:toggle-soft-wrap" - expect(editorView.scrollLeft()).toBe 0 - expect(editorView.editor.getSoftWrapColumn()).not.toBe 100 - - describe "text rendering", -> - describe "when all lines in the buffer are visible on screen", -> - beforeEach -> - editorView.attachToDom() - expect(editorView.trueHeight()).toBeCloseTo buffer.getLineCount() * editorView.lineHeight - - it "creates a line element for each line in the buffer with the html-escaped text of the line", -> - expect(editorView.renderedLines.find('.line').length).toEqual(buffer.getLineCount()) - expect(buffer.lineForRow(2)).toContain('<') - expect(editorView.renderedLines.find('.line:eq(2)').html()).toContain '<' - - # renders empty lines with a non breaking space - expect(buffer.lineForRow(10)).toBe '' - expect(editorView.renderedLines.find('.line:eq(10)').html()).toBe ' ' - - it "syntax highlights code based on the file type", -> - line0 = editorView.renderedLines.find('.line:first') - span0 = line0.children('span:eq(0)') - expect(span0).toMatchSelector '.source.js' - expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' - expect(span0.children('span:eq(0)').text()).toBe 'var' - - span0_1 = span0.children('span:eq(1)') - expect(span0_1).toMatchSelector '.meta.function.js' - expect(span0_1.text()).toBe 'quicksort = function ()' - expect(span0_1.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' - expect(span0_1.children('span:eq(0)').text()).toBe "quicksort" - expect(span0_1.children('span:eq(1)')).toMatchSelector '.keyword.operator.js' - expect(span0_1.children('span:eq(1)').text()).toBe "=" - expect(span0_1.children('span:eq(2)')).toMatchSelector '.storage.type.function.js' - expect(span0_1.children('span:eq(2)').text()).toBe "function" - expect(span0_1.children('span:eq(3)')).toMatchSelector '.punctuation.definition.parameters.begin.js' - expect(span0_1.children('span:eq(3)').text()).toBe "(" - expect(span0_1.children('span:eq(4)')).toMatchSelector '.punctuation.definition.parameters.end.js' - expect(span0_1.children('span:eq(4)').text()).toBe ")" - - expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js' - expect(span0.children('span:eq(2)').text()).toBe "{" - - line12 = editorView.renderedLines.find('.line:eq(11)').children('span:eq(0)') - expect(line12.children('span:eq(1)')).toMatchSelector '.keyword' - - it "wraps hard tabs in a span", -> - editor.setText('\t<- hard tab') - line0 = editorView.renderedLines.find('.line:first') - span0_0 = line0.children('span:eq(0)').children('span:eq(0)') - expect(span0_0).toMatchSelector '.hard-tab' - expect(span0_0.text()).toBe ' ' - expect(span0_0.text().length).toBe editor.getTabLength() - - it "wraps leading whitespace in a span", -> - line1 = editorView.renderedLines.find('.line:eq(1)') - span0_0 = line1.children('span:eq(0)').children('span:eq(0)') - expect(span0_0).toMatchSelector '.leading-whitespace' - expect(span0_0.text()).toBe ' ' - - describe "when the line has trailing whitespace", -> - it "wraps trailing whitespace in a span", -> - editor.setText('trailing whitespace -> ') - line0 = editorView.renderedLines.find('.line:first') - span0_last = line0.children('span:eq(0)').children('span:last') - expect(span0_last).toMatchSelector '.trailing-whitespace' - expect(span0_last.text()).toBe ' ' - - describe "when lines are updated in the buffer", -> - it "syntax highlights the updated lines", -> - expect(editorView.renderedLines.find('.line:eq(0) > span:first > span:first')).toMatchSelector '.storage.modifier.js' - buffer.insert([0, 0], "q") - expect(editorView.renderedLines.find('.line:eq(0) > span:first > span:first')).not.toMatchSelector '.storage.modifier.js' - - # verify that re-highlighting can occur below the changed line - buffer.insert([5,0], "/* */") - buffer.insert([1,0], "/*") - expect(editorView.renderedLines.find('.line:eq(2) > span:first > span:first')).toMatchSelector '.comment' - - describe "when some lines at the end of the buffer are not visible on screen", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5.5) - - it "only renders the visible lines plus the overdrawn lines, setting the padding-bottom of the lines element to account for the missing lines", -> - expect(editorView.renderedLines.find('.line').length).toBe 8 - expectedPaddingBottom = (buffer.getLineCount() - 8) * editorView.lineHeight - expect(editorView.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - it "renders additional lines when the editor view is resized", -> - setEditorHeightInLines(editorView, 10) - $(window).trigger 'resize' - - expect(editorView.renderedLines.find('.line').length).toBe 12 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - it "renders correctly when scrolling after text is added to the buffer", -> - editor.insertText("1\n") - _.times 4, -> editor.moveCursorDown() - expect(editorView.renderedLines.find('.line:eq(2)').text()).toBe editor.lineForBufferRow(2) - expect(editorView.renderedLines.find('.line:eq(7)').text()).toBe editor.lineForBufferRow(7) - - it "renders correctly when scrolling after text is removed from buffer", -> - editor.getBuffer().delete([[0,0],[1,0]]) - expect(editorView.renderedLines.find('.line:eq(0)').text()).toBe editor.lineForBufferRow(0) - expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe editor.lineForBufferRow(5) - - editorView.scrollTop(3 * editorView.lineHeight) - expect(editorView.renderedLines.find('.line:first').text()).toBe editor.lineForBufferRow(1) - expect(editorView.renderedLines.find('.line:last').text()).toBe editor.lineForBufferRow(10) - - describe "when creating and destroying folds that are longer than the visible lines", -> - describe "when the cursor precedes the fold when it is destroyed", -> - it "renders lines and line numbers correctly", -> - scrollHeightBeforeFold = editorView.scrollView.prop('scrollHeight') - fold = editor.createFold(1, 9) - fold.destroy() - expect(editorView.scrollView.prop('scrollHeight')).toBe scrollHeightBeforeFold - - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - expect(editorView.gutter.find('.line-number').length).toBe 8 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 8 - - editorView.scrollTop(4 * editorView.lineHeight) - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - describe "when the cursor follows the fold when it is destroyed", -> - it "renders lines and line numbers correctly", -> - fold = editor.createFold(1, 9) - editor.setCursorBufferPosition([10, 0]) - fold.destroy() - - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - expect(editorView.gutter.find('.line-number').length).toBe 8 - expect(editorView.gutter.find('.line-number:last').text()).toBe '13' - - editorView.scrollTop(4 * editorView.lineHeight) - - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(11) - - describe "when scrolling vertically", -> - describe "when scrolling less than the editor view's height", -> - it "draws new lines and removes old lines when the last visible line will exceed the last rendered line", -> - expect(editorView.renderedLines.find('.line').length).toBe 8 - - editorView.scrollTop(editorView.lineHeight * 1.5) - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - editorView.scrollTop(editorView.lineHeight * 3.5) # first visible row will be 3, last will be 8 - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 - - # here we don't scroll far enough to trigger additional rendering - editorView.scrollTop(editorView.lineHeight * 5.5) # first visible row will be 5, last will be 10 - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 - - editorView.scrollTop(editorView.lineHeight * 7.5) # first visible row is 7, last will be 12 - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - editorView.scrollTop(editorView.lineHeight * 3.5) # first visible row will be 3, last will be 8 - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(1) - expect(editorView.renderedLines.find('.line:last').html()).toBe ' ' # line 10 is blank - - editorView.scrollTop(0) - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - describe "when scrolling more than the editors height", -> - it "removes lines that are offscreen and not in range of the overdraw and builds lines that become visible", -> - editorView.scrollTop(editorView.layerHeight - editorView.scrollView.height()) - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(5) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(12) - - editorView.verticalScrollbar.scrollBottom(0) - editorView.verticalScrollbar.trigger 'scroll' - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.renderedLines.find('.line:first').text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find('.line:last').text()).toBe buffer.lineForRow(7) - - it "adjusts the vertical padding of the lines element to account for non-rendered lines", -> - editorView.scrollTop(editorView.lineHeight * 3) - firstVisibleBufferRow = 3 - expectedPaddingTop = (firstVisibleBufferRow - editorView.lineOverdraw) * editorView.lineHeight - expect(editorView.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" - - lastVisibleBufferRow = Math.ceil(3 + 5.5) # scroll top in lines + height in lines - lastOverdrawnRow = lastVisibleBufferRow + editorView.lineOverdraw - expectedPaddingBottom = ((buffer.getLineCount() - lastOverdrawnRow) * editorView.lineHeight) - expect(editorView.renderedLines.css('padding-bottom')).toBe "#{expectedPaddingBottom}px" - - editorView.scrollToBottom() - # scrolled to bottom, first visible row is 5 and first rendered row is 3 - firstVisibleBufferRow = Math.floor(buffer.getLineCount() - 5.5) - firstOverdrawnBufferRow = firstVisibleBufferRow - editorView.lineOverdraw - expectedPaddingTop = firstOverdrawnBufferRow * editorView.lineHeight - expect(editorView.renderedLines.css('padding-top')).toBe "#{expectedPaddingTop}px" - expect(editorView.renderedLines.css('padding-bottom')).toBe "0px" - - describe "when lines are added", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5) - - describe "when the change precedes the first rendered row", -> - it "inserts and removes rendered lines to account for upstream change", -> - editorView.scrollToBottom() - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.setTextInRange([[1,0], [3,0]], "1\n2\n3\n") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - describe "when the change straddles the first rendered row", -> - it "doesn't render rows that were not previously rendered", -> - editorView.scrollToBottom() - - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.setTextInRange([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n9\n") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - describe "when the change straddles the last rendered row", -> - it "doesn't render rows that were not previously rendered", -> - buffer.setTextInRange([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - describe "when the change the follows the last rendered row", -> - it "does not change the rendered lines", -> - buffer.setTextInRange([[12,0], [12,0]], "12\n13\n14\n") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - it "increases the width of the rendered lines element to be either the width of the longest line or the width of the scrollView (whichever is longer)", -> - maxLineLength = editor.getMaxScreenLineLength() - setEditorWidthInChars(editorView, maxLineLength) - widthBefore = editorView.renderedLines.width() - expect(widthBefore).toBe editorView.scrollView.width() + 20 - buffer.setTextInRange([[12,0], [12,0]], [1..maxLineLength*2].join('')) - expect(editorView.renderedLines.width()).toBeGreaterThan widthBefore - - describe "when lines are removed", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5) - - it "sets the rendered screen line's width to either the max line length or the scollView's width (whichever is greater)", -> - maxLineLength = editor.getMaxScreenLineLength() - setEditorWidthInChars(editorView, maxLineLength) - buffer.setTextInRange([[12,0], [12,0]], [1..maxLineLength*2].join('')) - expect(editorView.renderedLines.width()).toBeGreaterThan editorView.scrollView.width() - widthBefore = editorView.renderedLines.width() - buffer.delete([[12, 0], [12, Infinity]]) - expect(editorView.renderedLines.width()).toBe editorView.scrollView.width() + 20 - - describe "when the change the precedes the first rendered row", -> - it "removes rendered lines to account for upstream change", -> - editorView.scrollToBottom() - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.setTextInRange([[1,0], [2,0]], "") - expect(editorView.renderedLines.find(".line").length).toBe 6 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(11) - - describe "when the change straddles the first rendered row", -> - it "renders the correct rows", -> - editorView.scrollToBottom() - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12) - - buffer.setTextInRange([[7,0], [11,0]], "1\n2\n") - expect(editorView.renderedLines.find(".line").length).toBe 5 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(10) - - describe "when the change straddles the last rendered row", -> - it "renders the correct rows", -> - buffer.setTextInRange([[2,0], [7,0]], "") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - describe "when the change the follows the last rendered row", -> - it "does not change the rendered lines", -> - buffer.setTextInRange([[10,0], [12,0]], "") - expect(editorView.renderedLines.find(".line").length).toBe 7 - expect(editorView.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(0) - expect(editorView.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(6) - - describe "when the last line is removed when the editor view is scrolled to the bottom", -> - it "reduces the editor view's scrollTop (due to the reduced total scroll height) and renders the correct screen lines", -> - editor.setCursorScreenPosition([Infinity, Infinity]) - editor.insertText('\n\n\n') - editorView.scrollToBottom() - - expect(buffer.getLineCount()).toBe 16 - - initialScrollTop = editorView.scrollTop() - expect(editorView.firstRenderedScreenRow).toBe 9 - expect(editorView.lastRenderedScreenRow).toBe 15 - - editor.backspace() - - expect(editorView.scrollTop()).toBeLessThan initialScrollTop - expect(editorView.firstRenderedScreenRow).toBe 9 - expect(editorView.lastRenderedScreenRow).toBe 14 - - expect(editorView.find('.line').length).toBe 6 - - editor.backspace() - expect(editorView.firstRenderedScreenRow).toBe 9 - expect(editorView.lastRenderedScreenRow).toBe 13 - - expect(editorView.find('.line').length).toBe 5 - - editor.backspace() - expect(editorView.firstRenderedScreenRow).toBe 6 - expect(editorView.lastRenderedScreenRow).toBe 12 - - expect(editorView.find('.line').length).toBe 7 - - describe "when folding leaves less then a screen worth of text (regression)", -> - it "renders lines properly", -> - editorView.lineOverdraw = 1 - editorView.attachToDom(heightInLines: 5) - editorView.editor.foldBufferRow(4) - editorView.editor.foldBufferRow(0) - - expect(editorView.renderedLines.find('.line').length).toBe 1 - expect(editorView.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) - - describe "when folding leaves fewer screen lines than the first rendered screen line (regression)", -> - it "clears all screen lines and does not throw any exceptions", -> - editorView.lineOverdraw = 1 - editorView.attachToDom(heightInLines: 5) - editorView.scrollToBottom() - editorView.editor.foldBufferRow(0) - expect(editorView.renderedLines.find('.line').length).toBe 1 - expect(editorView.renderedLines.find('.line').text()).toBe buffer.lineForRow(0) - - describe "when autoscrolling at the end of the document", -> - it "renders lines properly", -> - waitsForPromise -> - atom.workspace.open('two-hundred.txt').then (editor) -> - editorView.edit(editor) - - runs -> - editorView.attachToDom(heightInLines: 5.5) - - expect(editorView.renderedLines.find('.line').length).toBe 8 - - editor.moveCursorToBottom() - - expect(editorView.renderedLines.find('.line').length).toBe 8 - - describe "when line has a character that could push it to be too tall (regression)", -> - it "does renders the line at a consistent height", -> - editorView.attachToDom() - buffer.insert([0, 0], "–") - expect(editorView.find('.line:eq(0)').outerHeight()).toBe editorView.find('.line:eq(1)').outerHeight() - - describe "when editor.showInvisibles config is set to true", -> - it "displays spaces, tabs, and newlines using visible non-empty values", -> - editor.setText " a line with tabs\tand spaces " - editorView.attachToDom() - - expect(atom.config.get("editor.showInvisibles")).toBeFalsy() - expect(editorView.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " - - atom.config.set("editor.showInvisibles", true) - space = editorView.invisibles?.space - expect(space).toBeTruthy() - tab = editorView.invisibles?.tab - expect(tab).toBeTruthy() - eol = editorView.invisibles?.eol - expect(eol).toBeTruthy() - expect(editorView.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab}and spaces#{space}#{eol}" - - atom.config.set("editor.showInvisibles", false) - expect(editorView.renderedLines.find('.line').text()).toBe " a line with tabs and spaces " - - it "displays newlines as their own token outside of the other tokens scope", -> - editorView.setShowInvisibles(true) - editorView.attachToDom() - editor.setText "var" - expect(editorView.find('.line').html()).toBe 'var¬' - - it "allows invisible glyphs to be customized via the editor.invisibles config", -> - editor.setText(" \t ") - editorView.attachToDom() - atom.config.set("editor.showInvisibles", true) - atom.config.set("editor.invisibles", eol: ";", space: "_", tab: "tab") - expect(editorView.find(".line:first").text()).toBe "_tab_;" - - it "displays trailing carriage return using a visible non-empty value", -> - editor.setText "a line that ends with a carriage return\r\n" - editorView.attachToDom() - - expect(atom.config.get("editor.showInvisibles")).toBeFalsy() - expect(editorView.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return" - - atom.config.set("editor.showInvisibles", true) - cr = editorView.invisibles?.cr - expect(cr).toBeTruthy() - eol = editorView.invisibles?.eol - expect(eol).toBeTruthy() - expect(editorView.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return#{cr}#{eol}" - - describe "when wrapping is on", -> - beforeEach -> - editor.setSoftWrap(true) - - it "doesn't show the end of line invisible at the end of lines broken due to wrapping", -> - editor.setText "a line that wraps " - editorView.attachToDom() - editorView.setWidthInChars(6) - atom.config.set "editor.showInvisibles", true - space = editorView.invisibles?.space - expect(space).toBeTruthy() - eol = editorView.invisibles?.eol - expect(eol).toBeTruthy() - expect(editorView.renderedLines.find('.line:first').text()).toBe "a line " - expect(editorView.renderedLines.find('.line:last').text()).toBe "wraps#{space}#{eol}" - - it "displays trailing carriage return using a visible non-empty value", -> - editor.setText "a line that \r\n" - editorView.attachToDom() - editorView.setWidthInChars(6) - atom.config.set "editor.showInvisibles", true - space = editorView.invisibles?.space - expect(space).toBeTruthy() - cr = editorView.invisibles?.cr - expect(cr).toBeTruthy() - eol = editorView.invisibles?.eol - expect(eol).toBeTruthy() - expect(editorView.renderedLines.find('.line:first').text()).toBe "a line " - expect(editorView.renderedLines.find('.line:eq(1)').text()).toBe "that#{space}#{cr}#{eol}" - expect(editorView.renderedLines.find('.line:last').text()).toBe "#{eol}" - - describe "when editor.showIndentGuide is set to true", -> - it "adds an indent-guide class to each leading whitespace span", -> - editorView.attachToDom() - - expect(atom.config.get("editor.showIndentGuide")).toBeFalsy() - atom.config.set("editor.showIndentGuide", true) - expect(editorView.showIndentGuide).toBeTruthy() - - expect(editorView.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 - - expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(3) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(3) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(4) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(4) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(5) .indent-guide').length).toBe 3 - expect(editorView.renderedLines.find('.line:eq(5) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(6) .indent-guide').length).toBe 3 - expect(editorView.renderedLines.find('.line:eq(6) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(7) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(7) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(8) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(8) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(9) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(9) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(11) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(11) .indent-guide').text()).toBe ' ' - - expect(editorView.renderedLines.find('.line:eq(12) .indent-guide').length).toBe 0 - - describe "when the indentation level on a line before an empty line is changed", -> - it "updates the indent guide on the empty line", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - editor.setCursorBufferPosition([9]) - editor.indentSelectedRows() - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - describe "when the indentation level on a line after an empty line is changed", -> - it "updates the indent guide on the empty line", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - editor.setCursorBufferPosition([11]) - editor.indentSelectedRows() - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - describe "when a line contains only whitespace", -> - it "displays an indent guide on the line", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.setCursorBufferPosition([10]) - editor.indent() - editor.indent() - expect(editor.getCursorBufferPosition()).toEqual [10, 4] - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe ' ' - - it "uses the highest indent guide level from the next or previous non-empty line", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.setCursorBufferPosition([1, Infinity]) - editor.insertNewline() - expect(editor.getCursorBufferPosition()).toEqual [2, 0] - expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(2) .indent-guide').text()).toBe ' ' - - describe "when the line has leading and trailing whitespace", -> - it "does not display the indent guide in the trailing whitespace", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - - editor.insertText("/*\n * \n*/") - expect(editorView.renderedLines.find('.line:eq(1) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(1) .indent-guide')).toHaveClass('leading-whitespace') - - describe "when the line is empty and end of show invisibles are enabled", -> - it "renders the indent guides interleaved with the end of line invisibles", -> - editorView.attachToDom() - atom.config.set("editor.showIndentGuide", true) - atom.config.set("editor.showInvisibles", true) - eol = editorView.invisibles?.eol - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 1 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " - expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol - - editor.setCursorBufferPosition([9]) - editor.indent() - - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').length).toBe 2 - expect(editorView.renderedLines.find('.line:eq(10) .indent-guide').text()).toBe "#{eol} " - expect(editorView.renderedLines.find('.line:eq(10) .invisible-character').text()).toBe eol - - describe "when editor.showIndentGuide is set to false", -> - it "does not render the indent guide on whitespace only lines (regression)", -> - editorView.attachToDom() - editor.setText(' ') - atom.config.set('editor.showIndentGuide', false) - expect(editorView.renderedLines.find('.line:eq(0) .indent-guide').length).toBe 0 - - describe "when soft-wrap is enabled", -> - beforeEach -> - jasmine.unspy(window, 'setTimeout') - editor.setSoftWrap(true) - editorView.attachToDom() - setEditorHeightInLines(editorView, 20) - setEditorWidthInChars(editorView, 50) - expect(editorView.editor.getSoftWrapColumn()).toBe 50 - - it "wraps lines that are too long to fit within the editor view's width, adjusting cursor positioning accordingly", -> - expect(editorView.renderedLines.find('.line').length).toBe 16 - expect(editorView.renderedLines.find('.line:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], " - expect(editorView.renderedLines.find('.line:eq(4)').text()).toBe "right = [];" - - editor.setCursorBufferPosition([3, 51], wrapAtSoftNewlines: true) - expect(editorView.find('.cursor').offset()).toEqual(editorView.renderedLines.find('.line:eq(4)').offset()) - - editor.setCursorBufferPosition([4, 0]) - expect(editorView.find('.cursor').offset()).toEqual(editorView.renderedLines.find('.line:eq(5)').offset()) - - editor.getSelection().setBufferRange([[6, 30], [6, 55]]) - [region1, region2] = editorView.getSelectionView().regions - expect(region1.offset().top).toBeCloseTo(editorView.renderedLines.find('.line:eq(7)').offset().top) - expect(region2.offset().top).toBeCloseTo(editorView.renderedLines.find('.line:eq(8)').offset().top) - - it "handles changes to wrapped lines correctly", -> - buffer.insert([6, 28], '1234567') - expect(editorView.renderedLines.find('.line:eq(7)').text()).toBe ' current < pivot ? left1234567.push(current) ' - expect(editorView.renderedLines.find('.line:eq(8)').text()).toBe ': right.push(current);' - expect(editorView.renderedLines.find('.line:eq(9)').text()).toBe ' }' - - it "changes the max line length and repositions the cursor when the window size changes", -> - editor.setCursorBufferPosition([3, 60]) - setEditorWidthInChars(editorView, 40) - expect(editorView.renderedLines.find('.line').length).toBe 19 - expect(editorView.renderedLines.find('.line:eq(4)').text()).toBe "left = [], right = [];" - expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe " while(items.length > 0) {" - expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] - - it "does not wrap the lines of any newly assigned buffers", -> - otherEditor = null - waitsForPromise -> - atom.workspace.open().then (o) -> otherEditor = o - - runs -> - otherEditor.buffer.setText([1..100].join('')) - editorView.edit(otherEditor) - expect(editorView.renderedLines.find('.line').length).toBe(1) - - it "unwraps lines when softwrap is disabled", -> - editorView.toggleSoftWrap() - expect(editorView.renderedLines.find('.line:eq(3)').text()).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - it "allows the cursor to move down to the last line", -> - _.times editor.getLastScreenRow(), -> editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 0] - editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 2] - - it "allows the cursor to move up to a shorter soft wrapped line", -> - editor.setCursorScreenPosition([11, 15]) - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] - editor.moveCursorUp() - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [8, 15] - - it "it allows the cursor to wrap when moving horizontally past the beginning / end of a wrapped line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveCursorLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] - - editor.moveCursorRight() - expect(editor.getCursorScreenPosition()).toEqual [11, 0] - - it "calls .setWidthInChars() when the editor view is attached because now its dimensions are available to calculate it", -> - otherEditorView = new EditorView(editor) - spyOn(otherEditorView, 'setWidthInChars') - - otherEditorView.editor.setSoftWrap(true) - expect(otherEditorView.setWidthInChars).not.toHaveBeenCalled() - - otherEditorView.simulateDomAttachment() - expect(otherEditorView.setWidthInChars).toHaveBeenCalled() - otherEditorView.remove() - - describe "when the editor view's width changes", -> - it "updates the width in characters on the edit session", -> - previousSoftWrapColumn = editor.getSoftWrapColumn() - - spyOn(editorView, 'setWidthInChars').andCallThrough() - editorView.width(editorView.width() / 2) - - waitsFor -> - editorView.setWidthInChars.callCount > 0 - - runs -> - expect(editor.getSoftWrapColumn()).toBeLessThan previousSoftWrapColumn - - it "accounts for the width of the scrollbar if there is one", -> - # force the scrollbar to always be visible, regardless of OS visibility setting - $('#jasmine-content').prepend """ - - """ - setEditorHeightInLines(editorView, 5) - setEditorWidthInChars(editorView, 40) - expect(editor.lineForScreenRow(2).text.length).toBe 34 - - describe "gutter rendering", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5.5) - - it "creates a line number element for each visible line with   padding to the left of the number", -> - expect(editorView.gutter.find('.line-number').length).toBe 8 - expect(editorView.find('.line-number:first').html()).toMatch /^ 1/ - expect(editorView.gutter.find('.line-number:last').html()).toMatch /^ 8/ - - # here we don't scroll far enough to trigger additional rendering - editorView.scrollTop(editorView.lineHeight * 1.5) - expect(editorView.renderedLines.find('.line').length).toBe 8 - expect(editorView.gutter.find('.line-number:first').html()).toMatch /^ 1/ - expect(editorView.gutter.find('.line-number:last').html()).toMatch /^ 8/ - - editorView.scrollTop(editorView.lineHeight * 3.5) - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.gutter.find('.line-number:first').html()).toMatch /^ 2/ - expect(editorView.gutter.find('.line-number:last').html()).toMatch /^11/ - - it "adds a .foldable class to lines that start foldable regions", -> - expect(editorView.gutter.find('.line-number:eq(0)')).toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(1)')).toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(2)')).not.toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(3)')).not.toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(4)')).toHaveClass 'foldable' - - # changes to indentation update foldability - editor.setIndentationForBufferRow(1, 0) - expect(editorView.gutter.find('.line-number:eq(0)')).not.toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(1)')).toHaveClass 'foldable' - - # changes to comments update foldability - editor.toggleLineCommentsForBufferRows(2, 3) - expect(editorView.gutter.find('.line-number:eq(2)')).toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(3)')).not.toHaveClass 'foldable' - editor.toggleLineCommentForBufferRow(2) - expect(editorView.gutter.find('.line-number:eq(2)')).not.toHaveClass 'foldable' - expect(editorView.gutter.find('.line-number:eq(3)')).not.toHaveClass 'foldable' - editor.toggleLineCommentForBufferRow(4) - expect(editorView.gutter.find('.line-number:eq(3)')).toHaveClass 'foldable' - - describe "when lines are inserted", -> - it "re-renders the correct line number range in the gutter", -> - editorView.scrollTop(3 * editorView.lineHeight) - expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 - - buffer.insert([6, 0], '\n') - - expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 - - it "re-renders the correct line number range when there are folds", -> - editorView.editor.foldBufferRow(1) - expect(editorView.gutter.find('.line-number-1')).toHaveClass 'folded' - - buffer.insert([0, 0], '\n') - - expect(editorView.gutter.find('.line-number-2')).toHaveClass 'folded' - - describe "when wrapping is on", -> - it "renders a • instead of line number for wrapped portions of lines", -> - editor.setSoftWrap(true) - editorView.setWidthInChars(50) - expect(editorView.gutter.find('.line-number').length).toEqual(8) - expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editorView.gutter.find('.line-number:eq(4)').html()).toMatch /^ â€¢/ - expect(editorView.gutter.find('.line-number:eq(5)').intValue()).toBe 5 - - describe "when there are folds", -> - it "skips line numbers covered by the fold and updates them when the fold changes", -> - editor.createFold(3, 5) - expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 7 - - buffer.insert([4,0], "\n\n") - expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 9 - - buffer.delete([[3,0], [6,0]]) - expect(editorView.gutter.find('.line-number:eq(3)').intValue()).toBe 4 - expect(editorView.gutter.find('.line-number:eq(4)').intValue()).toBe 6 - - it "redraws gutter numbers when lines are unfolded", -> - setEditorHeightInLines(editorView, 20) - fold = editor.createFold(2, 12) - expect(editorView.gutter.find('.line-number').length).toBe 3 - - fold.destroy() - expect(editorView.gutter.find('.line-number').length).toBe 13 - - it "styles folded line numbers", -> - editor.createFold(3, 5) - expect(editorView.gutter.find('.line-number.folded').length).toBe 1 - expect(editorView.gutter.find('.line-number.folded:eq(0)').intValue()).toBe 4 - - describe "when the scrollView is scrolled to the right", -> - it "adds a drop shadow to the gutter", -> - editorView.attachToDom() - editorView.width(100) - - expect(editorView.gutter).not.toHaveClass('drop-shadow') - - editorView.scrollLeft(10) - editorView.scrollView.trigger('scroll') - - expect(editorView.gutter).toHaveClass('drop-shadow') - - editorView.scrollLeft(0) - editorView.scrollView.trigger('scroll') - - expect(editorView.gutter).not.toHaveClass('drop-shadow') - - describe "when the editor view is scrolled vertically", -> - it "adjusts the padding-top to account for non-rendered line numbers", -> - editorView.scrollTop(editorView.lineHeight * 3.5) - expect(editorView.gutter.lineNumbers.css('padding-top')).toBe "#{editorView.lineHeight * 1}px" - expect(editorView.gutter.lineNumbers.css('padding-bottom')).toBe "#{editorView.lineHeight * 2}px" - expect(editorView.renderedLines.find('.line').length).toBe 10 - expect(editorView.gutter.find('.line-number:first').intValue()).toBe 2 - expect(editorView.gutter.find('.line-number:last').intValue()).toBe 11 - - describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", -> - it "updates the line numbers to reflect the shorter buffer", -> - emptyEditor = null - waitsForPromise -> - atom.workspace.open().then (o) -> emptyEditor = o - - runs -> - editorView.edit(emptyEditor) - expect(editorView.gutter.lineNumbers.find('.line-number').length).toBe 1 - - editorView.edit(editor) - expect(editorView.gutter.lineNumbers.find('.line-number').length).toBeGreaterThan 1 - - editorView.edit(emptyEditor) - expect(editorView.gutter.lineNumbers.find('.line-number').length).toBe 1 - - describe "when the editor view is mini", -> - it "hides the gutter", -> - miniEditor = new EditorView(mini: true) - miniEditor.attachToDom() - expect(miniEditor.gutter).toBeHidden() - - it "doesn't highlight the only line", -> - miniEditor = new EditorView(mini: true) - miniEditor.attachToDom() - expect(miniEditor.getEditor().getCursorBufferPosition().row).toBe 0 - expect(miniEditor.find('.line.cursor-line').length).toBe 0 - - it "doesn't show the end of line invisible", -> - atom.config.set "editor.showInvisibles", true - miniEditor = new EditorView(mini: true) - miniEditor.attachToDom() - space = miniEditor.invisibles?.space - expect(space).toBeTruthy() - tab = miniEditor.invisibles?.tab - expect(tab).toBeTruthy() - miniEditor.getEditor().setText(" a line with tabs\tand spaces ") - expect(miniEditor.renderedLines.find('.line').text()).toBe "#{space}a line with tabs#{tab}and spaces#{space}" - - it "doesn't show the indent guide", -> - atom.config.set "editor.showIndentGuide", true - miniEditor = new EditorView(mini: true) - miniEditor.attachToDom() - miniEditor.getEditor().setText(" and indented line") - expect(miniEditor.renderedLines.find('.indent-guide').length).toBe 0 - - it "lets you set the grammar", -> - miniEditor = new EditorView(mini: true) - miniEditor.getEditor().setText("var something") - previousTokens = miniEditor.getEditor().lineForScreenRow(0).tokens - miniEditor.getEditor().setGrammar(atom.syntax.selectGrammar('something.js')) - expect(miniEditor.getEditor().getGrammar().name).toBe "JavaScript" - expect(previousTokens).not.toEqual miniEditor.getEditor().lineForScreenRow(0).tokens - - # doesn't allow regular editors to set grammars - expect(-> editor.setGrammar()).toThrow() - - describe "placeholderText", -> - it "is hidden and shown when appropriate", -> - miniEditor = new EditorView(mini: true, placeholderText: 'octokitten') - miniEditor.attachToDom() - - expect(miniEditor.underlayer.find('.placeholder-text')).toExist() - - miniEditor.getEditor().setText("var something") - expect(miniEditor.underlayer.find('.placeholder-text')).not.toExist() - - miniEditor.getEditor().setText("") - expect(miniEditor.underlayer.find('.placeholder-text')).toExist() - - it "can be set", -> - miniEditor = new EditorView(mini: true) - miniEditor.attachToDom() - - expect(miniEditor.find('.placeholder-text').text()).toEqual '' - - miniEditor.setPlaceholderText 'octokitten' - expect(miniEditor.find('.placeholder-text').text()).toEqual 'octokitten' - - miniEditor.setPlaceholderText 'new one' - expect(miniEditor.find('.placeholder-text').text()).toEqual 'new one' - - describe "when the editor.showLineNumbers config is false", -> - it "doesn't render any line numbers", -> - expect(editorView.gutter.lineNumbers).toBeVisible() - atom.config.set("editor.showLineNumbers", false) - expect(editorView.gutter.lineNumbers).not.toBeVisible() - - describe "using gutter's api", -> - it "can get all the line number elements", -> - elements = editorView.gutter.getLineNumberElements() - len = editorView.gutter.lastScreenRow - editorView.gutter.firstScreenRow + 1 - expect(elements).toHaveLength(len) - - it "can get a single line number element", -> - element = editorView.gutter.getLineNumberElement(3) - expect(element).toBeTruthy() - - it "returns falsy when there is no line element", -> - expect(editorView.gutter.getLineNumberElement(42)).toHaveLength 0 - - it "can add and remove classes to all the line numbers", -> - wasAdded = editorView.gutter.addClassToAllLines('heyok') - expect(wasAdded).toBe true - - elements = editorView.gutter.getLineNumberElementsForClass('heyok') - expect($(elements)).toHaveClass('heyok') - - editorView.gutter.removeClassFromAllLines('heyok') - expect($(editorView.gutter.getLineNumberElements())).not.toHaveClass('heyok') - - it "can add and remove classes from a single line number", -> - wasAdded = editorView.gutter.addClassToLine(3, 'heyok') - expect(wasAdded).toBe true - - element = editorView.gutter.getLineNumberElement(2) - expect($(element)).not.toHaveClass('heyok') - - it "can fetch line numbers by their class", -> - editorView.gutter.addClassToLine(1, 'heyok') - editorView.gutter.addClassToLine(3, 'heyok') - - elements = editorView.gutter.getLineNumberElementsForClass('heyok') - expect(elements.length).toBe 2 - - expect($(elements[0])).toHaveClass 'line-number-1' - expect($(elements[0])).toHaveClass 'heyok' - - expect($(elements[1])).toHaveClass 'line-number-3' - expect($(elements[1])).toHaveClass 'heyok' - - describe "gutter line highlighting", -> - beforeEach -> - editorView.attachToDom(heightInLines: 5.5) - - describe "when there is no wrapping", -> - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 - - describe "when there is wrapping", -> - beforeEach -> - editorView.attachToDom(30) - editor.setSoftWrap(true) - setEditorWidthInChars(editorView, 20) - - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 1 - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').intValue()).toBe 2 - - describe "when the selection spans multiple lines", -> - beforeEach -> - editorView.attachToDom(30) - - it "highlights the foreground of the gutter", -> - editor.getSelection().setBufferRange([[0,0],[2,2]]) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editorView.find('.line-number.cursor-line').length).toBe 3 - - it "doesn't highlight the background of the gutter", -> - editor.getSelection().setBufferRange([[0,0],[2,0]]) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editorView.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 0 - - it "doesn't highlight the last line if it ends at the beginning of a line", -> - editor.getSelection().setBufferRange([[0,0],[1,0]]) - expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editorView.find('.line-number.cursor-line').length).toBe 1 - expect(editorView.find('.line-number.cursor-line').intValue()).toBe 1 - - it "when a newline is deleted with backspace, the line number of the new cursor position is highlighted", -> - editor.setCursorScreenPosition([1,0]) - editor.backspace() - expect(editorView.find('.line-number.cursor-line').length).toBe 1 - expect(editorView.find('.line-number.cursor-line').intValue()).toBe 1 - - describe "line highlighting", -> - beforeEach -> - editorView.attachToDom(30) - - describe "when there is no wrapping", -> - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editorView.find('.line.cursor-line').length).toBe 1 - expect(editorView.find('.line.cursor-line').text()).toBe buffer.lineForRow(0) - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editorView.find('.line.cursor-line').length).toBe 1 - expect(editorView.find('.line.cursor-line').text()).toBe buffer.lineForRow(1) - - it "when a newline is deleted with backspace, the line of the new cursor position is highlighted", -> - editor.setCursorScreenPosition([1,0]) - editor.backspace() - expect(editorView.find('.line.cursor-line').length).toBe 1 - - describe "when there is wrapping", -> - beforeEach -> - editor.setSoftWrap(true) - setEditorWidthInChars(editorView, 20) - - it "highlights the line where the initial cursor position is", -> - expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editorView.find('.line.cursor-line').length).toBe 1 - expect(editorView.find('.line.cursor-line').text()).toBe 'var quicksort = ' - - it "updates the highlighted line when the cursor position changes", -> - editor.setCursorBufferPosition([1,0]) - expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editorView.find('.line.cursor-line').length).toBe 1 - expect(editorView.find('.line.cursor-line').text()).toBe ' var sort = ' - - describe "when there is a non-empty selection", -> - it "does not highlight the line", -> - editor.setSelectedBufferRange([[1, 0], [1, 1]]) - expect(editorView.find('.line.cursor-line').length).toBe 0 - - describe "folding", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('two-hundred.txt').then (o) -> editor = o - - runs -> - buffer = editor.buffer - editorView.edit(editor) - editorView.attachToDom() - - describe "when a fold-selection event is triggered", -> - it "folds the lines covered by the selection into a single line with a fold class and marker", -> - editor.getSelection().setBufferRange([[4,29],[7,4]]) - editorView.trigger 'editor:fold-selection' - - expect(editorView.renderedLines.find('.line:eq(4)')).toHaveClass('fold') - expect(editorView.renderedLines.find('.line:eq(4) > .fold-marker')).toExist() - expect(editorView.renderedLines.find('.line:eq(5)').text()).toBe '8' - - expect(editor.getSelection().isEmpty()).toBeTruthy() - expect(editor.getCursorScreenPosition()).toEqual [5, 0] - - it "keeps the gutter line and the editor view line the same heights (regression)", -> - editor.getSelection().setBufferRange([[4,29],[7,4]]) - editorView.trigger 'editor:fold-selection' - - expect(editorView.gutter.find('.line-number:eq(4)').height()).toBe editorView.renderedLines.find('.line:eq(4)').height() - - describe "when a fold placeholder line is clicked", -> - it "removes the associated fold and places the cursor at its beginning", -> - editor.setCursorBufferPosition([3,0]) - editor.createFold(3, 5) - - foldLine = editorView.find('.line.fold') - expect(foldLine).toExist() - foldLine.mousedown() - - expect(editorView.find('.fold')).not.toExist() - expect(editorView.find('.fold-marker')).not.toExist() - expect(editorView.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ - expect(editorView.renderedLines.find('.line:eq(5)').text()).toMatch /5/ - - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - - describe "when the unfold-current-row event is triggered when the cursor is on a fold placeholder line", -> - it "removes the associated fold and places the cursor at its beginning", -> - editor.setCursorBufferPosition([3,0]) - editorView.trigger 'editor:fold-current-row' - - editor.setCursorBufferPosition([3,0]) - editorView.trigger 'editor:unfold-current-row' - - expect(editorView.find('.fold')).not.toExist() - expect(editorView.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ - expect(editorView.renderedLines.find('.line:eq(5)').text()).toMatch /5/ - - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - - describe "when a selection starts/stops intersecting a fold", -> - it "adds/removes the 'fold-selected' class to the fold's line element and hides the cursor if it is on the fold line", -> - editor.createFold(2, 4) - - editor.setSelectedBufferRange([[1, 0], [2, 0]], preserveFolds: true, reversed: true) - expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - editor.setSelectedBufferRange([[1, 0], [1, 1]], preserveFolds: true) - expect(editorView.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') - - editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) - expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - editor.setCursorScreenPosition([3,0]) - expect(editorView.lineElementForScreenRow(2)).not.toMatchSelector('.fold.fold-selected') - - editor.setCursorScreenPosition([2,0]) - expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - expect(editorView.find('.cursor')).toBeHidden() - - editor.setCursorScreenPosition([3,0]) - expect(editorView.find('.cursor')).toBeVisible() - - describe "when a selected fold is scrolled into view (and the fold line was not previously rendered)", -> - it "renders the fold's line element with the 'fold-selected' class", -> - setEditorHeightInLines(editorView, 5) - editorView.resetDisplay() - - editor.createFold(2, 4) - editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true) - expect(editorView.renderedLines.find('.fold.fold-selected')).toExist() - - editorView.scrollToBottom() - expect(editorView.renderedLines.find('.fold.fold-selected')).not.toExist() - - editorView.scrollTop(0) - expect(editorView.lineElementForScreenRow(2)).toMatchSelector('.fold.fold-selected') - - describe "paging up and down", -> - beforeEach -> - editorView.attachToDom() - - it "moves to the last line when page down is repeated from the first line", -> - rows = editor.getLineCount() - 1 - expect(rows).toBeGreaterThan(0) - row = editor.getCursor().getScreenPosition().row - expect(row).toBe(0) - while row < rows - editorView.pageDown() - newRow = editor.getCursor().getScreenPosition().row - expect(newRow).toBeGreaterThan(row) - if (newRow <= row) - break - row = newRow - expect(row).toBe(rows) - expect(editorView.getLastVisibleScreenRow()).toBe(rows) - - it "moves to the first line when page up is repeated from the last line", -> - editor.moveCursorToBottom() - row = editor.getCursor().getScreenPosition().row - expect(row).toBeGreaterThan(0) - while row > 0 - editorView.pageUp() - newRow = editor.getCursor().getScreenPosition().row - expect(newRow).toBeLessThan(row) - if (newRow >= row) - break - row = newRow - expect(row).toBe(0) - expect(editorView.getFirstVisibleScreenRow()).toBe(0) - - it "resets to original position when down is followed by up", -> - expect(editor.getCursor().getScreenPosition().row).toBe(0) - editorView.pageDown() - expect(editor.getCursor().getScreenPosition().row).toBeGreaterThan(0) - editorView.pageUp() - expect(editor.getCursor().getScreenPosition().row).toBe(0) - expect(editorView.getFirstVisibleScreenRow()).toBe(0) - - describe ".pixelPositionForBufferPosition(position)", -> - describe "when the editor view is detached", -> - it "returns top and left values of 0", -> - expect(editorView.isOnDom()).toBeFalsy() - expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 - - describe "when the editor view is invisible", -> - it "returns top and left values of 0", -> - editorView.attachToDom() - editorView.hide() - expect(editorView.isVisible()).toBeFalsy() - expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 - - describe "when the editor view is attached and visible", -> - beforeEach -> - editorView.attachToDom() - - it "returns the top and left pixel positions", -> - expect(editorView.pixelPositionForBufferPosition([2,7])).toEqual top: 40, left: 70 - - it "caches the left position", -> - editorView.renderedLines.css('font-size', '16px') - expect(editorView.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - - # make characters smaller - editorView.renderedLines.css('font-size', '15px') - - expect(editorView.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - - describe "when clicking in the gutter", -> - beforeEach -> - editorView.attachToDom() - - describe "when single clicking", -> - it "moves the cursor to the start of the selected line", -> - expect(editor.getCursorScreenPosition()).toEqual [0,0] - event = $.Event("mousedown") - event.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top - event.originalEvent = {detail: 1} - editorView.gutter.find(".line-number:eq(1)").trigger event - expect(editor.getCursorScreenPosition()).toEqual [1,0] - - describe "when shift-clicking", -> - it "selects to the start of the selected line", -> - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] - event = $.Event("mousedown") - event.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top - event.originalEvent = {detail: 1} - event.shiftKey = true - editorView.gutter.find(".line-number:eq(1)").trigger event - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [2,0]] - - describe "when mousing down and then moving across multiple lines before mousing up", -> - describe "when selecting from top to bottom", -> - it "selects the lines", -> - mousedownEvent = $.Event("mousedown") - mousedownEvent.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top - mousedownEvent.originalEvent = {detail: 1} - editorView.gutter.find(".line-number:eq(1)").trigger mousedownEvent - - mousemoveEvent = $.Event("mousemove") - mousemoveEvent.pageY = editorView.gutter.find(".line-number:eq(5)").offset().top - mousemoveEvent.originalEvent = {detail: 1} - editorView.gutter.find(".line-number:eq(5)").trigger mousemoveEvent - - $(document).trigger 'mouseup' - - expect(editor.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] - - describe "when selecting from bottom to top", -> - it "selects the lines", -> - mousedownEvent = $.Event("mousedown") - mousedownEvent.pageY = editorView.gutter.find(".line-number:eq(5)").offset().top - mousedownEvent.originalEvent = {detail: 1} - editorView.gutter.find(".line-number:eq(5)").trigger mousedownEvent - - mousemoveEvent = $.Event("mousemove") - mousemoveEvent.pageY = editorView.gutter.find(".line-number:eq(1)").offset().top - mousemoveEvent.originalEvent = {detail: 1} - editorView.gutter.find(".line-number:eq(1)").trigger mousemoveEvent - - $(document).trigger 'mouseup' - - expect(editor.getSelection().getScreenRange()).toEqual [[1,0], [6,0]] - - describe "when clicking below the last line", -> - beforeEach -> - editorView.attachToDom() - - it "move the cursor to the end of the file", -> - expect(editor.getCursorScreenPosition()).toEqual [0,0] - event = mousedownEvent(editorView: editorView, point: [Infinity, 10]) - editorView.underlayer.trigger event - expect(editor.getCursorScreenPosition()).toEqual [12,2] - - it "selects to the end of the files when shift is pressed", -> - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [0,0]] - event = mousedownEvent(editorView: editorView, point: [Infinity, 10], shiftKey: true) - editorView.underlayer.trigger event - expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [12,2]] - - describe "when the editor's grammar is changed", -> - it "emits an editor:grammar-changed event", -> - eventHandler = jasmine.createSpy('eventHandler') - editorView.on('editor:grammar-changed', eventHandler) - editor.setGrammar(atom.syntax.selectGrammar('.coffee')) - expect(eventHandler).toHaveBeenCalled() - - describe ".replaceSelectedText()", -> - it "doesn't call the replace function when the selection is empty", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - 'new' - - editor.moveCursorToTop() - edited = editorView.replaceSelectedText(replacer) - expect(replaced).toBe false - expect(edited).toBe false - - it "returns true when transformed text is non-empty", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - 'new' - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editorView.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe true - - it "returns false when transformed text is null", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - null - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editorView.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe false - - it "returns false when transformed text is undefined", -> - replaced = false - edited = false - replacer = (text) -> - replaced = true - undefined - - editor.moveCursorToTop() - editor.selectToEndOfLine() - edited = editorView.replaceSelectedText(replacer) - expect(replaced).toBe true - expect(edited).toBe false - - describe "when editor:copy-path is triggered", -> - it "copies the absolute path to the editor view's file to the clipboard", -> - editorView.trigger 'editor:copy-path' - expect(atom.clipboard.read()).toBe editor.getPath() - - describe "when editor:move-line-up is triggered", -> - describe "when there is no selection", -> - it "moves the line where the cursor is up", -> - editor.setCursorBufferPosition([1,0]) - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - it "moves the cursor to the new row and the same column", -> - editor.setCursorBufferPosition([1,2]) - editorView.trigger 'editor:move-line-up' - expect(editor.getCursorBufferPosition()).toEqual [0,2] - - describe "when the line above is folded", -> - it "moves the line around the fold", -> - editor.foldBufferRow(1) - editor.setCursorBufferPosition([10, 0]) - editorView.trigger 'editor:move-line-up' - - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - expect(buffer.lineForRow(1)).toBe '' - expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' - expect(editor.isFoldedAtBufferRow(1)).toBe false - expect(editor.isFoldedAtBufferRow(2)).toBe true - - describe "when the line being moved is folded", -> - it "moves the fold around the fold above it", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText """ - var a = function() { - b = 3; - }; - - """ - editor.foldBufferRow(0) - editor.foldBufferRow(3) - editor.setCursorBufferPosition([3, 0]) - editorView.trigger 'editor:move-line-up' - - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(13)).toBe 'var a = function() {' - expect(editor.isFoldedAtBufferRow(0)).toBe true - expect(editor.isFoldedAtBufferRow(13)).toBe true - - describe "when the line above is empty and the line above that is folded", -> - it "moves the line to the empty line", -> - editor.foldBufferRow(2) - editor.setCursorBufferPosition([11, 0]) - editorView.trigger 'editor:move-line-up' - - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - expect(buffer.lineForRow(9)).toBe ' };' - expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(buffer.lineForRow(11)).toBe '' - expect(editor.isFoldedAtBufferRow(2)).toBe true - expect(editor.isFoldedAtBufferRow(10)).toBe false - - describe "where there is a selection", -> - describe "when the selection falls inside the line", -> - it "maintains the selection", -> - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(editor.getSelectedText()).toBe 'var' - editorView.trigger 'editor:move-line-up' - expect(editor.getSelectedBufferRange()).toEqual [[0, 2], [0, 5]] - expect(editor.getSelectedText()).toBe 'var' - - describe "where there are multiple lines selected", -> - it "moves the selected lines up", -> - editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(3)).toBe ' var sort = function(items) {' - - it "maintains the selection", -> - editor.setSelectedBufferRange([[2, 0], [3, 62]]) - editorView.trigger 'editor:move-line-up' - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 62]] - - describe "when the last line is selected", -> - it "moves the selected line up", -> - editor.setSelectedBufferRange([[12, 0], [12, Infinity]]) - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' - - describe "when the last two lines are selected", -> - it "moves the selected lines up", -> - editor.setSelectedBufferRange([[11, 0], [12, Infinity]]) - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe '' - - describe "when the cursor is on the first line", -> - it "does not move the line", -> - editor.setCursorBufferPosition([0,0]) - originalText = editor.getText() - editorView.trigger 'editor:move-line-up' - expect(editor.getText()).toBe originalText - - describe "when the cursor is on the trailing newline", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.moveCursorToBottom() - originalText = editor.getText() - editorView.trigger 'editor:move-line-up' - expect(editor.getText()).toBe originalText - - describe "when the cursor is on a folded line", -> - it "moves all lines in the fold up and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - expect(buffer.lineForRow(7)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [3, 0]] - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - - describe "when the selection contains a folded and unfolded line", -> - it "moves the selected lines up and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorBufferPosition([3, 4]) - editor.selectDown() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[2, 4], [3, 0]] - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - - describe "when an entire line is selected including the newline", -> - it "moves the selected line up", -> - editor.setCursorBufferPosition([1]) - editor.selectToEndOfLine() - editor.selectRight() - editorView.trigger 'editor:move-line-up' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - describe "when editor:move-line-down is triggered", -> - describe "when there is no selection", -> - it "moves the line where the cursor is down", -> - editor.setCursorBufferPosition([0, 0]) - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - - it "moves the cursor to the new row and the same column", -> - editor.setCursorBufferPosition([0, 2]) - editorView.trigger 'editor:move-line-down' - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - - describe "when the line below is folded", -> - it "moves the line around the fold", -> - editor.setCursorBufferPosition([0, 0]) - editor.foldBufferRow(1) - editorView.trigger 'editor:move-line-down' - - expect(editor.getCursorBufferPosition()).toEqual [9, 0] - expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' - expect(buffer.lineForRow(9)).toBe 'var quicksort = function () {' - expect(editor.isFoldedAtBufferRow(0)).toBe true - expect(editor.isFoldedAtBufferRow(9)).toBe false - - describe "when the line being moved is folded", -> - it "moves the fold around the fold below it", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText """ - var a = function() { - b = 3; - }; - - """ - editor.foldBufferRow(0) - editor.foldBufferRow(3) - editor.setCursorBufferPosition([0, 0]) - editorView.trigger 'editor:move-line-down' - - expect(editor.getCursorBufferPosition()).toEqual [13, 0] - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(13)).toBe 'var a = function() {' - expect(editor.isFoldedAtBufferRow(0)).toBe true - expect(editor.isFoldedAtBufferRow(13)).toBe true - - describe "when the line below is empty and the line below that is folded", -> - it "moves the line to the empty line", -> - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText('\n') - editor.setCursorBufferPosition([0, 0]) - editor.foldBufferRow(2) - editorView.trigger 'editor:move-line-down' - - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - expect(buffer.lineForRow(0)).toBe '' - expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' - expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' - expect(editor.isFoldedAtBufferRow(0)).toBe false - expect(editor.isFoldedAtBufferRow(1)).toBe false - expect(editor.isFoldedAtBufferRow(2)).toBe true - - describe "when the cursor is on the last line", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(12)).toBe '};' - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] - - describe "when the cursor is on the second to last line", -> - it "moves the line down", -> - editor.setCursorBufferPosition([11, 0]) - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(11)).toBe '};' - expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' - expect(buffer.lineForRow(13)).toBeUndefined() - - describe "when the cursor is on the second to last line and the last line is empty", -> - it "does not move the line", -> - editor.moveCursorToBottom() - editor.insertNewline() - editor.setCursorBufferPosition([12, 2]) - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(12)).toBe '};' - expect(buffer.lineForRow(13)).toBe '' - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] - - describe "where there is a selection", -> - describe "when the selection falls inside the line", -> - it "maintains the selection", -> - editor.setSelectedBufferRange([[1, 2], [1, 5]]) - expect(editor.getSelectedText()).toBe 'var' - editorView.trigger 'editor:move-line-down' - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 5]] - expect(editor.getSelectedText()).toBe 'var' - - describe "where there are multiple lines selected", -> - it "moves the selected lines down", -> - editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(2)).toBe ' while(items.length > 0) {' - expect(buffer.lineForRow(3)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(5)).toBe ' current = items.shift();' - - it "maintains the selection", -> - editor.setSelectedBufferRange([[2, 0], [3, 62]]) - editorView.trigger 'editor:move-line-down' - expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [4, 62]] - - describe "when the cursor is on a folded line", -> - it "moves all lines in the fold down and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(4)).toBe ' return sort(left).concat(pivot).concat(sort(right));' - expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 0]] - expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() - - describe "when the selection contains a folded and unfolded line", -> - it "moves the selected lines down and preserves the fold", -> - editor.setCursorBufferPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorBufferPosition([3, 4]) - editor.selectDown() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(3)).toBe ' return sort(left).concat(pivot).concat(sort(right));' - expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' - expect(editor.getSelectedBufferRange()).toEqual [[4, 4], [5, 0]] - expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() - - describe "when an entire line is selected including the newline", -> - it "moves the selected line down", -> - editor.setCursorBufferPosition([1]) - editor.selectToEndOfLine() - editor.selectRight() - editorView.trigger 'editor:move-line-down' - expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' - - describe "when the escape key is pressed on the editor view", -> - it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", -> - atom.keymaps.add 'name', '.editor': {'escape': 'test-event'} - testEventHandler = jasmine.createSpy("testEventHandler") - - editorView.on 'test-event', testEventHandler - editorView.editor.addSelectionForBufferRange([[3, 0], [3, 0]]) - expect(editorView.editor.getSelections().length).toBe 2 - - editorView.trigger(keydownEvent('escape')) - expect(editorView.editor.getSelections().length).toBe 1 - expect(testEventHandler).not.toHaveBeenCalled() - - editorView.trigger(keydownEvent('escape')) - expect(testEventHandler).toHaveBeenCalled() - - describe "when the editor view is attached but invisible", -> - describe "when the editor view's text is changed", -> - it "redraws the editor view when it is next shown", -> - displayUpdatedHandler = null - atom.workspaceView = new WorkspaceView - waitsForPromise -> - atom.workspaceView.open('sample.txt').then (o) -> editor = o - - runs -> - atom.workspaceView.attachToDom() - editorView = atom.workspaceView.getActiveView() - - view = $$ -> @div id: 'view', tabindex: -1, 'View' - editorView.getPane().activateItem(view) - expect(editorView.isVisible()).toBeFalsy() - - editor.setText('hidden changes') - editor.setCursorBufferPosition([0,4]) - - displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") - editorView.on 'editor:display-updated', displayUpdatedHandler - editorView.getPane().activateItem(editorView.getModel()) - expect(editorView.isVisible()).toBeTruthy() - - waitsFor -> - displayUpdatedHandler.callCount is 1 - - runs -> - expect(editorView.renderedLines.find('.line').text()).toBe 'hidden changes' - - it "redraws the editor view when it is next reattached", -> - editorView.attachToDom() - editorView.hide() - editor.setText('hidden changes') - editor.setCursorBufferPosition([0,4]) - editorView.detach() - - displayUpdatedHandler = jasmine.createSpy("displayUpdatedHandler") - editorView.on 'editor:display-updated', displayUpdatedHandler - editorView.show() - editorView.attachToDom() - - waitsFor -> - displayUpdatedHandler.callCount is 1 - - runs -> - expect(editorView.renderedLines.find('.line').text()).toBe 'hidden changes' - - describe "editor:scroll-to-cursor", -> - it "scrolls to and centers the editor view on the cursor's position", -> - editorView.attachToDom(heightInLines: 3) - editor.setCursorBufferPosition([1, 2]) - editorView.scrollToBottom() - expect(editorView.getFirstVisibleScreenRow()).not.toBe 0 - expect(editorView.getLastVisibleScreenRow()).not.toBe 2 - editorView.trigger('editor:scroll-to-cursor') - expect(editorView.getFirstVisibleScreenRow()).toBe 0 - expect(editorView.getLastVisibleScreenRow()).toBe 2 - - describe "when the editor view is removed", -> - it "fires a editor:will-be-removed event", -> - atom.workspaceView = new WorkspaceView - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - atom.workspaceView.attachToDom() - editorView = atom.workspaceView.getActiveView() - - willBeRemovedHandler = jasmine.createSpy('willBeRemovedHandler') - editorView.on 'editor:will-be-removed', willBeRemovedHandler - editorView.getPane().destroyActiveItem() - expect(willBeRemovedHandler).toHaveBeenCalled() - - describe "when setInvisibles is toggled (regression)", -> - it "renders inserted newlines properly", -> - editorView.setShowInvisibles(true) - editor.setCursorBufferPosition([0, 0]) - editorView.attachToDom(heightInLines: 20) - editorView.setShowInvisibles(false) - editor.insertText("\n") - - for rowNumber in [1..5] - expect(editorView.lineElementForScreenRow(rowNumber).text()).toBe buffer.lineForRow(rowNumber) - - it "correctly calculates the position left for non-monospaced invisibles", -> - atom.config.set('editor.showInvisibles', true) - atom.config.set('editor.invisibles', tab: '♘') - editor.setText('\tx') - - editorView.setFontFamily('serif') - editorView.setFontSize(10) - editorView.attachToDom() - editorView.setWidthInChars(5) - - expect(editorView.pixelPositionForScreenPosition([0, 0]).left).toEqual 0 - expect(editorView.pixelPositionForScreenPosition([0, 1]).left).toEqual 10 - expect(editorView.pixelPositionForScreenPosition([0, 2]).left).toEqual 13 - - describe "when the window is resized", -> - it "updates the active edit session with the current soft wrap column", -> - editorView.attachToDom() - setEditorWidthInChars(editorView, 50) - expect(editorView.editor.getSoftWrapColumn()).toBe 50 - setEditorWidthInChars(editorView, 100) - $(window).trigger 'resize' - expect(editorView.editor.getSoftWrapColumn()).toBe 100 - - describe "character width caching", -> - describe "when soft wrap is enabled", -> - it "correctly calculates the the position left for a column", -> - editor.setSoftWrap(true) - editor.setText('lllll 00000') - editorView.setFontFamily('serif') - editorView.setFontSize(10) - editorView.attachToDom() - editorView.setWidthInChars(5) - - expect(editorView.pixelPositionForScreenPosition([0, 5]).left).toEqual 15 - expect(editorView.pixelPositionForScreenPosition([1, 5]).left).toEqual 25 - - # Check that widths are actually being cached - spyOn(editorView, 'measureToColumn').andCallThrough() - editorView.pixelPositionForScreenPosition([0, 5]) - editorView.pixelPositionForScreenPosition([1, 5]) - expect(editorView.measureToColumn.callCount).toBe 0 - - describe "when stylesheets are changed", -> - afterEach -> - atom.themes.removeStylesheet 'line-height' - atom.themes.removeStylesheet 'char-width' - - it "updates the editor if the line height or character width changes due to a stylesheet change", -> - editorView.attachToDom() - editor.setCursorScreenPosition([1, 3]) - expect(editorView.pixelPositionForScreenPosition([1, 3])).toEqual {top: 20, left: 30} - expect(editorView.getCursorView().position()).toEqual {top: 20, left: 30} - - atom.themes.applyStylesheet 'line-height', """ - .editor { line-height: 2; } - """ - - expect(editorView.pixelPositionForScreenPosition([1, 3])).toEqual {top: 20, left: 30} - expect(editorView.getCursorView().position()).toEqual {top: 20, left: 30} - - atom.themes.applyStylesheet 'char-width', """ - .editor { letter-spacing: 2px; } - """ - expect(editorView.pixelPositionForScreenPosition([1, 3])).toEqual {top: 20, left: 36} - expect(editorView.getCursorView().position()).toEqual {top: 20, left: 36} - - describe "when the editor contains hard tabs", -> - it "correctly calculates the the position left for a column", -> - editor.setText('\ttest') - editorView.attachToDom() - - expect(editorView.pixelPositionForScreenPosition([0, editor.getTabLength()]).left).toEqual 20 - expect(editorView.pixelPositionForScreenPosition([0, editor.getTabLength() + 1]).left).toEqual 30 - - # Check that widths are actually being cached - spyOn(editorView, 'measureToColumn').andCallThrough() - editorView.pixelPositionForScreenPosition([0, editor.getTabLength()]) - editorView.pixelPositionForScreenPosition([0, editor.getTabLength() + 1]) - expect(editorView.measureToColumn.callCount).toBe 0 diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 79f2e5a06..6fdbed35b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -95,8 +95,6 @@ beforeEach -> config.set "editor.autoIndent", false config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] - config.set "core.useReactEditor", true - config.set "core.useReactMiniEditors", true config.save.reset() atom.config = config diff --git a/src/atom.coffee b/src/atom.coffee index d497d1701..a2be4398b 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -17,29 +17,14 @@ WindowEventHandler = require './window-event-handler' # Public: Atom global for dealing with packages, themes, menus, and the window. # # An instance of this class is always available as the `atom` global. -# -# ## Useful properties available: -# -# * `atom.clipboard` - A {Clipboard} instance -# * `atom.config` - A {Config} instance -# * `atom.contextMenu` - A {ContextMenuManager} instance -# * `atom.deserializers` - A {DeserializerManager} instance -# * `atom.keymaps` - A {KeymapManager} instance -# * `atom.menu` - A {MenuManager} instance -# * `atom.packages` - A {PackageManager} instance -# * `atom.project` - A {Project} instance -# * `atom.syntax` - A {Syntax} instance -# * `atom.themes` - A {ThemeManager} instance -# * `atom.workspace` - A {Workspace} instance -# * `atom.workspaceView` - A {WorkspaceView} instance module.exports = class Atom extends Model @version: 1 # Increment this when the serialization format changes # Public: Load or create the Atom environment in the given mode. # - # mode - Pass 'editor' or 'spec' depending on the kind of environment you - # want to build. + # * `mode` A {String} mode that is either 'editor' or 'spec' depending on the + # kind of environment you want to build. # # Returns an Atom instance, fully initialized @loadOrCreate: (mode) -> @@ -111,6 +96,43 @@ class Atom extends Model remote.getCurrentWindow() workspaceViewParentSelector: 'body' + lastUncaughtError: null + + # Public: A {Clipboard} instance + clipboard: null + + # Public: A {Config} instance + config: null + + # Public: A {ContextMenuManager} instance + contextMenu: null + + # Public: A {DeserializerManager} instance + deserializers: null + + # Public: A {KeymapManager} instance + keymaps: null + + # Public: A {MenuManager} instance + menu: null + + # Public: A {PackageManager} instance + packages: null + + # Public: A {Project} instance + project: null + + # Public: A {Syntax} instance + syntax: null + + # Public: A {ThemeManager} instance + themes: null + + # Public: A {Workspace} instance + workspace: null + + # Public: A {WorkspaceView} instance + workspaceView: null # Call .loadOrCreate instead constructor: (@state) -> @@ -119,12 +141,14 @@ class Atom extends Model @deserializers = new DeserializerManager() # Public: Sets up the basic services that should be available in all modes - # (both spec and application). Call after this instance has been assigned to - # the `atom` global. + # (both spec and application). + # + # Call after this instance has been assigned to the `atom` global. initialize: -> window.onerror = => @openDevTools() @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()') + @lastUncaughtError = Array::slice.call(arguments) @emit 'uncaught-error', arguments... @unsubscribe() @@ -191,7 +215,11 @@ class Atom extends Model # Public: Get the dimensions of this window. # - # Returns an object with x, y, width, and height keys. + # Returns an {Object} with the following keys: + # * `x` The window's x-position {Number}. + # * `y` The window's y-position {Number}. + # * `width` The window's width {Number}. + # * `height` The window's height {Number}. getWindowDimensions: -> browserWindow = @getCurrentWindow() [x, y] = browserWindow.getPosition() @@ -205,11 +233,11 @@ class Atom extends Model # in the dimensions parameter. If x or y are omitted the window will be # centered. If height or width are omitted only the position will be changed. # - # dimensions - An {Object} with the following keys: - # :x - The new x coordinate. - # :y - The new y coordinate. - # :width - The new width. - # :height - The new height. + # * `dimensions` An {Object} with the following keys: + # * `x` The new x coordinate. + # * `y` The new y coordinate. + # * `width` The new width. + # * `height` The new height. setWindowDimensions: ({x, y, width, height}) -> if width? and height? @setSize(width, height) @@ -258,7 +286,7 @@ class Atom extends Model # Public: Get the load settings for the current window. # - # Returns an object containing all the load setting key/value pairs. + # Returns an {Object} containing all the load setting key/value pairs. getLoadSettings: -> @constructor.getLoadSettings() @@ -308,6 +336,9 @@ class Atom extends Model @themes.loadBaseStylesheets() @packages.loadPackages() @deserializeEditorWindow() + + @watchProjectPath() + @packages.activate() @keymaps.loadUserKeymap() @requireUserInitScript() @@ -347,19 +378,33 @@ class Atom extends Model pack.reloadStylesheets?() null + # Notify the browser project of the window's current project path + watchProjectPath: -> + onProjectPathChanged = => + ipc.send('window-command', 'project-path-changed', @project.getPath()) + @subscribe @project, 'path-changed', onProjectPathChanged + onProjectPathChanged() + # Public: Open a new Atom window using the given options. # # Calling this method without an options parameter will open a prompt to pick # a file/folder to open in the new window. # - # options - An {Object} with the following keys: - # :pathsToOpen - An {Array} of {String} paths to open. + # * `options` An {Object} with the following keys: + # * `pathsToOpen` An {Array} of {String} paths to open. + # * `newWindow` A {Boolean}, true to always open a new window instead of + # reusing existing windows depending on the paths to open. + # * `devMode` A {Boolean}, true to open the window in development mode. + # Development mode loads the Atom source from the locally cloned + # repository and also loads all the packages in ~/.atom/dev/packages + # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe + # mode prevents all packages installed to ~/.atom/packages from loading. open: (options) -> ipc.send('open', options) # Public: Open a confirm dialog. # - # ## Example + # ## Examples # # ```coffee # atom.confirm @@ -370,12 +415,12 @@ class Atom extends Model # Bad: -> window.alert('bummer') # ``` # - # options - An {Object} with the following keys: - # :message - The {String} message to display. - # :detailedMessage - The {String} detailed message to display. - # :buttons - Either an array of strings or an object where keys are - # button names and the values are callbacks to invoke when - # clicked. + # * `options` An {Object} with the following keys: + # * `message` The {String} message to display. + # * `detailedMessage` The {String} detailed message to display. + # * `buttons` Either an array of strings or an object where keys are + # button names and the values are callbacks to invoke when + # clicked. # # Returns the chosen button index {Number} if the buttons option was an array. confirm: ({message, detailedMessage, buttons}={}) -> @@ -438,15 +483,15 @@ class Atom extends Model # Public: Set the size of current window. # - # width - The {Number} of pixels. - # height - The {Number} of pixels. + # * `width` The {Number} of pixels. + # * `height` The {Number} of pixels. setSize: (width, height) -> @getCurrentWindow().setSize(width, height) # Public: Set the position of current window. # - # x - The {Number} of pixels. - # y - The {Number} of pixels. + # * `x` The {Number} of pixels. + # * `y` The {Number} of pixels. setPosition: (x, y) -> ipc.send('call-window-method', 'setPosition', x, y) @@ -533,7 +578,7 @@ class Atom extends Model # This time include things like loading and activating packages, creating # DOM elements for the editor, and reading the config. # - # Returns the number of milliseconds taken to load the window or null + # Returns the {Number} of milliseconds taken to load the window or null # if the window hasn't finished loading yet. getWindowLoadTime: -> @loadTime @@ -565,8 +610,8 @@ class Atom extends Model # The globals will be set on the `window` object and removed after the # require completes. # - # id - The {String} module name or path. - # globals - An {Object} to set as globals during require (default: {}) + # * `id` The {String} module name or path. + # * `globals` An optinal {Object} to set as globals during require. requireWithGlobals: (id, globals={}) -> existingGlobals = {} for key, value of globals diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index cc81beef9..673d9ec40 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -103,6 +103,12 @@ class AtomApplication window.once 'window:loaded', => @autoUpdateManager.emitUpdateAvailableEvent(window) + focusHandler = => @lastFocusedWindow = window + window.browserWindow.on 'focus', focusHandler + window.browserWindow.once 'closed', => + @lastFocusedWindow = null if window is @lastFocusedWindow + window.browserWindow.removeListener 'focus', focusHandler + # Creates server to listen for additional atom application launches. # # You can run the atom command multiple times, but after the first launch @@ -199,13 +205,15 @@ class AtomApplication # A request from the associated render process to open a new render process. ipc.on 'open', (event, options) => + window = @windowForEvent(event) if options? if options.pathsToOpen?.length > 0 + options.window = window @openPaths(options) else new AtomWindow(options) else - @promptForPath() + @promptForPath({window}) ipc.on 'update-application-menu', (event, template, keystrokesByCommand) => @applicationMenu.update(template, keystrokesByCommand) @@ -281,8 +289,12 @@ class AtomApplication # Returns the {AtomWindow} for the given path. windowForPath: (pathToOpen) -> - for atomWindow in @windows - return atomWindow if atomWindow.containsPath(pathToOpen) + _.find @windows, (atomWindow) -> atomWindow.containsPath(pathToOpen) + + # Returns the {AtomWindow} for the given ipc event. + windowForEvent: ({sender}) -> + window = BrowserWindow.fromWebContents(sender) + _.find @windows, ({browserWindow}) -> window is browserWindow # Public: Returns the currently focused {AtomWindow} or undefined if none. focusedWindow: -> @@ -296,9 +308,10 @@ class AtomApplication # :newWindow - Boolean of whether this should be opened in a new window. # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. - openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode}) -> + # :window - {AtomWindow} to open file paths in. + openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) -> for pathToOpen in pathsToOpen ? [] - @openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode}) + @openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) # Public: Opens a single path, in an existing window if possible. # @@ -309,15 +322,30 @@ class AtomApplication # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. # :windowDimensions - Object with height and width keys. - openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions}={}) -> + # :window - {AtomWindow} to open file paths in. + openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> {pathToOpen, initialLine, initialColumn} = @locationForPathToOpen(pathToOpen) - unless devMode - existingWindow = @windowForPath(pathToOpen) unless pidToKillWhenClosed or newWindow - if existingWindow + unless pidToKillWhenClosed or newWindow + pathToOpenStat = fs.statSyncNoException(pathToOpen) + + # Default to using the specified window or the last focused window + currentWindow = window ? @lastFocusedWindow + + if pathToOpenStat.isFile?() + # Open the file in the current window + existingWindow = currentWindow + else if pathToOpenStat.isDirectory?() + # Open the folder in the current window if it doesn't have a path + existingWindow = currentWindow unless currentWindow?.hasProjectPath() + + # Don't reuse windows in dev mode + existingWindow ?= @windowForPath(pathToOpen) unless devMode + + if existingWindow? openedWindow = existingWindow openedWindow.openPath(pathToOpen, initialLine) - openedWindow.restore() + openedWindow.restore() if openedWindow.isMinimized() else if devMode try @@ -331,7 +359,7 @@ class AtomApplication if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow - openedWindow.browserWindow.on 'closed', => + openedWindow.browserWindow.once 'closed', => @killProcessForWindow(openedWindow) # Kill all processes associated with opened windows. @@ -444,7 +472,8 @@ class AtomApplication # should be in dev mode or not. # :safeMode - A Boolean which controls whether any newly opened windows # should be in safe mode or not. - promptForPath: ({type, devMode, safeMode}={}) -> + # :window - An {AtomWindow} to use for opening a selected file path. + promptForPath: ({type, devMode, safeMode, window}={}) -> type ?= 'all' properties = switch type @@ -453,4 +482,4 @@ class AtomApplication when 'all' then ['openFile', 'openDirectory'] else throw new Error("#{type} is an invalid type for promptForPath") dialog.showOpenDialog title: 'Open', properties: properties.concat(['multiSelections', 'createDirectory']), (pathsToOpen) => - @openPaths({pathsToOpen, devMode, safeMode}) + @openPaths({pathsToOpen, devMode, safeMode, window}) diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index ad2b2ce53..1022f6f76 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -25,9 +25,9 @@ class AtomWindow # Normalize to make sure drive letter case is consistent on Windows @resourcePath = path.normalize(@resourcePath) if @resourcePath + @browserWindow = new BrowserWindow show: false, title: 'Atom', icon: @constructor.iconPath global.atomApplication.addWindow(this) - @browserWindow = new BrowserWindow show: false, title: 'Atom', icon: @constructor.iconPath @handleEvents() loadSettings = _.extend({}, settings) @@ -49,6 +49,8 @@ class AtomWindow @emit 'window:loaded' @loaded = true + @browserWindow.on 'project-path-changed', (@projectPath) => + @browserWindow.loadUrl @getUrl(loadSettings) @browserWindow.focusOnWebView() if @isSpec @@ -66,6 +68,8 @@ class AtomWindow slashes: true query: {loadSettings: JSON.stringify(loadSettings)} + hasProjectPath: -> @projectPath?.length > 0 + getInitialPath: -> @browserWindow.loadSettings.initialPath @@ -169,6 +173,8 @@ class AtomWindow isFocused: -> @browserWindow.isFocused() + isMinimized: -> @browserWindow.isMinimized() + isWebViewFocused: -> @browserWindow.isWebViewFocused() isSpecWindow: -> @isSpec diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 7d6d372e3..8955ab0a7 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -63,7 +63,14 @@ parseCommandLine = -> options.usage """ Atom Editor v#{version} - Usage: atom [options] [file ...] + Usage: atom [options] [path ...] + + One or more paths to files or folders to open may be specified. + + File paths will open in the current window. + + Folder paths will open in an existing window if that folder has already been + opened or a new window if it hasn't. """ options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.') options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.') diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index 463d9b801..808bc27a4 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -6,34 +6,35 @@ path = require 'path' # # This is necessary on Windows since it doesn't support shebang `#!` lines. # -# ## Requiring in packages +# ## Examples # # ```coffee -# {BufferedNodeProcess} = require 'atom' +# {BufferedNodeProcess} = require 'atom' # ``` module.exports = class BufferedNodeProcess extends BufferedProcess + # Public: Runs the given Node script by spawning a new child process. # - # options - An {Object} with the following keys: - # :command - The {String} path to the JavaScript script to execute. - # :args - The {Array} of arguments to pass to the script (optional). - # :options - The options {Object} to pass to Node's `ChildProcess.spawn` - # method (optional). - # :stdout - The callback {Function} that receives a single argument which - # contains the standard output from the command. The callback is - # called as data is received but it's buffered to ensure only - # complete lines are passed until the source stream closes. After - # the source stream has closed all remaining data is sent in a - # final call (optional). - # :stderr - The callback {Function} that receives a single argument which - # contains the standard error output from the command. The - # callback is called as data is received but it's buffered to - # ensure only complete lines are passed until the source stream - # closes. After the source stream has closed all remaining data - # is sent in a final call (optional). - # :exit - The callback {Function} which receives a single argument - # containing the exit status (optional). + # * `options` An {Object} with the following keys: + # * `command` The {String} path to the JavaScript script to execute. + # * `args` The {Array} of arguments to pass to the script (optional). + # * `options` The options {Object} to pass to Node's `ChildProcess.spawn` + # method (optional). + # * `stdout` The callback {Function} that receives a single argument which + # contains the standard output from the command. The callback is + # called as data is received but it's buffered to ensure only + # complete lines are passed until the source stream closes. After + # the source stream has closed all remaining data is sent in a + # final call (optional). + # * `stderr` The callback {Function} that receives a single argument which + # contains the standard error output from the command. The + # callback is called as data is received but it's buffered to + # ensure only complete lines are passed until the source stream + # closes. After the source stream has closed all remaining data + # is sent in a final call (optional). + # * `exit` The callback {Function} which receives a single argument + # containing the exit status (optional). constructor: ({command, args, options, stdout, stderr, exit}) -> node = if process.platform is 'darwin' diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index c3cda37e4..ed6474f3b 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -4,7 +4,7 @@ ChildProcess = require 'child_process' # Public: A wrapper which provides standard error/output line buffering for # Node's ChildProcess. # -# ## Requiring in packages +# ## Examples # # ```coffee # {BufferedProcess} = require 'atom' @@ -19,25 +19,25 @@ module.exports = class BufferedProcess # Public: Runs the given command by spawning a new child process. # - # options - An {Object} with the following keys: - # :command - The {String} command to execute. - # :args - The {Array} of arguments to pass to the command (optional). - # :options - The options {Object} to pass to Node's `ChildProcess.spawn` - # method (optional). - # :stdout - The callback {Function} that receives a single argument which - # contains the standard output from the command. The callback is - # called as data is received but it's buffered to ensure only - # complete lines are passed until the source stream closes. After - # the source stream has closed all remaining data is sent in a - # final call (optional). - # :stderr - The callback {Function} that receives a single argument which - # contains the standard error output from the command. The - # callback is called as data is received but it's buffered to - # ensure only complete lines are passed until the source stream - # closes. After the source stream has closed all remaining data - # is sent in a final call (optional). - # :exit - The callback {Function} which receives a single argument - # containing the exit status (optional). + # * `options` An {Object} with the following keys: + # * `command` The {String} command to execute. + # * `args` The {Array} of arguments to pass to the command (optional). + # * `options` The options {Object} to pass to Node's `ChildProcess.spawn` + # method (optional). + # * `stdout` The callback {Function} that receives a single argument which + # contains the standard output from the command. The callback is + # called as data is received but it's buffered to ensure only + # complete lines are passed until the source stream closes. After + # the source stream has closed all remaining data is sent in a + # final call (optional). + # * `stderr` The callback {Function} that receives a single argument which + # contains the standard error output from the command. The + # callback is called as data is received but it's buffered to + # ensure only complete lines are passed until the source stream + # closes. After the source stream has closed all remaining data + # is sent in a final call (optional). + # * `exit` The callback {Function} which receives a single argument + # containing the exit status (optional). constructor: ({command, args, options, stdout, stderr, exit}={}) -> options ?= {} # Related to joyent/node#2318 @@ -95,9 +95,9 @@ class BufferedProcess # Helper method to pass data line by line. # - # stream - The Stream to read from. - # onLines - The callback to call with each line of data. - # onDone - The callback to call when the stream has closed. + # * `stream` The Stream to read from. + # * `onLines` The callback to call with each line of data. + # * `onDone` The callback to call when the stream has closed. bufferStream: (stream, onLines, onDone) -> stream.setEncoding('utf8') buffered = '' diff --git a/src/clipboard.coffee b/src/clipboard.coffee index c717350ae..03d53d574 100644 --- a/src/clipboard.coffee +++ b/src/clipboard.coffee @@ -4,6 +4,14 @@ crypto = require 'crypto' # Public: Represents the clipboard used for copying and pasting in Atom. # # An instance of this class is always available as the `atom.clipboard` global. +# +# ## Examples +# +# ```coffee +# atom.clipboard.write('hello') +# +# console.log(atom.clipboard.read()) # 'hello' +# ``` module.exports = class Clipboard metadata: null @@ -11,7 +19,7 @@ class Clipboard # Creates an `md5` hash of some text. # - # text - A {String} to hash. + # * `text` A {String} to hash. # # Returns a hashed {String}. md5: (text) -> @@ -22,8 +30,8 @@ class Clipboard # The metadata associated with the text is available by calling # {::readWithMetadata}. # - # text - The {String} to store. - # metadata - The additional info to associate with the text. + # * `text` The {String} to store. + # * `metadata` The additional info to associate with the text. write: (text, metadata) -> @signatureForMetadata = @md5(text) @metadata = metadata @@ -39,8 +47,8 @@ class Clipboard # associated metadata. # # Returns an {Object} with the following keys: - # :text - The {String} clipboard text. - # :metadata - The metadata stored by an earlier call to {::write}. + # * `text` The {String} clipboard text. + # * `metadata` The metadata stored by an earlier call to {::write}. readWithMetadata: -> text = @read() if @signatureForMetadata is @md5(text) diff --git a/src/config.coffee b/src/config.coffee index 2c280aa27..f3606d7bb 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -15,9 +15,9 @@ pathWatcher = require 'pathwatcher' # * Create your own root keypath using your package's name. # * Don't depend on (or write to) configuration keys outside of your keypath. # -# ## Example +# ## Examples # -# ```coffeescript +# ```coffee # atom.config.set('my-package.key', 'value') # atom.config.observe 'my-package.key', -> # console.log 'My configuration changed:', atom.config.get('my-package.key') @@ -88,17 +88,17 @@ class Config _.extend hash, defaults @emit 'updated' - # Public: Get the {String} path to the config file being used. + # Extended: Get the {String} path to the config file being used. getUserConfigPath: -> @configFilePath - # Public: Returns a new {Object} containing all of settings and defaults. + # Extended: Returns a new {Object} containing all of settings and defaults. getSettings: -> _.deepExtend(@settings, @defaultSettings) - # Public: Retrieves the setting for the given key. + # Essential: Retrieves the setting for the given key. # - # keyPath - The {String} name of the key to retrieve. + # * `keyPath` The {String} name of the key to retrieve. # # Returns the value from Atom's default settings, the user's configuration # file, or `null` if the key doesn't exist in either. @@ -117,19 +117,19 @@ class Config value - # Public: Retrieves the setting for the given key as an integer. + # Extended: Retrieves the setting for the given key as an integer. # - # keyPath - The {String} name of the key to retrieve + # * `keyPath` The {String} name of the key to retrieve # # Returns the value from Atom's default settings, the user's configuration # file, or `NaN` if the key doesn't exist in either. getInt: (keyPath) -> parseInt(@get(keyPath)) - # Public: Retrieves the setting for the given key as a positive integer. + # Extended: Retrieves the setting for the given key as a positive integer. # - # keyPath - The {String} name of the key to retrieve - # defaultValue - The integer {Number} to fall back to if the value isn't + # * `keyPath` The {String} name of the key to retrieve + # * `defaultValue` The integer {Number} to fall back to if the value isn't # positive, defaults to 0. # # Returns the value from Atom's default settings, the user's configuration @@ -137,12 +137,12 @@ class Config getPositiveInt: (keyPath, defaultValue=0) -> Math.max(@getInt(keyPath), 0) or defaultValue - # Public: Sets the value for a configuration setting. + # Essential: Sets the value for a configuration setting. # # This value is stored in Atom's internal configuration file. # - # keyPath - The {String} name of the key. - # value - The value of the setting. + # * `keyPath` The {String} name of the key. + # * `value` The value of the setting. # # Returns the `value`. set: (keyPath, value) -> @@ -153,47 +153,47 @@ class Config @update() value - # Public: Toggle the value at the key path. + # Extended: Toggle the value at the key path. # # The new value will be `true` if the value is currently falsy and will be # `false` if the value is currently truthy. # - # keyPath - The {String} name of the key. + # * `keyPath` The {String} name of the key. # # Returns the new value. toggle: (keyPath) -> @set(keyPath, !@get(keyPath)) - # Public: Restore the key path to its default value. + # Extended: Restore the key path to its default value. # - # keyPath - The {String} name of the key. + # * `keyPath` The {String} name of the key. # # Returns the new value. restoreDefault: (keyPath) -> @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath)) - # Public: Get the default value of the key path. + # Extended: Get the default value of the key path. # - # keyPath - The {String} name of the key. + # * `keyPath` The {String} name of the key. # # Returns the default value. getDefault: (keyPath) -> defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) _.deepClone(defaultValue) - # Public: Is the key path value its default value? + # Extended: Is the key path value its default value? # - # keyPath - The {String} name of the key. + # * `keyPath` The {String} name of the key. # # Returns a {Boolean}, `true` if the current value is the default, `false` # otherwise. isDefault: (keyPath) -> not _.valueForKeyPath(@settings, keyPath)? - # Public: Push the value to the array at the key path. + # Extended: Push the value to the array at the key path. # - # keyPath - The {String} key path. - # value - The value to push to the array. + # * `keyPath` The {String} key path. + # * `value` The value to push to the array. # # Returns the new array length {Number} of the setting. pushAtKeyPath: (keyPath, value) -> @@ -202,10 +202,10 @@ class Config @set(keyPath, arrayValue) result - # Public: Add the value to the beginning of the array at the key path. + # Extended: Add the value to the beginning of the array at the key path. # - # keyPath - The {String} key path. - # value - The value to shift onto the array. + # * `keyPath` The {String} key path. + # * `value` The value to shift onto the array. # # Returns the new array length {Number} of the setting. unshiftAtKeyPath: (keyPath, value) -> @@ -216,8 +216,8 @@ class Config # Public: Remove the value from the array at the key path. # - # keyPath - The {String} key path. - # value - The value to remove from the array. + # * `keyPath` The {String} key path. + # * `value` The value to remove from the array. # # Returns the new array value of the setting. removeAtKeyPath: (keyPath, value) -> @@ -226,20 +226,17 @@ class Config @set(keyPath, arrayValue) result - # Public: Establishes an event listener for a given key. + # Essential: Add a listener for changes to a given key path. # - # `callback` is fired whenever the value of the key is changed and will - # be fired immediately unless the `callNow` option is `false`. - # - # keyPath - The {String} name of the key to observe - # options - An optional {Object} containing the `callNow` key. - # callback - The {Function} to call when the value of the key changes. - # The first argument will be the new value of the key and the - #   second argument will be an {Object} with a `previous` property - # that is the prior value of the key. + # * `keyPath` The {String} name of the key to observe + # * `options` An optional {Object} containing the `callNow` key. + # * `callback` The {Function} to call when the value of the key changes. + # The first argument will be the new value of the key and the + #   second argument will be an {Object} with a `previous` property + # that is the prior value of the key. # # Returns an {Object} with the following keys: - # :off - A {Function} that unobserves the `keyPath` when called. + # * `off` A {Function} that unobserves the `keyPath` when called. observe: (keyPath, options={}, callback) -> if _.isFunction(options) callback = options @@ -259,9 +256,9 @@ class Config callback(value) if options.callNow ? true subscription - # Public: Unobserve all callbacks on a given key. + # Unobserve all callbacks on a given key. # - # keyPath - The {String} name of the key to unobserve. + # * `keyPath` The {String} name of the key to unobserve. unobserve: (keyPath) -> @off("updated.#{keyPath.replace(/\./, '-')}") diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index fe285775a..52568340a 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -22,15 +22,13 @@ class ContextMenuManager ] # Public: Creates menu definitions from the object specified by the menu - # cson API. + # JSON API. # - # name - The path of the file that contains the menu definitions. - # object - The 'context-menu' object specified in the menu cson API. - # options - An {Object} with the following keys: - # :devMode - Determines whether the entries should only be shown when - # the window is in dev mode. - # - # Returns nothing. + # * `name` The path of the file that contains the menu definitions. + # * `object` The 'context-menu' object specified in the menu JSON API. + # * `options` An optional {Object} with the following keys: + # * `devMode` Determines whether the entries should only be shown when + # the window is in dev mode. add: (name, object, {devMode}={}) -> for selector, items of object for label, commandOrSubmenu of items @@ -43,6 +41,8 @@ class ContextMenuManager menuItem = @buildMenuItem(label, commandOrSubmenu) @addBySelector(selector, menuItem, {devMode}) + undefined + buildMenuItem: (label, command) -> if command is '-' {type: 'separator'} @@ -52,12 +52,12 @@ class ContextMenuManager # Registers a command to be displayed when the relevant item is right # clicked. # - # selector - The css selector for the active element which should include - # the given command in its context menu. - # definition - The object containing keys which match the menu template API. - # options - An {Object} with the following keys: - # :devMode - Indicates whether this command should only appear while the - # editor is in dev mode. + # * `selector` The css selector for the active element which should include + # the given command in its context menu. + # * `definition` The object containing keys which match the menu template API. + # * `options` An optional {Object} with the following keys: + # * `devMode` Indicates whether this command should only appear while the + # editor is in dev mode. addBySelector: (selector, definition, {devMode}={}) -> definitions = if devMode then @devModeDefinitions else @definitions if not _.findWhere(definitions[selector], definition) or _.isEqual(definition, {type: 'separator'}) @@ -79,7 +79,7 @@ class ContextMenuManager # active element are listed first. The further down the list you go, the higher # up the ancestor hierarchy they match. # - # element - The DOM element to generate the menu template for. + # * `element` The DOM element to generate the menu template for. menuTemplateForMostSpecificElement: (element, {devMode}={}) -> menuTemplate = @definitionsForElement(element, {devMode}) if element.parentElement @@ -109,9 +109,12 @@ class ContextMenuManager delete template.executeAtBuild # Public: Request a context menu to be displayed. + # + # * `event` A DOM event. showForEvent: (event) -> @activeElement = event.target menuTemplate = @combinedMenuTemplateForElement(event.target) return unless menuTemplate?.length > 0 @executeBuildHandlers(event, menuTemplate) remote.getCurrentWindow().emit('context-menu', menuTemplate) + undefined diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee deleted file mode 100644 index a3d67264b..000000000 --- a/src/cursor-view.coffee +++ /dev/null @@ -1,113 +0,0 @@ -{View} = require './space-pen-extensions' -_ = require 'underscore-plus' - -module.exports = -class CursorView extends View - @content: -> - @div class: 'cursor idle', => @raw ' ' - - @blinkPeriod: 800 - - @blinkCursors: -> - element.classList.toggle('blink-off') for [element] in @cursorViews - - @startBlinking: (cursorView) -> - @cursorViews ?= [] - @cursorViews.push(cursorView) - if @cursorViews.length is 1 - @blinkInterval = setInterval(@blinkCursors.bind(this), @blinkPeriod / 2) - - @stopBlinking: (cursorView) -> - cursorView[0].classList.remove('blink-off') - _.remove(@cursorViews, cursorView) - clearInterval(@blinkInterval) if @cursorViews.length is 0 - - blinking: false - visible: true - needsUpdate: true - needsRemoval: false - shouldPauseBlinking: false - - initialize: (@cursor, @editorView) -> - @subscribe @cursor, 'moved', => - @needsUpdate = true - @shouldPauseBlinking = true - - @subscribe @cursor, 'visibility-changed', => - @needsUpdate = true - - @subscribe @cursor, 'autoscrolled', => - @editorView.requestDisplayUpdate() - - @subscribe @cursor, 'destroyed', => - @needsRemoval = true - - beforeRemove: -> - @editorView.removeCursorView(this) - @stopBlinking() - - updateDisplay: -> - screenPosition = @getScreenPosition() - pixelPosition = @getPixelPosition() - - unless _.isEqual(@lastPixelPosition, pixelPosition) - @lastPixelPosition = pixelPosition - @css(pixelPosition) - @trigger 'cursor:moved' - - if @shouldPauseBlinking - @resetBlinking() - else if !@startBlinkingTimeout - @startBlinking() - - @setVisible(@cursor.isVisible() and not @editorView.getEditor().isFoldedAtScreenRow(screenPosition.row)) - - # Override for speed. The base function checks the computedStyle - isHidden: -> - this[0].style.display is 'none' or not @isOnDom() - - needsAutoscroll: -> - @cursor.needsAutoscroll - - clearAutoscroll: -> - @cursor.clearAutoscroll() - - getPixelPosition: -> - @editorView.pixelPositionForScreenPosition(@getScreenPosition()) - - setVisible: (visible) -> - unless @visible is visible - @visible = visible - hiddenCursor = 'hidden-cursor' - if visible - @removeClass hiddenCursor - else - @addClass hiddenCursor - - stopBlinking: -> - @constructor.stopBlinking(this) if @blinking - @blinking = false - - startBlinking: -> - @constructor.startBlinking(this) unless @blinking - @blinking = true - - resetBlinking: -> - @stopBlinking() - @startBlinking() - - getBufferPosition: -> - @cursor.getBufferPosition() - - getScreenPosition: -> - @cursor.getScreenPosition() - - removeIdleClassTemporarily: -> - @removeClass 'idle' - window.clearTimeout(@idleTimeout) if @idleTimeout - @idleTimeout = window.setTimeout (=> @addClass 'idle'), 200 - - resetCursorAnimation: -> - window.clearTimeout(@idleTimeout) if @idleTimeout - @removeClass 'idle' - _.defer => @addClass 'idle' diff --git a/src/cursor.coffee b/src/cursor.coffee index 8d99408fd..968b965dd 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -2,11 +2,36 @@ {Model} = require 'theorist' _ = require 'underscore-plus' -# Public: The `Cursor` class represents the little blinking line identifying +# Extended: The `Cursor` class represents the little blinking line identifying # where text can be inserted. # # Cursors belong to {Editor}s and have some metadata attached in the form # of a {Marker}. +# +# ## Events +# +# ### moved +# +# Extended: Emit when a cursor has been moved. If there are multiple cursors, +# it will be emit for each cursor. +# +# * `event` {Object} +# * `oldBufferPosition` {Point} +# * `oldScreenPosition` {Point} +# * `newBufferPosition` {Point} +# * `newScreenPosition` {Point} +# * `textChanged` {Boolean} +# +# ### destroyed +# +# Extended: Emit when the cursor is destroyed +# +# ### visibility-changed +# +# Extended: Emit when the Cursor is hidden or shown +# +# * `visible` {Boolean} true when cursor is visible +# module.exports = class Cursor extends Model screenPosition: null @@ -63,11 +88,10 @@ class Cursor extends Model # Public: Moves a cursor to a given screen position. # - # screenPosition - An {Array} of two numbers: the screen row, and the screen - # column. - # options - An {Object} with the following keys: - # :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever - # the cursor moves to. + # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. + # * `options` (optional) {Object} with the following keys: + # * `autoscroll` A Boolean which, if `true`, scrolls the {Editor} to wherever + # the cursor moves to. setScreenPosition: (screenPosition, options={}) -> @changePosition options, => @marker.setHeadScreenPosition(screenPosition, options) @@ -82,11 +106,10 @@ class Cursor extends Model # Public: Moves a cursor to a given buffer position. # - # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer - # column. - # options - An {Object} with the following keys: - # :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever - # the cursor moves to. + # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. + # * `options` (optional) {Object} with the following keys: + # * `autoscroll` A Boolean which, if `true`, scrolls the {Editor} to wherever + # the cursor moves to. setBufferPosition: (bufferPosition, options={}) -> @changePosition options, => @marker.setHeadBufferPosition(bufferPosition, options) @@ -114,10 +137,9 @@ class Cursor extends Model # Public: Get the RegExp used by the cursor to determine what a "word" is. # - # options: An optional {Object} with the following keys: - # :includeNonWordCharacters - A {Boolean} indicating whether to include - # non-word characters in the regex. - # (default: true) + # * `options` (optional) {Object} with the following keys: + # * `includeNonWordCharacters` A {Boolean} indicating whether to include + # non-word characters in the regex. (default: true) # # Returns a {RegExp}. wordRegExp: ({includeNonWordCharacters}={}) -> @@ -228,9 +250,9 @@ class Cursor extends Model # Public: Moves the cursor left one screen column. # - # options - An {Object} with the following keys: - # :moveToEndOfSelection - if true, move to the left of the selection if a - # selection exists. + # * `options` (optional) {Object} with the following keys: + # * `moveToEndOfSelection` if true, move to the left of the selection if a + # selection exists. moveLeft: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @@ -242,9 +264,9 @@ class Cursor extends Model # Public: Moves the cursor right one screen column. # - # options - An {Object} with the following keys: - # :moveToEndOfSelection - if true, move to the right of the selection if a - # selection exists. + # * `options` (optional) {Object} with the following keys: + # * `moveToEndOfSelection` if true, move to the right of the selection if a + # selection exists. moveRight: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @@ -332,14 +354,14 @@ class Cursor extends Model # Public: Retrieves the buffer position of where the current word starts. # - # options - An {Object} with the following keys: - # :wordRegex - A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # :includeNonWordCharacters - A {Boolean} indicating whether to include - # non-word characters in the default word regex. - # Has no effect if wordRegex is set. - # :allowPrevious - A {Boolean} indicating whether the beginning of the - # previous word can be returned. + # * `options` (optional) An {Object} with the following keys: + # * `wordRegex` A {RegExp} indicating what constitutes a "word" + # (default: {::wordRegExp}). + # * `includeNonWordCharacters` A {Boolean} indicating whether to include + # non-word characters in the default word regex. + # Has no effect if wordRegex is set. + # * `allowPrevious` A {Boolean} indicating whether the beginning of the + # previous word can be returned. # # Returns a {Range}. getBeginningOfCurrentWordBufferPosition: (options = {}) -> @@ -407,12 +429,12 @@ class Cursor extends Model # Public: Retrieves the buffer position of where the current word ends. # - # options - An {Object} with the following keys: - # :wordRegex - A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - # :includeNonWordCharacters - A Boolean indicating whether to include - # non-word characters in the default word regex. - # Has no effect if wordRegex is set. + # * `options` (optional) {Object} with the following keys: + # * `wordRegex` A {RegExp} indicating what constitutes a "word" + # (default: {::wordRegExp}) + # * `includeNonWordCharacters` A Boolean indicating whether to include + # non-word characters in the default word regex. Has no effect if + # wordRegex is set. # # Returns a {Range}. getEndOfCurrentWordBufferPosition: (options = {}) -> @@ -431,11 +453,11 @@ class Cursor extends Model # Public: Retrieves the buffer position of where the next word starts. # - # options - - # :wordRegex - A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). + # * `options` (optional) {Object} + # * `wordRegex` A {RegExp} indicating what constitutes a "word" + # (default: {::wordRegExp}). # - # Returns a {Range}. + # Returns a {Range} getBeginningOfNextWordBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() start = if @isInsideWord() then @getEndOfCurrentWordBufferPosition() else currentBufferPosition @@ -450,9 +472,9 @@ class Cursor extends Model # Public: Returns the buffer Range occupied by the word located under the cursor. # - # options - - # :wordRegex - A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). + # * `options` (optional) {Object} + # * `wordRegex` A {RegExp} indicating what constitutes a "word" + # (default: {::wordRegExp}). getCurrentWordBufferRange: (options={}) -> startOptions = _.extend(_.clone(options), allowPrevious: false) endOptions = _.extend(_.clone(options), allowNext: false) @@ -460,9 +482,9 @@ class Cursor extends Model # Public: Returns the buffer Range for the current line. # - # options - - # :includeNewline: - A {Boolean} which controls whether the Range should - # include the newline. + # * `options` (optional) {Object} + # * `includeNewline` A {Boolean} which controls whether the Range should + # include the newline. getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) diff --git a/src/decoration.coffee b/src/decoration.coffee index 96acfcee1..3d888ab34 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -4,7 +4,7 @@ _ = require 'underscore-plus' idCounter = 0 nextId = -> idCounter++ -# Public: Represents a decoration that follows a {Marker}. A decoration is +# Essential: Represents a decoration that follows a {Marker}. A decoration is # basically a visual representation of a marker. It allows you to add CSS # classes to line numbers in the gutter, lines, and add selection-line regions # around marked ranges of text. @@ -20,27 +20,39 @@ nextId = -> idCounter++ # # Best practice for destorying the decoration is by destroying the {Marker}. # -# ``` +# ```coffee # marker.destroy() # ``` # # You should only use {Decoration::destroy} when you still need or do not own # the marker. # -# ### IDs -# Each {Decoration} has a unique ID available via `decoration.id`. +# ## Events # -# ### Events -# A couple of events are emitted: +# ### updated # -# * `destroyed`: When the {Decoration} is destroyed -# * `updated`: When the {Decoration} is updated via {Decoration::update}. -# Event object has properties `oldParams` and `newParams` +# Extended: When the {Decoration} is updated via {Decoration::update}. +# +# * `event` {Object} +# * `oldParams` {Object} the old parameters the decoration used to have +# * `newParams` {Object} the new parameters the decoration now has +# +# ### destroyed +# +# Extended: When the {Decoration} is destroyed # module.exports = class Decoration Emitter.includeInto(this) + # Extended: Check if the `decorationParams.type` matches `type` + # + # * `decorationParams` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` + # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also + # be an {Array} of {String}s, where it will return true if the decoration's + # type matches any in the array. + # + # Returns {Boolean} @isType: (decorationParams, type) -> if _.isArray(decorationParams.type) type in decorationParams.type @@ -53,21 +65,34 @@ class Decoration @flashQueue = null @isDestroyed = false - # Public: Destroy this marker. - # - # If you own the marker, you should use {Marker::destroy} which will destroy - # this decoration. - destroy: -> - return if @isDestroyed - @isDestroyed = true - @displayBuffer.removeDecoration(this) - @emit 'destroyed' + # Essential: An id unique across all {Decoration} objects + getId: -> @id - # Public: Update the marker with new params. Allows you to change the decoration's class. + # Essential: Returns the marker associated with this {Decoration} + getMarker: -> @marker + + # Essential: Returns the {Decoration}'s params. + getParams: -> @params + + # Public: Check if this decoration is of type `type` # - # ``` + # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also + # be an {Array} of {String}s, where it will return true if the decoration's + # type matches any in the array. + # + # Returns {Boolean} + isType: (type) -> + Decoration.isType(@params, type) + + # Essential: Update the marker with new params. Allows you to change the decoration's class. + # + # ## Examples + # + # ```coffee # decoration.update({type: 'gutter', class: 'my-new-class'}) # ``` + # + # * `newParams` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` update: (newParams) -> return if @isDestroyed oldParams = @params @@ -76,19 +101,15 @@ class Decoration @displayBuffer.decorationUpdated(this) @emit 'updated', {oldParams, newParams} - # Public: Returns the marker associated with this {Decoration} - getMarker: -> @marker - - # Public: Returns the {Decoration}'s params. - getParams: -> @params - - # Public: Check if this decoration is of type `type` + # Essential: Destroy this marker. # - # type - A {String} type like `'gutter'` - # - # Returns a {Boolean} - isType: (type) -> - Decoration.isType(@params, type) + # If you own the marker, you should use {Marker::destroy} which will destroy + # this decoration. + destroy: -> + return if @isDestroyed + @isDestroyed = true + @displayBuffer.removeDecoration(this) + @emit 'destroyed' matchesPattern: (decorationPattern) -> return false unless decorationPattern? diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 16fa0c442..7a4390f6a 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -3,7 +3,7 @@ # An instance of this class is always available as the `atom.deserializers` # global. # -# ### Registering a deserializer +# ## Examples # # ```coffee # class MyPackageView extends View @@ -24,21 +24,21 @@ class DeserializerManager # Public: Register the given class(es) as deserializers. # - # classes - One or more classes to register. + # * `classes` One or more classes to register. add: (classes...) -> @deserializers[klass.name] = klass for klass in classes # Public: Remove the given class(es) as deserializers. # - # classes - One or more classes to remove. + # * `classes` One or more classes to remove. remove: (classes...) -> delete @deserializers[name] for {name} in classes # Public: Deserialize the state and params. # - # state - The state {Object} to deserialize. - # params - The params {Object} to pass as the second arguments to the - # deserialize method of the deserializer. + # * `state` The state {Object} to deserialize. + # * `params` The params {Object} to pass as the second arguments to the + # deserialize method of the deserializer. deserialize: (state, params) -> return unless state? @@ -51,7 +51,7 @@ class DeserializerManager # Get the deserializer for the state. # - # state - The state {Object} being deserialized. + # * `state` The state {Object} being deserialized. get: (state) -> return unless state? diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 76f83e713..0b7121acc 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1097,7 +1097,10 @@ class DisplayBuffer extends Model handleBufferMarkerCreated: (marker) => @createFoldForMarker(marker) if marker.matchesAttributes(@getFoldMarkerAttributes()) - @emit 'marker-created', @getMarker(marker.id) + if displayBufferMarker = @getMarker(marker.id) + # The marker might have been removed in some other handler called before + # this one. Only emit when the marker still exists. + @emit 'marker-created', displayBufferMarker createFoldForMarker: (marker) -> @decorateMarker(marker, type: 'gutter', class: 'folded') diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 5d3a88ee4..b14a169b8 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -59,6 +59,8 @@ EditorComponent = React.createClass [renderedStartRow, renderedEndRow] = renderedRowRange cursorPixelRects = @getCursorPixelRects(renderedRowRange) + tokenizedLines = editor.linesForScreenRows(renderedStartRow, renderedEndRow - 1) + decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow) highlightDecorations = @getHighlightDecorations(decorations) lineDecorations = @getLineDecorations(decorations) @@ -107,7 +109,7 @@ EditorComponent = React.createClass LinesComponent { ref: 'lines', - editor, lineHeightInPixels, defaultCharWidth, lineDecorations, highlightDecorations, + editor, lineHeightInPixels, defaultCharWidth, tokenizedLines, lineDecorations, highlightDecorations, showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow, visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration, @@ -191,11 +193,6 @@ EditorComponent = React.createClass componentWillReceiveProps: (newProps) -> @props.editor.setMini(newProps.mini) - componentWillUpdate: -> - @updatesPaused = true - @checkForVisibilityChange() - @updatesPaused = false - componentDidUpdate: (prevProps, prevState) -> cursorsMoved = @cursorsMoved selectionChanged = @selectionChanged @@ -211,6 +208,7 @@ EditorComponent = React.createClass @props.parentView.trigger 'editor:display-updated' becameVisible: -> + @updatesPaused = true @sampleFontStyling() @sampleBackgroundColors() @measureHeightAndWidth() @@ -219,6 +217,8 @@ EditorComponent = React.createClass @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown @props.editor.setVisible(true) @performedInitialMeasurement = true + @updatesPaused = false + @forceUpdate() if @updateRequestedWhilePaused requestUpdate: -> return unless @isMounted() @@ -610,6 +610,10 @@ EditorComponent = React.createClass {editor} = @props {detail, shiftKey, metaKey, ctrlKey} = event + + # CTRL+click brings up the context menu on OSX, so don't handle those either + return if ctrlKey and process.platform is 'darwin' + screenPosition = @screenPositionForMouseEvent(event) if event.target?.classList.contains('fold-marker') diff --git a/src/editor-view.coffee b/src/editor-view.coffee index 94f85392d..cc18f7e9e 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -1,23 +1,18 @@ -{View, $, $$$} = require './space-pen-extensions' -GutterView = require './gutter-view' -{Point, Range} = require 'text-buffer' -Editor = require './editor' -CursorView = require './cursor-view' -SelectionView = require './selection-view' -fs = require 'fs-plus' -_ = require 'underscore-plus' +{View, $} = require 'space-pen' +React = require 'react-atom-fork' +{defaults} = require 'underscore-plus' TextBuffer = require 'text-buffer' - -MeasureRange = document.createRange() -TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } -NoScope = ['no-scope'] -LongLineLength = 1000 +Editor = require './editor' +EditorComponent = require './editor-component' +{deprecate} = require 'grim' # Public: Represents the entire visual pane in Atom. # # The EditorView manages the {Editor}, which manages the file buffers. # -# ## Requiring in packages +# ## Examples +# +# Requiring in packages # # ```coffee # {EditorView} = require 'atom' @@ -25,22 +20,21 @@ LongLineLength = 1000 # miniEditorView = new EditorView(mini: true) # ``` # -# ## Iterating over the open editor views +# Iterating over the open editor views # # ```coffee # for editorView in atom.workspaceView.getEditorViews() -# console.log(editorView.getEditor().getPath()) +# console.log(editorView.getModel().getPath()) # ``` # -# ## Subscribing to every current and future editor +# Subscribing to every current and future editor # # ```coffee # atom.workspace.eachEditorView (editorView) -> -# console.log(editorView.getEditor().getPath()) +# console.log(editorView.getModel().getPath()) # ``` module.exports = class EditorView extends View - @characterWidthCache: {} @configDefaults: fontFamily: '' fontSize: 16 @@ -65,724 +59,167 @@ class EditorView extends View tab: '\u00bb' cr: '\u00a4' - @nextEditorId: 1 - @content: (params) -> - attributes = { class: @classes(params), tabindex: -1 } - _.extend(attributes, params.attributes) if params.attributes - @div attributes, => - @subview 'gutter', new GutterView - @div class: 'scroll-view', outlet: 'scrollView', => - @div class: 'overlayer', outlet: 'overlayer' - @div class: 'lines', outlet: 'renderedLines' - @div class: 'underlayer', outlet: 'underlayer', => - @input class: 'hidden-input', outlet: 'hiddenInput' - @div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', => - @div outlet: 'verticalScrollbarContent' + attributes = params.attributes ? {} + attributes.class = 'editor react editor-colors' + attributes.tabIndex = -1 + @div attributes - @classes: ({mini} = {}) -> - classes = ['editor', 'editor-colors'] - classes.push 'mini' if mini - classes.join(' ') - - vScrollMargin: 2 - hScrollMargin: 10 - lineHeight: null - charWidth: null - charHeight: null - cursorViews: null - selectionViews: null - lineCache: null - isFocused: false - editor: null - attached: false - lineOverdraw: 10 - pendingChanges: null - newCursors: null - newSelections: null - redrawOnReattach: false - bottomPaddingInLines: 10 + focusOnAttach: false # The constructor for setting up an `EditorView` instance. # - # editorOrOptions - Either an {Editor}, or an object with one property, `mini`. - # If `mini` is `true`, a "miniature" `Editor` is constructed. - # Typically, this is ideal for scenarios where you need an Atom editor, - # but without all the chrome, like scrollbars, gutter, _e.t.c._. + # * `editorOrParams` Either an {Editor}, or an object with one property, `mini`. + # If `mini` is `true`, a "miniature" `Editor` is constructed. + # Typically, this is ideal for scenarios where you need an Atom editor, + # but without all the chrome, like scrollbars, gutter, _e.t.c._. # - initialize: (editorOrOptions) -> - if editorOrOptions instanceof Editor - editor = editorOrOptions + constructor: (editorOrParams, props) -> + super + + if editorOrParams instanceof Editor + @editor = editorOrParams else - {editor, @mini, placeholderText} = editorOrOptions ? {} - - @id = EditorView.nextEditorId++ - @lineCache = [] - @configure() - @bindKeys() - @handleEvents() - @handleInputEvents() - @cursorViews = [] - @selectionViews = [] - @pendingChanges = [] - @newCursors = [] - @newSelections = [] - - @setPlaceholderText(placeholderText) if placeholderText - - if editor? - @edit(editor) - else if @mini - @edit(new Editor + {@editor, mini, placeholderText} = editorOrParams + props ?= {} + props.mini = mini + props.placeholderText = placeholderText + @editor ?= new Editor buffer: new TextBuffer softWrap: false tabLength: 2 softTabs: true - ) - else - throw new Error("Must supply an Editor or mini: true") + mini: mini - # Sets up the core Atom commands. - # - # Some commands are excluded from mini-editors. - bindKeys: -> - editorBindings = - 'core:move-left': => @editor.moveCursorLeft() - 'core:move-right': => @editor.moveCursorRight() - 'core:select-left': => @editor.selectLeft() - 'core:select-right': => @editor.selectRight() - 'core:select-all': => @editor.selectAll() - 'core:backspace': => @editor.backspace() - 'core:delete': => @editor.delete() - 'core:undo': => @editor.undo() - 'core:redo': => @editor.redo() - 'core:cut': => @editor.cutSelectedText() - 'core:copy': => @editor.copySelectedText() - 'core:paste': => @editor.pasteText() - 'editor:move-to-previous-word': => @editor.moveCursorToPreviousWord() - 'editor:select-word': => @editor.selectWord() - 'editor:consolidate-selections': (event) => @consolidateSelections(event) - 'editor:delete-to-beginning-of-word': => @editor.deleteToBeginningOfWord() - 'editor:delete-to-beginning-of-line': => @editor.deleteToBeginningOfLine() - 'editor:delete-to-end-of-line': => @editor.deleteToEndOfLine() - 'editor:delete-to-end-of-word': => @editor.deleteToEndOfWord() - 'editor:delete-line': => @editor.deleteLine() - 'editor:cut-to-end-of-line': => @editor.cutToEndOfLine() - 'editor:move-to-beginning-of-next-paragraph': => @editor.moveCursorToBeginningOfNextParagraph() - 'editor:move-to-beginning-of-previous-paragraph': => @editor.moveCursorToBeginningOfPreviousParagraph() - 'editor:move-to-beginning-of-screen-line': => @editor.moveCursorToBeginningOfScreenLine() - 'editor:move-to-beginning-of-line': => @editor.moveCursorToBeginningOfLine() - 'editor:move-to-end-of-screen-line': => @editor.moveCursorToEndOfScreenLine() - 'editor:move-to-end-of-line': => @editor.moveCursorToEndOfLine() - 'editor:move-to-first-character-of-line': => @editor.moveCursorToFirstCharacterOfLine() - 'editor:move-to-beginning-of-word': => @editor.moveCursorToBeginningOfWord() - 'editor:move-to-end-of-word': => @editor.moveCursorToEndOfWord() - 'editor:move-to-beginning-of-next-word': => @editor.moveCursorToBeginningOfNextWord() - 'editor:move-to-previous-word-boundary': => @editor.moveCursorToPreviousWordBoundary() - 'editor:move-to-next-word-boundary': => @editor.moveCursorToNextWordBoundary() - 'editor:select-to-beginning-of-next-paragraph': => @editor.selectToBeginningOfNextParagraph() - 'editor:select-to-beginning-of-previous-paragraph': => @editor.selectToBeginningOfPreviousParagraph() - 'editor:select-to-end-of-line': => @editor.selectToEndOfLine() - 'editor:select-to-beginning-of-line': => @editor.selectToBeginningOfLine() - 'editor:select-to-end-of-word': => @editor.selectToEndOfWord() - 'editor:select-to-beginning-of-word': => @editor.selectToBeginningOfWord() - 'editor:select-to-beginning-of-next-word': => @editor.selectToBeginningOfNextWord() - 'editor:select-to-next-word-boundary': => @editor.selectToNextWordBoundary() - 'editor:select-to-previous-word-boundary': => @editor.selectToPreviousWordBoundary() - 'editor:select-to-first-character-of-line': => @editor.selectToFirstCharacterOfLine() - 'editor:select-line': => @editor.selectLine() - 'editor:transpose': => @editor.transpose() - 'editor:upper-case': => @editor.upperCase() - 'editor:lower-case': => @editor.lowerCase() + props = defaults({@editor, parentView: this}, props) + @component = React.renderComponent(EditorComponent(props), @element) - unless @mini - _.extend editorBindings, - 'core:move-up': => @editor.moveCursorUp() - 'core:move-down': => @editor.moveCursorDown() - 'core:move-to-top': => @editor.moveCursorToTop() - 'core:move-to-bottom': => @editor.moveCursorToBottom() - 'core:page-up': => @pageUp() - 'core:page-down': => @pageDown() - 'core:select-up': => @editor.selectUp() - 'core:select-down': => @editor.selectDown() - 'core:select-to-top': => @editor.selectToTop() - 'core:select-to-bottom': => @editor.selectToBottom() - 'core:select-page-up': => @editor.selectUp(@getPageRows()) - 'core:select-page-down': => @editor.selectDown(@getPageRows()) - 'editor:indent': => @editor.indent() - 'editor:auto-indent': => @editor.autoIndentSelectedRows() - 'editor:indent-selected-rows': => @editor.indentSelectedRows() - 'editor:outdent-selected-rows': => @editor.outdentSelectedRows() - 'editor:newline': => @editor.insertNewline() - 'editor:newline-below': => @editor.insertNewlineBelow() - 'editor:newline-above': => @editor.insertNewlineAbove() - 'editor:add-selection-below': => @editor.addSelectionBelow() - 'editor:add-selection-above': => @editor.addSelectionAbove() - 'editor:split-selections-into-lines': => @editor.splitSelectionsIntoLines() - 'editor:toggle-soft-tabs': => @toggleSoftTabs() - 'editor:toggle-soft-wrap': => @toggleSoftWrap() - 'editor:fold-all': => @editor.foldAll() - 'editor:unfold-all': => @editor.unfoldAll() - 'editor:fold-current-row': => @editor.foldCurrentRow() - 'editor:unfold-current-row': => @editor.unfoldCurrentRow() - 'editor:fold-selection': => @editor.foldSelectedLines() - 'editor:fold-at-indent-level-1': => @editor.foldAllAtIndentLevel(0) - 'editor:fold-at-indent-level-2': => @editor.foldAllAtIndentLevel(1) - 'editor:fold-at-indent-level-3': => @editor.foldAllAtIndentLevel(2) - 'editor:fold-at-indent-level-4': => @editor.foldAllAtIndentLevel(3) - 'editor:fold-at-indent-level-5': => @editor.foldAllAtIndentLevel(4) - 'editor:fold-at-indent-level-6': => @editor.foldAllAtIndentLevel(5) - 'editor:fold-at-indent-level-7': => @editor.foldAllAtIndentLevel(6) - 'editor:fold-at-indent-level-8': => @editor.foldAllAtIndentLevel(7) - 'editor:fold-at-indent-level-9': => @editor.foldAllAtIndentLevel(8) - 'editor:toggle-line-comments': => @toggleLineCommentsInSelection() - 'editor:log-cursor-scope': => @logCursorScope() - 'editor:checkout-head-revision': => atom.project.getRepo()?.checkoutHeadForEditor(@editor) - 'editor:copy-path': => @copyPathToClipboard() - 'editor:move-line-up': => @editor.moveLineUp() - 'editor:move-line-down': => @editor.moveLineDown() - 'editor:duplicate-lines': => @editor.duplicateLines() - 'editor:join-lines': => @editor.joinLines() - 'editor:toggle-indent-guide': -> atom.config.toggle('editor.showIndentGuide') - 'editor:toggle-line-numbers': -> atom.config.toggle('editor.showLineNumbers') - 'editor:scroll-to-cursor': => @scrollToCursorPosition() + node = @component.getDOMNode() - documentation = {} - for name, method of editorBindings - do (name, method) => - @command name, (e) -> method(e); false + @scrollView = $(node).find('.scroll-view') + @underlayer = $(node).find('.highlights').addClass('underlayer') + @overlayer = $(node).find('.lines').addClass('overlayer') + @hiddenInput = $(node).find('.hidden-input') + + # FIXME: there should be a better way to deal with the gutter element + @subscribe atom.config.observe 'editor.showLineNumbers', => + @gutter = $(node).find('.gutter') + + @gutter.removeClassFromAllLines = (klass) => + deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html') + @gutter.find('.line-number').removeClass(klass) + + @gutter.getLineNumberElement = (bufferRow) => + deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html') + @gutter.find("[data-buffer-row='#{bufferRow}']") + + @gutter.addClassToLine = (bufferRow, klass) => + deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html') + lines = @gutter.find("[data-buffer-row='#{bufferRow}']") + lines.addClass(klass) + lines.length > 0 + + @on 'focus', => + if @component? + @component.onFocus() + else + @focusOnAttach = true # Public: Get the underlying editor model for this view. # - # Returns an {Editor}. - getEditor: -> - @editor + # Returns an {Editor} + getModel: -> @editor - # {Delegates to: Editor.getText} - getText: -> - @editor.getText() + getEditor: -> @editor - # {Delegates to: Editor.setText} - setText: (text) -> - @editor.setText(text) - - # {Delegates to: Editor.insertText} - insertText: (text, options) -> - @editor.insertText(text, options) - - setHeightInLines: (heightInLines) -> - heightInLines ?= @calculateHeightInLines() - @heightInLines = heightInLines if heightInLines - - # {Delegates to: Editor.setEditorWidthInChars} - setWidthInChars: (widthInChars) -> - widthInChars ?= @calculateWidthInChars() - @editor.setEditorWidthInChars(widthInChars) if widthInChars - - # Public: Emulates the "page down" key, where the last row of a buffer scrolls - # to become the first. - pageDown: -> - newScrollTop = @scrollTop() + @scrollView[0].clientHeight - @editor.moveCursorDown(@getPageRows()) - @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - - # Public: Emulates the "page up" key, where the frst row of a buffer scrolls - # to become the last. - pageUp: -> - newScrollTop = @scrollTop() - @scrollView[0].clientHeight - @editor.moveCursorUp(@getPageRows()) - @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - - # Gets the number of actual page rows existing in an editor. - # - # Returns a {Number}. - getPageRows: -> - Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) - - # Public: Set whether invisible characters are shown. - # - # showInvisibles - A {Boolean} which, if `true`, show invisible characters. - setShowInvisibles: (showInvisibles) -> - return if showInvisibles == @showInvisibles - @showInvisibles = showInvisibles - @resetDisplay() - - # Public: Defines which characters are invisible. - # - # invisibles - An {Object} defining the invisible characters: - # :eol - The end of line invisible {String} (default: `\u00ac`). - # :space - The space invisible {String} (default: `\u00b7`). - # :tab - The tab invisible {String} (default: `\u00bb`). - # :cr - The carriage return invisible {String} (default: `\u00a4`). - setInvisibles: (@invisibles={}) -> - _.defaults @invisibles, - eol: '\u00ac' - space: '\u00b7' - tab: '\u00bb' - cr: '\u00a4' - @resetDisplay() - - # Public: Sets whether you want to show the indentation guides. - # - # showIndentGuide - A {Boolean} you can set to `true` if you want to see the - # indentation guides. - setShowIndentGuide: (showIndentGuide) -> - return if showIndentGuide == @showIndentGuide - @showIndentGuide = showIndentGuide - @resetDisplay() - - # Public: Set the text to appear in the editor when it is empty. - # - # This only affects mini editors. - # - # placeholderText - A {String} of text to display when empty. - setPlaceholderText: (placeholderText) -> - return unless @mini - @placeholderText = placeholderText - @requestDisplayUpdate() - - getPlaceholderText: -> - @placeholderText - - configure: -> - @subscribe atom.config.observe 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) - @subscribe atom.config.observe 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) - @subscribe atom.config.observe 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) - @subscribe atom.config.observe 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) - @subscribe atom.config.observe 'editor.fontSize', (fontSize) => @setFontSize(fontSize) - @subscribe atom.config.observe 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) - @subscribe atom.config.observe 'editor.lineHeight', (lineHeight) => @setLineHeight(lineHeight) - - handleEvents: -> - @on 'focus', => - @hiddenInput.focus() - false - - @hiddenInput.on 'focus', => - @bringHiddenInputIntoView() - @isFocused = true - @addClass 'is-focused' - - @hiddenInput.on 'focusout', => - @bringHiddenInputIntoView() - @isFocused = false - @removeClass 'is-focused' - - @underlayer.on 'mousedown', (e) => - @renderedLines.trigger(e) - false if @isFocused - - @overlayer.on 'mousedown', (e) => - return unless e.which is 1 # only handle the left mouse button - - @overlayer.hide() - clickedElement = document.elementFromPoint(e.pageX, e.pageY) - @overlayer.show() - e.target = clickedElement - $(clickedElement).trigger(e) - false if @isFocused - - @renderedLines.on 'mousedown', '.fold.line', (e) => - id = $(e.currentTarget).attr('fold-id') - marker = @editor.displayBuffer.getMarker(id) - @editor.setCursorBufferPosition(marker.getBufferRange().start) - @editor.destroyFoldWithId(id) - false - - @gutter.on 'mousedown', '.foldable .icon-right', (e) => - bufferRow = $(e.target).parent().data('bufferRow') - @editor.toggleFoldAtBufferRow(bufferRow) - false - - @renderedLines.on 'mousedown', (e) => - clickCount = e.originalEvent.detail - - screenPosition = @screenPositionFromMouseEvent(e) - if clickCount == 1 - if e.metaKey or (process.platform isnt 'darwin' and e.ctrlKey) - @editor.addCursorAtScreenPosition(screenPosition) - else if e.shiftKey - @editor.selectToScreenPosition(screenPosition) - else - @editor.setCursorScreenPosition(screenPosition) - else if clickCount == 2 - @editor.selectWord() unless e.shiftKey - else if clickCount == 3 - @editor.selectLine() unless e.shiftKey - - @selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1 - - unless @mini - @scrollView.on 'mousewheel', (e) => - if delta = e.originalEvent.wheelDeltaY - @scrollTop(@scrollTop() - delta) - false - - @verticalScrollbar.on 'scroll', => - @scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false) - - @scrollView.on 'scroll', => - if @scrollLeft() == 0 - @gutter.removeClass('drop-shadow') - else - @gutter.addClass('drop-shadow') - - # Listen for overflow events to detect when the editor's width changes - # to update the soft wrap column. - updateWidthInChars = _.debounce((=> @setWidthInChars()), 100) - @scrollView.on 'overflowchanged', => - updateWidthInChars() if @[0].classList.contains('soft-wrap') - - @subscribe atom.themes, 'stylesheets-changed', => @recalculateDimensions() - - handleInputEvents: -> - @on 'cursor:moved', => - return unless @isFocused - cursorView = @getCursorView() - - if cursorView.isVisible() - # This is an order of magnitude faster than checking .offset(). - style = cursorView[0].style - @hiddenInput[0].style.top = style.top - @hiddenInput[0].style.left = style.left - - selectedText = null - @hiddenInput.on 'compositionstart', => - selectedText = @editor.getSelectedText() - @hiddenInput.css('width', '100%') - @hiddenInput.on 'compositionupdate', (e) => - @editor.insertText(e.originalEvent.data, {select: true, undo: 'skip'}) - @hiddenInput.on 'compositionend', => - @editor.insertText(selectedText, {select: true, undo: 'skip'}) - @hiddenInput.css('width', '1px') - - lastInput = '' - @on "textInput", (e) => - # Work around of the accented character suggestion feature in OS X. - selectedLength = @hiddenInput[0].selectionEnd - @hiddenInput[0].selectionStart - if selectedLength is 1 and lastInput is @hiddenInput.val() - @editor.selectLeft() - - lastInput = e.originalEvent.data - @editor.insertText(lastInput) - - if lastInput is ' ' - true # Prevents parent elements from scrolling when a space is typed - else - @hiddenInput.val(lastInput) - false - - # Ignore paste event, on Linux is wrongly emitted when user presses ctrl-v. - @on "paste", -> false - - bringHiddenInputIntoView: -> - @hiddenInput.css(top: @scrollTop(), left: @scrollLeft()) - - selectOnMousemoveUntilMouseup: -> - lastMoveEvent = null - - finalizeSelections = => - clearInterval(interval) - $(document).off 'mousemove', moveHandler - $(document).off 'mouseup', finalizeSelections - - unless @editor.isDestroyed() - @editor.mergeIntersectingSelections(reversed: @editor.getLastSelection().isReversed()) - @editor.finalizeSelections() - @syncCursorAnimations() - - moveHandler = (event = lastMoveEvent) => - return unless event? - - if event.which is 1 and @[0].style.display isnt 'none' - @editor.selectToScreenPosition(@screenPositionFromMouseEvent(event)) - lastMoveEvent = event - else - finalizeSelections() - - $(document).on "mousemove.editor-#{@id}", moveHandler - interval = setInterval(moveHandler, 20) - $(document).one "mouseup.editor-#{@id}", finalizeSelections + Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels() + Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth() + Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] + Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] + Object.defineProperty @::, 'active', get: -> @is(@getPane()?.activeView) + Object.defineProperty @::, 'isFocused', get: -> @component?.state.focused + Object.defineProperty @::, 'mini', get: -> @component?.props.mini afterAttach: (onDom) -> return unless onDom - - # TODO: Remove this guard when we understand why this is happening - unless @editor.isAlive() - if atom.isReleasedVersion() - return - else - throw new Error("Assertion failure: EditorView is getting attached to a dead editor. Why?") - - @redraw() if @redrawOnReattach return if @attached @attached = true - @calculateDimensions() - @setWidthInChars() - @subscribe $(window), "resize.editor-#{@id}", => - @setHeightInLines() - @setWidthInChars() - @updateLayerDimensions() - @requestDisplayUpdate() - @focus() if @isFocused + @component.pollDOM() + @focus() if @focusOnAttach - if pane = @getPane() - @active = @is(pane.activeView) - @subscribe pane, 'pane:active-item-changed', (event, item) => - wasActive = @active - @active = @is(pane.activeView) - @redraw() if @active and not wasActive - - @resetDisplay() + @addGrammarScopeAttribute() + @subscribe @editor, 'grammar-changed', => + @addGrammarScopeAttribute() @trigger 'editor:attached', [this] - edit: (editor) -> - return if editor is @editor + addGrammarScopeAttribute: -> + grammarScope = @editor.getGrammar()?.scopeName?.replace(/\./g, ' ') + @attr('data-grammar', grammarScope) - if @editor - @saveScrollPositionForEditor() - @unsubscribe(@editor) - - @editor = editor - - return unless @editor? - - @editor.setVisible(true) - - @subscribe @editor, "destroyed", => - @remove() - - @subscribe @editor, "contents-conflicted", => - @showBufferConflictAlert(@editor) - - @subscribe @editor, "path-changed", => - @trigger 'editor:path-changed' - - @subscribe @editor, "grammar-changed", => - @trigger 'editor:grammar-changed' - - @subscribe @editor, 'selection-added', (selection) => - @newCursors.push(selection.cursor) - @newSelections.push(selection) - @requestDisplayUpdate() - - @subscribe @editor, 'screen-lines-changed', (e) => - @handleScreenLinesChange(e) - - @subscribe @editor, 'scroll-top-changed', (scrollTop) => - @scrollTop(scrollTop) - - @subscribe @editor, 'scroll-left-changed', (scrollLeft) => - @scrollView.scrollLeft(scrollLeft) - - @subscribe @editor, 'soft-wrap-changed', (softWrap) => - @setSoftWrap(softWrap) - - @trigger 'editor:path-changed' - @resetDisplay() - - if @attached and @editor.buffer.isInConflict() - _.defer => @showBufferConflictAlert(@editor) # Display after editor has a chance to display - - getModel: -> - @editor - - setModel: (editor) -> - @edit(editor) - - showBufferConflictAlert: (editor) -> - atom.confirm - message: editor.getPath() - detailedMessage: "Has changed on disk. Do you want to reload it?" - buttons: - Reload: -> editor.getBuffer().reload() - Cancel: null - - scrollTop: (scrollTop, options={}) -> - return @cachedScrollTop or 0 unless scrollTop? - maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height() - scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop))) - return if scrollTop == @cachedScrollTop - @cachedScrollTop = scrollTop - - @updateDisplay() if @attached - - @renderedLines.css('top', -scrollTop) - @underlayer.css('top', -scrollTop) - @overlayer.css('top', -scrollTop) - @gutter.lineNumbers.css('top', -scrollTop) - - if options?.adjustVerticalScrollbar ? true - @verticalScrollbar.scrollTop(scrollTop) - @editor.setScrollTop(@scrollTop()) - - scrollBottom: (scrollBottom) -> - if scrollBottom? - @scrollTop(scrollBottom - @scrollView.height()) + scrollTop: (scrollTop) -> + if scrollTop? + @editor.setScrollTop(scrollTop) else - @scrollTop() + @scrollView.height() + @editor.getScrollTop() scrollLeft: (scrollLeft) -> if scrollLeft? - @scrollView.scrollLeft(scrollLeft) - @editor.setScrollLeft(@scrollLeft()) + @editor.setScrollLeft(scrollLeft) else - @scrollView.scrollLeft() - - scrollRight: (scrollRight) -> - if scrollRight? - @scrollView.scrollRight(scrollRight) - @editor.setScrollLeft(@scrollLeft()) - else - @scrollView.scrollRight() + @editor.getScrollLeft() # Public: Scrolls the editor to the bottom. scrollToBottom: -> - @scrollBottom(@editor.getScreenLineCount() * @lineHeight) - - # Public: Scrolls the editor to the position of the most recently added - # cursor if it isn't current on screen. - # - # The editor is centered around the cursor's position if possible. - scrollToCursorPosition: -> - @scrollToBufferPosition(@editor.getCursorBufferPosition(), center: true) - - # Public: Scrolls the editor to the given buffer position. - # - # bufferPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash matching the options available to {::scrollToPixelPosition} - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options) + @editor.setScrollBottom(Infinity) # Public: Scrolls the editor to the given screen position. # - # screenPosition - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # options - A hash matching the options available to {::scrollToPixelPosition} + # * `screenPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # * `options` (optional) {Object} matching the options available to {::scrollToScreenPosition} scrollToScreenPosition: (screenPosition, options) -> - @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options) + @editor.scrollToScreenPosition(screenPosition, options) - # Public: Scrolls the editor to the given pixel position. + # Public: Scrolls the editor to the given buffer position. # - # pixelPosition - An object that represents a pixel position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or - # {Point}. - # options - A hash with the following keys: - # :center - if `true`, the position is scrolled such that it's in - # the center of the editor - scrollToPixelPosition: (pixelPosition, options) -> + # * `bufferPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # * `options` (optional) {Object} matching the options available to {::scrollToBufferPosition} + scrollToBufferPosition: (bufferPosition, options) -> + @editor.scrollToBufferPosition(bufferPosition, options) + + scrollToCursorPosition: -> + @editor.scrollToCursorPosition() + + # Public: Converts a buffer position to a pixel position. + # + # * `bufferPosition` An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. + pixelPositionForBufferPosition: (bufferPosition) -> + @editor.pixelPositionForBufferPosition(bufferPosition) + + # Public: Converts a screen position to a pixel position. + # + # * `screenPosition` An object that represents a screen position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an object with two values: `top` and `left`, representing the pixel positions. + pixelPositionForScreenPosition: (screenPosition) -> + @editor.pixelPositionForScreenPosition(screenPosition) + + appendToLinesView: (view) -> + view.css('position', 'absolute') + view.css('z-index', 1) + @find('.lines').prepend(view) + + beforeRemove: -> return unless @attached - @scrollVertically(pixelPosition, options) - @scrollHorizontally(pixelPosition) - - # Public: Highlight all the folds within the given buffer range. - # - # "Highlighting" essentially just adds the `fold-selected` class to the line's - # DOM element. - # - # bufferRange - The {Range} to check. - highlightFoldsContainingBufferRange: (bufferRange) -> - screenLines = @editor.linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow) - for screenLine, i in screenLines - if fold = screenLine.fold - screenRow = @firstRenderedScreenRow + i - element = @lineElementForScreenRow(screenRow) - - if bufferRange.intersectsWith(fold.getBufferRange()) - element.addClass('fold-selected') - else - element.removeClass('fold-selected') - - saveScrollPositionForEditor: -> - if @attached - @editor.setScrollTop(@scrollTop()) - @editor.setScrollLeft(@scrollLeft()) - - # Public: Toggle soft tabs on the edit session. - toggleSoftTabs: -> - @editor.setSoftTabs(not @editor.getSoftTabs()) - - # Public: Toggle soft wrap on the edit session. - toggleSoftWrap: -> - @setWidthInChars() - @editor.setSoftWrap(not @editor.getSoftWrap()) - - calculateWidthInChars: -> - Math.floor((@scrollView.width() - @getScrollbarWidth()) / @charWidth) - - calculateHeightInLines: -> - Math.ceil($(window).height() / @lineHeight) - - getScrollbarWidth: -> - scrollbarElement = @verticalScrollbar[0] - scrollbarElement.offsetWidth - scrollbarElement.clientWidth - - # Public: Enables/disables soft wrap on the editor. - # - # softWrap - A {Boolean} which, if `true`, enables soft wrap - setSoftWrap: (softWrap) -> - if softWrap - @addClass 'soft-wrap' - @scrollLeft(0) - else - @removeClass 'soft-wrap' - - # Public: Sets the font size for the editor. - # - # fontSize - A {Number} indicating the font size in pixels. - setFontSize: (fontSize) -> - @css('font-size', "#{fontSize}px") - - @clearCharacterWidthCache() - - if @isOnDom() - @redraw() - else - @redrawOnReattach = @attached - - # Public: Retrieves the font size for the editor. - # - # Returns a {Number} indicating the font size in pixels. - getFontSize: -> - parseInt(@css("font-size")) - - # Public: Sets the font family for the editor. - # - # fontFamily - A {String} identifying the CSS `font-family`. - setFontFamily: (fontFamily='') -> - @css('font-family', fontFamily) - - @clearCharacterWidthCache() - - @redraw() - - # Public: Gets the font family for the editor. - # - # Returns a {String} identifying the CSS `font-family`. - getFontFamily: -> @css("font-family") - - # Public: Sets the line height of the editor. - # - # Calling this method has no effect when called on a mini editor. - # - # lineHeight - A {Number} without a unit suffix identifying the CSS - # `line-height`. - setLineHeight: (lineHeight) -> - return if @mini - @css('line-height', lineHeight) - @redraw() - - # Public: Redraw the editor - redraw: -> - return unless @hasParent() - return unless @attached - @redrawOnReattach = false - @calculateDimensions() - @updatePaddingOfRenderedLines() - @updateLayerDimensions() - @requestDisplayUpdate() + @attached = false + React.unmountComponentAtNode(@element) if @component.isMounted() + @trigger 'editor:detached', this # Public: Split the editor view left. splitLeft: -> @@ -810,776 +247,127 @@ class EditorView extends View getPane: -> @parent('.item-views').parents('.pane').view() - remove: (selector, keepData) -> - return super if keepData or @removed + show: -> super - atom.workspaceView?.focus() + @component?.checkForVisibilityChange() - beforeRemove: -> - @trigger 'editor:will-be-removed' - @removed = true - @editor?.destroy() - $(window).off(".editor-#{@id}") - $(document).off(".editor-#{@id}") + hide: -> + super + @component?.checkForVisibilityChange() - getCursorView: (index) -> - index ?= @cursorViews.length - 1 - @cursorViews[index] + pageDown: -> + deprecate('Use editorView.getModel().pageDown()') + @editor.pageDown() - getCursorViews: -> - new Array(@cursorViews...) - - addCursorView: (cursor, options) -> - cursorView = new CursorView(cursor, this, options) - @cursorViews.push(cursorView) - @overlayer.append(cursorView) - cursorView - - removeCursorView: (cursorView) -> - _.remove(@cursorViews, cursorView) - - getSelectionView: (index) -> - index ?= @selectionViews.length - 1 - @selectionViews[index] - - getSelectionViews: -> - new Array(@selectionViews...) - - addSelectionView: (selection) -> - selectionView = new SelectionView({editorView: this, selection}) - @selectionViews.push(selectionView) - @underlayer.append(selectionView) - selectionView - - removeSelectionView: (selectionView) -> - _.remove(@selectionViews, selectionView) - - removeAllCursorAndSelectionViews: -> - cursorView.remove() for cursorView in @getCursorViews() - selectionView.remove() for selectionView in @getSelectionViews() - - appendToLinesView: (view) -> - @overlayer.append(view) - - # Scrolls the editor vertically to a given position. - scrollVertically: (pixelPosition, {center}={}) -> - scrollViewHeight = @scrollView.height() - scrollTop = @scrollTop() - scrollBottom = scrollTop + scrollViewHeight - - if center - unless scrollTop < pixelPosition.top < scrollBottom - @scrollTop(pixelPosition.top - (scrollViewHeight / 2)) - else - linesInView = @scrollView.height() / @lineHeight - maxScrollMargin = Math.floor((linesInView - 1) / 2) - scrollMargin = Math.min(@vScrollMargin, maxScrollMargin) - margin = scrollMargin * @lineHeight - desiredTop = pixelPosition.top - margin - desiredBottom = pixelPosition.top + @lineHeight + margin - if desiredBottom > scrollBottom - @scrollTop(desiredBottom - scrollViewHeight) - else if desiredTop < scrollTop - @scrollTop(desiredTop) - - # Scrolls the editor horizontally to a given position. - scrollHorizontally: (pixelPosition) -> - return if @editor.getSoftWrap() - - charsInView = @scrollView.width() / @charWidth - maxScrollMargin = Math.floor((charsInView - 1) / 2) - scrollMargin = Math.min(@hScrollMargin, maxScrollMargin) - margin = scrollMargin * @charWidth - desiredRight = pixelPosition.left + @charWidth + margin - desiredLeft = pixelPosition.left - margin - - if desiredRight > @scrollRight() - @scrollRight(desiredRight) - else if desiredLeft < @scrollLeft() - @scrollLeft(desiredLeft) - @saveScrollPositionForEditor() - - calculateDimensions: -> - fragment = $('') - @renderedLines.append(fragment) - - lineRect = fragment[0].getBoundingClientRect() - charRect = fragment.find('span')[0].getBoundingClientRect() - @lineHeight = lineRect.height - @charWidth = charRect.width - @charHeight = charRect.height - fragment.remove() - @setHeightInLines() - - recalculateDimensions: -> - return unless @attached and @isVisible() - - oldCharWidth = @charWidth - oldLineHeight = @lineHeight - - @calculateDimensions() - - unless @charWidth is oldCharWidth and @lineHeight is oldLineHeight - @clearCharacterWidthCache() - @requestDisplayUpdate() - - updateLayerDimensions: (scrollViewWidth) -> - height = @lineHeight * @editor.getScreenLineCount() - unless @layerHeight == height - @layerHeight = height - @underlayer.height(@layerHeight) - @renderedLines.height(@layerHeight) - @overlayer.height(@layerHeight) - @verticalScrollbarContent.height(@layerHeight) - @scrollBottom(height) if @scrollBottom() > height - - minWidth = Math.max(@charWidth * @editor.getMaxScreenLineLength() + 20, scrollViewWidth) - unless @layerMinWidth == minWidth - @renderedLines.css('min-width', minWidth) - @underlayer.css('min-width', minWidth) - @overlayer.css('min-width', minWidth) - @layerMinWidth = minWidth - @trigger 'editor:min-width-changed' - - # Override for speed. The base function checks computedStyle, unnecessary here. - isHidden: -> - style = this[0].style - if style.display == 'none' or not @isOnDom() - true - else - false - - clearRenderedLines: -> - @renderedLines.empty() - @firstRenderedScreenRow = null - @lastRenderedScreenRow = null - - resetDisplay: -> - return unless @attached - - @clearRenderedLines() - @removeAllCursorAndSelectionViews() - editorScrollTop = @editor.getScrollTop() ? 0 - editorScrollLeft = @editor.getScrollLeft() ? 0 - @updateLayerDimensions() - @scrollTop(editorScrollTop) - @scrollLeft(editorScrollLeft) - @setSoftWrap(@editor.getSoftWrap()) - @newCursors = @editor.getCursors() - @newSelections = @editor.getSelections() - @updateDisplay(suppressAutoscroll: true) - - requestDisplayUpdate: -> - return if @pendingDisplayUpdate - return unless @isVisible() - @pendingDisplayUpdate = true - setImmediate => - @updateDisplay() - @pendingDisplayUpdate = false - - updateDisplay: (options) -> - return unless @attached and @editor - return if @editor.isDestroyed() - unless @isOnDom() and @isVisible() - @redrawOnReattach = true - return - - scrollViewWidth = @scrollView.width() - @updateRenderedLines(scrollViewWidth) - @updatePlaceholderText() - @highlightCursorLine() - @updateCursorViews() - @updateSelectionViews() - @autoscroll(options?.suppressAutoscroll ? false) - @trigger 'editor:display-updated' - - updateCursorViews: -> - if @newCursors.length > 0 - @addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed - @syncCursorAnimations() - @newCursors = [] - - for cursorView in @getCursorViews() - if cursorView.needsRemoval - cursorView.remove() - else if @shouldUpdateCursor(cursorView) - cursorView.updateDisplay() - - shouldUpdateCursor: (cursorView) -> - return false unless cursorView.needsUpdate - - pos = cursorView.getScreenPosition() - pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow - - updateSelectionViews: -> - if @newSelections.length > 0 - @addSelectionView(selection) for selection in @newSelections when not selection.destroyed - @newSelections = [] - - for selectionView in @getSelectionViews() - if selectionView.needsRemoval - selectionView.remove() - else if @shouldUpdateSelection(selectionView) - selectionView.updateDisplay() - - shouldUpdateSelection: (selectionView) -> - screenRange = selectionView.getScreenRange() - startRow = screenRange.start.row - endRow = screenRange.end.row - (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range - (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range - (startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items - - syncCursorAnimations: -> - cursorView.resetBlinking() for cursorView in @getCursorViews() - - autoscroll: (suppressAutoscroll) -> - for cursorView in @getCursorViews() - if !suppressAutoscroll and cursorView.needsAutoscroll() - @scrollToPixelPosition(cursorView.getPixelPosition()) - cursorView.clearAutoscroll() - - for selectionView in @getSelectionViews() - if !suppressAutoscroll and selectionView.needsAutoscroll() - @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) - selectionView.highlight() - selectionView.clearAutoscroll() - - updatePlaceholderText: -> - return unless @mini - if (not @placeholderText) or @editor.getText() - @find('.placeholder-text').remove() - else if @placeholderText and not @editor.getText() - element = @find('.placeholder-text') - if element.length - element.text(@placeholderText) - else - @underlayer.append($('', class: 'placeholder-text', text: @placeholderText)) - - updateRenderedLines: (scrollViewWidth) -> - firstVisibleScreenRow = @getFirstVisibleScreenRow() - lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1 - lastScreenRow = @editor.getLastScreenRow() - - if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow - renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow) - renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow) - else - renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw)) - renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw) - - if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow - return - - changes = @pendingChanges - intactRanges = @computeIntactRanges(renderFrom, renderTo) - - @gutter.updateLineNumbers(changes, renderFrom, renderTo) - - @clearDirtyRanges(intactRanges) - @fillDirtyRanges(intactRanges, renderFrom, renderTo) - @firstRenderedScreenRow = renderFrom - @lastRenderedScreenRow = renderTo - @updateLayerDimensions(scrollViewWidth) - @updatePaddingOfRenderedLines() - - computeSurroundingEmptyLineChanges: (change) -> - emptyLineChanges = [] - - if change.bufferDelta? - afterStart = change.end + change.bufferDelta + 1 - if @editor.lineForBufferRow(afterStart) is '' - afterEnd = afterStart - afterEnd++ while @editor.lineForBufferRow(afterEnd + 1) is '' - emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0}) - - beforeEnd = change.start - 1 - if @editor.lineForBufferRow(beforeEnd) is '' - beforeStart = beforeEnd - beforeStart-- while @editor.lineForBufferRow(beforeStart - 1) is '' - emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0}) - - emptyLineChanges - - computeIntactRanges: (renderFrom, renderTo) -> - return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? - - intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] - - if not @mini and @showIndentGuide - emptyLineChanges = [] - for change in @pendingChanges - emptyLineChanges.push(@computeSurroundingEmptyLineChanges(change)...) - @pendingChanges.push(emptyLineChanges...) - - for change in @pendingChanges - newIntactRanges = [] - for range in intactRanges - if change.end < range.start and change.screenDelta != 0 - newIntactRanges.push( - start: range.start + change.screenDelta - end: range.end + change.screenDelta - domStart: range.domStart - ) - else if change.end < range.start or change.start > range.end - newIntactRanges.push(range) - else - if change.start > range.start - newIntactRanges.push( - start: range.start - end: change.start - 1 - domStart: range.domStart) - if change.end < range.end - newIntactRanges.push( - start: change.end + change.screenDelta + 1 - end: range.end + change.screenDelta - domStart: range.domStart + change.end + 1 - range.start - ) - intactRanges = newIntactRanges - - @truncateIntactRanges(intactRanges, renderFrom, renderTo) - - @pendingChanges = [] - - intactRanges - - truncateIntactRanges: (intactRanges, renderFrom, renderTo) -> - i = 0 - while i < intactRanges.length - range = intactRanges[i] - if range.start < renderFrom - range.domStart += renderFrom - range.start - range.start = renderFrom - if range.end > renderTo - range.end = renderTo - if range.start >= range.end - intactRanges.splice(i--, 1) - i++ - intactRanges.sort (a, b) -> a.domStart - b.domStart - - clearDirtyRanges: (intactRanges) -> - if intactRanges.length == 0 - @renderedLines[0].innerHTML = '' - else if currentLine = @renderedLines[0].firstChild - domPosition = 0 - for intactRange in intactRanges - while intactRange.domStart > domPosition - currentLine = @clearLine(currentLine) - domPosition++ - for i in [intactRange.start..intactRange.end] - currentLine = currentLine.nextSibling - domPosition++ - while currentLine - currentLine = @clearLine(currentLine) - - clearLine: (lineElement) -> - next = lineElement.nextSibling - @renderedLines[0].removeChild(lineElement) - next - - fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> - i = 0 - nextIntact = intactRanges[i] - currentLine = @renderedLines[0].firstChild - - row = renderFrom - while row <= renderTo - if row == nextIntact?.end + 1 - nextIntact = intactRanges[++i] - - if !nextIntact or row < nextIntact.start - if nextIntact - dirtyRangeEnd = nextIntact.start - 1 - else - dirtyRangeEnd = renderTo - - for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) - @renderedLines[0].insertBefore(lineElement, currentLine) - row++ - else - currentLine = currentLine.nextSibling - row++ - - updatePaddingOfRenderedLines: -> - paddingTop = @firstRenderedScreenRow * @lineHeight - @renderedLines.css('padding-top', paddingTop) - @gutter.lineNumbers.css('padding-top', paddingTop) - - paddingBottom = (@editor.getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight - @renderedLines.css('padding-bottom', paddingBottom) - @gutter.lineNumbers.css('padding-bottom', paddingBottom) + pageUp: -> + deprecate('Use editorView.getModel().pageUp()') + @editor.pageUp() # Public: Retrieves the number of the row that is visible and currently at the # top of the editor. # # Returns a {Number}. getFirstVisibleScreenRow: -> - screenRow = Math.floor(@scrollTop() / @lineHeight) - screenRow = 0 if isNaN(screenRow) - screenRow + @editor.getVisibleRowRange()[0] # Public: Retrieves the number of the row that is visible and currently at the # bottom of the editor. # # Returns a {Number}. getLastVisibleScreenRow: -> - calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 - screenRow = Math.max(0, Math.min(@editor.getScreenLineCount() - 1, calculatedRow)) - screenRow = 0 if isNaN(screenRow) - screenRow + @editor.getVisibleRowRange()[1] - # Public: Given a row number, identifies if it is currently visible. + # Public: Gets the font family for the editor. # - # row - A row {Number} to check + # Returns a {String} identifying the CSS `font-family`. + getFontFamily: -> + @component?.getFontFamily() + + # Public: Sets the font family for the editor. # - # Returns a {Boolean}. - isScreenRowVisible: (row) -> - @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() + # * `fontFamily` A {String} identifying the CSS `font-family`. + setFontFamily: (fontFamily) -> + @component?.setFontFamily(fontFamily) - handleScreenLinesChange: (change) -> - @pendingChanges.push(change) - @requestDisplayUpdate() + # Public: Retrieves the font size for the editor. + # + # Returns a {Number} indicating the font size in pixels. + getFontSize: -> + @component?.getFontSize() - buildLineElementForScreenRow: (screenRow) -> - @buildLineElementsForScreenRows(screenRow, screenRow)[0] + # Public: Sets the font size for the editor. + # + # * `fontSize` A {Number} indicating the font size in pixels. + setFontSize: (fontSize) -> + @component?.setFontSize(fontSize) - buildLineElementsForScreenRows: (startRow, endRow) -> - div = document.createElement('div') - div.innerHTML = @htmlForScreenRows(startRow, endRow) - new Array(div.children...) + setWidthInChars: (widthInChars) -> + @component.getDOMNode().style.width = (@editor.getDefaultCharWidth() * widthInChars) + 'px' - htmlForScreenRows: (startRow, endRow) -> - htmlLines = '' - screenRow = startRow - for line in @editor.linesForScreenRows(startRow, endRow) - htmlLines += @htmlForScreenLine(line, screenRow++) - htmlLines + # Public: Sets the line height of the editor. + # + # Calling this method has no effect when called on a mini editor. + # + # * `lineHeight` A {Number} without a unit suffix identifying the CSS `line-height`. + setLineHeight: (lineHeight) -> + @component.setLineHeight(lineHeight) - htmlForScreenLine: (screenLine, screenRow) -> - { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine - if fold - attributes = { class: 'fold line', 'fold-id': fold.id } + # Public: Sets whether you want to show the indentation guides. + # + # * `showIndentGuide` A {Boolean} you can set to `true` if you want to see the + # indentation guides. + setShowIndentGuide: (showIndentGuide) -> + @component.setShowIndentGuide(showIndentGuide) + + # Public: Enables/disables soft wrap on the editor. + # + # * `softWrap` A {Boolean} which, if `true`, enables soft wrap + setSoftWrap: (softWrap) -> + @editor.setSoftWrap(softWrap) + + # Public: Set whether invisible characters are shown. + # + # * `showInvisibles` A {Boolean} which, if `true`, show invisible characters. + setShowInvisibles: (showInvisibles) -> + @component.setShowInvisibles(showInvisibles) + + getText: -> + @editor.getText() + + setText: (text) -> + @editor.setText(text) + + insertText: (text) -> + @editor.insertText(text) + + isInputEnabled: -> + @component.isInputEnabled() + + setInputEnabled: (inputEnabled) -> + @component.setInputEnabled(inputEnabled) + + requestDisplayUpdate: -> + deprecate('Please remove from your code. ::requestDisplayUpdate no longer does anything') + + updateDisplay: -> + deprecate('Please remove from your code. ::updateDisplay no longer does anything') + + resetDisplay: -> + deprecate('Please remove from your code. ::resetDisplay no longer does anything') + + redraw: -> + deprecate('Please remove from your code. ::redraw no longer does anything') + + # Public: Set the text to appear in the editor when it is empty. + # + # This only affects mini editors. + # + # * `placeholderText` A {String} of text to display when empty. + setPlaceholderText: (placeholderText) -> + if @component? + @component.setProps({placeholderText}) else - attributes = { class: 'line' } - - invisibles = @invisibles if @showInvisibles - eolInvisibles = @getEndOfLineInvisibles(screenLine) - htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine) - - indentation = EditorView.buildIndentation(screenRow, @editor) - - EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @editor, @mini}) - - @buildIndentation: (screenRow, editor) -> - bufferRow = editor.bufferPositionForScreenPosition([screenRow]).row - bufferLine = editor.lineForBufferRow(bufferRow) - if bufferLine is '' - indentation = 0 - nextRow = screenRow + 1 - while nextRow < editor.getBuffer().getLineCount() - bufferRow = editor.bufferPositionForScreenPosition([nextRow]).row - bufferLine = editor.lineForBufferRow(bufferRow) - if bufferLine isnt '' - indentation = Math.ceil(editor.indentLevelForLine(bufferLine)) - break - nextRow++ - - previousRow = screenRow - 1 - while previousRow >= 0 - bufferRow = editor.bufferPositionForScreenPosition([previousRow]).row - bufferLine = editor.lineForBufferRow(bufferRow) - if bufferLine isnt '' - indentation = Math.max(indentation, Math.ceil(editor.indentLevelForLine(bufferLine))) - break - previousRow-- - - indentation - else - Math.ceil(editor.indentLevelForLine(bufferLine)) - - buildHtmlEndOfLineInvisibles: (screenLine) -> - invisibles = [] - for invisible in @getEndOfLineInvisibles(screenLine) - invisibles.push("#{invisible}") - invisibles.join('') - - getEndOfLineInvisibles: (screenLine) -> - return [] unless @showInvisibles and @invisibles - return [] if @mini or screenLine.isSoftWrapped() - - invisibles = [] - invisibles.push(@invisibles.cr) if @invisibles.cr and screenLine.lineEnding is '\r\n' - invisibles.push(@invisibles.eol) if @invisibles.eol - invisibles + @props.placeholderText = placeholderText lineElementForScreenRow: (screenRow) -> - @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})") - - toggleLineCommentsInSelection: -> - @editor.toggleLineCommentsInSelection() - - # Public: Converts a buffer position to a pixel position. - # - # position - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an object with two values: `top` and `left`, representing the pixel positions. - pixelPositionForBufferPosition: (position) -> - @pixelPositionForScreenPosition(@editor.screenPositionForBufferPosition(position)) - - # Public: Converts a screen position to a pixel position. - # - # position - An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # - # Returns an object with two values: `top` and `left`, representing the pixel positions. - pixelPositionForScreenPosition: (position) -> - return { top: 0, left: 0 } unless @isOnDom() and @isVisible() - {row, column} = Point.fromObject(position) - actualRow = Math.floor(row) - - lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0] - unless existingLineElement - lineElement = @buildLineElementForScreenRow(actualRow) - @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(lineElement, actualRow, column) - unless existingLineElement - @renderedLines[0].removeChild(lineElement) - { top: row * @lineHeight, left } - - positionLeftForLineAndColumn: (lineElement, screenRow, screenColumn) -> - return 0 if screenColumn == 0 - - tokenizedLine = @editor.displayBuffer.lineForRow(screenRow) - textContent = lineElement.textContent - - left = 0 - index = 0 - for token in tokenizedLine.tokens - for bufferChar in token.value - return left if index >= screenColumn - - # Invisibles might cause renderedChar to be different than bufferChar - renderedChar = textContent[index] - val = @getCharacterWidthCache(token.scopes, renderedChar) - if val? - left += val - else - return @measureToColumn(lineElement, tokenizedLine, screenColumn) - - index++ - left - - measureToColumn: (lineElement, tokenizedLine, screenColumn) -> - left = oldLeft = index = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter) - - returnLeft = null - - offsetLeft = @scrollView.offset().left - paddingLeft = parseInt(@scrollView.css('padding-left')) - - while textNode = iterator.nextNode() - content = textNode.textContent - - for char, i in content - # Don't continue caching long lines :racehorse: - break if index > LongLineLength and screenColumn < index - - # Dont return right away, finish caching the whole line - returnLeft = left if index == screenColumn - oldLeft = left - - scopes = tokenizedLine.tokenAtBufferColumn(index)?.scopes - cachedCharWidth = @getCharacterWidthCache(scopes, char) - - if cachedCharWidth? - left = oldLeft + cachedCharWidth - else - # i + 1 to measure to the end of the current character - MeasureRange.setEnd(textNode, i + 1) - MeasureRange.collapse() - rects = MeasureRange.getClientRects() - return 0 if rects.length == 0 - left = rects[0].left - Math.floor(offsetLeft) + Math.floor(@scrollLeft()) - paddingLeft - - if scopes? - cachedCharWidth = left - oldLeft - @setCharacterWidthCache(scopes, char, cachedCharWidth) - - # Assume all the characters are the same width when dealing with long - # lines :racehorse: - return screenColumn * cachedCharWidth if index > LongLineLength - - index++ - - returnLeft ? left - - getCharacterWidthCache: (scopes, char) -> - scopes ?= NoScope - obj = @constructor.characterWidthCache - for scope in scopes - obj = obj[scope] - return null unless obj? - obj[char] - - setCharacterWidthCache: (scopes, char, val) -> - scopes ?= NoScope - obj = @constructor.characterWidthCache - for scope in scopes - obj[scope] ?= {} - obj = obj[scope] - obj[char] = val - - clearCharacterWidthCache: -> - @constructor.characterWidthCache = {} - - pixelOffsetForScreenPosition: (position) -> - {top, left} = @pixelPositionForScreenPosition(position) - offset = @renderedLines.offset() - {top: top + offset.top, left: left + offset.left} - - screenPositionFromMouseEvent: (e) -> - { pageX, pageY } = e - offset = @scrollView.offset() - - editorRelativeTop = pageY - offset.top + @scrollTop() - row = Math.floor(editorRelativeTop / @lineHeight) - column = 0 - - if pageX > offset.left and lineElement = @lineElementForScreenRow(row)[0] - range = document.createRange() - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) - while node = iterator.nextNode() - range.selectNodeContents(node) - column += node.textContent.length - {left, right} = range.getClientRects()[0] - break if left <= pageX <= right - - if node - for characterPosition in [node.textContent.length...0] - range.setStart(node, characterPosition - 1) - range.setEnd(node, characterPosition) - {left, right, width} = range.getClientRects()[0] - break if left <= pageX - width / 2 <= right - column-- - - range.detach() - - new Point(row, column) - - # Highlights the current line the cursor is on. - highlightCursorLine: -> - return if @mini - - @highlightedLine?.removeClass('cursor-line') - if @editor.getSelection().isEmpty() - @highlightedLine = @lineElementForScreenRow(@editor.getCursorScreenRow()) - @highlightedLine.addClass('cursor-line') - else - @highlightedLine = null - - # Copies the current file path to the native clipboard. - copyPathToClipboard: -> - path = @editor.getPath() - atom.clipboard.write(path) if path? - - @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, editor, mini}) -> - scopeStack = [] - line = [] - - attributePairs = '' - attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes - line.push("
") - - if text == '' - html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) - line.push(html) if html - else - firstTrailingWhitespacePosition = text.search(/\s*$/) - lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 - position = 0 - for token in tokens - @updateScopeStack(line, scopeStack, token.scopes) - hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and lineIsWhitespaceOnly)) - line.push(token.getValueAsHtml({invisibles, hasIndentGuide})) - position += token.value.length - - @popScope(line, scopeStack) while scopeStack.length > 0 - line.push(htmlEolInvisibles) unless text == '' - line.push("") if fold - - line.push('
') - line.join('') - - @updateScopeStack: (line, scopeStack, desiredScopes) -> - excessScopes = scopeStack.length - desiredScopes.length - if excessScopes > 0 - @popScope(line, scopeStack) while excessScopes-- - - # pop until common prefix - for i in [scopeStack.length..0] - break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) - @popScope(line, scopeStack) - - # push on top of common prefix until scopeStack == desiredScopes - for j in [i...desiredScopes.length] - @pushScope(line, scopeStack, desiredScopes[j]) - - null - - @pushScope: (line, scopeStack, scope) -> - scopeStack.push(scope) - line.push("") - - @popScope: (line, scopeStack) -> - scopeStack.pop() - line.push("") - - @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) -> - indentCharIndex = 0 - if not mini and showIndentGuide - if indentation > 0 - tabLength = editor.getTabLength() - indentGuideHtml = '' - for level in [0...indentation] - indentLevelHtml = "" - for characterPosition in [0...tabLength] - if invisible = eolInvisibles[indentCharIndex++] - indentLevelHtml += "#{invisible}" - else - indentLevelHtml += ' ' - indentLevelHtml += "" - indentGuideHtml += indentLevelHtml - - while indentCharIndex < eolInvisibles.length - indentGuideHtml += "#{eolInvisibles[indentCharIndex++]}" - - return indentGuideHtml - - if htmlEolInvisibles.length > 0 - htmlEolInvisibles - else - ' ' - - replaceSelectedText: (replaceFn) -> - selection = @editor.getSelection() - return false if selection.isEmpty() - - text = replaceFn(@editor.getTextInRange(selection.getBufferRange())) - return false if text is null or text is undefined - - @editor.insertText(text, select: true) - true - - consolidateSelections: (e) -> e.abortKeyBinding() unless @editor.consolidateSelections() - - logCursorScope: -> - console.log @editor.getCursorScopes() - - logScreenLines: (start, end) -> - @editor.logScreenLines(start, end) - - logRenderedLines: -> - @renderedLines.find('.line').each (n) -> - console.log n, $(this).text() + $(@component.lineNodeForScreenRow(screenRow)) diff --git a/src/editor.coffee b/src/editor.coffee index 2e8269ff9..817fd60af 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -28,9 +28,9 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # be called with all current editor instances and also when any editor is # created in the future. # -# ```coffeescript -# atom.workspace.eachEditor (editor) -> -# editor.insertText('Hello World') +# ```coffee +# atom.workspace.eachEditor (editor) -> +# editor.insertText('Hello World') # ``` # # ## Buffer vs. Screen Coordinates @@ -51,90 +51,133 @@ TextMateScopeSelector = require('first-mate').ScopeSelector # **When in doubt, just default to buffer coordinates**, then experiment with # soft wraps and folds to ensure your code interacts with them correctly. # -# ## Common Tasks +# ## Events # -# This is a subset of methods on this class. Refer to the complete summary for -# its full capabilities. +# ### path-changed # -# ### Cursors -# - {::setCursorBufferPosition} -# - {::setCursorScreenPosition} -# - {::moveCursorUp} -# - {::moveCursorDown} -# - {::moveCursorLeft} -# - {::moveCursorRight} -# - {::moveCursorToBeginningOfWord} -# - {::moveCursorToEndOfWord} -# - {::moveCursorToPreviousWordBoundary} -# - {::moveCursorToNextWordBoundary} -# - {::moveCursorToBeginningOfNextWord} -# - {::moveCursorToBeginningOfLine} -# - {::moveCursorToEndOfLine} -# - {::moveCursorToFirstCharacterOfLine} -# - {::moveCursorToTop} -# - {::moveCursorToBottom} +# Essential: Emit when the buffer's path, and therefore title, has changed. # -# ### Selections -# - {::getSelectedBufferRange} -# - {::getSelectedBufferRanges} -# - {::setSelectedBufferRange} -# - {::setSelectedBufferRanges} -# - {::selectUp} -# - {::selectDown} -# - {::selectLeft} -# - {::selectRight} -# - {::selectToBeginningOfWord} -# - {::selectToEndOfWord} -# - {::selectToPreviousWordBoundary} -# - {::selectToNextWordBoundary} -# - {::selectWord} -# - {::selectToBeginningOfLine} -# - {::selectToEndOfLine} -# - {::selectToFirstCharacterOfLine} -# - {::selectToTop} -# - {::selectToBottom} -# - {::selectAll} -# - {::addSelectionForBufferRange} -# - {::addSelectionAbove} -# - {::addSelectionBelow} -# - {::splitSelectionsIntoLines} +# ### title-changed # -# ### Manipulating Text -# - {::getText} -# - {::getSelectedText} -# - {::setText} -# - {::setTextInBufferRange} -# - {::insertText} -# - {::insertNewline} -# - {::insertNewlineAbove} -# - {::insertNewlineBelow} -# - {::backspace} -# - {::deleteToBeginningOfWord} -# - {::deleteToBeginningOfLine} -# - {::delete} -# - {::deleteToEndOfLine} -# - {::deleteToEndOfWord} -# - {::deleteLine} -# - {::cutSelectedText} -# - {::cutToEndOfLine} -# - {::copySelectedText} -# - {::pasteText} +# Essential: Emit when the buffer's path, and therefore title, has changed. # -# ### Undo, Redo, and Transactions -# - {::undo} -# - {::redo} -# - {::transact} -# - {::abortTransaction} +# ### modified-status-changed # -# ### Markers -# - {::markBufferRange} -# - {::markScreenRange} -# - {::getMarker} -# - {::findMarkers} +# Extended: Emit when the result of {::isModified} changes. +# +# ### soft-wrap-changed +# +# Extended: Emit when soft wrap was enabled or disabled. +# +# * `softWrap` {Boolean} indicating whether soft wrap is enabled or disabled. +# +# ### grammar-changed +# +# Extended: Emit when the grammar that interprets and colorizes the text has +# been changed. +# +# +# +# ### contents-modified +# +# Essential: Emit when the buffer's contents change. It is emit asynchronously +# 300ms after the last buffer change. This is a good place to handle changes to +# the buffer without compromising typing performance. +# +# ### contents-conflicted +# +# Extended: Emitted when the buffer's underlying file changes on disk at a +# moment when the result of {::isModified} is true. +# +# ### will-insert-text +# +# Extended: Emit before the text has been inserted. +# +# * `event` event {Object} +# * `text` {String} text to be inserted +# * `cancel` {Function} Call to prevent the text from being inserted +# +# ### did-insert-text +# +# Extended: Emit after the text has been inserted. +# +# * `event` event {Object} +# * `text` {String} text to be inserted +# +# +# +# ### cursor-moved +# +# Essential: Emit when a cursor has been moved. If there are multiple cursors, +# it will be emit for each cursor. +# +# * `event` {Object} +# * `oldBufferPosition` {Point} +# * `oldScreenPosition` {Point} +# * `newBufferPosition` {Point} +# * `newScreenPosition` {Point} +# * `textChanged` {Boolean} +# +# ### cursor-added +# +# Extended: Emit when a cursor has been added. +# +# * `cursor` {Cursor} that was added +# +# ### cursor-removed +# +# Extended: Emit when a cursor has been removed. +# +# * `cursor` {Cursor} that was removed +# +# +# +# ### selection-screen-range-changed +# +# Essential: Emit when a selection's screen range changes. +# +# * `selection`: {Selection} object that has a changed range +# +# ### selection-added +# +# Extended: Emit when a selection's was added. +# +# * `selection`: {Selection} object that was added +# +# ### selection-removed +# +# Extended: Emit when a selection's was removed. +# +# * `selection`: {Selection} object that was removed +# +# +# +# ### decoration-added +# +# Extended: Emit when a {Decoration} is added to the editor. +# +# * `decoration` {Decoration} that was added +# +# ### decoration-removed +# +# Extended: Emit when a {Decoration} is removed from the editor. +# +# * `decoration` {Decoration} that was removed +# +# ### decoration-changed +# +# Extended: Emit when a {Decoration}'s underlying marker changes. Say the user +# inserts newlines above a decoration. That action will move the marker down, +# and fire this event. +# +# * `decoration` {Decoration} that was added +# +# ### decoration-updated +# +# Extended: Emit when a {Decoration} is updated via the {Decoration::update} method. +# +# * `decoration` {Decoration} that was updated # -# ### Decorations -# - {::decorateMarker} -# - {::decorationsForScreenRowRange} module.exports = class Editor extends Model Serializable.includeInto(this) @@ -235,10 +278,7 @@ class Editor extends Model @subscribe @displayBuffer, "character-widths-changed", (changeCount) => @emit 'character-widths-changed', changeCount getViewClass: -> - if atom.config.get('core.useReactEditor') - require './react-editor-view' - else - require './editor-view' + require './editor-view' destroyed: -> @unsubscribe() @@ -247,6 +287,12 @@ class Editor extends Model @displayBuffer.destroy() @languageMode.destroy() + # Retrieves the current {TextBuffer}. + getBuffer: -> @buffer + + # Retrieves the current buffer's URI. + getUri: -> @buffer.getUri() + # Create an {Editor} with its initial state based on this object copy: -> tabLength = @getTabLength() @@ -257,6 +303,26 @@ class Editor extends Model marker.copy(editorId: newEditor.id, preserveFolds: true) newEditor + # Controls visibility based on the given {Boolean}. + setVisible: (visible) -> @displayBuffer.setVisible(visible) + + setMini: (mini) -> + if mini isnt @mini + @mini = mini + @updateInvisibles() + + # Set the number of characters that can be displayed horizontally in the + # editor. + # + # * `editorWidthInChars` A {Number} representing the width of the {EditorView} + # in characters. + setEditorWidthInChars: (editorWidthInChars) -> + @displayBuffer.setEditorWidthInChars(editorWidthInChars) + + ### + Section: File Details + ### + # Public: Get the title the editor's title for display in other parts of the # UI such as the tabs. # @@ -286,155 +352,8 @@ class Editor extends Model else 'untitled' - # Controls visibility based on the given {Boolean}. - setVisible: (visible) -> @displayBuffer.setVisible(visible) - - setMini: (mini) -> - if mini isnt @mini - @mini = mini - @updateInvisibles() - - # Set the number of characters that can be displayed horizontally in the - # editor. - # - # editorWidthInChars - A {Number} representing the width of the {EditorView} - # in characters. - setEditorWidthInChars: (editorWidthInChars) -> - @displayBuffer.setEditorWidthInChars(editorWidthInChars) - - # Public: Sets the column at which column will soft wrap - getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - - # Public: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Public: Enable or disable soft tabs for this editor. - # - # softTabs - A {Boolean} - setSoftTabs: (@softTabs) -> @softTabs - - # Public: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Public: Get whether soft wrap is enabled for this editor. - getSoftWrap: -> @displayBuffer.getSoftWrap() - - # Public: Enable or disable soft wrap for this editor. - # - # softWrap - A {Boolean} - setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap) - - # Public: Toggle soft wrap for this editor - toggleSoftWrap: -> @setSoftWrap(not @getSoftWrap()) - - # Public: Get the text representing a single level of indent. - # - # If soft tabs are enabled, the text is composed of N spaces, where N is the - # tab length. Otherwise the text is a tab character (`\t`). - # - # Returns a {String}. - getTabText: -> @buildIndentString(1) - - # Public: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @displayBuffer.getTabLength() - - # Public: Set the on-screen length of tab characters. - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) - - # Public: Determine if the buffer uses hard or soft tabs. - # - # Returns `true` if the first non-comment line with leading whitespace starts - # with a space character. Returns `false` if it starts with a hard tab (`\t`). - # - # Returns a {Boolean}, - usesSoftTabs: -> - for bufferRow in [0..@buffer.getLastRow()] - continue if @displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment() - if match = @buffer.lineForRow(bufferRow).match(/^\s/) - return match[0][0] == ' ' - undefined - - # Public: Clip the given {Point} to a valid position in the buffer. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the buffer, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # For example: - # * `[-1, -1]` is converted to `[0, 0]`. - # * If the line at row 2 is 10 long, `[2, Infinity]` is converted to - # `[2, 10]`. - # - # bufferPosition - The {Point} representing the position to clip. - # - # Returns a {Point}. - clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - - # Public: Clip the start and end of the given range to valid positions in the - # buffer. See {::clipBufferPosition} for more information. - # - # range - The {Range} to clip. - # - # Returns a {Range}. - clipBufferRange: (range) -> @buffer.clipRange(range) - - # Public: Get the indentation level of the given a buffer row. - # - # Returns how deeply the given row is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # bufferRow - A {Number} indicating the buffer row. - # - # Returns a {Number}. - indentationForBufferRow: (bufferRow) -> - @indentLevelForLine(@lineForBufferRow(bufferRow)) - - # Public: Set the indentation level for the given buffer row. - # - # Inserts or removes hard tabs or spaces based on the soft tabs and tab length - # settings of this editor in order to bring it to the given indentation level. - # Note that if soft tabs are enabled and the tab length is 2, a row with 4 - # leading spaces would have an indentation level of 2. - # - # bufferRow - A {Number} indicating the buffer row. - # newLevel - A {Number} indicating the new indentation level. - # options - An {Object} with the following keys: - # :preserveLeadingWhitespace - true to preserve any whitespace already at - # the beginning of the line (default: false). - setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> - if preserveLeadingWhitespace - endColumn = 0 - else - endColumn = @lineForBufferRow(bufferRow).match(/^\s*/)[0].length - newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - - # Public: Get the indentation level of the given line of text. - # - # Returns how deeply the given line is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # line - A {String} representing a line of text. - # - # Returns a {Number}. - indentLevelForLine: (line) -> - @displayBuffer.indentLevelForLine(line) - - # Constructs the string used for tabs. - buildIndentString: (number, column=0) -> - if @getSoftTabs() - tabStopViolation = column % @getTabLength() - _.multiplyString(" ", Math.floor(number * @getTabLength()) - tabStopViolation) - else - _.multiplyString("\t", Math.floor(number)) + # Public: Returns the {String} path of this editor's text buffer. + getPath: -> @buffer.getPath() # Public: Saves the editor's text buffer. # @@ -445,508 +364,132 @@ class Editor extends Model # # See {TextBuffer::saveAs} for more details. # - # filePath - A {String} path. + # * `filePath` A {String} path. saveAs: (filePath) -> @buffer.saveAs(filePath) + # Public: Determine whether the user should be prompted to save before closing + # this editor. + shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() + + # Public: Returns {Boolean} `true` if this editor has been modified. + isModified: -> @buffer.isModified() + + isEmpty: -> @buffer.isEmpty() + # Copies the current file path to the native clipboard. copyPathToClipboard: -> if filePath = @getPath() atom.clipboard.write(filePath) - # Public: Returns the {String} path of this editor's text buffer. - getPath: -> @buffer.getPath() + ### + Section: Reading Text + ### # Public: Returns a {String} representing the entire contents of the editor. getText: -> @buffer.getText() - # Public: Replaces the entire contents of the buffer with the given {String}. - setText: (text) -> @buffer.setText(text) - - # Get the text in the given {Range}. + # Public: Get the text in the given {Range} in buffer coordinates. + # + # * `range` A {Range} or range-compatible {Array}. # # Returns a {String}. - getTextInRange: (range) -> @buffer.getTextInRange(range) + getTextInBufferRange: (range) -> + @buffer.getTextInRange(range) # Public: Returns a {Number} representing the number of lines in the editor. getLineCount: -> @buffer.getLineCount() - # Retrieves the current {TextBuffer}. - getBuffer: -> @buffer - - # Public: Retrieves the current buffer's URI. - getUri: -> @buffer.getUri() - - # {Delegates to: TextBuffer.isRowBlank} - isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - - # Public: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineForBufferRow(bufferRow).match(/\S/) - scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes - new TextMateScopeSelector('comment.*').matches(scopes) - - # {Delegates to: TextBuffer.nextNonBlankRow} - nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) - - # {Delegates to: TextBuffer.getEndPosition} - getEofBufferPosition: -> @buffer.getEndPosition() + # {Delegates to: DisplayBuffer.getLineCount} + getScreenLineCount: -> @displayBuffer.getLineCount() # Public: Returns a {Number} representing the last zero-indexed buffer row # number of the editor. getLastBufferRow: -> @buffer.getLastRow() - # Returns the range for the given buffer row. - # - # row - A row {Number}. - # options - An options hash with an `includeNewline` key. - # - # Returns a {Range}. - bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) + # {Delegates to: DisplayBuffer.getLastRow} + getLastScreenRow: -> @displayBuffer.getLastRow() # Public: Returns a {String} representing the contents of the line at the # given buffer row. # - # row - A {Number} representing a zero-indexed buffer row. + # * `row` A {Number} representing a zero-indexed buffer row. lineForBufferRow: (row) -> @buffer.lineForRow(row) - # Public: Returns a {Number} representing the line length for the given - # buffer row, exclusive of its line-ending character(s). - # - # row - A {Number} indicating the buffer row. - lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) - - # {Delegates to: TextBuffer.scan} - scan: (args...) -> @buffer.scan(args...) - - # {Delegates to: TextBuffer.scanInRange} - scanInBufferRange: (args...) -> @buffer.scanInRange(args...) - - # {Delegates to: TextBuffer.backwardsScanInRange} - backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) - - # {Delegates to: TextBuffer.isModified} - isModified: -> @buffer.isModified() - - isEmpty: -> @buffer.isEmpty() - - # Public: Determine whether the user should be prompted to save before closing - # this editor. - shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() - - # Public: Convert a position in buffer-coordinates to screen-coordinates. - # - # The position is clipped via {::clipBufferPosition} prior to the conversion. - # The position is also clipped via {::clipScreenPosition} following the - # conversion, which only makes a difference when `options` are supplied. - # - # bufferPosition - A {Point} or {Array} of [row, column]. - # options - An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) - - # Public: Convert a position in screen-coordinates to buffer-coordinates. - # - # The position is clipped via {::clipScreenPosition} prior to the conversion. - # - # bufferPosition - A {Point} or {Array} of [row, column]. - # options - An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) - - # Public: Convert a range in buffer-coordinates to screen-coordinates. - # - # Returns a {Range}. - screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) - - # Public: Convert a range in screen-coordinates to buffer-coordinates. - # - # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) - - # Public: Clip the given {Point} to a valid position on screen. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the screen, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # For example: - # * `[-1, -1]` is converted to `[0, 0]`. - # * If the line at screen row 2 is 10 long, `[2, Infinity]` is converted to - # `[2, 10]`. - # - # bufferPosition - The {Point} representing the position to clip. - # - # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) - # {Delegates to: DisplayBuffer.lineForRow} lineForScreenRow: (row) -> @displayBuffer.lineForRow(row) # {Delegates to: DisplayBuffer.linesForRows} linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end) - # {Delegates to: DisplayBuffer.getLineCount} - getScreenLineCount: -> @displayBuffer.getLineCount() + # Public: Returns a {Number} representing the line length for the given + # buffer row, exclusive of its line-ending character(s). + # + # * `row` A {Number} indicating the buffer row. + lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) - # {Delegates to: DisplayBuffer.getMaxLineLength} - getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength() - - # {Delegates to: DisplayBuffer.getLastRow} - getLastScreenRow: -> @displayBuffer.getLastRow() + bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row) # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) - bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row) + # {Delegates to: DisplayBuffer.getMaxLineLength} + getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength() - # Public: Get the syntactic scopes for the given position in buffer - # coordinates. + # Returns the range for the given buffer row. # - # For example, if called with a position inside the parameter list of an - # anonymous CoffeeScript function, the method returns the following array: - # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` - # - # bufferPosition - A {Point} or {Array} of [row, column]. - # - # Returns an {Array} of {String}s. - scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) - - # Public: Get the range in buffer coordinates of all tokens surrounding the - # cursor that match the given scope selector. - # - # For example, if you wanted to find the string surrounding the cursor, you - # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. + # * `row` A row {Number}. + # * `options` (optional) An options hash with an `includeNewline` key. # # Returns a {Range}. - bufferRangeForScopeAtCursor: (selector) -> - @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) - # {Delegates to: DisplayBuffer.tokenForBufferPosition} - tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) - - # Public: Get the syntactic scopes for the most recently added cursor's - # position. See {::scopesForBufferPosition} for more information. + # Get the text in the given {Range}. # - # Returns an {Array} of {String}s. - getCursorScopes: -> @getCursor().getScopes() + # Returns a {String}. + getTextInRange: (range) -> @buffer.getTextInRange(range) - logCursorScope: -> - console.log @getCursorScopes() + # {Delegates to: TextBuffer.isRowBlank} + isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - # Public: For each selection, replace the selected text with the given text. + # {Delegates to: TextBuffer.nextNonBlankRow} + nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) + + # {Delegates to: TextBuffer.getEndPosition} + getEofBufferPosition: -> @buffer.getEndPosition() + + # Public: Get the {Range} of the paragraph surrounding the most recently added + # cursor. # - # Emits: `will-insert-text -> ({text, cancel})` before the text has - # been inserted. Calling `cancel` will prevent the text from being - # inserted. - # Emits: `did-insert-text -> ({text})` after the text has been inserted. + # Returns a {Range}. + getCurrentParagraphBufferRange: -> + @getCursor().getCurrentParagraphBufferRange() + + + ### + Section: Mutating Text + ### + + # Public: Replaces the entire contents of the buffer with the given {String}. + setText: (text) -> @buffer.setText(text) + + # Public: Set the text in the given {Range} in buffer coordinates. # - # text - A {String} representing the text to insert. - # options - See {Selection::insertText}. + # * `range` A {Range} or range-compatible {Array}. + # * `text` A {String} # - # Returns a {Boolean} indicating whether or not the text was inserted. - insertText: (text, options={}) -> - willInsert = true - cancel = -> willInsert = false - @emit('will-insert-text', {cancel, text}) + # Returns the {Range} of the newly-inserted text. + setTextInBufferRange: (range, text, normalizeLineEndings) -> @getBuffer().setTextInRange(range, text, normalizeLineEndings) - if willInsert - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) => - range = selection.insertText(text, options) - @emit('did-insert-text', {text, range}) - range - else - false - - # Public: For each selection, replace the selected text with a newline. - insertNewline: -> - @insertText('\n') - - # Public: For each cursor, insert a newline at beginning the following line. - insertNewlineBelow: -> - @transact => - @moveCursorToEndOfLine() - @insertNewline() - - # Public: For each cursor, insert a newline at the end of the preceding line. - insertNewlineAbove: -> - @transact => - bufferRow = @getCursorBufferPosition().row - indentLevel = @indentationForBufferRow(bufferRow) - onFirstLine = bufferRow is 0 - - @moveCursorToBeginningOfLine() - @moveCursorLeft() - @insertNewline() - - if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel - @setIndentationForBufferRow(bufferRow, indentLevel) - - if onFirstLine - @moveCursorUp() - @moveCursorToEndOfLine() - - # Indent all lines intersecting selections. See {Selection::indent} for more - # information. - indent: (options={}) -> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Public: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() - - # Deprecated: Use {::deleteToBeginningOfWord} instead. - backspaceToBeginningOfWord: -> - deprecate("Use Editor::deleteToBeginningOfWord() instead") - @deleteToBeginningOfWord() - - # Deprecated: Use {::deleteToBeginningOfLine} instead. - backspaceToBeginningOfLine: -> - deprecate("Use Editor::deleteToBeginningOfLine() instead") - @deleteToBeginningOfLine() - - # Public: For each selection, if the selection is empty, delete all characters - # of the containing word that precede the cursor. Otherwise delete the - # selected text. - deleteToBeginningOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() - - # Public: For each selection, if the selection is empty, delete all characters - # of the containing line that precede the cursor. Otherwise delete the - # selected text. - deleteToBeginningOfLine: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() - - # Public: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Public: For each selection, if the selection is not empty, deletes the - # selection; otherwise, deletes all characters of the containing line - # following the cursor. If the cursor is already at the end of the line, - # deletes the following newline. - deleteToEndOfLine: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfLine() - - # Public: For each selection, if the selection is empty, delete all characters - # of the containing word following the cursor. Otherwise delete the selected - # text. - deleteToEndOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfWord() - - # Public: Delete all lines intersecting selections. - deleteLine: -> - @mutateSelectedText (selection) -> selection.deleteLine() - - # Public: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Public: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Public: Toggle line comments for rows intersecting selections. + # Public: Mutate the text of all the selections in a single transaction. # - # If the current grammar doesn't support comments, does nothing. + # All the changes made inside the given {Function} can be reverted with a + # single call to {::undo}. # - # Returns an {Array} of the commented {Range}s. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - # Public: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # If soft tabs are enabled, convert all hard tabs to soft tabs in the given - # {Range}. - normalizeTabsInBufferRange: (bufferRange) -> - return unless @getSoftTabs() - @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) - - # Public: For each selection, if the selection is empty, cut all characters - # of the containing line following the cursor. Otherwise cut the selected - # text. - cutToEndOfLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cut(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, copy the selected text. - copySelectedText: -> - maintainClipboard = false - for selection in @getSelections() - selection.copy(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, replace the selected text with the contents of - # the clipboard. - # - # If the clipboard contains the same number of selections as the current - # editor, each selection will be replaced with the content of the - # corresponding clipboard selection text. - # - # options - See {Selection::insertText}. - pasteText: (options={}) -> - {text, metadata} = atom.clipboard.readWithMetadata() - - containsNewlines = text.indexOf('\n') isnt -1 - - if metadata?.selections? and metadata.selections.length is @getSelections().length - @mutateSelectedText (selection, index) -> - text = metadata.selections[index] - selection.insertText(text, options) - - return - - else if atom.config.get("editor.normalizeIndentOnPaste") and metadata?.indentBasis? - if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines - options.indentBasis ?= metadata.indentBasis - - @insertText(text, options) - - # Public: Undo the last change. - undo: -> - @getCursor().needsAutoscroll = true - @buffer.undo(this) - - # Public: Redo the last change. - redo: -> - @getCursor().needsAutoscroll = true - @buffer.redo(this) - - # Public: Fold the most recent cursor's row based on its indentation level. - # - # The fold will extend from the nearest preceding line with a lower - # indentation level up to the nearest following row with a lower indentation - # level. - foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) - - # Public: Unfold the most recent cursor's row by one level. - unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) - - # Public: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - - # Public: Fold all foldable lines. - foldAll: -> - @languageMode.foldAll() - - # Public: Unfold all existing folds. - unfoldAll: -> - @languageMode.unfoldAll() - - # Public: Fold all foldable lines at the given indent level. - # - # level - A {Number}. - foldAllAtIndentLevel: (level) -> - @languageMode.foldAllAtIndentLevel(level) - - # Public: Fold the given row in buffer coordinates based on its indentation - # level. - # - # If the given row is foldable, the fold will begin there. Otherwise, it will - # begin at the first foldable row preceding the given row. - # - # bufferRow - A {Number}. - foldBufferRow: (bufferRow) -> - @languageMode.foldBufferRow(bufferRow) - - # Public: Unfold all folds containing the given row in buffer coordinates. - # - # bufferRow - A {Number} - unfoldBufferRow: (bufferRow) -> - @displayBuffer.unfoldBufferRow(bufferRow) - - # Public: Determine whether the given row in buffer coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # bufferRow - A {Number} - # - # Returns a {Boolean}. - isFoldableAtBufferRow: (bufferRow) -> - @languageMode.isFoldableAtBufferRow(bufferRow) - - isFoldableAtScreenRow: (screenRow) -> - bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) - @isFoldableAtBufferRow(bufferRow) - - # TODO: Rename to foldRowRange? - createFold: (startRow, endRow) -> - @displayBuffer.createFold(startRow, endRow) - - # {Delegates to: DisplayBuffer.destroyFoldWithId} - destroyFoldWithId: (id) -> - @displayBuffer.destroyFoldWithId(id) - - # Remove any {Fold}s found that intersect the given buffer row. - destroyFoldsIntersectingBufferRange: (bufferRange) -> - for row in [bufferRange.start.row..bufferRange.end.row] - @unfoldBufferRow(row) - - # Public: Fold the given buffer row if it isn't currently folded, and unfold - # it otherwise. - toggleFoldAtBufferRow: (bufferRow) -> - if @isFoldedAtBufferRow(bufferRow) - @unfoldBufferRow(bufferRow) - else - @foldBufferRow(bufferRow) - - # Public: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtScreenRow(@getCursorScreenRow()) - - # Public: Determine whether the given row in buffer coordinates is folded. - # - # bufferRow - A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldedAtBufferRow(bufferRow) - - # Public: Determine whether the given row in screen coordinates is folded. - # - # screenRow - A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @displayBuffer.isFoldedAtScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} - largestFoldContainingBufferRow: (bufferRow) -> - @displayBuffer.largestFoldContainingBufferRow(bufferRow) - - # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} - largestFoldStartingAtScreenRow: (screenRow) -> - @displayBuffer.largestFoldStartingAtScreenRow(screenRow) - - # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} - outermostFoldsInBufferRowRange: (startRow, endRow) -> - @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) + # * `fn` A {Function} that will be called once for each {Selection}. The first + # argument will be a {Selection} and the second argument will be the + # {Number} index of that selection. + mutateSelectedText: (fn) -> + @transact => fn(selection, index) for selection, index in @getSelections() # Move lines intersection the most recent selection up by one row in screen # coordinates. @@ -1088,17 +631,6 @@ class Editor extends Model deprecate("Use Editor::duplicateLines() instead") @duplicateLines() - # Public: Mutate the text of all the selections in a single transaction. - # - # All the changes made inside the given {Function} can be reverted with a - # single call to {::undo}. - # - # fn - A {Function} that will be called once for each {Selection}. The first - # argument will be a {Selection} and the second argument will be the - # {Number} index of that selection. - mutateSelectedText: (fn) -> - @transact => fn(selection, index) for selection, index in @getSelections() - replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @mutateSelectedText (selection) -> @@ -1110,10 +642,736 @@ class Editor extends Model selection.insertText(fn(text)) selection.setBufferRange(range) + # Public: Split multi-line selections into one selection per line. + # + # Operates on all selections. This method breaks apart all multi-line + # selections to create multiple single-line selections that cumulatively cover + # the same original area. + splitSelectionsIntoLines: -> + for selection in @getSelections() + range = selection.getBufferRange() + continue if range.isSingleLine() + + selection.destroy() + {start, end} = range + @addSelectionForBufferRange([start, [start.row, Infinity]]) + {row} = start + while ++row < end.row + @addSelectionForBufferRange([[row, 0], [row, Infinity]]) + @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 + + # Public: For each selection, transpose the selected text. + # + # If the selection is empty, the characters preceding and following the cursor + # are swapped. Otherwise, the selected characters are reversed. + transpose: -> + @mutateSelectedText (selection) -> + if selection.isEmpty() + selection.selectRight() + text = selection.getText() + selection.delete() + selection.cursor.moveLeft() + selection.insertText text + else + selection.insertText selection.getText().split('').reverse().join('') + + # Public: Convert the selected text to upper case. + # + # For each selection, if the selection is empty, converts the containing word + # to upper case. Otherwise convert the selected text to upper case. + upperCase: -> + @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toUpperCase() + + # Public: Convert the selected text to lower case. + # + # For each selection, if the selection is empty, converts the containing word + # to upper case. Otherwise convert the selected text to upper case. + lowerCase: -> + @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toLowerCase() + + # Convert multiple lines to a single line. + # + # Operates on all selections. If the selection is empty, joins the current + # line with the next line. Otherwise it joins all lines that intersect the + # selection. + # + # Joining a line means that multiple lines are converted to a single line with + # the contents of each of the original non-empty lines separated by a space. + joinLines: -> + @mutateSelectedText (selection) -> selection.joinLines() + + ### + Section: Adding Text + ### + + # Public: For each selection, replace the selected text with the given text. + # + # * `text` A {String} representing the text to insert. + # * `options` (optional) See {Selection::insertText}. + # + # Returns a {Range} when the text has been inserted + # Returns a {Bool} false when the text has not been inserted + insertText: (text, options={}) -> + willInsert = true + cancel = -> willInsert = false + @emit('will-insert-text', {cancel, text}) + + if willInsert + options.autoIndentNewline ?= @shouldAutoIndent() + options.autoDecreaseIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) => + range = selection.insertText(text, options) + @emit('did-insert-text', {text, range}) + range + else + false + + # Public: For each selection, replace the selected text with a newline. + insertNewline: -> + @insertText('\n') + + # Public: For each cursor, insert a newline at beginning the following line. + insertNewlineBelow: -> + @transact => + @moveCursorToEndOfLine() + @insertNewline() + + # Public: For each cursor, insert a newline at the end of the preceding line. + insertNewlineAbove: -> + @transact => + bufferRow = @getCursorBufferPosition().row + indentLevel = @indentationForBufferRow(bufferRow) + onFirstLine = bufferRow is 0 + + @moveCursorToBeginningOfLine() + @moveCursorLeft() + @insertNewline() + + if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel + @setIndentationForBufferRow(bufferRow, indentLevel) + + if onFirstLine + @moveCursorUp() + @moveCursorToEndOfLine() + + ### + Section: Removing Text + ### + + # Public: For each selection, if the selection is empty, delete the character + # preceding the cursor. Otherwise delete the selected text. + backspace: -> + @mutateSelectedText (selection) -> selection.backspace() + + # Deprecated: Use {::deleteToBeginningOfWord} instead. + backspaceToBeginningOfWord: -> + deprecate("Use Editor::deleteToBeginningOfWord() instead") + @deleteToBeginningOfWord() + + # Deprecated: Use {::deleteToBeginningOfLine} instead. + backspaceToBeginningOfLine: -> + deprecate("Use Editor::deleteToBeginningOfLine() instead") + @deleteToBeginningOfLine() + + # Public: For each selection, if the selection is empty, delete all characters + # of the containing word that precede the cursor. Otherwise delete the + # selected text. + deleteToBeginningOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() + + # Public: For each selection, if the selection is empty, delete all characters + # of the containing line that precede the cursor. Otherwise delete the + # selected text. + deleteToBeginningOfLine: -> + @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() + + # Public: For each selection, if the selection is empty, delete the character + # preceding the cursor. Otherwise delete the selected text. + delete: -> + @mutateSelectedText (selection) -> selection.delete() + + # Public: For each selection, if the selection is not empty, deletes the + # selection; otherwise, deletes all characters of the containing line + # following the cursor. If the cursor is already at the end of the line, + # deletes the following newline. + deleteToEndOfLine: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfLine() + + # Public: For each selection, if the selection is empty, delete all characters + # of the containing word following the cursor. Otherwise delete the selected + # text. + deleteToEndOfWord: -> + @mutateSelectedText (selection) -> selection.deleteToEndOfWord() + + # Public: Delete all lines intersecting selections. + deleteLine: -> + @mutateSelectedText (selection) -> selection.deleteLine() + + ### + Section: Searching Text + ### + + # {Delegates to: TextBuffer.scan} + scan: (args...) -> @buffer.scan(args...) + + # {Delegates to: TextBuffer.scanInRange} + scanInBufferRange: (args...) -> @buffer.scanInRange(args...) + + # {Delegates to: TextBuffer.backwardsScanInRange} + backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) + + + ### + Section: Tab Behavior + ### + + # Public: Determine if the buffer uses hard or soft tabs. + # + # Returns `true` if the first non-comment line with leading whitespace starts + # with a space character. Returns `false` if it starts with a hard tab (`\t`). + # + # Returns a {Boolean} + usesSoftTabs: -> + for bufferRow in [0..@buffer.getLastRow()] + continue if @displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment() + if match = @buffer.lineForRow(bufferRow).match(/^\s/) + return match[0][0] == ' ' + undefined + + # Public: Returns a {Boolean} indicating whether softTabs are enabled for this + # editor. + getSoftTabs: -> @softTabs + + # Public: Enable or disable soft tabs for this editor. + # + # * `softTabs` A {Boolean} + setSoftTabs: (@softTabs) -> @softTabs + + # Public: Toggle soft tabs for this editor + toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) + + # Public: Get the text representing a single level of indent. + # + # If soft tabs are enabled, the text is composed of N spaces, where N is the + # tab length. Otherwise the text is a tab character (`\t`). + # + # Returns a {String}. + getTabText: -> @buildIndentString(1) + + # Public: Get the on-screen length of tab characters. + # + # Returns a {Number}. + getTabLength: -> @displayBuffer.getTabLength() + + # Public: Set the on-screen length of tab characters. + setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + + # If soft tabs are enabled, convert all hard tabs to soft tabs in the given + # {Range}. + normalizeTabsInBufferRange: (bufferRange) -> + return unless @getSoftTabs() + @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) + + + + ### + Section: Soft Wrap Behavior + ### + + # Public: Sets the column at which column will soft wrap + getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() + + # Public: Get whether soft wrap is enabled for this editor. + getSoftWrap: -> @displayBuffer.getSoftWrap() + + # Public: Enable or disable soft wrap for this editor. + # + # * `softWrap` A {Boolean} + setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap) + + # Public: Toggle soft wrap for this editor + toggleSoftWrap: -> @setSoftWrap(not @getSoftWrap()) + + + + ### + Section: Indentation + ### + + # Public: Get the indentation level of the given a buffer row. + # + # Returns how deeply the given row is indented based on the soft tabs and + # tab length settings of this editor. Note that if soft tabs are enabled and + # the tab length is 2, a row with 4 leading spaces would have an indentation + # level of 2. + # + # * `bufferRow` A {Number} indicating the buffer row. + # + # Returns a {Number}. + indentationForBufferRow: (bufferRow) -> + @indentLevelForLine(@lineForBufferRow(bufferRow)) + + # Public: Set the indentation level for the given buffer row. + # + # Inserts or removes hard tabs or spaces based on the soft tabs and tab length + # settings of this editor in order to bring it to the given indentation level. + # Note that if soft tabs are enabled and the tab length is 2, a row with 4 + # leading spaces would have an indentation level of 2. + # + # * `bufferRow` A {Number} indicating the buffer row. + # * `newLevel` A {Number} indicating the new indentation level. + # * `options` (optional) An {Object} with the following keys: + # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at + # the beginning of the line (default: false). + setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> + if preserveLeadingWhitespace + endColumn = 0 + else + endColumn = @lineForBufferRow(bufferRow).match(/^\s*/)[0].length + newIndentString = @buildIndentString(newLevel) + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + + # Public: Get the indentation level of the given line of text. + # + # Returns how deeply the given line is indented based on the soft tabs and + # tab length settings of this editor. Note that if soft tabs are enabled and + # the tab length is 2, a row with 4 leading spaces would have an indentation + # level of 2. + # + # * `line` A {String} representing a line of text. + # + # Returns a {Number}. + indentLevelForLine: (line) -> + @displayBuffer.indentLevelForLine(line) + + # Indent all lines intersecting selections. See {Selection::indent} for more + # information. + indent: (options={}) -> + options.autoIndent ?= @shouldAutoIndent() + @mutateSelectedText (selection) -> selection.indent(options) + + # Public: Indent rows intersecting selections by one level. + indentSelectedRows: -> + @mutateSelectedText (selection) -> selection.indentSelectedRows() + + # Public: Outdent rows intersecting selections by one level. + outdentSelectedRows: -> + @mutateSelectedText (selection) -> selection.outdentSelectedRows() + + # Public: Indent rows intersecting selections based on the grammar's suggested + # indent level. + autoIndentSelectedRows: -> + @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() + + # Constructs the string used for tabs. + buildIndentString: (number, column=0) -> + if @getSoftTabs() + tabStopViolation = column % @getTabLength() + _.multiplyString(" ", Math.floor(number * @getTabLength()) - tabStopViolation) + else + _.multiplyString("\t", Math.floor(number)) + + + ### + Section: Undo Operations + ### + # Public: Undo the last change. + + undo: -> + @getCursor().needsAutoscroll = true + @buffer.undo(this) + + # Public: Redo the last change. + redo: -> + @getCursor().needsAutoscroll = true + @buffer.redo(this) + + ### + Section: Text Mutation Transactions + ### + + # Public: Batch multiple operations as a single undo/redo step. + # + # Any group of operations that are logically grouped from the perspective of + # undoing and redoing should be performed in a transaction. If you want to + # abort the transaction, call {::abortTransaction} to terminate the function's + # execution and revert any changes performed up to the abortion. + # + # * `fn` A {Function} to call inside the transaction. + transact: (fn) -> @buffer.transact(fn) + + # Public: Start an open-ended transaction. + # + # Call {::commitTransaction} or {::abortTransaction} to terminate the + # transaction. If you nest calls to transactions, only the outermost + # transaction is considered. You must match every begin with a matching + # commit, but a single call to abort will cancel all nested transactions. + beginTransaction: -> @buffer.beginTransaction() + + # Public: Commit an open-ended transaction started with {::beginTransaction} + # and push it to the undo stack. + # + # If transactions are nested, only the outermost commit takes effect. + commitTransaction: -> @buffer.commitTransaction() + + # Public: Abort an open transaction, undoing any operations performed so far + # within the transaction. + abortTransaction: -> @buffer.abortTransaction() + + ### + Section: Editor Coordinates + ### + + # Public: Convert a position in buffer-coordinates to screen-coordinates. + # + # The position is clipped via {::clipBufferPosition} prior to the conversion. + # The position is also clipped via {::clipScreenPosition} following the + # conversion, which only makes a difference when `options` are supplied. + # + # * `bufferPosition` A {Point} or {Array} of [row, column]. + # * `options` (optional) An options hash for {::clipScreenPosition}. + # + # Returns a {Point}. + screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) + + # Public: Convert a position in screen-coordinates to buffer-coordinates. + # + # The position is clipped via {::clipScreenPosition} prior to the conversion. + # + # * `bufferPosition` A {Point} or {Array} of [row, column]. + # * `options` (optional) An options hash for {::clipScreenPosition}. + # + # Returns a {Point}. + bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) + + # Public: Convert a range in buffer-coordinates to screen-coordinates. + # + # Returns a {Range}. + screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) + + # Public: Convert a range in screen-coordinates to buffer-coordinates. + # + # Returns a {Range}. + bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) + + # Public: Clip the given {Point} to a valid position in the buffer. + # + # If the given {Point} describes a position that is actually reachable by the + # cursor based on the current contents of the buffer, it is returned + # unchanged. If the {Point} does not describe a valid position, the closest + # valid position is returned instead. + # + # ## Examples + # + # ```coffee + # editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` + # + # # When the line at buffer row 2 is 10 characters long + # editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]` + # ``` + # + # * `bufferPosition` The {Point} representing the position to clip. + # + # Returns a {Point}. + clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) + + # Public: Clip the start and end of the given range to valid positions in the + # buffer. See {::clipBufferPosition} for more information. + # + # * `range` The {Range} to clip. + # + # Returns a {Range}. + clipBufferRange: (range) -> @buffer.clipRange(range) + + # Public: Clip the given {Point} to a valid position on screen. + # + # If the given {Point} describes a position that is actually reachable by the + # cursor based on the current contents of the screen, it is returned + # unchanged. If the {Point} does not describe a valid position, the closest + # valid position is returned instead. + # + # ## Examples + # + # ```coffee + # editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` + # + # # When the line at screen row 2 is 10 characters long + # editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]` + # ``` + # + # * `bufferPosition` The {Point} representing the position to clip. + # + # Returns a {Point}. + clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) + + + + + ### + Section: Grammars + ### + + # Public: Get the current {Grammar} of this editor. + getGrammar: -> + @displayBuffer.getGrammar() + + # Public: Set the current {Grammar} of this editor. + # + # Assigning a grammar will cause the editor to re-tokenize based on the new + # grammar. + setGrammar: (grammar) -> + @displayBuffer.setGrammar(grammar) + + # Reload the grammar based on the file name. + reloadGrammar: -> + @displayBuffer.reloadGrammar() + + ### + Section: Syntatic Queries + ### + + # Public: Get the syntactic scopes for the given position in buffer + # coordinates. + # + # For example, if called with a position inside the parameter list of an + # anonymous CoffeeScript function, the method returns the following array: + # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` + # + # * `bufferPosition` A {Point} or {Array} of [row, column]. + # + # Returns an {Array} of {String}s. + scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) + + # Public: Get the range in buffer coordinates of all tokens surrounding the + # cursor that match the given scope selector. + # + # For example, if you wanted to find the string surrounding the cursor, you + # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. + # + # Returns a {Range}. + bufferRangeForScopeAtCursor: (selector) -> + @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + + # {Delegates to: DisplayBuffer.tokenForBufferPosition} + tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) + + # Public: Get the syntactic scopes for the most recently added cursor's + # position. See {::scopesForBufferPosition} for more information. + # + # Returns an {Array} of {String}s. + getCursorScopes: -> @getCursor().getScopes() + + logCursorScope: -> + console.log @getCursorScopes() + + + # Public: Determine if the given row is entirely a comment + isBufferRowCommented: (bufferRow) -> + if match = @lineForBufferRow(bufferRow).match(/\S/) + scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes + new TextMateScopeSelector('comment.*').matches(scopes) + + # Public: Toggle line comments for rows intersecting selections. + # + # If the current grammar doesn't support comments, does nothing. + # + # Returns an {Array} of the commented {Range}s. + toggleLineCommentsInSelection: -> + @mutateSelectedText (selection) -> selection.toggleLineComments() + + + + + + + + ### + Section: Clipboard Operations + ### + + # Public: For each selection, copy the selected text. + copySelectedText: -> + maintainClipboard = false + for selection in @getSelections() + selection.copy(maintainClipboard) + maintainClipboard = true + + # Public: For each selection, replace the selected text with the contents of + # the clipboard. + # + # If the clipboard contains the same number of selections as the current + # editor, each selection will be replaced with the content of the + # corresponding clipboard selection text. + # + # * `options` (optional) See {Selection::insertText}. + pasteText: (options={}) -> + {text, metadata} = atom.clipboard.readWithMetadata() + + containsNewlines = text.indexOf('\n') isnt -1 + + if metadata?.selections? and metadata.selections.length is @getSelections().length + @mutateSelectedText (selection, index) -> + text = metadata.selections[index] + selection.insertText(text, options) + + return + + else if atom.config.get("editor.normalizeIndentOnPaste") and metadata?.indentBasis? + if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines + options.indentBasis ?= metadata.indentBasis + + @insertText(text, options) + + # Public: For each selection, cut the selected text. + cutSelectedText: -> + maintainClipboard = false + @mutateSelectedText (selection) -> + selection.cut(maintainClipboard) + maintainClipboard = true + + # Public: For each selection, if the selection is empty, cut all characters + # of the containing line following the cursor. Otherwise cut the selected + # text. + cutToEndOfLine: -> + maintainClipboard = false + @mutateSelectedText (selection) -> + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true + + + ### + Section: Folds + ### + + # Public: Fold the most recent cursor's row based on its indentation level. + # + # The fold will extend from the nearest preceding line with a lower + # indentation level up to the nearest following row with a lower indentation + # level. + foldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @foldBufferRow(bufferRow) + + # Public: Unfold the most recent cursor's row by one level. + unfoldCurrentRow: -> + bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row + @unfoldBufferRow(bufferRow) + + # Public: For each selection, fold the rows it intersects. + foldSelectedLines: -> + selection.fold() for selection in @getSelections() + + # Public: Fold all foldable lines. + foldAll: -> + @languageMode.foldAll() + + # Public: Unfold all existing folds. + unfoldAll: -> + @languageMode.unfoldAll() + + # Public: Fold all foldable lines at the given indent level. + # + # * `level` A {Number}. + foldAllAtIndentLevel: (level) -> + @languageMode.foldAllAtIndentLevel(level) + + # Public: Fold the given row in buffer coordinates based on its indentation + # level. + # + # If the given row is foldable, the fold will begin there. Otherwise, it will + # begin at the first foldable row preceding the given row. + # + # * `bufferRow` A {Number}. + foldBufferRow: (bufferRow) -> + @languageMode.foldBufferRow(bufferRow) + + # Public: Unfold all folds containing the given row in buffer coordinates. + # + # * `bufferRow` A {Number} + unfoldBufferRow: (bufferRow) -> + @displayBuffer.unfoldBufferRow(bufferRow) + + # Public: Determine whether the given row in buffer coordinates is foldable. + # + # A *foldable* row is a row that *starts* a row range that can be folded. + # + # * `bufferRow` A {Number} + # + # Returns a {Boolean}. + isFoldableAtBufferRow: (bufferRow) -> + @languageMode.isFoldableAtBufferRow(bufferRow) + + isFoldableAtScreenRow: (screenRow) -> + bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) + @isFoldableAtBufferRow(bufferRow) + + # TODO: Rename to foldRowRange? + createFold: (startRow, endRow) -> + @displayBuffer.createFold(startRow, endRow) + + # {Delegates to: DisplayBuffer.destroyFoldWithId} + destroyFoldWithId: (id) -> + @displayBuffer.destroyFoldWithId(id) + + # Remove any {Fold}s found that intersect the given buffer row. + destroyFoldsIntersectingBufferRange: (bufferRange) -> + for row in [bufferRange.start.row..bufferRange.end.row] + @unfoldBufferRow(row) + + # Public: Fold the given buffer row if it isn't currently folded, and unfold + # it otherwise. + toggleFoldAtBufferRow: (bufferRow) -> + if @isFoldedAtBufferRow(bufferRow) + @unfoldBufferRow(bufferRow) + else + @foldBufferRow(bufferRow) + + # Public: Determine whether the most recently added cursor's row is folded. + # + # Returns a {Boolean}. + isFoldedAtCursorRow: -> + @isFoldedAtScreenRow(@getCursorScreenRow()) + + # Public: Determine whether the given row in buffer coordinates is folded. + # + # * `bufferRow` A {Number} + # + # Returns a {Boolean}. + isFoldedAtBufferRow: (bufferRow) -> + @displayBuffer.isFoldedAtBufferRow(bufferRow) + + # Public: Determine whether the given row in screen coordinates is folded. + # + # * `screenRow` A {Number} + # + # Returns a {Boolean}. + isFoldedAtScreenRow: (screenRow) -> + @displayBuffer.isFoldedAtScreenRow(screenRow) + + # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} + largestFoldContainingBufferRow: (bufferRow) -> + @displayBuffer.largestFoldContainingBufferRow(bufferRow) + + # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow} + largestFoldStartingAtScreenRow: (screenRow) -> + @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + + # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} + outermostFoldsInBufferRowRange: (startRow, endRow) -> + @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) + + + + + + ### + Section: Decorations + ### + # Public: Get all the decorations within a screen row range. # - # startScreenRow - the {Number} beginning screen row - # endScreenRow - the {Number} end screen row (inclusive) + # * `startScreenRow` the {Number} beginning screen row + # * `endScreenRow` the {Number} end screen row (inclusive) # # Returns an {Object} of decorations in the form # `{1: [{id: 10, type: 'gutter', class: 'someclass'}], 2: ...}` @@ -1128,14 +1386,15 @@ class Editor extends Model # the marker's state. # # There are three types of supported decorations: - # * `line`: Adds your CSS `class` to the line nodes within the range + # + # * __line__: Adds your CSS `class` to the line nodes within the range # marked by the marker - # * `gutter`: Adds your CSS `class` to the line number nodes within the + # * __gutter__: Adds your CSS `class` to the line number nodes within the # range marked by the marker - # * `highlight`: Adds a new highlight div to the editor surrounding the + # * __highlight__: Adds a new highlight div to the editor surrounding the # range marked by the marker. When the user selects text, the selection is # visualized with a highlight decoration internally. The structure of this - # highlight will be: + # highlight will be # ```html #
# @@ -1143,20 +1402,19 @@ class Editor extends Model #
# ``` # - # marker - A {Marker} you want this decoration to follow. - # decorationParams - An {Object} representing the decoration eg. `{type: 'gutter', class: 'linter-error'}` - # :type - There are a few supported decoration types: - # * `gutter`: Applies the decoration to the line numbers spanned by the marker. - # * `line`: Applies the decoration to the lines spanned by the marker. - # * `highlight`: Applies the decoration to a "highlight" behind the marked range. - # :class - This CSS class will be applied to the decorated line number, + # ## Arguments + # + # * `marker` A {Marker} you want this decoration to follow. + # * `decorationParams` An {Object} representing the decoration eg. `{type: 'gutter', class: 'linter-error'}` + # * `type` There are a few supported decoration types: `gutter`, `line`, and `highlight` + # * `class` This CSS class will be applied to the decorated line number, # line, or highlight. - # :onlyHead - If `true`, the decoration will only be applied to the head + # * `onlyHead` (optional) If `true`, the decoration will only be applied to the head # of the marker. Only applicable to the `line` and `gutter` types. - # :onlyEmpty - If `true`, the decoration will only be applied if the + # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if the # associated marker is empty. Only applicable to the `line` and # `gutter` types. - # :onlyNonEmpty - If `true`, the decoration will only be applied if the + # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied if the # associated marker is non-empty. Only applicable to the `line` and # gutter types. # @@ -1167,6 +1425,10 @@ class Editor extends Model decorationForId: (id) -> @displayBuffer.decorationForId(id) + ### + Section: Markers + ### + # Public: Get the {DisplayBufferMarker} for the given marker id. getMarker: (id) -> @displayBuffer.getMarker(id) @@ -1182,25 +1444,25 @@ class Editor extends Model # In addition, there are several special properties that will be compared # with the range of the markers rather than their properties. # - # properties - An {Object} containing properties that each returned marker + # * `properties` An {Object} containing properties that each returned marker # must satisfy. Markers can be associated with custom properties, which are # compared with basic equality. In addition, several reserved properties # can be used to filter markers based on their current range: - # :startBufferRow - Only include markers starting at this row in buffer + # * `startBufferRow` Only include markers starting at this row in buffer # coordinates. - # :endBufferRow - Only include markers ending at this row in buffer + # * `endBufferRow` Only include markers ending at this row in buffer # coordinates. - # :containsBufferRange - Only include markers containing this {Range} or + # * `containsBufferRange` Only include markers containing this {Range} or # in range-compatible {Array} in buffer coordinates. - # :containsBufferPosition - Only include markers containing this {Point} + # * `containsBufferPosition` Only include markers containing this {Point} # or {Array} of `[row, column]` in buffer coordinates. findMarkers: (properties) -> @displayBuffer.findMarkers(properties) # Public: Mark the given range in screen coordinates. # - # range - A {Range} or range-compatible {Array}. - # options - See {TextBuffer::markRange}. + # * `range` A {Range} or range-compatible {Array}. + # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayBufferMarker}. markScreenRange: (args...) -> @@ -1208,8 +1470,8 @@ class Editor extends Model # Public: Mark the given range in buffer coordinates. # - # range - A {Range} or range-compatible {Array}. - # options - See {TextBuffer::markRange}. + # * `range` A {Range} or range-compatible {Array}. + # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayBufferMarker}. markBufferRange: (args...) -> @@ -1217,8 +1479,8 @@ class Editor extends Model # Public: Mark the given position in screen coordinates. # - # position - A {Point} or {Array} of `[row, column]`. - # options - See {TextBuffer::markRange}. + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayBufferMarker}. markScreenPosition: (args...) -> @@ -1226,8 +1488,8 @@ class Editor extends Model # Public: Mark the given position in buffer coordinates. # - # position - A {Point} or {Array} of `[row, column]`. - # options - See {TextBuffer::markRange}. + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayBufferMarker}. markBufferPosition: (args...) -> @@ -1243,6 +1505,11 @@ class Editor extends Model getMarkerCount: -> @buffer.getMarkerCount() + + ### + Section: Cursors + ### + # Public: Determine if there are multiple cursors. hasMultipleCursors: -> @getCursors().length > 1 @@ -1281,163 +1548,15 @@ class Editor extends Model # Remove the given cursor from this editor. removeCursor: (cursor) -> _.remove(@cursors, cursor) - - # Add a {Selection} based on the given {DisplayBufferMarker}. - # - # marker - The {DisplayBufferMarker} to highlight - # options - An {Object} that pertains to the {Selection} constructor. - # - # Returns the new {Selection}. - addSelection: (marker, options={}) -> - unless marker.getAttributes().preserveFolds - @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) - cursor = @addCursor(marker) - selection = new Selection(_.extend({editor: this, marker, cursor}, options)) - @selections.push(selection) - selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections() - if selection.destroyed - for selection in @getSelections() - if selection.intersectsBufferRange(selectionBufferRange) - return selection - else - @emit 'selection-added', selection - selection - - # Public: Add a selection for the given range in buffer coordinates. - # - # bufferRange - A {Range} - # options - An options {Object}: - # :reversed - A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # - # Returns the added {Selection}. - addSelectionForBufferRange: (bufferRange, options={}) -> - @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) - selection = @getLastSelection() - selection.autoscroll() if @manageScrollPosition - selection - - # Public: Set the selected range in buffer coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # bufferRange - A {Range} or range-compatible {Array}. - # options - An options {Object}: - # :reversed - A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedBufferRange: (bufferRange, options) -> - @setSelectedBufferRanges([bufferRange], options) - - # Public: Set the selected range in screen coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # screenRange - A {Range} or range-compatible {Array}. - # options - An options {Object}: - # :reversed - A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedScreenRange: (screenRange, options) -> - @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options) - - # Public: Set the selected ranges in buffer coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # bufferRanges - An {Array} of {Range}s or range-compatible {Array}s. - # options - An options {Object}: - # :reversed - A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedBufferRanges: (bufferRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[bufferRanges.length...] - - @mergeIntersectingSelections options, => - for bufferRange, i in bufferRanges - bufferRange = Range.fromObject(bufferRange) - if selections[i] - selections[i].setBufferRange(bufferRange, options) - else - @addSelectionForBufferRange(bufferRange, options) - - # Remove the given selection. - removeSelection: (selection) -> - _.remove(@selections, selection) - @emit 'selection-removed', selection - - # Reduce one or more selections to a single empty selection based on the most - # recently added cursor. - clearSelections: -> - @consolidateSelections() - @getSelection().clear() - - # Reduce multiple selections to the most recently added selection. - consolidateSelections: -> - selections = @getSelections() - if selections.length > 1 - selection.destroy() for selection in selections[0...-1] - true - else - false - - selectionScreenRangeChanged: (selection) -> - @emit 'selection-screen-range-changed', selection - - # Public: Get current {Selection}s. - # - # Returns: An {Array} of {Selection}s. - getSelections: -> new Array(@selections...) - - selectionsForScreenRows: (startRow, endRow) -> - @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) - - # Public: Get the most recent {Selection} or the selection at the given - # index. - # - # index - Optional. The index of the selection to return, based on the order - # in which the selections were added. - # - # Returns a {Selection}. - # or the at the specified index. - getSelection: (index) -> - index ?= @selections.length - 1 - @selections[index] - - # Public: Get the most recently added {Selection}. - # - # Returns a {Selection}. - getLastSelection: -> - _.last(@selections) - - # Public: Get all {Selection}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getSelectionsOrderedByBufferPosition: -> - @getSelections().sort (a, b) -> a.compare(b) - - # Public: Get the last {Selection} based on its position in the buffer. - # - # Returns a {Selection}. - getLastSelectionInBuffer: -> - _.last(@getSelectionsOrderedByBufferPosition()) - - # Public: Determine if a given range in buffer coordinates intersects a - # selection. - # - # bufferRange - A {Range} or range-compatible {Array}. - # - # Returns a {Boolean}. - selectionIntersectsBufferRange: (bufferRange) -> - _.any @getSelections(), (selection) -> - selection.intersectsBufferRange(bufferRange) + @emit 'cursor-removed', cursor # Public: Move the cursor to the given position in screen coordinates. # # If there are multiple cursors, they will be consolidated to a single cursor. # - # position - A {Point} or {Array} of `[row, column]` - # options - An {Object} combining options for {::clipScreenPosition} with: - # :autoscroll - Determines whether the editor scrolls to the new cursor's + # * `position` A {Point} or {Array} of `[row, column]` + # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: + # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorScreenPosition: (position, options) -> @moveCursors (cursor) -> cursor.setScreenPosition(position, options) @@ -1459,9 +1578,9 @@ class Editor extends Model # # If there are multiple cursors, they will be consolidated to a single cursor. # - # position - A {Point} or {Array} of `[row, column]` - # options - An {Object} combining options for {::clipScreenPosition} with: - # :autoscroll - Determines whether the editor scrolls to the new cursor's + # * `position` A {Point} or {Array} of `[row, column]` + # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: + # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorBufferPosition: (position, options) -> @moveCursors (cursor) -> cursor.setBufferPosition(position, options) @@ -1473,68 +1592,9 @@ class Editor extends Model getCursorBufferPosition: -> @getCursor().getBufferPosition() - # Public: Get the {Range} of the most recently added selection in screen - # coordinates. - # - # Returns a {Range}. - getSelectedScreenRange: -> - @getLastSelection().getScreenRange() - - # Public: Get the {Range} of the most recently added selection in buffer - # coordinates. - # - # Returns a {Range}. - getSelectedBufferRange: -> - @getLastSelection().getBufferRange() - - # Public: Get the {Range}s of all selections in buffer coordinates. - # - # The ranges are sorted by their position in the buffer. - # - # Returns an {Array} of {Range}s. - getSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() - - # Public: Get the {Range}s of all selections in screen coordinates. - # - # The ranges are sorted by their position in the buffer. - # - # Returns an {Array} of {Range}s. - getSelectedScreenRanges: -> - selection.getScreenRange() for selection in @getSelectionsOrderedByBufferPosition() - - # Public: Get the selected text of the most recently added selection. - # - # Returns a {String}. - getSelectedText: -> - @getLastSelection().getText() - - # Public: Get the text in the given {Range} in buffer coordinates. - # - # range - A {Range} or range-compatible {Array}. - # - # Returns a {String}. - getTextInBufferRange: (range) -> - @buffer.getTextInRange(range) - - # Public: Set the text in the given {Range} in buffer coordinates. - # - # range - A {Range} or range-compatible {Array}. - # text - A {String} - # - # Returns the {Range} of the newly-inserted text. - setTextInBufferRange: (range, text, normalizeLineEndings) -> @getBuffer().setTextInRange(range, text, normalizeLineEndings) - - # Public: Get the {Range} of the paragraph surrounding the most recently added - # cursor. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @getCursor().getCurrentParagraphBufferRange() - # Public: Returns the word surrounding the most recently added cursor. # - # options - See {Cursor::getBeginningOfCurrentWordBufferPosition}. + # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. getWordUnderCursor: (options) -> @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) @@ -1614,35 +1674,6 @@ class Editor extends Model moveCursorToBeginningOfPreviousParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() - # Public: Scroll the editor to reveal the most recently added cursor if it is - # off-screen. - # - # options - An optional hash of options. - # :center - Center the editor around the cursor if possible. Defauls to - # true. - scrollToCursorPosition: (options) -> - @getCursor().autoscroll(center: options?.center ? true) - - pageUp: -> - newScrollTop = @getScrollTop() - @getHeight() - @moveCursorUp(@getRowsPerPage()) - @setScrollTop(newScrollTop) - - pageDown: -> - newScrollTop = @getScrollTop() + @getHeight() - @moveCursorDown(@getRowsPerPage()) - @setScrollTop(newScrollTop) - - selectPageUp: -> - @selectUp(@getRowsPerPage()) - - selectPageDown: -> - @selectDown(@getRowsPerPage()) - - # Returns the number of rows per page - getRowsPerPage: -> - Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels())) - moveCursors: (fn) -> @movingCursors = true fn(cursor) for cursor in @getCursors() @@ -1654,12 +1685,220 @@ class Editor extends Model @emit 'cursor-moved', event @emit 'cursors-moved' unless @movingCursors + # Merge cursors that have the same screen position + mergeCursors: -> + positions = [] + for cursor in @getCursors() + position = cursor.getBufferPosition().toString() + if position in positions + cursor.destroy() + else + positions.push(position) + + preserveCursorPositionOnBufferReload: -> + cursorPosition = null + @subscribe @buffer, "will-reload", => + cursorPosition = @getCursorBufferPosition() + @subscribe @buffer, "reloaded", => + @setCursorBufferPosition(cursorPosition) if cursorPosition + cursorPosition = null + + + ### + Section: Selections + ### + + # Add a {Selection} based on the given {DisplayBufferMarker}. + # + # * `marker` The {DisplayBufferMarker} to highlight + # * `options` (optional) An {Object} that pertains to the {Selection} constructor. + # + # Returns the new {Selection}. + addSelection: (marker, options={}) -> + unless marker.getAttributes().preserveFolds + @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) + cursor = @addCursor(marker) + selection = new Selection(_.extend({editor: this, marker, cursor}, options)) + @selections.push(selection) + selectionBufferRange = selection.getBufferRange() + @mergeIntersectingSelections() + if selection.destroyed + for selection in @getSelections() + if selection.intersectsBufferRange(selectionBufferRange) + return selection + else + @emit 'selection-added', selection + selection + + # Public: Add a selection for the given range in buffer coordinates. + # + # * `bufferRange` A {Range} + # * `options` (optional) An options {Object}: + # * `reversed` A {Boolean} indicating whether to create the selection in a + # reversed orientation. + # + # Returns the added {Selection}. + addSelectionForBufferRange: (bufferRange, options={}) -> + @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) + selection = @getLastSelection() + selection.autoscroll() if @manageScrollPosition + selection + + # Public: Set the selected range in buffer coordinates. If there are multiple + # selections, they are reduced to a single selection with the given range. + # + # * `bufferRange` A {Range} or range-compatible {Array}. + # * `options` (optional) An options {Object}: + # * `reversed` A {Boolean} indicating whether to create the selection in a + # reversed orientation. + setSelectedBufferRange: (bufferRange, options) -> + @setSelectedBufferRanges([bufferRange], options) + + # Public: Set the selected range in screen coordinates. If there are multiple + # selections, they are reduced to a single selection with the given range. + # + # * `screenRange` A {Range} or range-compatible {Array}. + # * `options` (optional) An options {Object}: + # * `reversed` A {Boolean} indicating whether to create the selection in a + # reversed orientation. + setSelectedScreenRange: (screenRange, options) -> + @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options) + + # Public: Set the selected ranges in buffer coordinates. If there are multiple + # selections, they are replaced by new selections with the given ranges. + # + # * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. + # * `options` (optional) An options {Object}: + # * `reversed` A {Boolean} indicating whether to create the selection in a + # reversed orientation. + setSelectedBufferRanges: (bufferRanges, options={}) -> + throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length + + selections = @getSelections() + selection.destroy() for selection in selections[bufferRanges.length...] + + @mergeIntersectingSelections options, => + for bufferRange, i in bufferRanges + bufferRange = Range.fromObject(bufferRange) + if selections[i] + selections[i].setBufferRange(bufferRange, options) + else + @addSelectionForBufferRange(bufferRange, options) + + # Remove the given selection. + removeSelection: (selection) -> + _.remove(@selections, selection) + @emit 'selection-removed', selection + + # Reduce one or more selections to a single empty selection based on the most + # recently added cursor. + clearSelections: -> + @consolidateSelections() + @getSelection().clear() + + # Reduce multiple selections to the most recently added selection. + consolidateSelections: -> + selections = @getSelections() + if selections.length > 1 + selection.destroy() for selection in selections[0...-1] + true + else + false + + selectionScreenRangeChanged: (selection) -> + @emit 'selection-screen-range-changed', selection + + # Public: Get current {Selection}s. + # + # Returns: An {Array} of {Selection}s. + getSelections: -> new Array(@selections...) + + selectionsForScreenRows: (startRow, endRow) -> + @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) + + # Public: Get the most recent {Selection} or the selection at the given + # index. + # + # * `index` (optional) The index of the selection to return, based on the order + # in which the selections were added. + # + # Returns a {Selection}. + # or the at the specified index. + getSelection: (index) -> + index ?= @selections.length - 1 + @selections[index] + + # Public: Get the most recently added {Selection}. + # + # Returns a {Selection}. + getLastSelection: -> + _.last(@selections) + + # Public: Get all {Selection}s, ordered by their position in the buffer + # instead of the order in which they were added. + # + # Returns an {Array} of {Selection}s. + getSelectionsOrderedByBufferPosition: -> + @getSelections().sort (a, b) -> a.compare(b) + + # Public: Get the last {Selection} based on its position in the buffer. + # + # Returns a {Selection}. + getLastSelectionInBuffer: -> + _.last(@getSelectionsOrderedByBufferPosition()) + + # Public: Determine if a given range in buffer coordinates intersects a + # selection. + # + # * `bufferRange` A {Range} or range-compatible {Array}. + # + # Returns a {Boolean}. + selectionIntersectsBufferRange: (bufferRange) -> + _.any @getSelections(), (selection) -> + selection.intersectsBufferRange(bufferRange) + + # Public: Get the {Range} of the most recently added selection in screen + # coordinates. + # + # Returns a {Range}. + getSelectedScreenRange: -> + @getLastSelection().getScreenRange() + + # Public: Get the {Range} of the most recently added selection in buffer + # coordinates. + # + # Returns a {Range}. + getSelectedBufferRange: -> + @getLastSelection().getBufferRange() + + # Public: Get the {Range}s of all selections in buffer coordinates. + # + # The ranges are sorted by their position in the buffer. + # + # Returns an {Array} of {Range}s. + getSelectedBufferRanges: -> + selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() + + # Public: Get the {Range}s of all selections in screen coordinates. + # + # The ranges are sorted by their position in the buffer. + # + # Returns an {Array} of {Range}s. + getSelectedScreenRanges: -> + selection.getScreenRange() for selection in @getSelectionsOrderedByBufferPosition() + + # Public: Get the selected text of the most recently added selection. + # + # Returns a {String}. + getSelectedText: -> + @getLastSelection().getText() + # Public: Select from the current cursor position to the given position in # screen coordinates. # # This method may merge selections that end up intesecting. # - # position - An instance of {Point}, with a given `row` and `column`. + # * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> lastSelection = @getLastSelection() lastSelection.selectToScreenPosition(position) @@ -1778,64 +2017,6 @@ class Editor extends Model addSelectionAbove: -> @expandSelectionsBackward (selection) -> selection.addSelectionAbove() - # Public: Split multi-line selections into one selection per line. - # - # Operates on all selections. This method breaks apart all multi-line - # selections to create multiple single-line selections that cumulatively cover - # the same original area. - splitSelectionsIntoLines: -> - for selection in @getSelections() - range = selection.getBufferRange() - continue if range.isSingleLine() - - selection.destroy() - {start, end} = range - @addSelectionForBufferRange([start, [start.row, Infinity]]) - {row} = start - while ++row < end.row - @addSelectionForBufferRange([[row, 0], [row, Infinity]]) - @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 - - # Public: For each selection, transpose the selected text. - # - # If the selection is empty, the characters preceding and following the cursor - # are swapped. Otherwise, the selected characters are reversed. - transpose: -> - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectRight() - text = selection.getText() - selection.delete() - selection.cursor.moveLeft() - selection.insertText text - else - selection.insertText selection.getText().split('').reverse().join('') - - # Public: Convert the selected text to upper case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - upperCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toUpperCase() - - # Public: Convert the selected text to lower case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - lowerCase: -> - @replaceSelectedText selectWordIfEmpty:true, (text) -> text.toLowerCase() - - # Convert multiple lines to a single line. - # - # Operates on all selections. If the selection is empty, joins the current - # line with the next line. Otherwise it joins all lines that intersect the - # selection. - # - # Joining a line means that multiple lines are converted to a single line with - # the contents of each of the original non-empty lines separated by a space. - joinLines: -> - @mutateSelectedText (selection) -> selection.joinLines() - # Public: Expand selections to the beginning of their containing word. # # Operates on all selections. Moves the cursor to the beginning of the @@ -1877,7 +2058,7 @@ class Editor extends Model # Public: Select the range of the given marker if it is valid. # - # marker - A {DisplayBufferMarker} + # * `marker` A {DisplayBufferMarker} # # Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker: (marker) -> @@ -1886,16 +2067,6 @@ class Editor extends Model @setSelectedBufferRange(range) range - # Merge cursors that have the same screen position - mergeCursors: -> - positions = [] - for cursor in @getCursors() - position = cursor.getBufferPosition().toString() - if position in positions - cursor.destroy() - else - positions.push(position) - # Calls the given function with each selection, then merges selections expandSelectionsForward: (fn) -> @mergeIntersectingSelections => @@ -1934,28 +2105,44 @@ class Editor extends Model _.reduce(@getSelections(), reducer, []) - preserveCursorPositionOnBufferReload: -> - cursorPosition = null - @subscribe @buffer, "will-reload", => - cursorPosition = @getCursorBufferPosition() - @subscribe @buffer, "reloaded", => - @setCursorBufferPosition(cursorPosition) if cursorPosition - cursorPosition = null - # Public: Get the current {Grammar} of this editor. - getGrammar: -> - @displayBuffer.getGrammar() - # Public: Set the current {Grammar} of this editor. + ### + Section: Scrolling the Editor + ### + + # Public: Scroll the editor to reveal the most recently added cursor if it is + # off-screen. # - # Assigning a grammar will cause the editor to re-tokenize based on the new - # grammar. - setGrammar: (grammar) -> - @displayBuffer.setGrammar(grammar) + # * `options` (optional) {Object} + # * `center` Center the editor around the cursor if possible. Defauls to true. + scrollToCursorPosition: (options) -> + @getCursor().autoscroll(center: options?.center ? true) - # Reload the grammar based on the file name. - reloadGrammar: -> - @displayBuffer.reloadGrammar() + pageUp: -> + newScrollTop = @getScrollTop() - @getHeight() + @moveCursorUp(@getRowsPerPage()) + @setScrollTop(newScrollTop) + + pageDown: -> + newScrollTop = @getScrollTop() + @getHeight() + @moveCursorDown(@getRowsPerPage()) + @setScrollTop(newScrollTop) + + selectPageUp: -> + @selectUp(@getRowsPerPage()) + + selectPageDown: -> + @selectDown(@getRowsPerPage()) + + # Returns the number of rows per page + getRowsPerPage: -> + Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels())) + + + ### + Section: Config + ### shouldAutoIndent: -> atom.config.get("editor.autoIndent") @@ -1969,38 +2156,10 @@ class Editor extends Model else @displayBuffer.setInvisibles(null) - # Public: Batch multiple operations as a single undo/redo step. - # - # Any group of operations that are logically grouped from the perspective of - # undoing and redoing should be performed in a transaction. If you want to - # abort the transaction, call {::abortTransaction} to terminate the function's - # execution and revert any changes performed up to the abortion. - # - # fn - A {Function} to call inside the transaction. - transact: (fn) -> @buffer.transact(fn) - # Public: Start an open-ended transaction. - # - # Call {::commitTransaction} or {::abortTransaction} to terminate the - # transaction. If you nest calls to transactions, only the outermost - # transaction is considered. You must match every begin with a matching - # commit, but a single call to abort will cancel all nested transactions. - beginTransaction: -> @buffer.beginTransaction() - - # Public: Commit an open-ended transaction started with {::beginTransaction} - # and push it to the undo stack. - # - # If transactions are nested, only the outermost commit takes effect. - commitTransaction: -> @buffer.commitTransaction() - - # Public: Abort an open transaction, undoing any operations performed so far - # within the transaction. - abortTransaction: -> @buffer.abortTransaction() - - inspect: -> - "" - - logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) + ### + Section: Event Handlers + ### handleTokenization: -> @softTabs = @usesSoftTabs() ? @softTabs @@ -2013,6 +2172,10 @@ class Editor extends Model if marker.matchesAttributes(@getSelectionMarkerAttributes()) @addSelection(marker) + ### + Section: Editor Rendering + ### + getSelectionMarkerAttributes: -> type: 'selection', editorId: @id, invalidate: 'never' @@ -2094,3 +2257,12 @@ class Editor extends Model joinLine: -> deprecate("Use Editor::joinLines() instead") @joinLines() + + ### + Section: Utility + ### + + inspect: -> + "" + + logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) diff --git a/src/git.coffee b/src/git.coffee index b8e35911f..7cf4014a9 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -20,16 +20,16 @@ Task = require './task' # For a repository with submodules this would have the following outcome: # # ```coffee -# repo = atom.project.getRepo() -# repo.getShortHead() # 'master' -# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' +# repo = atom.project.getRepo() +# repo.getShortHead() # 'master' +# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' # ``` # -# ## Example +# ## Examples # -# ```coffeescript -# git = atom.project.getRepo() -# console.log git.getOriginUrl() +# ```coffee +# git = atom.project.getRepo() +# console.log git.getOriginUrl() # ``` # # ## Requiring in packages @@ -44,12 +44,12 @@ class Git # Public: Creates a new Git instance. # - # path - The path to the Git repository to open. - # options - An object with the following keys (default: {}): - # :refreshOnWindowFocus - `true` to refresh the index and statuses when the - # window is focused. + # * `path` The {String} path to the Git repository to open. + # * `options` An optinal {Object} with the following keys: + # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and + # statuses when the window is focused. # - # Returns a Git instance or null if the repository could not be opened. + # Returns a {Git} instance or `null` if the repository could not be opened. @open: (path, options) -> return null unless path try @@ -114,8 +114,10 @@ class Git else checkoutHead() - # Public: Destroy this `Git` object. This destroys any tasks and subscriptions - # and releases the underlying libgit2 repository handle. + # Public: Destroy this {Git} object. + # + # This destroys any tasks and subscriptions and releases the underlying + # libgit2 repository handle. destroy: -> if @statusTask? @statusTask.terminate() @@ -147,7 +149,7 @@ class Git # Public: Get the status of a single path in the repository. # - # path - A {String} repository-relative path. + # `path` A {String} repository-relative path. # # Returns a {Number} representing the status. This value can be passed to # {::isStatusModified} or {::isStatusNew} to get more information. @@ -196,8 +198,8 @@ class Git # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 # characters. # - # path - An optional {String} path in the repository to get this information - # for, only needed if the repository contains submodules. + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository contains submodules. # # Returns a {String}. getShortHead: (path) -> @getRepo(path).getShortHead() @@ -207,12 +209,12 @@ class Git # # This is essentially the same as running: # - # ``` - # git reset HEAD -- - # git checkout HEAD -- + # ```sh + # git reset HEAD -- + # git checkout HEAD -- # ``` # - # path - The {String} path to checkout. + # * `path` The {String} path to checkout. # # Returns a {Boolean} that's true if the method was successful. checkoutHead: (path) -> @@ -223,9 +225,9 @@ class Git # Public: Checks out a branch in your repository. # - # reference - The String reference to checkout - # create - A Boolean value which, if true creates the new reference if it - # doesn't exist. + # * `reference` The {String} reference to checkout. + # * `create` A {Boolean} value which, if true creates the new reference if + # it doesn't exist. # # Returns a Boolean that's true if the method was successful. checkoutReference: (reference, create) -> @@ -236,18 +238,18 @@ class Git # This compares the working directory contents of the path to the `HEAD` # version. # - # path - The {String} path to check. + # * `path` The {String} path to check. # # Returns an {Object} with the following keys: - # :added - The {Number} of added lines. - # :deleted - The {Number} of deleted lines. + # * `added` The {Number} of added lines. + # * `deleted` The {Number} of deleted lines. getDiffStats: (path) -> repo = @getRepo(path) repo.getDiffStats(repo.relativize(path)) # Public: Is the given path a submodule in the repository? # - # path - The {String} path to check. + # * `path` The {String} path to check. # # Returns a {Boolean}. isSubmodule: (path) -> @@ -262,7 +264,7 @@ class Git # Public: Get the status of a directory in the repository's working directory. # - # path - The {String} path to check. + # * `path` The {String} path to check. # # Returns a {Number} representing the status. This value can be passed to # {::isStatusModified} or {::isStatusNew} to get more information. @@ -276,14 +278,14 @@ class Git # Public: Retrieves the line diffs comparing the `HEAD` version of the given # path and the given text. # - # path - The {String} path relative to the repository. - # text - The {String} to compare against the `HEAD` contents + # * `path` The {String} path relative to the repository. + # * `text` The {String} to compare against the `HEAD` contents # # Returns an {Array} of hunk {Object}s with the following keys: - # :oldStart - The line {Number} of the old hunk. - # :newStart - The line {Number} of the new hunk. - # :oldLines - The {Number} of lines in the old hunk. - # :newLines - The {Number} of lines in the new hunk + # * `oldStart` The line {Number} of the old hunk. + # * `newStart` The line {Number} of the new hunk. + # * `oldLines` The {Number} of lines in the old hunk. + # * `newLines` The {Number} of lines in the new hunk getLineDiffs: (path, text) -> # Ignore eol of line differences on windows so that files checked in as # LF don't report every line modified when the text contains CRLF endings. @@ -293,68 +295,68 @@ class Git # Public: Returns the git configuration value specified by the key. # - # path - An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) # Public: Returns the origin url of the repository. # - # path - An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. getOriginUrl: (path) -> @getConfigValue('remote.origin.url', path) # Public: Returns the upstream branch for the current HEAD, or null if there # is no upstream branch for the current HEAD. # - # path - An optional {String} path in the repo to get this information for, - # only needed if the repository contains submodules. + # * `path` An optional {String} path in the repo to get this information for, + # only needed if the repository contains submodules. # # Returns a {String} branch name such as `refs/remotes/origin/master`. getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() - # Public: Returns the current SHA for the given reference. + # Public: Returns the current {String} SHA for the given reference. # - # reference - The {String} reference to get the target of. - # path - An optional {String} path in the repo to get the reference target - # for. Only needed if the repository contains submodules. + # * `reference` The {String} reference to get the target of. + # * `path` An optional {String} path in the repo to get the reference target + # for. Only needed if the repository contains submodules. getReferenceTarget: (reference, path) -> @getRepo(path).getReferenceTarget(reference) # Public: Gets all the local and remote references. # - # path - An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. # # Returns an {Object} with the following keys: - # :heads - An {Array} of head reference names. - # :remotes - An {Array} of remote reference names. - # :tags - An {Array} of tag reference names. + # * `heads` An {Array} of head reference names. + # * `remotes` An {Array} of remote reference names. + # * `tags` An {Array} of tag reference names. getReferences: (path) -> @getRepo(path).getReferences() # Public: Returns the number of commits behind the current branch is from the # its upstream remote branch. # - # reference - The {String} branch reference name. - # path - The {String} path in the repository to get this information for, - # only needed if the repository contains submodules. + # * `reference` The {String} branch reference name. + # * `path` The {String} path in the repository to get this information for, + # only needed if the repository contains submodules. getAheadBehindCount: (reference, path) -> @getRepo(path).getAheadBehindCount(reference) # Public: Get the cached ahead/behind commit counts for the current branch's # upstream branch. # - # path - An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. # # Returns an {Object} with the following keys: - # :ahead - The {Number} of commits ahead. - # :behind - The {Number} of commits behind. + # * `ahead` The {Number} of commits ahead. + # * `behind` The {Number} of commits behind. getCachedUpstreamAheadBehindCount: (path) -> @getRepo(path).upstream ? @upstream # Public: Get the cached status for the given path. # - # path - A {String} path in the repository, relative or absolute. + # * `path` A {String} path in the repository, relative or absolute. # # Returns a status {Number} or null if the path is not in the cache. getCachedPathStatus: (path) -> diff --git a/src/gutter-view.coffee b/src/gutter-view.coffee deleted file mode 100644 index 088d3740f..000000000 --- a/src/gutter-view.coffee +++ /dev/null @@ -1,275 +0,0 @@ -{View, $, $$, $$$} = require './space-pen-extensions' -{Range} = require 'text-buffer' -_ = require 'underscore-plus' - -# Represents the portion of the {EditorView} containing row numbers. -# -# The gutter also indicates if rows are folded. -module.exports = -class GutterView extends View - @content: -> - @div class: 'gutter', => - @div outlet: 'lineNumbers', class: 'line-numbers' - - firstScreenRow: null - lastScreenRow: null - - initialize: -> - @elementBuilder = document.createElement('div') - - afterAttach: (onDom) -> - return if @attached or not onDom - @attached = true - - highlightLines = => @highlightLines() - @getEditorView().on 'cursor:moved', highlightLines - @getEditorView().on 'selection:changed', highlightLines - @on 'mousedown', (e) => @handleMouseEvents(e) - - beforeRemove: -> - $(document).off(".gutter-#{@getEditorView().id}") - - handleMouseEvents: (e) -> - editorView = @getEditorView() - editor = @getEditor() - startRow = editorView.screenPositionFromMouseEvent(e).row - if e.shiftKey - editor.selectToScreenPosition([startRow + 1, 0]) - return - else - editor.getSelection().setScreenRange([[startRow, 0], [startRow, 0]]) - - moveHandler = (e) -> - start = startRow - end = editorView.screenPositionFromMouseEvent(e).row - if end > start then end++ else start++ - editor.getSelection().setScreenRange([[start, 0], [end, 0]]) - - $(document).on "mousemove.gutter-#{editorView.id}", moveHandler - $(document).one "mouseup.gutter-#{editorView.id}", -> $(document).off 'mousemove', moveHandler - - # Retrieves the containing {EditorView}. - # - # Returns an {EditorView}. - getEditorView: -> - @parentView - - getEditor: -> - @getEditorView().getEditor() - - # Defines whether to show the gutter or not. - # - # showLineNumbers - A {Boolean} which, if `false`, hides the gutter - setShowLineNumbers: (showLineNumbers) -> - if showLineNumbers then @lineNumbers.show() else @lineNumbers.hide() - - # Get all the line-number divs. - # - # Returns a list of {HTMLElement}s. - getLineNumberElements: -> - @lineNumbers[0].children - - # Get all the line-number divs. - # - # Returns a list of {HTMLElement}s. - getLineNumberElementsForClass: (klass) -> - @lineNumbers[0].getElementsByClassName(klass) - - # Get a single line-number div. - # - # * bufferRow: 0 based line number - # - # Returns a list of {HTMLElement}s that correspond to the bufferRow. More than - # one in the list indicates a wrapped line. - getLineNumberElement: (bufferRow) -> - @getLineNumberElementsForClass("line-number-#{bufferRow}") - - # Add a class to all line-number divs. - # - # * klass: string class name - # - # Returns true if the class was added to any lines - addClassToAllLines: (klass) -> - elements = @getLineNumberElements() - el.classList.add(klass) for el in elements - !!elements.length - - # Remove a class from all line-number divs. - # - # * klass: string class name. Can only be one class name. i.e. 'my-class' - # - # Returns true if the class was removed from any lines - removeClassFromAllLines: (klass) -> - # This is faster than calling $.removeClass on all lines, and faster than - # making a new array and iterating through it. - elements = @getLineNumberElementsForClass(klass) - willRemoveClasses = !!elements.length - elements[0].classList.remove(klass) while elements.length > 0 - willRemoveClasses - - # Add a class to a single line-number div - # - # * bufferRow: 0 based line number - # * klass: string class name - # - # Returns true if there were lines the class was added to - addClassToLine: (bufferRow, klass) -> - elements = @getLineNumberElement(bufferRow) - el.classList.add(klass) for el in elements - !!elements.length - - # Remove a class from a single line-number div - # - # * bufferRow: 0 based line number - # * klass: string class name - # - # Returns true if there were lines the class was removed from - removeClassFromLine: (bufferRow, klass) -> - classesRemoved = false - elements = @getLineNumberElement(bufferRow) - for el in elements - hasClass = el.classList.contains(klass) - classesRemoved |= hasClass - el.classList.remove(klass) if hasClass - classesRemoved - - updateLineNumbers: (changes, startScreenRow, endScreenRow) -> - # Check if we have something already rendered that overlaps the requested range - updateAllLines = not (startScreenRow? and endScreenRow?) - updateAllLines |= endScreenRow <= @firstScreenRow or startScreenRow >= @lastScreenRow - - unless updateAllLines - for change in changes - if change.screenDelta or change.bufferDelta - updateAllLines = true - break - - # Rebuild the entire gutter if a change added or removed lines - if updateAllLines - @lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow) - - # Handle changes that didn't add/remove lines more optimally - else - if startScreenRow < @firstScreenRow - @prependLineElements(@buildLineElements(startScreenRow, @firstScreenRow-1)) - else if startScreenRow != @firstScreenRow - @removeLineElements(startScreenRow - @firstScreenRow) - - if endScreenRow > @lastScreenRow - @appendLineElements(@buildLineElements(@lastScreenRow+1, endScreenRow)) - else if endScreenRow != @lastScreenRow - @removeLineElements(endScreenRow - @lastScreenRow) - - @updateFoldableClasses(changes) - - @firstScreenRow = startScreenRow - @lastScreenRow = endScreenRow - @highlightedRows = null - @highlightLines() - - prependLineElements: (lineElements) -> - anchor = @lineNumbers[0].children[0] - return appendLineElements(lineElements) unless anchor? - @lineNumbers[0].insertBefore(lineElements[0], anchor) while lineElements.length > 0 - null # defeat coffeescript array return - - appendLineElements: (lineElements) -> - @lineNumbers[0].appendChild(lineElements[0]) while lineElements.length > 0 - null # defeat coffeescript array return - - removeLineElements: (numberOfElements) -> - children = @getLineNumberElements() - - # children is a live NodeList, so remove from the desired end {numberOfElements} times - if numberOfElements < 0 - @lineNumbers[0].removeChild(children[children.length-1]) while numberOfElements++ - else if numberOfElements > 0 - @lineNumbers[0].removeChild(children[0]) while numberOfElements-- - - null # defeat coffeescript array return - - buildLineElements: (startScreenRow, endScreenRow) -> - @elementBuilder.innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow) - @elementBuilder.children - - buildLineElementsHtml: (startScreenRow, endScreenRow) => - editor = @getEditor() - maxDigits = editor.getLineCount().toString().length - rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) - - html = '' - for row in rows - if row is lastRow - rowValue = '•' - else - rowValue = (row + 1).toString() - - classes = "line-number line-number-#{row}" - classes += ' foldable' if row isnt lastRow and editor.isFoldableAtBufferRow(row) - classes += ' folded' if editor.isFoldedAtBufferRow(row) - - rowValuePadding = _.multiplyString(' ', maxDigits - rowValue.length) - - html += """
#{rowValuePadding}#{rowValue}
""" - - lastRow = row - - html - - # Called to update the 'foldable' class of line numbers when there's - # a change to the display buffer that doesn't regenerate all the line numbers - # anyway. - updateFoldableClasses: (changes) -> - editor = @getEditor() - for {start, end} in changes when start <= @lastScreenRow and end >= @firstScreenRow - startScreenRow = Math.max(start - 1, @firstScreenRow) - endScreenRow = Math.min(end + 1, @lastScreenRow) - lastBufferRow = null - for bufferRow in editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) when bufferRow isnt lastBufferRow - lastBufferRow = bufferRow - if lineNumberElement = @getLineNumberElement(bufferRow)[0] - if editor.isFoldableAtBufferRow(bufferRow) - lineNumberElement.classList.add('foldable') - else - lineNumberElement.classList.remove('foldable') - - removeLineHighlights: -> - return unless @highlightedLineNumbers - for line in @highlightedLineNumbers - line.classList.remove('cursor-line') - line.classList.remove('cursor-line-no-selection') - @highlightedLineNumbers = null - - addLineHighlight: (row, emptySelection) -> - return if row < @firstScreenRow or row > @lastScreenRow - @highlightedLineNumbers ?= [] - if highlightedLineNumber = @lineNumbers[0].children[row - @firstScreenRow] - highlightedLineNumber.classList.add('cursor-line') - highlightedLineNumber.classList.add('cursor-line-no-selection') if emptySelection - @highlightedLineNumbers.push(highlightedLineNumber) - - highlightLines: -> - editor = @getEditor() - return unless editor?.isAlive() - - if editor.getSelection().isEmpty() - row = editor.getCursorScreenPosition().row - rowRange = new Range([row, 0], [row, 0]) - return if @selectionEmpty and @highlightedRows?.isEqual(rowRange) - - @removeLineHighlights() - @addLineHighlight(row, true) - @highlightedRows = rowRange - @selectionEmpty = true - else - selectedRows = editor.getSelection().getScreenRange() - endRow = selectedRows.end.row - endRow-- if selectedRows.end.column is 0 - selectedRows = new Range([selectedRows.start.row, 0], [endRow, 0]) - return if not @selectionEmpty and @highlightedRows?.isEqual(selectedRows) - - @removeLineHighlights() - for row in [selectedRows.start.row..selectedRows.end.row] - @addLineHighlight(row, false) - @highlightedRows = selectedRows - @selectionEmpty = false diff --git a/src/lines-component.coffee b/src/lines-component.coffee index e960be282..8ccc6dcdd 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -91,12 +91,11 @@ LinesComponent = React.createClass @lineIdsByScreenRow = {} updateLines: (updateWidth) -> - {editor, renderedRowRange, showIndentGuide, selectionChanged, lineDecorations} = @props - [startRow, endRow] = renderedRowRange + {tokenizedLines, renderedRowRange, showIndentGuide, selectionChanged, lineDecorations} = @props + [startRow] = renderedRowRange - visibleLines = editor.linesForScreenRows(startRow, endRow - 1) - @removeLineNodes(visibleLines) - @appendOrUpdateVisibleLineNodes(visibleLines, startRow, updateWidth) + @removeLineNodes(tokenizedLines) + @appendOrUpdateVisibleLineNodes(tokenizedLines, startRow, updateWidth) removeLineNodes: (visibleLines=[]) -> {mouseWheelScreenRow} = @props @@ -147,7 +146,7 @@ LinesComponent = React.createClass @lineNodesByLineId.hasOwnProperty(lineId) buildLineHTML: (line, screenRow) -> - {editor, mini, showIndentGuide, lineHeightInPixels, lineDecorations, lineWidth} = @props + {mini, showIndentGuide, lineHeightInPixels, lineDecorations, lineWidth} = @props {tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line classes = '' @@ -244,7 +243,7 @@ LinesComponent = React.createClass "" updateLineNode: (line, screenRow, updateWidth) -> - {editor, lineHeightInPixels, lineDecorations, lineWidth} = @props + {lineHeightInPixels, lineDecorations, lineWidth} = @props lineNode = @lineNodesByLineId[line.id] decorations = lineDecorations[screenRow] @@ -292,12 +291,12 @@ LinesComponent = React.createClass @measureCharactersInNewLines() measureCharactersInNewLines: -> - {editor, renderedRowRange} = @props - [visibleStartRow, visibleEndRow] = renderedRowRange + {editor, tokenizedLines, renderedRowRange} = @props + [visibleStartRow] = renderedRowRange node = @getDOMNode() editor.batchCharacterMeasurement => - for tokenizedLine in editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) + for tokenizedLine in tokenizedLines unless @measuredLines.has(tokenizedLine) lineNode = @lineNodesByLineId[tokenizedLine.id] @measureCharactersInLine(tokenizedLine, lineNode) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index e1c1743d4..b9f64a2fc 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -17,9 +17,9 @@ class MenuManager atom.keymaps.on 'bundled-keymaps-loaded', => @loadPlatformItems() atom.packages.on 'activated', => @sortPackagesMenu() - # Public: Adds the given item definition to the existing template. + # Public: Adds the given items to the application menu. # - # ## Example + # ## Examples # ```coffee # atom.menu.add [ # { @@ -29,23 +29,22 @@ class MenuManager # ] # ``` # - # items - An {Array} of menu item {Object}s containing the keys: - # :label - The {String} menu label. - # :submenu - An optional {Array} of sub menu items. - # :command - An optional {String} command to trigger when the item is - # clicked. - # - # Returns nothing. + # * `items` An {Array} of menu item {Object}s containing the keys: + # * `label` The {String} menu label. + # * `submenu` An optional {Array} of sub menu items. + # * `command` An optional {String} command to trigger when the item is + # clicked. add: (items) -> @merge(@template, item) for item in items @update() + undefined # Should the binding for the given selector be included in the menu # commands. # - # selector - A {String} selector to check. + # * `selector` A {String} selector to check. # - # Returns true to include the selector, false otherwise. + # Returns a {Boolean}, true to include the selector, false otherwise. includeSelector: (selector) -> try return true if document.body.webkitMatchesSelector(selector) diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 0a4e2457b..4935804dd 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -21,7 +21,7 @@ ThemePackage = require './theme-package' # `deactivate()` on the package's main module. # * Unloading a package removes it completely from the package manager. # -# Packages can also be enabled/disabled via the `core.disabledPackages` config +# Packages can be enabled/disabled via the `core.disabledPackages` config # settings and also by calling `enablePackage()/disablePackage()`. module.exports = class PackageManager @@ -41,15 +41,17 @@ class PackageManager @packageActivators = [] @registerPackageActivator(this, ['atom', 'textmate']) - # Public: Get the path to the apm command + # Extended: Get the path to the apm command. + # + # Return a {String} file path to apm. getApmPath: -> commandName = 'apm' commandName += '.cmd' if process.platform is 'win32' @apmPath ?= path.resolve(__dirname, '..', 'apm', 'node_modules', 'atom-package-manager', 'bin', commandName) - # Public: Get the paths being used to look for packages. + # Extended: Get the paths being used to look for packages. # - # Returns an Array of String directory paths. + # Returns an {Array} of {String} directory paths. getPackageDirPaths: -> _.clone(@packageDirPaths) @@ -59,13 +61,17 @@ class PackageManager setPackageState: (name, state) -> @packageStates[name] = state - # Public: Enable the package with the given name + # Extended: Enable the package with the given name. + # + # Returns the {Package} that was enabled or null if it isn't loaded. enablePackage: (name) -> pack = @loadPackage(name) pack?.enable() pack - # Public: Disable the package with the given name + # Extended: Disable the package with the given name. + # + # Returns the {Package} that was disabled or null if it isn't loaded. disablePackage: (name) -> pack = @loadPackage(name) pack?.disable() @@ -110,15 +116,23 @@ class PackageManager pack.deactivate() delete @activePackages[pack.name] - # Public: Get an array of all the active packages + # Essential: Get an {Array} of all the active {Package}s. getActivePackages: -> _.values(@activePackages) - # Public: Get the active package with the given name + # Essential: Get the active {Package} with the given name. + # + # * `name` - The {String} package name. + # + # Returns a {Package} or undefined. getActivePackage: (name) -> @activePackages[name] - # Public: Is the package with the given name active? + # Public: Is the {Package} with the given name active? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. isPackageActive: (name) -> @getActivePackage(name)? @@ -179,25 +193,37 @@ class PackageManager else throw new Error("No loaded package for name '#{name}'") - # Public: Get the loaded package with the given name + # Essential: Get the loaded {Package} with the given name. + # + # * `name` - The {String} package name. + # + # Returns a {Package} or undefined. getLoadedPackage: (name) -> @loadedPackages[name] - # Public: Is the package with the given name loaded? + # Essential: Is the package with the given name loaded? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. isPackageLoaded: (name) -> @getLoadedPackage(name)? - # Public: Get an array of all the loaded packages + # Essential: Get an {Array} of all the loaded {Package}s getLoadedPackages: -> _.values(@loadedPackages) # Get packages for a certain package type # - # types - an {Array} of {String}s like ['atom', 'textmate']. + # * `types` an {Array} of {String}s like ['atom', 'textmate']. getLoadedPackagesForTypes: (types) -> pack for pack in @getLoadedPackages() when pack.getType() in types - # Public: Resolve the given package name to a path on disk. + # Extended: Resolve the given package name to a path on disk. + # + # * `name` - The {String} package name. + # + # Return a {String} folder path or undefined if it could not be resolved. resolvePackagePath: (name) -> return name if fs.isDirectorySync(name) @@ -207,7 +233,11 @@ class PackageManager packagePath = path.join(@resourcePath, 'node_modules', name) return packagePath if @hasAtomEngine(packagePath) - # Public: Is the package with the given name disabled? + # Essential: Is the package with the given name disabled? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. isPackageDisabled: (name) -> _.include(atom.config.get('core.disabledPackages') ? [], name) @@ -215,7 +245,11 @@ class PackageManager metadata = Package.loadMetadata(packagePath, true) metadata?.engines?.atom? - # Public: Is the package with the given name bundled with Atom? + # Extended: Is the package with the given name bundled with Atom? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. isBundledPackage: (name) -> @getPackageDependencies().hasOwnProperty(name) @@ -228,7 +262,7 @@ class PackageManager @packageDependencies - # Public: Get an array of all the available package paths. + # Extended: Get an {Array} of {String}s of all the available package paths. getAvailablePackagePaths: -> packagePaths = [] @@ -243,11 +277,11 @@ class PackageManager _.uniq(packagePaths) - # Public: Get an array of all the available package names. + # Extended: Get an {Array} of {String}s of all the available package names. getAvailablePackageNames: -> _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath) - # Public: Get an array of all the available package metadata. + # Extended: Get an {Array} of {String}s of all the available package metadata. getAvailablePackageMetadata: -> packages = [] for packagePath in @getAvailablePackagePaths() diff --git a/src/pane-view.coffee b/src/pane-view.coffee index d4f663d16..0d641582e 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -5,7 +5,7 @@ PropertyAccessors = require 'property-accessors' Pane = require './pane' -# Public: A container which can contains multiple items to be switched between. +# Extended: A container which can contains multiple items to be switched between. # # Items can be almost anything however most commonly they're {EditorView}s. # diff --git a/src/pane.coffee b/src/pane.coffee index 84947bbb7..678e2156e 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -5,9 +5,43 @@ PaneAxis = require './pane-axis' Editor = require './editor' PaneView = null -# Public: A container for multiple items, one of which is *active* at a given +# Extended: A container for multiple items, one of which is *active* at a given # time. With the default packages, a tab is displayed for each item and the # active item's view is displayed. +# +# ## Events +# ### activated +# +# Extended: Emit when this pane as been activated +# +# ### item-added +# +# Extended: Emit when an item was added to the pane +# +# * `item` The pane item that has been added +# * `index` {Number} Index in the pane +# +# ### before-item-destroyed +# +# Extended: Emit before the item is destroyed +# +# * `item` The pane item that will be destoryed +# +# ### item-removed +# +# Extended: Emit when the item was removed from the pane +# +# * `item` The pane item that was removed +# * `index` {Number} Index in the pane +# * `destroying` {Boolean} `true` when the item is being removed because of destruction +# +# ### item-moved +# +# Extended: Emit when an item was moved within the pane +# +# * `item` The pane item that was moved +# * `newIndex` {Number} Index that the item was moved to +# module.exports = class Pane extends Model atom.deserializers.add(this) @@ -130,9 +164,9 @@ class Pane extends Model # Public: Adds the item to the pane. # - # item - The item to add. It can be a model with an associated view or a view. - # index - An optional index at which to add the item. If omitted, the item is - # added after the current active item. + # * `item` The item to add. It can be a model with an associated view or a view. + # * `index` (optional) {Number} at which to add the item. If omitted, the item is + # added after the current active item. # # Returns the added item addItem: (item, index=@getActiveItemIndex() + 1) -> @@ -145,11 +179,11 @@ class Pane extends Model # Public: Adds the given items to the pane. # - # items - An {Array} of items to add. Items can be models with associated - # views or views. Any items that are already present in items will - # not be added. - # index - An optional index at which to add the item. If omitted, the item is - # added after the current active item. + # * `items` An {Array} of items to add. Items can be models with associated + # views or views. Any items that are already present in items will + # not be added. + # * `index` (optional) {Number} index at which to add the item. If omitted, the item is + # added after the current active item. # # Returns an {Array} of the added items addItems: (items, index=@getActiveItemIndex() + 1) -> @@ -246,9 +280,8 @@ class Pane extends Model # Public: Saves the specified item. # - # item - The item to save. - # nextAction - An optional function which will be called after the item is - # saved. + # * `item` The item to save. + # * `nextAction` (optional) {Function} which will be called after the item is saved. saveItem: (item, nextAction) -> if item?.getUri?() item.save?() @@ -258,9 +291,8 @@ class Pane extends Model # Public: Saves the given item at a prompted-for location. # - # item - The item to save. - # nextAction - An optional function which will be called after the item is - # saved. + # * `item` The item to save. + # * `nextAction` (optional) {Function} which will be called after the item is saved. saveItemAs: (item, nextAction) -> return unless item?.saveAs? @@ -294,8 +326,8 @@ class Pane extends Model # Public: Creates a new pane to the left of the receiver. # - # params - An object with keys: - # :items - An optional array of items with which to construct the new pane. + # * `params` {Object} with keys + # * `items` (optional) {Array} of items with which to construct the new pane. # # Returns the new {Pane}. splitLeft: (params) -> @@ -303,8 +335,8 @@ class Pane extends Model # Public: Creates a new pane to the right of the receiver. # - # params - An object with keys: - # :items - An optional array of items with which to construct the new pane. + # * `params` {Object} with keys: + # * `items` (optional) {Array} of items with which to construct the new pane. # # Returns the new {Pane}. splitRight: (params) -> @@ -312,8 +344,8 @@ class Pane extends Model # Public: Creates a new pane above the receiver. # - # params - An object with keys: - # :items - An optional array of items with which to construct the new pane. + # * `params` {Object} with keys: + # * `items` (optional) {Array} of items with which to construct the new pane. # # Returns the new {Pane}. splitUp: (params) -> @@ -321,8 +353,8 @@ class Pane extends Model # Public: Creates a new pane below the receiver. # - # params - An object with keys: - # :items - An optional array of items with which to construct the new pane. + # * `params` {Object} with keys: + # * `items` (optional) {Array} of items with which to construct the new pane. # # Returns the new {Pane}. splitDown: (params) -> diff --git a/src/project.coffee b/src/project.coffee index 5943e319a..c0b81ddd0 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -15,15 +15,30 @@ Editor = require './editor' Task = require './task' Git = require './git' -# Public: Represents a project that's opened in Atom. +# Extended: Represents a project that's opened in Atom. # # An instance of this class is always available as the `atom.project` global. +# +# ## Events +# +# ### path-changed +# +# Extended: Emit when the project's path has changed. Use {::getPath} to get the new path +# +# ### buffer-created +# +# Extended: Emit when a buffer is created. For example, when {::open} is called, this is fired. +# +# * `buffer` {TextBuffer} the new buffer that was created. +# module.exports = class Project extends Model atom.deserializers.add(this) Serializable.includeInto(this) # Public: Find the local path for the given repository URL. + # + # * `repoUrl` {String} url to a git repository @pathForRepositoryUrl: (repoUrl) -> [repoName] = url.parse(repoUrl).path.split('/')[-1..] repoName = repoName.replace(/\.git$/, '') @@ -61,11 +76,13 @@ class Project extends Model # Public: Returns the {Git} repository if available. getRepo: -> @repo - # Public: Returns the project's fullpath. + # Public: Returns the project's {String} fullpath. getPath: -> @rootDirectory?.path # Public: Sets the project's fullpath. + # + # * `projectPath` {String} path setPath: (projectPath) -> @path = projectPath @rootDirectory?.off() @@ -90,7 +107,7 @@ class Project extends Model # the path is already absolute or if it is prefixed with a scheme, it is # returned unchanged. # - # uri - The {String} name of the path to convert. + # * `uri` The {String} name of the path to convert. # # Returns a {String} or undefined if the uri is not missing or empty. resolve: (uri) -> @@ -100,26 +117,30 @@ class Project extends Model uri else if fs.isAbsolute(uri) - fs.absolute(uri) + path.normalize(fs.absolute(uri)) else if projectPath = @getPath() - fs.absolute(path.join(projectPath, uri)) + path.normalize(fs.absolute(path.join(projectPath, uri))) else undefined # Public: Make the given path relative to the project directory. + # + # * `fullPath` {String} full path relativize: (fullPath) -> return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme @rootDirectory?.relativize(fullPath) ? fullPath # Public: Returns whether the given path is inside this project. + # + # * `pathToCheck` {String} path contains: (pathToCheck) -> @rootDirectory?.contains(pathToCheck) ? false # Given a path to a file, this constructs and associates a new # {Editor}, showing the file. # - # filePath - The {String} path of the file to associate with. - # options - Options that you can pass to the {Editor} constructor. + # * `filePath` The {String} path of the file to associate with. + # * `options` Options that you can pass to the {Editor} constructor. # # Returns a promise that resolves to an {Editor}. open: (filePath, options={}) -> @@ -158,7 +179,7 @@ class Project extends Model # If the `filePath` already has a `buffer`, that value is used instead. Otherwise, # `text` is used as the contents of the new buffer. # - # filePath - A {String} representing a path. If `null`, an "Untitled" buffer is created. + # * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. # # Returns a promise that resolves to the {TextBuffer}. bufferForPath: (filePath) -> @@ -178,8 +199,8 @@ class Project extends Model # Given a file path, this sets its {TextBuffer}. # - # absoluteFilePath - A {String} representing a path. - # text - The {String} text to use as a buffer. + # * `absoluteFilePath` A {String} representing a path. + # * `text` The {String} text to use as a buffer. # # Returns a promise that resolves to the {TextBuffer}. buildBuffer: (absoluteFilePath) -> @@ -215,10 +236,10 @@ class Project extends Model # Public: Performs a search across all the files in the project. # - # regex - A {RegExp} to search with. - # options - An optional options {Object} (default: {}): - # :paths - An {Array} of glob patterns to search within - # iterator - A {Function} callback on each file found + # * `regex` {RegExp} to search with. + # * `options` (optional) {Object} (default: {}) + # * `paths` An {Array} of glob patterns to search within + # * `iterator` {Function} callback on each file found scan: (regex, options={}, iterator) -> if _.isFunction(options) iterator = options @@ -261,11 +282,11 @@ class Project extends Model # Public: Performs a replace across all the specified files in the project. # - # regex - A {RegExp} to search with. - # replacementText - Text to replace all matches of regex with - # filePaths - List of file path strings to run the replace on. - # iterator - A {Function} callback on each file with replacements: - # `({filePath, replacements}) ->`. + # * `regex` A {RegExp} to search with. + # * `replacementText` Text to replace all matches of regex with + # * `filePaths` List of file path strings to run the replace on. + # * `iterator` A {Function} callback on each file with replacements: + # * `options` {Object} with keys `filePath` and `replacements` replace: (regex, replacementText, filePaths, iterator) -> deferred = Q.defer() diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee deleted file mode 100644 index 3b166cbbf..000000000 --- a/src/react-editor-view.coffee +++ /dev/null @@ -1,244 +0,0 @@ -{View, $} = require 'space-pen' -React = require 'react-atom-fork' -{defaults} = require 'underscore-plus' -TextBuffer = require 'text-buffer' -Editor = require './editor' -EditorComponent = require './editor-component' - -module.exports = -class ReactEditorView extends View - @content: (params) -> - attributes = params.attributes ? {} - attributes.class = 'editor react editor-colors' - attributes.tabIndex = -1 - @div attributes - - focusOnAttach: false - - constructor: (editorOrParams, props) -> - super - - if editorOrParams instanceof Editor - @editor = editorOrParams - else - {@editor, mini, placeholderText} = editorOrParams - props ?= {} - props.mini = mini - props.placeholderText = placeholderText - @editor ?= new Editor - buffer: new TextBuffer - softWrap: false - tabLength: 2 - softTabs: true - mini: mini - - props = defaults({@editor, parentView: this}, props) - @component = React.renderComponent(EditorComponent(props), @element) - - node = @component.getDOMNode() - - @scrollView = $(node).find('.scroll-view') - @underlayer = $(node).find('.highlights').addClass('underlayer') - @overlayer = $(node).find('.lines').addClass('overlayer') - @hiddenInput = $(node).find('.hidden-input') - - # FIXME: there should be a better way to deal with the gutter element - @subscribe atom.config.observe 'editor.showLineNumbers', => - @gutter = $(node).find('.gutter') - - @gutter.removeClassFromAllLines = (klass) => - @gutter.find('.line-number').removeClass(klass) - - @gutter.getLineNumberElement = (bufferRow) => - @gutter.find("[data-buffer-row='#{bufferRow}']") - - @gutter.addClassToLine = (bufferRow, klass) => - lines = @gutter.find("[data-buffer-row='#{bufferRow}']") - lines.addClass(klass) - lines.length > 0 - - @on 'focus', => - if @component? - @component.onFocus() - else - @focusOnAttach = true - - getEditor: -> @editor - - getModel: -> @editor - - Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels() - Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth() - Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0] - Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1] - Object.defineProperty @::, 'active', get: -> @is(@getPane()?.activeView) - Object.defineProperty @::, 'isFocused', get: -> @component?.state.focused - Object.defineProperty @::, 'mini', get: -> @component?.props.mini - - afterAttach: (onDom) -> - return unless onDom - return if @attached - @attached = true - @component.pollDOM() - @focus() if @focusOnAttach - @trigger 'editor:attached', [this] - - scrollTop: (scrollTop) -> - if scrollTop? - @editor.setScrollTop(scrollTop) - else - @editor.getScrollTop() - - scrollLeft: (scrollLeft) -> - if scrollLeft? - @editor.setScrollLeft(scrollLeft) - else - @editor.getScrollLeft() - - scrollToBottom: -> - @editor.setScrollBottom(Infinity) - - scrollToScreenPosition: (screenPosition, options) -> - @editor.scrollToScreenPosition(screenPosition, options) - - scrollToBufferPosition: (bufferPosition, options) -> - @editor.scrollToBufferPosition(bufferPosition, options) - - scrollToCursorPosition: -> - @editor.scrollToCursorPosition() - - scrollToPixelPosition: (pixelPosition) -> - screenPosition = screenPositionForPixelPosition(pixelPosition) - @editor.scrollToScreenPosition(screenPosition) - - pixelPositionForBufferPosition: (bufferPosition) -> - @editor.pixelPositionForBufferPosition(bufferPosition) - - pixelPositionForScreenPosition: (screenPosition) -> - @editor.pixelPositionForScreenPosition(screenPosition) - - appendToLinesView: (view) -> - view.css('position', 'absolute') - view.css('z-index', 1) - @find('.lines').prepend(view) - - beforeRemove: -> - return unless @attached - @attached = false - React.unmountComponentAtNode(@element) if @component.isMounted() - @trigger 'editor:detached', this - - # Public: Split the editor view left. - splitLeft: -> - pane = @getPane() - pane?.splitLeft(pane?.copyActiveItem()).activeView - - # Public: Split the editor view right. - splitRight: -> - pane = @getPane() - pane?.splitRight(pane?.copyActiveItem()).activeView - - # Public: Split the editor view up. - splitUp: -> - pane = @getPane() - pane?.splitUp(pane?.copyActiveItem()).activeView - - # Public: Split the editor view down. - splitDown: -> - pane = @getPane() - pane?.splitDown(pane?.copyActiveItem()).activeView - - getPane: -> - @parent('.item-views').parents('.pane').view() - - hide: -> - super - @pollComponentDOM() - - show: -> - super - @pollComponentDOM() - - pollComponentDOM: -> - return unless @component? - valueToRestore = @component.performSyncUpdates - @component.performSyncUpdates = true - @component.pollDOM() - @component.performSyncUpdates = valueToRestore - - pageDown: -> - @editor.pageDown() - - pageUp: -> - @editor.pageUp() - - getFirstVisibleScreenRow: -> - @editor.getVisibleRowRange()[0] - - getLastVisibleScreenRow: -> - @editor.getVisibleRowRange()[1] - - getFontFamily: -> - @component?.getFontFamily() - - setFontFamily: (fontFamily) -> - @component?.setFontFamily(fontFamily) - - getFontSize: -> - @component?.getFontSize() - - setFontSize: (fontSize) -> - @component?.setFontSize(fontSize) - - setWidthInChars: (widthInChars) -> - @component.getDOMNode().style.width = (@editor.getDefaultCharWidth() * widthInChars) + 'px' - - setLineHeight: (lineHeight) -> - @component.setLineHeight(lineHeight) - - setShowIndentGuide: (showIndentGuide) -> - @component.setShowIndentGuide(showIndentGuide) - - setSoftWrap: (softWrap) -> - @editor.setSoftWrap(softWrap) - - setShowInvisibles: (showInvisibles) -> - @component.setShowInvisibles(showInvisibles) - - toggleSoftWrap: -> - @editor.toggleSoftWrap() - - toggleSoftTabs: -> - @editor.toggleSoftTabs() - - getText: -> - @editor.getText() - - setText: (text) -> - @editor.setText(text) - - insertText: (text) -> - @editor.insertText(text) - - isInputEnabled: -> - @component.isInputEnabled() - - setInputEnabled: (inputEnabled) -> - @component.setInputEnabled(inputEnabled) - - requestDisplayUpdate: -> # No-op shim for find-and-replace - - updateDisplay: -> # No-op shim for package specs - - resetDisplay: -> # No-op shim for package specs - - redraw: -> # No-op shim - - setPlaceholderText: (placeholderText) -> - if @component? - @component.setProps({placeholderText}) - else - @props.placeholderText = placeholderText - - lineElementForScreenRow: (screenRow) -> - $(@component.lineNodeForScreenRow(screenRow)) diff --git a/src/scroll-view.coffee b/src/scroll-view.coffee index dc210494d..10e760e1b 100644 --- a/src/scroll-view.coffee +++ b/src/scroll-view.coffee @@ -2,22 +2,31 @@ # Public: Represents a view that scrolls. # -# Subclasses must call `super` if overriding the `initialize` method or else -# the following events won't be handled by the ScrollView. +# Handles several core events to update scroll position: # -# ## Events -# * `core:move-up` -# * `core:move-down` -# * `core:page-up` -# * `core:page-down` -# * `core:move-to-top` -# * `core:move-to-bottom` +# * `core:move-up` Scrolls the view up +# * `core:move-down` Scrolls the view down +# * `core:page-up` Scrolls the view up by the height of the page +# * `core:page-down` Scrolls the view down by the height of the page +# * `core:move-to-top` Scrolls the editor to the top +# * `core:move-to-bottom` Scroll the editor to the bottom # -# ## Requiring in packages +# Subclasses must call `super` if overriding the `initialize` method. +# +# ## Examples # # ```coffee -# {ScrollView} = require 'atom' +# {ScrollView} = require 'atom' +# +# class MyView extends ScrollView +# @content: -> +# @div() +# +# initialize: -> +# super +# @text('super long content that will scroll') # ``` +# module.exports = class ScrollView extends View initialize: -> diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index 8776b2757..b9227df18 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -1,11 +1,8 @@ {$, View} = require './space-pen-extensions' -if atom.config.get('core.useReactMiniEditors') - EditorView = require './react-editor-view' -else - EditorView = require './editor-view' +EditorView = require './editor-view' fuzzyFilter = require('fuzzaldrin').filter -# Public: Provides a view that renders a list of items with an editor that +# Essential: Provides a view that renders a list of items with an editor that # filters the items. Used by many packages such as the fuzzy-finder, # command-palette, symbols-view and autocomplete. # @@ -99,14 +96,14 @@ class SelectListView extends View # This should be model items not actual views. {::viewForItem} will be # called to render the item when it is being appended to the list view. # - # items - The {Array} of model items to display in the list (default: []). + # * `items` The {Array} of model items to display in the list (default: []). setItems: (@items=[]) -> @populateList() @setLoading() # Public: Set the error message to display. # - # message - The {String} error message (default: ''). + # * `message` The {String} error message (default: ''). setError: (message='') -> if message.length is 0 @error.text('').hide() @@ -116,7 +113,7 @@ class SelectListView extends View # Public: Set the loading message to display. # - # message - The {String} loading message (default: ''). + # * `message` The {String} loading message (default: ''). setLoading: (message='') -> if message.length is 0 @loading.text("") @@ -168,15 +165,15 @@ class SelectListView extends View # # Subclasses may override this method to customize the message. # - # itemCount - The {Number} of items in the array specified to {::setItems} - # filteredItemCount - The {Number} of items that pass the fuzzy filter test. + # * `itemCount` The {Number} of items in the array specified to {::setItems} + # * `filteredItemCount` The {Number} of items that pass the fuzzy filter test. # # Returns a {String} message (default: 'No matches found'). getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found' # Public: Set the maximum numbers of items to display in the list. # - # maxItems - The maximum {Number} of items to display. + # * `maxItems` The maximum {Number} of items to display. setMaxItems: (@maxItems) -> selectPreviousItemView: -> @@ -227,8 +224,8 @@ class SelectListView extends View # # This is called when the item is about to appended to the list view. # - # item - The model item being rendered. This will always be one of the items - # previously passed to {::setItems}. + # * `item` The model item being rendered. This will always be one of the items + # previously passed to {::setItems}. # # Returns a String of HTML, DOM element, jQuery object, or View. viewForItem: (item) -> @@ -238,8 +235,8 @@ class SelectListView extends View # # This method must be overridden by subclasses. # - # item - The selected model item. This will always be one of the items - # previously passed to {::setItems}. + # * `item` The selected model item. This will always be one of the items + # previously passed to {::setItems}. # # Returns a DOM element, jQuery object, or {View}. confirmed: (item) -> @@ -275,7 +272,6 @@ class SelectListView extends View cancelled: -> @filterEditorView.getEditor().setText('') - @filterEditorView.updateDisplay() # Public: Cancel and close this select list view. # diff --git a/src/selection-view.coffee b/src/selection-view.coffee deleted file mode 100644 index e0413b30c..000000000 --- a/src/selection-view.coffee +++ /dev/null @@ -1,85 +0,0 @@ -{Point, Range} = require 'text-buffer' -{View, $$} = require './space-pen-extensions' - -module.exports = -class SelectionView extends View - - @content: -> - @div class: 'selection' - - regions: null - needsRemoval: false - - initialize: ({@editorView, @selection} = {}) -> - @regions = [] - @selection.on 'screen-range-changed', => @editorView.requestDisplayUpdate() - @selection.on 'destroyed', => - @needsRemoval = true - @editorView.requestDisplayUpdate() - - updateDisplay: -> - @clearRegions() - range = @getScreenRange() - - @trigger 'selection:changed' - @editorView.highlightFoldsContainingBufferRange(@getBufferRange()) - return if range.isEmpty() - - rowSpan = range.end.row - range.start.row - - if rowSpan == 0 - @appendRegion(1, range.start, range.end) - else - @appendRegion(1, range.start, null) - if rowSpan > 1 - @appendRegion(rowSpan - 1, { row: range.start.row + 1, column: 0}, null) - @appendRegion(1, { row: range.end.row, column: 0 }, range.end) - - appendRegion: (rows, start, end) -> - { lineHeight, charWidth } = @editorView - css = @editorView.pixelPositionForScreenPosition(start) - css.height = lineHeight * rows - if end - css.width = @editorView.pixelPositionForScreenPosition(end).left - css.left - else - css.right = 0 - - region = ($$ -> @div class: 'region').css(css) - @append(region) - @regions.push(region) - - getCenterPixelPosition: -> - { start, end } = @getScreenRange() - startRow = start.row - endRow = end.row - endRow-- if end.column == 0 - @editorView.pixelPositionForScreenPosition([((startRow + endRow + 1) / 2), start.column]) - - clearRegions: -> - region.remove() for region in @regions - @regions = [] - - getScreenRange: -> - @selection.getScreenRange() - - getBufferRange: -> - @selection.getBufferRange() - - needsAutoscroll: -> - @selection.needsAutoscroll - - clearAutoscroll: -> - @selection.clearAutoscroll() - - highlight: -> - @unhighlight() - @addClass('highlighted') - clearTimeout(@unhighlightTimeout) - @unhighlightTimeout = setTimeout((=> @unhighlight()), 1000) - - unhighlight: -> - @removeClass('highlighted') - - remove: -> - @editorView.removeSelectionView(this) - super diff --git a/src/selection.coffee b/src/selection.coffee index b182f864d..efbb40592 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -2,7 +2,20 @@ {Model} = require 'theorist' {pick} = require 'underscore-plus' -# Public: Represents a selection in the {Editor}. +# Extended: Represents a selection in the {Editor}. +# +# ## Events +# +# ### screen-range-changed +# +# Extended: Emit when the selection was moved. +# +# * `screenRange` {Range} indicating the new screenrange +# +# ### destroyed +# +# Extended: Emit when the selection was destroyed +# module.exports = class Selection extends Model cursor: null @@ -56,8 +69,8 @@ class Selection extends Model # Public: Modifies the screen range for the selection. # - # screenRange - The new {Range} to use. - # options - A hash of options matching those found in {::setBufferRange}. + # * `screenRange` The new {Range} to use. + # * `options` (optional) {Object} options matching those found in {::setBufferRange}. setScreenRange: (screenRange, options) -> @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options) @@ -67,11 +80,10 @@ class Selection extends Model # Public: Modifies the buffer {Range} for the selection. # - # screenRange - The new {Range} to select. - # options - An {Object} with the keys: - # :preserveFolds - if `true`, the fold settings are preserved after the - # selection moves. - # :autoscroll - if `true`, the {Editor} scrolls to the new selection. + # * `screenRange` The new {Range} to select. + # * `options` (optional) {Object} with the keys: + # * `preserveFolds` if `true`, the fold settings are preserved after the selection moves. + # * `autoscroll` if `true`, the {Editor} scrolls to the new selection. setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) @needsAutoscroll = options.autoscroll @@ -141,7 +153,7 @@ class Selection extends Model # Public: Selects an entire line in the buffer. # - # row - The line {Number} to select (default: the row of the cursor). + # * `row` The line {Number} to select (default: the row of the cursor). selectLine: (row=@cursor.getBufferPosition().row) -> range = @editor.bufferRangeForBufferRow(row, includeNewline: true) @setBufferRange(@getBufferRange().union(range)) @@ -160,7 +172,7 @@ class Selection extends Model # Public: Selects the text from the current cursor position to a given screen # position. # - # position - An instance of {Point}, with a given `row` and `column`. + # * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> position = Point.fromObject(position) @@ -181,7 +193,7 @@ class Selection extends Model # Public: Selects the text from the current cursor position to a given buffer # position. # - # position - An instance of {Point}, with a given `row` and `column`. + # * `position` An instance of {Point}, with a given `row` and `column`. selectToBufferPosition: (position) -> @modifySelection => @cursor.setBufferPosition(position) @@ -306,14 +318,14 @@ class Selection extends Model # Public: Replaces text at the current selection. # - # text - A {String} representing the text to add - # options - An {Object} with keys: - # :select - if `true`, selects the newly added text. - # :autoIndent - if `true`, indents all inserted text appropriately. - # :autoIndentNewline - if `true`, indent newline appropriately. - # :autoDecreaseIndent - if `true`, decreases indent level appropriately - # (for example, when a closing bracket is inserted). - # :undo - if `skip`, skips the undo stack for this operation. + # * `text` A {String} representing the text to add + # * `options` (optional) {Object} with keys: + # * `select` if `true`, selects the newly added text. + # * `autoIndent` if `true`, indents all inserted text appropriately. + # * `autoIndentNewline` if `true`, indent newline appropriately. + # * `autoDecreaseIndent` if `true`, decreases indent level appropriately + # (for example, when a closing bracket is inserted). + # * `undo` if `skip`, skips the undo stack for this operation. insertText: (text, options={}) -> oldBufferRange = @getBufferRange() @editor.unfoldBufferRow(oldBufferRange.end.row) @@ -345,8 +357,8 @@ class Selection extends Model # Public: Indents the given text to the suggested level based on the grammar. # - # text - The {String} to indent within the selection. - # indentBasis - The beginning indent level. + # * `text` The {String} to indent within the selection. + # * `indentBasis` The beginning indent level. normalizeIndents: (text, indentBasis) -> textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) @@ -378,9 +390,9 @@ class Selection extends Model # non-whitespace characters, and otherwise inserts a tab. If the selection is # non empty, calls {::indentSelectedRows}. # - # options - A {Object} with the keys: - # :autoIndent - If `true`, the line is indented to an automatically-inferred - # level. Otherwise, {Editor::getTabText} is inserted. + # * `options` (optional) {Object} with the keys: + # * `autoIndent` If `true`, the line is indented to an automatically-inferred + # level. Otherwise, {Editor::getTabText} is inserted. indent: ({ autoIndent }={}) -> { row, column } = @cursor.getBufferPosition() @@ -549,16 +561,18 @@ class Selection extends Model @cut(maintainClipboard) # Public: Copies the selection to the clipboard and then deletes it. + # + # * `maintainClipboard` {Boolean} (default: false) See {::copy} cut: (maintainClipboard=false) -> @copy(maintainClipboard) @delete() # Public: Copies the current selection to the clipboard. # - # If the `maintainClipboard` is set to `true`, a specific metadata property - # is created to store each content copied to the clipboard. The clipboard - # `text` still contains the concatenation of the clipboard with the - # current selection. + # * `maintainClipboard` {Boolean} if `true`, a specific metadata property + # is created to store each content copied to the clipboard. The clipboard + # `text` still contains the concatenation of the clipboard with the + # current selection. (default: false) copy: (maintainClipboard=false) -> return if @isEmpty() text = @editor.buffer.getTextInRange(@getBufferRange()) @@ -598,9 +612,9 @@ class Selection extends Model # Public: Identifies if a selection intersects with a given buffer range. # - # bufferRange - A {Range} to check against. + # * `bufferRange` A {Range} to check against. # - # Returns a Boolean. + # Returns a {Boolean} intersectsBufferRange: (bufferRange) -> @getBufferRange().intersectsWith(bufferRange) @@ -612,17 +626,17 @@ class Selection extends Model # Public: Identifies if a selection intersects with another selection. # - # otherSelection - A {Selection} to check against. + # * `otherSelection` A {Selection} to check against. # - # Returns a Boolean. + # Returns a {Boolean} intersectsWith: (otherSelection) -> @getBufferRange().intersectsWith(otherSelection.getBufferRange()) # Public: Combines the given selection into this selection and then destroys # the given selection. # - # otherSelection - A {Selection} to merge with. - # options - A hash of options matching those found in {::setBufferRange}. + # * `otherSelection` A {Selection} to merge with. + # * `options` (optional) {Object} options matching those found in {::setBufferRange}. merge: (otherSelection, options) -> myGoalBufferRange = @getGoalBufferRange() otherGoalBufferRange = otherSelection.getGoalBufferRange() @@ -638,7 +652,7 @@ class Selection extends Model # # See {Range::compare} for more details. # - # otherSelection - A {Selection} to compare against. + # * `otherSelection` A {Selection} to compare against compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) diff --git a/src/syntax.coffee b/src/syntax.coffee index 9756a0d5e..e4ab7de03 100644 --- a/src/syntax.coffee +++ b/src/syntax.coffee @@ -14,7 +14,7 @@ Token = require './token' # An instance of this class is always available as the `atom.syntax` global. # # The Syntax class also contains properties for things such as the -# language-specific comment regexes. +# language-specific comment regexes. See {::getProperty} for more details. module.exports = class Syntax extends GrammarRegistry PropertyAccessors.includeInto(this) @@ -55,14 +55,15 @@ class Syntax extends GrammarRegistry # Public: Get a property for the given scope and key path. # - # ## Example + # ## Examples + # # ```coffee # comment = atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart') # console.log(comment) # '# ' # ``` # - # scope - An {Array} of {String} scopes. - # keyPath - A {String} key path. + # * `scope` An {Array} of {String} scopes. + # * `keyPath` A {String} key path. # # Returns a {String} property value or undefined. getProperty: (scope, keyPath) -> diff --git a/src/task.coffee b/src/task.coffee index 5a1b90faa..8d8d0316c 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -6,27 +6,41 @@ child_process = require 'child_process' # # Used by the fuzzy-finder. # -# ## Events -# -# * task:log - Emitted when console.log is called within the task. -# * task:warn - Emitted when console.warn is called within the task. -# * task:error - Emitted when console.error is called within the task. -# * task:completed - Emitted when the task has succeeded or failed. -# -# ## Requiring in packages +# ## Examples # # ```coffee -# {Task} = require 'atom' +# {Task} = require 'atom' # ``` +# +# ## Events +# +# ### task:log +# +# Emitted when console.log is called within the task. +# +# ### task:warn +# +# Emitted when console.warn is called within the task. +# +# ### task:error +# +# Emitted when console.error is called within the task. +# +# ### task:completed +# +# Emitted when the task has succeeded or failed. +# module.exports = class Task Emitter.includeInto(this) # Public: A helper method to easily launch and run a task once. # - # taskPath - The {String} path to the CoffeeScript/JavaScript file which - # exports a single {Function} to execute. - # args - The arguments to pass to the exported function. + # * `taskPath` The {String} path to the CoffeeScript/JavaScript file which + # exports a single {Function} to execute. + # * `args` The arguments to pass to the exported function. + # + # Returns the created {Task}. @once: (taskPath, args...) -> task = new Task(taskPath) task.once 'task:completed', -> task.terminate() @@ -43,8 +57,8 @@ class Task # Public: Creates a task. # - # taskPath - The {String} path to the CoffeeScript/JavaScript file that - # exports a single {Function} to execute. + # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that + # exports a single {Function} to execute. constructor: (taskPath) -> coffeeCacheRequire = "require('#{require.resolve('./coffee-cache')}').register();" coffeeScriptRequire = "require('#{require.resolve('coffee-script')}').register();" @@ -81,10 +95,10 @@ class Task # Throws an error if this task has already been terminated or if sending a # message to the child process fails. # - # args - The arguments to pass to the function exported by this task's script. - # callback - An optional {Function} to call when the task completes. + # * `args` The arguments to pass to the function exported by this task's script. + # * `callback` (optional) A {Function} to call when the task completes. start: (args..., callback) -> - throw new Error("Cannot start terminated process") unless @childProcess? + throw new Error('Cannot start terminated process') unless @childProcess? @handleEvents() if _.isFunction(callback) @@ -92,20 +106,24 @@ class Task else args.push(callback) @send({event: 'start', args}) + undefined # Public: Send message to the task. # # Throws an error if this task has already been terminated or if sending a # message to the child process fails. # - # message - The message to send to the task. + # * `message` The message to send to the task. send: (message) -> - throw new Error("Cannot send message to terminated process") unless @childProcess? - @childProcess.send(message) + if @childProcess? + @childProcess.send(message) + else + throw new Error('Cannot send message to terminated process') + undefined # Public: Forcefully stop the running task. # - # No events are emitted. + # No more events are emitted once this method is called. terminate: -> return unless @childProcess? @@ -114,3 +132,4 @@ class Task @childProcess = null @off() + undefined diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 542227187..fa254f31f 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -9,9 +9,32 @@ Q = require 'q' Package = require './package' {File} = require 'pathwatcher' -# Public: Handles loading and activating available themes. +# Extended: Handles loading and activating available themes. # # An instance of this class is always available as the `atom.themes` global. +# +# ## Events +# +# ### reloaded +# +# Extended: Emit when all styles have been reloaded. +# +# ### stylesheet-added +# +# Extended: Emit when a stylesheet has been added. +# +# * `stylesheet` {StyleSheet} object that was removed +# +# ### stylesheet-removed +# +# Extended: Emit when a stylesheet has been removed. +# +# * `stylesheet` {StyleSheet} object that was removed +# +# ### stylesheets-changed +# +# Extended: Emit anytime any style sheet is added or removed from the editor +# module.exports = class ThemeManager Emitter.includeInto(this) @@ -98,7 +121,7 @@ class ThemeManager @refreshLessCache() # Update cache again now that @getActiveThemes() is populated @loadUserStylesheet() @reloadBaseStylesheets() - @emit('reloaded') + @emit 'reloaded' deferred.resolve() deferred.promise @@ -124,7 +147,7 @@ class ThemeManager # Public: Set the list of enabled themes. # - # enabledThemeNames - An {Array} of {String} theme names. + # * `enabledThemeNames` An {Array} of {String} theme names. setEnabledThemes: (enabledThemeNames) -> atom.config.set('core.themes', enabledThemeNames) @@ -187,9 +210,8 @@ class ThemeManager # # This supports both CSS and LESS stylsheets. # - # stylesheetPath - A {String} path to the stylesheet that can be an absolute - # path or a relative path that will be resolved against the - # load path. + # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute + # path or a relative path that will be resolved against the load path. # # Returns the absolute path to the required stylesheet. requireStylesheet: (stylesheetPath, type = 'bundled', htmlElement) -> diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index bdf50bd4b..5f2b09b62 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -28,7 +28,9 @@ class WindowEventHandler @subscribe $(window), 'blur', -> document.body.classList.add('is-blurred') @subscribe $(window), 'window:open-path', (event, {pathToOpen, initialLine, initialColumn}) -> - unless fs.isDirectorySync(pathToOpen) + if fs.isDirectorySync(pathToOpen) + atom.project.setPath(pathToOpen) unless atom.project.getPath() + else atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) @subscribe $(window), 'beforeunload', => diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index 8e3af09d0..9da0d7b90 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -9,14 +9,13 @@ scrollbarStyle = require 'scrollbar-style' fs = require 'fs-plus' Workspace = require './workspace' CommandInstaller = require './command-installer' -EditorView = require './editor-view' PaneView = require './pane-view' PaneColumnView = require './pane-column-view' PaneRowView = require './pane-row-view' PaneContainerView = require './pane-container-view' Editor = require './editor' -# Public: The top-level view for the entire window. An instance of this class is +# Essential: The top-level view for the entire window. An instance of this class is # available via the `atom.workspaceView` global. # # It is backed by a model object, an instance of {Workspace}, which is available @@ -45,7 +44,7 @@ Editor = require './editor' # the built-in `atom` module. # # ```coffee -# {WorkspaceView} = require 'atom' +# {WorkspaceView} = require 'atom' # ``` # # You can assign it to the `atom.workspaceView` global in the spec or just use @@ -71,8 +70,6 @@ class WorkspaceView extends View projectHome: path.join(fs.getHomeDirectory(), 'github') audioBeep: true destroyEmptyPanes: true - useReactEditor: true - useReactMiniEditors: true @content: -> @div class: 'workspace', tabindex: -1, => @@ -247,40 +244,56 @@ class WorkspaceView extends View # Public: Prepend an element or view to the panels at the top of the # workspace. + # + # * `element` jQuery object or DOM element prependToTop: (element) -> @vertical.prepend(element) # Public: Append an element or view to the panels at the top of the workspace. + # + # * `element` jQuery object or DOM element appendToTop: (element) -> @panes.before(element) # Public: Prepend an element or view to the panels at the bottom of the # workspace. + # + # * `element` jQuery object or DOM element prependToBottom: (element) -> @panes.after(element) # Public: Append an element or view to the panels at the bottom of the # workspace. + # + # * `element` jQuery object or DOM element appendToBottom: (element) -> @vertical.append(element) # Public: Prepend an element or view to the panels at the left of the # workspace. + # + # * `element` jQuery object or DOM element prependToLeft: (element) -> @horizontal.prepend(element) # Public: Append an element or view to the panels at the left of the # workspace. + # + # * `element` jQuery object or DOM element appendToLeft: (element) -> @vertical.before(element) # Public: Prepend an element or view to the panels at the right of the # workspace. + # + # * `element` jQuery object or DOM element prependToRight: (element) -> @vertical.after(element) # Public: Append an element or view to the panels at the right of the # workspace. + # + # * `element` jQuery object or DOM element appendToRight: (element) -> @horizontal.append(element) @@ -320,7 +333,8 @@ class WorkspaceView extends View # Public: Register a function to be called for every current and future # pane view in the workspace. # - # callback - A {Function} with a {PaneView} as its only argument. + # * `callback` A {Function} with a {PaneView} as its only argument. + # * `paneView` {PaneView} # # Returns a subscription object with an `.off` method that you can call to # unregister the callback. @@ -341,7 +355,8 @@ class WorkspaceView extends View # editor view in the workspace (only includes {EditorView}s that are pane # items). # - # callback - A {Function} with an {EditorView} as its only argument. + # * `callback` A {Function} with an {EditorView} as its only argument. + # * `editorView` {EditorView} # # Returns a subscription object with an `.off` method that you can call to # unregister the callback. diff --git a/src/workspace.coffee b/src/workspace.coffee index 6ee8a4600..85870ef1b 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -15,6 +15,20 @@ Pane = require './pane' # Interact with this object to open files, be notified of current and future # editors, and manipulate panes. To add panels, you'll need to use the # {WorkspaceView} class for now until we establish APIs at the model layer. +# +# ## Events +# +# ### uri-opened +# +# Extended: Emit when something has been opened. This can be anything, from an +# editor to the settings view. You can get the new item via {::getActivePaneItem} +# +# ### editor-created +# +# Extended: Emit when an editor is created (a file opened). +# +# * `editor` {Editor} the new editor +# module.exports = class Workspace extends Model atom.deserializers.add(this) @@ -78,7 +92,7 @@ class Workspace extends Model # Public: Register a function to be called for every current and future # {Editor} in the workspace. # - # callback - A {Function} with an {Editor} as its only argument. + # * `callback` A {Function} with an {Editor} as its only argument. # # Returns a subscription object with an `.off` method that you can call to # unregister the callback. @@ -98,22 +112,21 @@ class Workspace extends Model # Public: Open a given a URI in Atom asynchronously. # - # uri - A {String} containing a URI. - # options - An optional options {Object} - # :initialLine - A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # :initialColumn - A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # :split - Either 'left' or 'right'. If 'left', the item will be opened in - # leftmost pane of the current active pane's row. If 'right', the - # item will be opened in the rightmost pane of the current active - # pane's row. - # :activatePane - A {Boolean} indicating whether to call {Pane::activate} on - # the containing pane. Defaults to `true`. - # :searchAllPanes - A {Boolean}. If `true`, the workspace will attempt to - # activate an existing item for the given URI on any pane. - # If `false`, only the active pane will be searched for - # an existing item for the same URI. Defaults to `false`. + # * `uri` A {String} containing a URI. + # * `options` (optional) {Object} + # * `initialLine` A {Number} indicating which row to move the cursor to + # initially. Defaults to `0`. + # * `initialColumn` A {Number} indicating which column to move the cursor to + # initially. Defaults to `0`. + # * `split` Either 'left' or 'right'. If 'left', the item will be opened in + # leftmost pane of the current active pane's row. If 'right', the + # item will be opened in the rightmost pane of the current active pane's row. + # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + # containing pane. Defaults to `true`. + # * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to + # activate an existing item for the given URI on any pane. + # If `false`, only the active pane will be searched for + # an existing item for the same URI. Defaults to `false`. # # Returns a promise that resolves to the {Editor} for the file URI. open: (uri, options={}) -> @@ -132,7 +145,7 @@ class Workspace extends Model @openUriInPane(uri, pane, options) - # Public: Open Atom's license in the active pane. + # Open Atom's license in the active pane. openLicense: -> @open(join(atom.getLoadSettings().resourcePath, 'LICENSE.md')) @@ -140,14 +153,14 @@ class Workspace extends Model # in specs. Calling this in production code will block the UI thread and # everyone will be mad at you.** # - # uri - A {String} containing a URI. - # options - An optional options {Object} - # :initialLine - A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # :initialColumn - A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # :activatePane - A {Boolean} indicating whether to call {Pane::activate} on - # the containing pane. Defaults to `true`. + # * `uri` A {String} containing a URI. + # * `options` An optional options {Object} + # * `initialLine` A {Number} indicating which row to move the cursor to + # initially. Defaults to `0`. + # * `initialColumn` A {Number} indicating which column to move the cursor to + # initially. Defaults to `0`. + # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + # the containing pane. Defaults to `true`. openSync: (uri='', options={}) -> deprecate("Don't use the `changeFocus` option") if options.changeFocus? @@ -207,14 +220,15 @@ class Workspace extends Model # # An {Editor} will be used if no openers return a value. # - # ## Example - # ```coffeescript - # atom.project.registerOpener (uri) -> - # if path.extname(uri) is '.toml' - # return new TomlEditor(uri) + # ## Examples + # + # ```coffee + # atom.project.registerOpener (uri) -> + # if path.extname(uri) is '.toml' + # return new TomlEditor(uri) # ``` # - # opener - A {Function} to be called when a path is being opened. + # * `opener` A {Function} to be called when a path is being opened. registerOpener: (opener) -> @openers.push(opener) @@ -251,6 +265,8 @@ class Workspace extends Model # Public: Get the first pane {Pane} with an item for the given URI. # + # * `uri` {String} uri + # # Returns a {Pane} or `undefined` if no pane exists for the given URI. paneForUri: (uri) -> @paneContainer.paneForUri(uri)