From c632e6ca58f6c965956d1e75e9bc0c78134a8e6e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:22:02 -0400 Subject: [PATCH 001/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/project-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: ``` $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes spec/project-spec.js $ standard --fix spec/project-spec.js ``` --- spec/project-spec.coffee | 802 --------------------------------- spec/project-spec.js | 935 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 935 insertions(+), 802 deletions(-) delete mode 100644 spec/project-spec.coffee create mode 100644 spec/project-spec.js diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee deleted file mode 100644 index 1f5eb54a4..000000000 --- a/spec/project-spec.coffee +++ /dev/null @@ -1,802 +0,0 @@ -temp = require('temp').track() -TextBuffer = require('text-buffer') -Project = require '../src/project' -fs = require 'fs-plus' -path = require 'path' -{Directory} = require 'pathwatcher' -{stopAllWatchers} = require '../src/path-watcher' -GitRepository = require '../src/git-repository' - -describe "Project", -> - beforeEach -> - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) - - # Wait for project's service consumers to be asynchronously added - waits(1) - - describe "serialization", -> - deserializedProject = null - notQuittingProject = null - quittingProject = null - - afterEach -> - deserializedProject?.destroy() - notQuittingProject?.destroy() - quittingProject?.destroy() - - it "does not deserialize paths to directories that don't exist", -> - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - state = atom.project.serialize() - state.paths.push('/directory/that/does/not/exist') - - err = null - waitsForPromise -> - deserializedProject.deserialize(state, atom.deserializers) - .catch (e) -> err = e - - runs -> - expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) - expect(err.missingProjectPaths).toEqual ['/directory/that/does/not/exist'] - - it "does not deserialize paths that are now files", -> - childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') - fs.mkdirSync(childPath) - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - atom.project.setPaths([childPath]) - state = atom.project.serialize() - - fs.rmdirSync(childPath) - fs.writeFileSync(childPath, 'surprise!\n') - - err = null - waitsForPromise -> - deserializedProject.deserialize(state, atom.deserializers) - .catch (e) -> err = e - - runs -> - expect(deserializedProject.getPaths()).toEqual([]) - expect(err.missingProjectPaths).toEqual [childPath] - - it "does not include unretained buffers in the serialized state", -> - waitsForPromise -> - atom.project.bufferForPath('a') - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", -> - waitsForPromise -> - atom.workspace.open('a') - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 1 - deserializedProject.getBuffers()[0].destroy() - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers when their path is now a directory", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.mkdirSync(pathToOpen) - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers when their path is inaccessible", -> - return if process.platform is 'win32' # chmod not supported on win32 - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - fs.writeFileSync(pathToOpen, '') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.chmodSync(pathToOpen, '000') - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers with their path is no longer present", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - fs.writeFileSync(pathToOpen, '') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.unlinkSync(pathToOpen) - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "deserializes buffers that have never been saved before", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - atom.workspace.getActiveTextEditor().setText('unsaved\n') - expect(atom.project.getBuffers().length).toBe 1 - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 1 - expect(deserializedProject.getBuffers()[0].getPath()).toBe pathToOpen - expect(deserializedProject.getBuffers()[0].getText()).toBe 'unsaved\n' - - it "serializes marker layers and history only if Atom is quitting", -> - waitsForPromise -> atom.workspace.open('a') - - bufferA = null - layerA = null - markerA = null - - runs -> - bufferA = atom.project.getBuffers()[0] - layerA = bufferA.addMarkerLayer(persistent: true) - markerA = layerA.markPosition([0, 3]) - bufferA.append('!') - notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) - quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> quittingProject.deserialize(atom.project.serialize({isUnloading: true})) - - runs -> - expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined() - expect(quittingProject.getBuffers()[0].undo()).toBe(true) - - describe "when an editor is saved and the project has no path", -> - it "sets the project's path to the saved file's parent directory", -> - tempFile = temp.openSync().path - atom.project.setPaths([]) - expect(atom.project.getPaths()[0]).toBeUndefined() - editor = null - - waitsForPromise -> - atom.workspace.open().then (o) -> editor = o - - waitsForPromise -> - editor.saveAs(tempFile) - - runs -> - expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile) - - describe "before and after saving a buffer", -> - [buffer] = [] - beforeEach -> - waitsForPromise -> - atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) -> - buffer = o - buffer.retain() - - afterEach -> - buffer.release() - - it "emits save events on the main process", -> - spyOn(atom.project.applicationDelegate, 'emitDidSavePath') - spyOn(atom.project.applicationDelegate, 'emitWillSavePath') - - waitsForPromise -> buffer.save() - - runs -> - expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) - expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) - expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) - expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) - - describe "when a watch error is thrown from the TextBuffer", -> - editor = null - beforeEach -> - waitsForPromise -> - atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o - - it "creates a warning notification", -> - atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy() - - error = new Error('SomeError') - error.eventType = 'resurrect' - editor.buffer.emitter.emit 'will-throw-watch-error', - handle: jasmine.createSpy() - error: error - - expect(noteSpy).toHaveBeenCalled() - - notification = noteSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getDetail()).toBe 'SomeError' - expect(notification.getMessage()).toContain '`resurrect`' - expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a') - - describe "when a custom repository-provider service is provided", -> - [fakeRepositoryProvider, fakeRepository] = [] - - beforeEach -> - fakeRepository = {destroy: -> null} - fakeRepositoryProvider = { - repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository) - repositoryForDirectorySync: (directory) -> fakeRepository - } - - it "uses it to create repositories for any directories that need one", -> - projectPath = temp.mkdirSync('atom-project') - atom.project.setPaths([projectPath]) - expect(atom.project.getRepositories()).toEqual [null] - - atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> atom.project.getRepositories()[0] is fakeRepository - - it "does not create any new repositories if every directory has a repository", -> - repositories = atom.project.getRepositories() - expect(repositories.length).toEqual 1 - expect(repositories[0]).toBeTruthy() - - atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> expect(atom.project.getRepositories()).toBe repositories - - it "stops using it to create repositories when the service is removed", -> - atom.project.setPaths([]) - - disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> - disposable.dispose() - atom.project.addPath(temp.mkdirSync('atom-project')) - expect(atom.project.getRepositories()).toEqual [null] - - describe "when a custom directory-provider service is provided", -> - class DummyDirectory - constructor: (@path) -> - getPath: -> @path - getFile: -> {existsSync: -> false} - getSubdirectory: -> {existsSync: -> false} - isRoot: -> true - existsSync: -> @path.endsWith('does-exist') - contains: (filePath) -> filePath.startsWith(@path) - - serviceDisposable = null - - beforeEach -> - serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { - directoryForURISync: (uri) -> - if uri.startsWith("ssh://") - new DummyDirectory(uri) - else - null - }) - - waitsFor -> - atom.project.directoryProviders.length > 0 - - it "uses the provider's custom directories for any paths that it handles", -> - localPath = temp.mkdirSync('local-path') - remotePath = "ssh://foreign-directory:8080/does-exist" - - atom.project.setPaths([localPath, remotePath]) - - directories = atom.project.getDirectories() - expect(directories[0].getPath()).toBe localPath - expect(directories[0] instanceof Directory).toBe true - expect(directories[1].getPath()).toBe remotePath - expect(directories[1] instanceof DummyDirectory).toBe true - - # It does not add new remote paths that do not exist - nonExistentRemotePath = "ssh://another-directory:8080/does-not-exist" - atom.project.addPath(nonExistentRemotePath) - expect(atom.project.getDirectories().length).toBe 2 - - # It adds new remote paths if their directories exist. - newRemotePath = "ssh://another-directory:8080/does-exist" - atom.project.addPath(newRemotePath) - directories = atom.project.getDirectories() - expect(directories[2].getPath()).toBe newRemotePath - expect(directories[2] instanceof DummyDirectory).toBe true - - it "stops using the provider when the service is removed", -> - serviceDisposable.dispose() - atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"]) - expect(atom.project.getDirectories().length).toBe(0) - - describe ".open(path)", -> - [absolutePath, newBufferHandler] = [] - - beforeEach -> - absolutePath = require.resolve('./fixtures/dir/a') - newBufferHandler = jasmine.createSpy('newBufferHandler') - atom.project.onDidAddBuffer(newBufferHandler) - - describe "when given an absolute path that isn't currently open", -> - it "returns a new edit session for the given path and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - - describe "when given a relative path that isn't currently opened", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - - describe "when passed the path to a buffer that is currently opened", -> - it "returns a new edit session containing currently opened buffer", -> - editor = null - - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - newBufferHandler.reset() - - waitsForPromise -> - atom.workspace.open(absolutePath).then ({buffer}) -> - expect(buffer).toBe editor.buffer - - waitsForPromise -> - atom.workspace.open('a').then ({buffer}) -> - expect(buffer).toBe editor.buffer - expect(newBufferHandler).not.toHaveBeenCalled() - - describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open().then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBeUndefined() - expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) - - describe ".bufferForPath(path)", -> - buffer = null - - beforeEach -> - waitsForPromise -> - atom.project.bufferForPath("a").then (o) -> - buffer = o - buffer.retain() - - afterEach -> - buffer.release() - - describe "when opening a previously opened path", -> - it "does not create a new buffer", -> - waitsForPromise -> - atom.project.bufferForPath("a").then (anotherBuffer) -> - expect(anotherBuffer).toBe buffer - - waitsForPromise -> - atom.project.bufferForPath("b").then (anotherBuffer) -> - expect(anotherBuffer).not.toBe buffer - - waitsForPromise -> - Promise.all([ - atom.project.bufferForPath('c'), - atom.project.bufferForPath('c') - ]).then ([buffer1, buffer2]) -> - expect(buffer1).toBe(buffer2) - - it "retries loading the buffer if it previously failed", -> - waitsForPromise shouldReject: true, -> - spyOn(TextBuffer, 'load').andCallFake -> - Promise.reject(new Error('Could not open file')) - atom.project.bufferForPath('b') - - waitsForPromise shouldReject: false, -> - TextBuffer.load.andCallThrough() - atom.project.bufferForPath('b') - - it "creates a new buffer if the previous buffer was destroyed", -> - buffer.release() - - waitsForPromise -> - atom.project.bufferForPath("b").then (anotherBuffer) -> - expect(anotherBuffer).not.toBe buffer - - describe ".repositoryForDirectory(directory)", -> - it "resolves to null when the directory does not have a repository", -> - waitsForPromise -> - directory = new Directory("/tmp") - atom.project.repositoryForDirectory(directory).then (result) -> - expect(result).toBeNull() - expect(atom.project.repositoryProviders.length).toBeGreaterThan 0 - expect(atom.project.repositoryPromisesByPath.size).toBe 0 - - it "resolves to a GitRepository and is cached when the given directory is a Git repo", -> - waitsForPromise -> - directory = new Directory(path.join(__dirname, '..')) - promise = atom.project.repositoryForDirectory(directory) - promise.then (result) -> - expect(result).toBeInstanceOf GitRepository - dirPath = directory.getRealPathSync() - expect(result.getPath()).toBe path.join(dirPath, '.git') - - # Verify that the result is cached. - expect(atom.project.repositoryForDirectory(directory)).toBe(promise) - - it "creates a new repository if a previous one with the same directory had been destroyed", -> - repository = null - directory = new Directory(path.join(__dirname, '..')) - - waitsForPromise -> - atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo - - runs -> - expect(repository.isDestroyed()).toBe(false) - repository.destroy() - expect(repository.isDestroyed()).toBe(true) - - waitsForPromise -> - atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo - - runs -> - expect(repository.isDestroyed()).toBe(false) - - describe ".setPaths(paths, options)", -> - describe "when path is a file", -> - it "sets its path to the file's parent directory and updates the root directory", -> - filePath = require.resolve('./fixtures/dir/a') - atom.project.setPaths([filePath]) - expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath) - expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath) - - describe "when path is a directory", -> - it "assigns the directories and repositories", -> - directory1 = temp.mkdirSync("non-git-repo") - directory2 = temp.mkdirSync("git-repo1") - directory3 = temp.mkdirSync("git-repo2") - - gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')) - fs.copySync(gitDirPath, path.join(directory2, ".git")) - fs.copySync(gitDirPath, path.join(directory3, ".git")) - - atom.project.setPaths([directory1, directory2, directory3]) - - [repo1, repo2, repo3] = atom.project.getRepositories() - expect(repo1).toBeNull() - expect(repo2.getShortHead()).toBe "master" - expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git")) - expect(repo3.getShortHead()).toBe "master" - expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) - - it "calls callbacks registered with ::onDidChangePaths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ] - atom.project.setPaths(paths) - - expect(onDidChangePathsSpy.callCount).toBe 1 - expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) - - it "optionally throws an error with any paths that did not exist", -> - paths = [temp.mkdirSync("exists0"), "/doesnt-exists/0", temp.mkdirSync("exists1"), "/doesnt-exists/1"] - - try - atom.project.setPaths paths, mustExist: true - expect('no exception thrown').toBeUndefined() - catch e - expect(e.missingProjectPaths).toEqual [paths[1], paths[3]] - - expect(atom.project.getPaths()).toEqual [paths[0], paths[2]] - - describe "when no paths are given", -> - it "clears its path", -> - atom.project.setPaths([]) - expect(atom.project.getPaths()).toEqual [] - expect(atom.project.getDirectories()).toEqual [] - - it "normalizes the path to remove consecutive slashes, ., and .. segments", -> - atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."]) - expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a')) - expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) - - describe ".addPath(path, options)", -> - it "calls callbacks registered with ::onDidChangePaths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - [oldPath] = atom.project.getPaths() - - newPath = temp.mkdirSync("dir") - atom.project.addPath(newPath) - - expect(onDidChangePathsSpy.callCount).toBe 1 - expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) - - it "doesn't add redundant paths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - [oldPath] = atom.project.getPaths() - - # Doesn't re-add an existing root directory - atom.project.addPath(oldPath) - expect(atom.project.getPaths()).toEqual([oldPath]) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - # Doesn't add an entry for a file-path within an existing root directory - atom.project.addPath(path.join(oldPath, 'some-file.txt')) - expect(atom.project.getPaths()).toEqual([oldPath]) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - # Does add an entry for a directory within an existing directory - newPath = path.join(oldPath, "a-dir") - atom.project.addPath(newPath) - expect(atom.project.getPaths()).toEqual([oldPath, newPath]) - expect(onDidChangePathsSpy).toHaveBeenCalled() - - it "doesn't add non-existent directories", -> - previousPaths = atom.project.getPaths() - atom.project.addPath('/this-definitely/does-not-exist') - expect(atom.project.getPaths()).toEqual(previousPaths) - - it "optionally throws on non-existent directories", -> - expect -> - atom.project.addPath '/this-definitely/does-not-exist', mustExist: true - .toThrow() - - describe ".removePath(path)", -> - onDidChangePathsSpy = null - - beforeEach -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - it "removes the directory and repository for the path", -> - result = atom.project.removePath(atom.project.getPaths()[0]) - expect(atom.project.getDirectories()).toEqual([]) - expect(atom.project.getRepositories()).toEqual([]) - expect(atom.project.getPaths()).toEqual([]) - expect(result).toBe true - expect(onDidChangePathsSpy).toHaveBeenCalled() - - it "does nothing if the path is not one of the project's root paths", -> - originalPaths = atom.project.getPaths() - result = atom.project.removePath(originalPaths[0] + "xyz") - expect(result).toBe false - expect(atom.project.getPaths()).toEqual(originalPaths) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - it "doesn't destroy the repository if it is shared by another root directory", -> - atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")]) - atom.project.removePath(__dirname) - expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")]) - expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false - - it "removes a path that is represented as a URI", -> - atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { - directoryForURISync: (uri) -> - { - getPath: -> uri - getSubdirectory: -> {} - isRoot: -> true - existsSync: -> true - off: -> - } - }) - - ftpURI = "ftp://example.com/some/folder" - - atom.project.setPaths([ftpURI]) - expect(atom.project.getPaths()).toEqual [ftpURI] - - atom.project.removePath(ftpURI) - expect(atom.project.getPaths()).toEqual [] - - describe ".onDidChangeFiles()", -> - sub = [] - events = [] - checkCallback = -> - - beforeEach -> - sub = atom.project.onDidChangeFiles (incoming) -> - events.push incoming... - checkCallback() - - afterEach -> - sub.dispose() - - waitForEvents = (paths) -> - remaining = new Set(fs.realpathSync(p) for p in paths) - new Promise (resolve, reject) -> - checkCallback = -> - remaining.delete(event.path) for event in events - resolve() if remaining.size is 0 - - expire = -> - checkCallback = -> - console.error "Paths not seen:", Array.from(remaining) - reject(new Error('Expired before all expected events were delivered.')) - - checkCallback() - setTimeout expire, 2000 - - it "reports filesystem changes within project paths", -> - dirOne = temp.mkdirSync('atom-spec-project-one') - fileOne = path.join(dirOne, 'file-one.txt') - fileTwo = path.join(dirOne, 'file-two.txt') - dirTwo = temp.mkdirSync('atom-spec-project-two') - fileThree = path.join(dirTwo, 'file-three.txt') - - # Ensure that all preexisting watchers are stopped - waitsForPromise -> stopAllWatchers() - - runs -> atom.project.setPaths([dirOne]) - waitsForPromise -> atom.project.getWatcherPromise dirOne - - runs -> - expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual undefined - - fs.writeFileSync fileThree, "three\n" - fs.writeFileSync fileTwo, "two\n" - fs.writeFileSync fileOne, "one\n" - - waitsForPromise -> waitForEvents [fileOne, fileTwo] - - runs -> - expect(events.some (event) -> event.path is fileThree).toBeFalsy() - - describe ".onDidAddBuffer()", -> - it "invokes the callback with added text buffers", -> - buffers = [] - added = [] - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 1 - atom.project.onDidAddBuffer (buffer) -> added.push(buffer) - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 2 - expect(added).toEqual [buffers[1]] - - describe ".observeBuffers()", -> - it "invokes the observer with current and future text buffers", -> - buffers = [] - observed = [] - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) - .then (o) -> buffers.push(o) - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 2 - atom.project.observeBuffers (buffer) -> observed.push(buffer) - expect(observed).toEqual buffers - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(observed.length).toBe 3 - expect(buffers.length).toBe 3 - expect(observed).toEqual buffers - - describe ".relativize(path)", -> - it "returns the path, relative to whichever root directory it is inside of", -> - atom.project.addPath(temp.mkdirSync("another-path")) - - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") - - rootPath = atom.project.getPaths()[1] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") - - it "returns the given path if it is not in any of the root directories", -> - randomPath = path.join("some", "random", "path") - expect(atom.project.relativize(randomPath)).toBe randomPath - - describe ".relativizePath(path)", -> - it "returns the root path that contains the given path, and the path relativized to that root path", -> - atom.project.addPath(temp.mkdirSync("another-path")) - - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")] - - rootPath = atom.project.getPaths()[1] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")] - - describe "when the given path isn't inside of any of the project's path", -> - it "returns null for the root path, and the given path unchanged", -> - randomPath = path.join("some", "random", "path") - expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath] - - describe "when the given path is a URL", -> - it "returns null for the root path, and the given path unchanged", -> - url = "http://the-path" - expect(atom.project.relativizePath(url)).toEqual [null, url] - - describe "when the given path is inside more than one root folder", -> - it "uses the root folder that is closest to the given path", -> - atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) - - inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') - - expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true - expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true - expect(atom.project.relativizePath(inputPath)).toEqual [ - atom.project.getPaths()[1], - path.join('somewhere', 'something.txt') - ] - - describe ".contains(path)", -> - it "returns whether or not the given path is in one of the root directories", -> - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.contains(childPath)).toBe true - - randomPath = path.join("some", "random", "path") - expect(atom.project.contains(randomPath)).toBe false - - describe ".resolvePath(uri)", -> - it "normalizes disk drive letter in passed path on #win32", -> - expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt" diff --git a/spec/project-spec.js b/spec/project-spec.js new file mode 100644 index 000000000..d54c0c9a2 --- /dev/null +++ b/spec/project-spec.js @@ -0,0 +1,935 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const temp = require('temp').track() +const TextBuffer = require('text-buffer') +const Project = require('../src/project') +const fs = require('fs-plus') +const path = require('path') +const {Directory} = require('pathwatcher') +const {stopAllWatchers} = require('../src/path-watcher') +const GitRepository = require('../src/git-repository') + +describe('Project', function () { + beforeEach(function () { + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) + + // Wait for project's service consumers to be asynchronously added + return waits(1) + }) + + describe('serialization', function () { + let deserializedProject = null + let notQuittingProject = null + let quittingProject = null + + afterEach(function () { + if (deserializedProject != null) { + deserializedProject.destroy() + } + if (notQuittingProject != null) { + notQuittingProject.destroy() + } + return (quittingProject != null ? quittingProject.destroy() : undefined) + }) + + it("does not deserialize paths to directories that don't exist", function () { + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + const state = atom.project.serialize() + state.paths.push('/directory/that/does/not/exist') + + let err = null + waitsForPromise(() => + deserializedProject.deserialize(state, atom.deserializers) + .catch(e => err = e) + ) + + return runs(function () { + expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) + return expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) + }) + }) + + it('does not deserialize paths that are now files', function () { + const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') + fs.mkdirSync(childPath) + + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + atom.project.setPaths([childPath]) + const state = atom.project.serialize() + + fs.rmdirSync(childPath) + fs.writeFileSync(childPath, 'surprise!\n') + + let err = null + waitsForPromise(() => + deserializedProject.deserialize(state, atom.deserializers) + .catch(e => err = e) + ) + + return runs(function () { + expect(deserializedProject.getPaths()).toEqual([]) + return expect(err.missingProjectPaths).toEqual([childPath]) + }) + }) + + it('does not include unretained buffers in the serialized state', function () { + waitsForPromise(() => atom.project.bufferForPath('a')) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { + waitsForPromise(() => atom.workspace.open('a')) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(function () { + expect(deserializedProject.getBuffers().length).toBe(1) + deserializedProject.getBuffers()[0].destroy() + return expect(deserializedProject.getBuffers().length).toBe(0) + }) + }) + + it('does not deserialize buffers when their path is now a directory', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.mkdirSync(pathToOpen) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('does not deserialize buffers when their path is inaccessible', function () { + if (process.platform === 'win32') { return } // chmod not supported on win32 + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + fs.writeFileSync(pathToOpen, '') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.chmodSync(pathToOpen, '000') + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('does not deserialize buffers with their path is no longer present', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + fs.writeFileSync(pathToOpen, '') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.unlinkSync(pathToOpen) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('deserializes buffers that have never been saved before', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + atom.workspace.getActiveTextEditor().setText('unsaved\n') + expect(atom.project.getBuffers().length).toBe(1) + + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(function () { + expect(deserializedProject.getBuffers().length).toBe(1) + expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) + return expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') + }) + }) + + return it('serializes marker layers and history only if Atom is quitting', function () { + waitsForPromise(() => atom.workspace.open('a')) + + let bufferA = null + let layerA = null + let markerA = null + + runs(function () { + bufferA = atom.project.getBuffers()[0] + layerA = bufferA.addMarkerLayer({persistent: true}) + markerA = layerA.markPosition([0, 3]) + bufferA.append('!') + return notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) + + runs(function () { + expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() + expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) + return quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) + + return runs(function () { + expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() + return expect(quittingProject.getBuffers()[0].undo()).toBe(true) + }) + }) + }) + + describe('when an editor is saved and the project has no path', () => + it("sets the project's path to the saved file's parent directory", function () { + const tempFile = temp.openSync().path + atom.project.setPaths([]) + expect(atom.project.getPaths()[0]).toBeUndefined() + let editor = null + + waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + + waitsForPromise(() => editor.saveAs(tempFile)) + + return runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) + }) + ) + + describe('before and after saving a buffer', function () { + let [buffer] = Array.from([]) + beforeEach(() => + waitsForPromise(() => + atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { + buffer = o + return buffer.retain() + }) + ) + ) + + afterEach(() => buffer.release()) + + return it('emits save events on the main process', function () { + spyOn(atom.project.applicationDelegate, 'emitDidSavePath') + spyOn(atom.project.applicationDelegate, 'emitWillSavePath') + + waitsForPromise(() => buffer.save()) + + return runs(function () { + expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) + expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) + expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) + return expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) + }) + }) + }) + + describe('when a watch error is thrown from the TextBuffer', function () { + let editor = null + beforeEach(() => + waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) + ) + + return it('creates a warning notification', function () { + let noteSpy + atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) + + const error = new Error('SomeError') + error.eventType = 'resurrect' + editor.buffer.emitter.emit('will-throw-watch-error', { + handle: jasmine.createSpy(), + error + } + ) + + expect(noteSpy).toHaveBeenCalled() + + const notification = noteSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getDetail()).toBe('SomeError') + expect(notification.getMessage()).toContain('`resurrect`') + return expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) + }) + }) + + describe('when a custom repository-provider service is provided', function () { + let [fakeRepositoryProvider, fakeRepository] = Array.from([]) + + beforeEach(function () { + fakeRepository = {destroy () { return null }} + return fakeRepositoryProvider = { + repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, + repositoryForDirectorySync (directory) { return fakeRepository } + } + }) + + it('uses it to create repositories for any directories that need one', function () { + const projectPath = temp.mkdirSync('atom-project') + atom.project.setPaths([projectPath]) + expect(atom.project.getRepositories()).toEqual([null]) + + atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(() => atom.project.getRepositories()[0] === fakeRepository) + }) + + it('does not create any new repositories if every directory has a repository', function () { + const repositories = atom.project.getRepositories() + expect(repositories.length).toEqual(1) + expect(repositories[0]).toBeTruthy() + + atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(() => expect(atom.project.getRepositories()).toBe(repositories)) + }) + + return it('stops using it to create repositories when the service is removed', function () { + atom.project.setPaths([]) + + const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(function () { + disposable.dispose() + atom.project.addPath(temp.mkdirSync('atom-project')) + return expect(atom.project.getRepositories()).toEqual([null]) + }) + }) + }) + + describe('when a custom directory-provider service is provided', function () { + class DummyDirectory { + constructor (path1) { + this.path = path1 + } + getPath () { return this.path } + getFile () { return {existsSync () { return false }} } + getSubdirectory () { return {existsSync () { return false }} } + isRoot () { return true } + existsSync () { return this.path.endsWith('does-exist') } + contains (filePath) { return filePath.startsWith(this.path) } + } + + let serviceDisposable = null + + beforeEach(function () { + serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + if (uri.startsWith('ssh://')) { + return new DummyDirectory(uri) + } else { + return null + } + } + }) + + return waitsFor(() => atom.project.directoryProviders.length > 0) + }) + + it("uses the provider's custom directories for any paths that it handles", function () { + const localPath = temp.mkdirSync('local-path') + const remotePath = 'ssh://foreign-directory:8080/does-exist' + + atom.project.setPaths([localPath, remotePath]) + + let directories = atom.project.getDirectories() + expect(directories[0].getPath()).toBe(localPath) + expect(directories[0] instanceof Directory).toBe(true) + expect(directories[1].getPath()).toBe(remotePath) + expect(directories[1] instanceof DummyDirectory).toBe(true) + + // It does not add new remote paths that do not exist + const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist' + atom.project.addPath(nonExistentRemotePath) + expect(atom.project.getDirectories().length).toBe(2) + + // It adds new remote paths if their directories exist. + const newRemotePath = 'ssh://another-directory:8080/does-exist' + atom.project.addPath(newRemotePath) + directories = atom.project.getDirectories() + expect(directories[2].getPath()).toBe(newRemotePath) + return expect(directories[2] instanceof DummyDirectory).toBe(true) + }) + + return it('stops using the provider when the service is removed', function () { + serviceDisposable.dispose() + atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) + return expect(atom.project.getDirectories().length).toBe(0) + }) + }) + + describe('.open(path)', function () { + let [absolutePath, newBufferHandler] = Array.from([]) + + beforeEach(function () { + absolutePath = require.resolve('./fixtures/dir/a') + newBufferHandler = jasmine.createSpy('newBufferHandler') + return atom.project.onDidAddBuffer(newBufferHandler) + }) + + describe("when given an absolute path that isn't currently open", () => + it("returns a new edit session for the given path and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBe(absolutePath) + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + + describe("when given a relative path that isn't currently opened", () => + it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBe(absolutePath) + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + + describe('when passed the path to a buffer that is currently opened', () => + it('returns a new edit session containing currently opened buffer', function () { + let editor = null + + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + runs(() => newBufferHandler.reset()) + + waitsForPromise(() => + atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer)) + ) + + return waitsForPromise(() => + atom.workspace.open('a').then(function ({buffer}) { + expect(buffer).toBe(editor.buffer) + return expect(newBufferHandler).not.toHaveBeenCalled() + }) + ) + }) + ) + + return describe('when not passed a path', () => + it("returns a new edit session and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBeUndefined() + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + }) + + describe('.bufferForPath(path)', function () { + let buffer = null + + beforeEach(() => + waitsForPromise(() => + atom.project.bufferForPath('a').then(function (o) { + buffer = o + return buffer.retain() + }) + ) + ) + + afterEach(() => buffer.release()) + + return describe('when opening a previously opened path', function () { + it('does not create a new buffer', function () { + waitsForPromise(() => + atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) + ) + + waitsForPromise(() => + atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) + ) + + return waitsForPromise(() => + Promise.all([ + atom.project.bufferForPath('c'), + atom.project.bufferForPath('c') + ]).then(function (...args) { + const [buffer1, buffer2] = Array.from(args[0]) + return expect(buffer1).toBe(buffer2) + }) + ) + }) + + it('retries loading the buffer if it previously failed', function () { + waitsForPromise({shouldReject: true}, function () { + spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file'))) + return atom.project.bufferForPath('b') + }) + + return waitsForPromise({shouldReject: false}, function () { + TextBuffer.load.andCallThrough() + return atom.project.bufferForPath('b') + }) + }) + + return it('creates a new buffer if the previous buffer was destroyed', function () { + buffer.release() + + return waitsForPromise(() => + atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) + ) + }) + }) + }) + + describe('.repositoryForDirectory(directory)', function () { + it('resolves to null when the directory does not have a repository', () => + waitsForPromise(function () { + const directory = new Directory('/tmp') + return atom.project.repositoryForDirectory(directory).then(function (result) { + expect(result).toBeNull() + expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) + return expect(atom.project.repositoryPromisesByPath.size).toBe(0) + }) + }) + ) + + it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => + waitsForPromise(function () { + const directory = new Directory(path.join(__dirname, '..')) + const promise = atom.project.repositoryForDirectory(directory) + return promise.then(function (result) { + expect(result).toBeInstanceOf(GitRepository) + const dirPath = directory.getRealPathSync() + expect(result.getPath()).toBe(path.join(dirPath, '.git')) + + // Verify that the result is cached. + return expect(atom.project.repositoryForDirectory(directory)).toBe(promise) + }) + }) + ) + + return it('creates a new repository if a previous one with the same directory had been destroyed', function () { + let repository = null + const directory = new Directory(path.join(__dirname, '..')) + + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + + runs(function () { + expect(repository.isDestroyed()).toBe(false) + repository.destroy() + return expect(repository.isDestroyed()).toBe(true) + }) + + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + + return runs(() => expect(repository.isDestroyed()).toBe(false)) + }) + }) + + describe('.setPaths(paths, options)', function () { + describe('when path is a file', () => + it("sets its path to the file's parent directory and updates the root directory", function () { + const filePath = require.resolve('./fixtures/dir/a') + atom.project.setPaths([filePath]) + expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) + return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) + }) + ) + + describe('when path is a directory', function () { + it('assigns the directories and repositories', function () { + const directory1 = temp.mkdirSync('non-git-repo') + const directory2 = temp.mkdirSync('git-repo1') + const directory3 = temp.mkdirSync('git-repo2') + + const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')) + fs.copySync(gitDirPath, path.join(directory2, '.git')) + fs.copySync(gitDirPath, path.join(directory3, '.git')) + + atom.project.setPaths([directory1, directory2, directory3]) + + const [repo1, repo2, repo3] = Array.from(atom.project.getRepositories()) + expect(repo1).toBeNull() + expect(repo2.getShortHead()).toBe('master') + expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) + expect(repo3.getShortHead()).toBe('master') + return expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) + }) + + it('calls callbacks registered with ::onDidChangePaths', function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ] + atom.project.setPaths(paths) + + expect(onDidChangePathsSpy.callCount).toBe(1) + return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + }) + + return it('optionally throws an error with any paths that did not exist', function () { + const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] + + try { + atom.project.setPaths(paths, {mustExist: true}) + expect('no exception thrown').toBeUndefined() + } catch (e) { + expect(e.missingProjectPaths).toEqual([paths[1], paths[3]]) + } + + return expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) + }) + }) + + describe('when no paths are given', () => + it('clears its path', function () { + atom.project.setPaths([]) + expect(atom.project.getPaths()).toEqual([]) + return expect(atom.project.getDirectories()).toEqual([]) + }) + ) + + return it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) + expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + }) + }) + + describe('.addPath(path, options)', function () { + it('calls callbacks registered with ::onDidChangePaths', function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + const [oldPath] = Array.from(atom.project.getPaths()) + + const newPath = temp.mkdirSync('dir') + atom.project.addPath(newPath) + + expect(onDidChangePathsSpy.callCount).toBe(1) + return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + }) + + it("doesn't add redundant paths", function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + const [oldPath] = Array.from(atom.project.getPaths()) + + // Doesn't re-add an existing root directory + atom.project.addPath(oldPath) + expect(atom.project.getPaths()).toEqual([oldPath]) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + + // Doesn't add an entry for a file-path within an existing root directory + atom.project.addPath(path.join(oldPath, 'some-file.txt')) + expect(atom.project.getPaths()).toEqual([oldPath]) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + + // Does add an entry for a directory within an existing directory + const newPath = path.join(oldPath, 'a-dir') + atom.project.addPath(newPath) + expect(atom.project.getPaths()).toEqual([oldPath, newPath]) + return expect(onDidChangePathsSpy).toHaveBeenCalled() + }) + + it("doesn't add non-existent directories", function () { + const previousPaths = atom.project.getPaths() + atom.project.addPath('/this-definitely/does-not-exist') + return expect(atom.project.getPaths()).toEqual(previousPaths) + }) + + return it('optionally throws on non-existent directories', () => + expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow() + ) + }) + + describe('.removePath(path)', function () { + let onDidChangePathsSpy = null + + beforeEach(function () { + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') + return atom.project.onDidChangePaths(onDidChangePathsSpy) + }) + + it('removes the directory and repository for the path', function () { + const result = atom.project.removePath(atom.project.getPaths()[0]) + expect(atom.project.getDirectories()).toEqual([]) + expect(atom.project.getRepositories()).toEqual([]) + expect(atom.project.getPaths()).toEqual([]) + expect(result).toBe(true) + return expect(onDidChangePathsSpy).toHaveBeenCalled() + }) + + it("does nothing if the path is not one of the project's root paths", function () { + const originalPaths = atom.project.getPaths() + const result = atom.project.removePath(originalPaths[0] + 'xyz') + expect(result).toBe(false) + expect(atom.project.getPaths()).toEqual(originalPaths) + return expect(onDidChangePathsSpy).not.toHaveBeenCalled() + }) + + it("doesn't destroy the repository if it is shared by another root directory", function () { + atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) + atom.project.removePath(__dirname) + expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) + return expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) + }) + + return it('removes a path that is represented as a URI', function () { + atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + return { + getPath () { return uri }, + getSubdirectory () { return {} }, + isRoot () { return true }, + existsSync () { return true }, + off () {} + } + } + }) + + const ftpURI = 'ftp://example.com/some/folder' + + atom.project.setPaths([ftpURI]) + expect(atom.project.getPaths()).toEqual([ftpURI]) + + atom.project.removePath(ftpURI) + return expect(atom.project.getPaths()).toEqual([]) + }) + }) + + describe('.onDidChangeFiles()', function () { + let sub = [] + const events = [] + let checkCallback = function () {} + + beforeEach(() => + sub = atom.project.onDidChangeFiles(function (incoming) { + events.push(...Array.from(incoming || [])) + return checkCallback() + }) + ) + + afterEach(() => sub.dispose()) + + const waitForEvents = function (paths) { + const remaining = new Set(paths.map((p) => fs.realpathSync(p))) + return new Promise(function (resolve, reject) { + checkCallback = function () { + for (let event of events) { remaining.delete(event.path) } + if (remaining.size === 0) { return resolve() } + } + + const expire = function () { + checkCallback = function () {} + console.error('Paths not seen:', Array.from(remaining)) + return reject(new Error('Expired before all expected events were delivered.')) + } + + checkCallback() + return setTimeout(expire, 2000) + }) + } + + return it('reports filesystem changes within project paths', function () { + const dirOne = temp.mkdirSync('atom-spec-project-one') + const fileOne = path.join(dirOne, 'file-one.txt') + const fileTwo = path.join(dirOne, 'file-two.txt') + const dirTwo = temp.mkdirSync('atom-spec-project-two') + const fileThree = path.join(dirTwo, 'file-three.txt') + + // Ensure that all preexisting watchers are stopped + waitsForPromise(() => stopAllWatchers()) + + runs(() => atom.project.setPaths([dirOne])) + waitsForPromise(() => atom.project.getWatcherPromise(dirOne)) + + runs(function () { + expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined) + + fs.writeFileSync(fileThree, 'three\n') + fs.writeFileSync(fileTwo, 'two\n') + return fs.writeFileSync(fileOne, 'one\n') + }) + + waitsForPromise(() => waitForEvents([fileOne, fileTwo])) + + return runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) + }) + }) + + describe('.onDidAddBuffer()', () => + it('invokes the callback with added text buffers', function () { + const buffers = [] + const added = [] + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) + .then(o => buffers.push(o)) + ) + + runs(function () { + expect(buffers.length).toBe(1) + return atom.project.onDidAddBuffer(buffer => added.push(buffer)) + }) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + return runs(function () { + expect(buffers.length).toBe(2) + return expect(added).toEqual([buffers[1]]) + }) + }) +) + + describe('.observeBuffers()', () => + it('invokes the observer with current and future text buffers', function () { + const buffers = [] + const observed = [] + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) + .then(o => buffers.push(o)) + ) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + runs(function () { + expect(buffers.length).toBe(2) + atom.project.observeBuffers(buffer => observed.push(buffer)) + return expect(observed).toEqual(buffers) + }) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + return runs(function () { + expect(observed.length).toBe(3) + expect(buffers.length).toBe(3) + return expect(observed).toEqual(buffers) + }) + }) + ) + + describe('.relativize(path)', function () { + it('returns the path, relative to whichever root directory it is inside of', function () { + atom.project.addPath(temp.mkdirSync('another-path')) + + let rootPath = atom.project.getPaths()[0] + let childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + + rootPath = atom.project.getPaths()[1] + childPath = path.join(rootPath, 'some', 'child', 'directory') + return expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + }) + + return it('returns the given path if it is not in any of the root directories', function () { + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.relativize(randomPath)).toBe(randomPath) + }) + }) + + describe('.relativizePath(path)', function () { + it('returns the root path that contains the given path, and the path relativized to that root path', function () { + atom.project.addPath(temp.mkdirSync('another-path')) + + let rootPath = atom.project.getPaths()[0] + let childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + + rootPath = atom.project.getPaths()[1] + childPath = path.join(rootPath, 'some', 'child', 'directory') + return expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + }) + + describe("when the given path isn't inside of any of the project's path", () => + it('returns null for the root path, and the given path unchanged', function () { + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) + }) + ) + + describe('when the given path is a URL', () => + it('returns null for the root path, and the given path unchanged', function () { + const url = 'http://the-path' + return expect(atom.project.relativizePath(url)).toEqual([null, url]) + }) + ) + + return describe('when the given path is inside more than one root folder', () => + it('uses the root folder that is closest to the given path', function () { + atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) + + const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') + + expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true) + expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true) + return expect(atom.project.relativizePath(inputPath)).toEqual([ + atom.project.getPaths()[1], + path.join('somewhere', 'something.txt') + ]) + }) + ) + }) + + describe('.contains(path)', () => + it('returns whether or not the given path is in one of the root directories', function () { + const rootPath = atom.project.getPaths()[0] + const childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.contains(childPath)).toBe(true) + + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.contains(randomPath)).toBe(false) + }) + ) + + return describe('.resolvePath(uri)', () => + it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) + ) +}) + +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +} From 61b228d8a056a14bf79dacdafd305e736d3ac92b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:25:10 -0400 Subject: [PATCH 002/161] Remove unnecessary use of Array.from --- spec/project-spec.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index d54c0c9a2..31646b8a4 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments @@ -230,7 +229,7 @@ describe('Project', function () { ) describe('before and after saving a buffer', function () { - let [buffer] = Array.from([]) + let buffer beforeEach(() => waitsForPromise(() => atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { @@ -286,7 +285,7 @@ describe('Project', function () { }) describe('when a custom repository-provider service is provided', function () { - let [fakeRepositoryProvider, fakeRepository] = Array.from([]) + let fakeRepositoryProvider, fakeRepository beforeEach(function () { fakeRepository = {destroy () { return null }} @@ -391,7 +390,7 @@ describe('Project', function () { }) describe('.open(path)', function () { - let [absolutePath, newBufferHandler] = Array.from([]) + let absolutePath, newBufferHandler beforeEach(function () { absolutePath = require.resolve('./fixtures/dir/a') @@ -485,8 +484,7 @@ describe('Project', function () { Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') - ]).then(function (...args) { - const [buffer1, buffer2] = Array.from(args[0]) + ]).then(function ([buffer1, buffer2]) { return expect(buffer1).toBe(buffer2) }) ) @@ -581,7 +579,7 @@ describe('Project', function () { atom.project.setPaths([directory1, directory2, directory3]) - const [repo1, repo2, repo3] = Array.from(atom.project.getRepositories()) + const [repo1, repo2, repo3] = atom.project.getRepositories() expect(repo1).toBeNull() expect(repo2.getShortHead()).toBe('master') expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) @@ -634,7 +632,7 @@ describe('Project', function () { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) - const [oldPath] = Array.from(atom.project.getPaths()) + const [oldPath] = atom.project.getPaths() const newPath = temp.mkdirSync('dir') atom.project.addPath(newPath) @@ -646,7 +644,7 @@ describe('Project', function () { it("doesn't add redundant paths", function () { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) - const [oldPath] = Array.from(atom.project.getPaths()) + const [oldPath] = atom.project.getPaths() // Doesn't re-add an existing root directory atom.project.addPath(oldPath) @@ -738,7 +736,7 @@ describe('Project', function () { beforeEach(() => sub = atom.project.onDidChangeFiles(function (incoming) { - events.push(...Array.from(incoming || [])) + events.push(...incoming || []) return checkCallback() }) ) @@ -755,7 +753,7 @@ describe('Project', function () { const expire = function () { checkCallback = function () {} - console.error('Paths not seen:', Array.from(remaining)) + console.error('Paths not seen:', remaining) return reject(new Error('Expired before all expected events were delivered.')) } From db115d3ab843445de8b30374a1c95436ff731cc8 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:30:46 -0400 Subject: [PATCH 003/161] Remove unnecessary code created because of implicit returns --- spec/project-spec.js | 211 +++++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 106 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 31646b8a4..999b63989 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks @@ -20,7 +19,7 @@ describe('Project', function () { atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) // Wait for project's service consumers to be asynchronously added - return waits(1) + waits(1) }) describe('serialization', function () { @@ -35,7 +34,7 @@ describe('Project', function () { if (notQuittingProject != null) { notQuittingProject.destroy() } - return (quittingProject != null ? quittingProject.destroy() : undefined) + (quittingProject != null ? quittingProject.destroy() : undefined) }) it("does not deserialize paths to directories that don't exist", function () { @@ -49,9 +48,9 @@ describe('Project', function () { .catch(e => err = e) ) - return runs(function () { + runs(function () { expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) - return expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) + expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) }) }) @@ -72,9 +71,9 @@ describe('Project', function () { .catch(e => err = e) ) - return runs(function () { + runs(function () { expect(deserializedProject.getPaths()).toEqual([]) - return expect(err.missingProjectPaths).toEqual([childPath]) + expect(err.missingProjectPaths).toEqual([childPath]) }) }) @@ -84,12 +83,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { @@ -97,15 +96,15 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(function () { + runs(function () { expect(deserializedProject.getBuffers().length).toBe(1) deserializedProject.getBuffers()[0].destroy() - return expect(deserializedProject.getBuffers().length).toBe(0) + expect(deserializedProject.getBuffers().length).toBe(0) }) }) @@ -117,12 +116,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.mkdirSync(pathToOpen) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('does not deserialize buffers when their path is inaccessible', function () { @@ -135,12 +134,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.chmodSync(pathToOpen, '000') - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('does not deserialize buffers with their path is no longer present', function () { @@ -152,12 +151,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.unlinkSync(pathToOpen) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('deserializes buffers that have never been saved before', function () { @@ -169,19 +168,19 @@ describe('Project', function () { atom.workspace.getActiveTextEditor().setText('unsaved\n') expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(function () { + runs(function () { expect(deserializedProject.getBuffers().length).toBe(1) expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) - return expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') + expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') }) }) - return it('serializes marker layers and history only if Atom is quitting', function () { + it('serializes marker layers and history only if Atom is quitting', function () { waitsForPromise(() => atom.workspace.open('a')) let bufferA = null @@ -193,7 +192,7 @@ describe('Project', function () { layerA = bufferA.addMarkerLayer({persistent: true}) markerA = layerA.markPosition([0, 3]) bufferA.append('!') - return notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) @@ -201,14 +200,14 @@ describe('Project', function () { runs(function () { expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) - return quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) - return runs(function () { + runs(function () { expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() - return expect(quittingProject.getBuffers()[0].undo()).toBe(true) + expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) }) }) @@ -224,7 +223,7 @@ describe('Project', function () { waitsForPromise(() => editor.saveAs(tempFile)) - return runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) + runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) }) ) @@ -234,24 +233,24 @@ describe('Project', function () { waitsForPromise(() => atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { buffer = o - return buffer.retain() + buffer.retain() }) ) ) afterEach(() => buffer.release()) - return it('emits save events on the main process', function () { + it('emits save events on the main process', function () { spyOn(atom.project.applicationDelegate, 'emitDidSavePath') spyOn(atom.project.applicationDelegate, 'emitWillSavePath') waitsForPromise(() => buffer.save()) - return runs(function () { + runs(function () { expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) - return expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) + expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) }) }) }) @@ -262,7 +261,7 @@ describe('Project', function () { waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) ) - return it('creates a warning notification', function () { + it('creates a warning notification', function () { let noteSpy atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) @@ -280,7 +279,7 @@ describe('Project', function () { expect(notification.getType()).toBe('warning') expect(notification.getDetail()).toBe('SomeError') expect(notification.getMessage()).toContain('`resurrect`') - return expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) + expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) }) }) @@ -289,7 +288,7 @@ describe('Project', function () { beforeEach(function () { fakeRepository = {destroy () { return null }} - return fakeRepositoryProvider = { + fakeRepositoryProvider = { repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, repositoryForDirectorySync (directory) { return fakeRepository } } @@ -302,7 +301,7 @@ describe('Project', function () { atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(() => atom.project.getRepositories()[0] === fakeRepository) + runs(() => atom.project.getRepositories()[0] === fakeRepository) }) it('does not create any new repositories if every directory has a repository', function () { @@ -312,18 +311,18 @@ describe('Project', function () { atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(() => expect(atom.project.getRepositories()).toBe(repositories)) + runs(() => expect(atom.project.getRepositories()).toBe(repositories)) }) - return it('stops using it to create repositories when the service is removed', function () { + it('stops using it to create repositories when the service is removed', function () { atom.project.setPaths([]) const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(function () { + runs(function () { disposable.dispose() atom.project.addPath(temp.mkdirSync('atom-project')) - return expect(atom.project.getRepositories()).toEqual([null]) + expect(atom.project.getRepositories()).toEqual([null]) }) }) }) @@ -354,7 +353,7 @@ describe('Project', function () { } }) - return waitsFor(() => atom.project.directoryProviders.length > 0) + waitsFor(() => atom.project.directoryProviders.length > 0) }) it("uses the provider's custom directories for any paths that it handles", function () { @@ -379,13 +378,13 @@ describe('Project', function () { atom.project.addPath(newRemotePath) directories = atom.project.getDirectories() expect(directories[2].getPath()).toBe(newRemotePath) - return expect(directories[2] instanceof DummyDirectory).toBe(true) + expect(directories[2] instanceof DummyDirectory).toBe(true) }) - return it('stops using the provider when the service is removed', function () { + it('stops using the provider when the service is removed', function () { serviceDisposable.dispose() atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) - return expect(atom.project.getDirectories().length).toBe(0) + expect(atom.project.getDirectories().length).toBe(0) }) }) @@ -395,7 +394,7 @@ describe('Project', function () { beforeEach(function () { absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') - return atom.project.onDidAddBuffer(newBufferHandler) + atom.project.onDidAddBuffer(newBufferHandler) }) describe("when given an absolute path that isn't currently open", () => @@ -403,9 +402,9 @@ describe('Project', function () { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBe(absolutePath) - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -415,9 +414,9 @@ describe('Project', function () { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBe(absolutePath) - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -434,23 +433,23 @@ describe('Project', function () { atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer)) ) - return waitsForPromise(() => + waitsForPromise(() => atom.workspace.open('a').then(function ({buffer}) { expect(buffer).toBe(editor.buffer) - return expect(newBufferHandler).not.toHaveBeenCalled() + expect(newBufferHandler).not.toHaveBeenCalled() }) ) }) ) - return describe('when not passed a path', () => + describe('when not passed a path', () => it("returns a new edit session and emits 'buffer-created'", function () { let editor = null waitsForPromise(() => atom.workspace.open().then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBeUndefined() - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -463,14 +462,14 @@ describe('Project', function () { waitsForPromise(() => atom.project.bufferForPath('a').then(function (o) { buffer = o - return buffer.retain() + buffer.retain() }) ) ) afterEach(() => buffer.release()) - return describe('when opening a previously opened path', function () { + describe('when opening a previously opened path', function () { it('does not create a new buffer', function () { waitsForPromise(() => atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) @@ -480,12 +479,12 @@ describe('Project', function () { atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) ) - return waitsForPromise(() => + waitsForPromise(() => Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') ]).then(function ([buffer1, buffer2]) { - return expect(buffer1).toBe(buffer2) + expect(buffer1).toBe(buffer2) }) ) }) @@ -496,16 +495,16 @@ describe('Project', function () { return atom.project.bufferForPath('b') }) - return waitsForPromise({shouldReject: false}, function () { + waitsForPromise({shouldReject: false}, function () { TextBuffer.load.andCallThrough() return atom.project.bufferForPath('b') }) }) - return it('creates a new buffer if the previous buffer was destroyed', function () { + it('creates a new buffer if the previous buffer was destroyed', function () { buffer.release() - return waitsForPromise(() => + waitsForPromise(() => atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) ) }) @@ -519,7 +518,7 @@ describe('Project', function () { return atom.project.repositoryForDirectory(directory).then(function (result) { expect(result).toBeNull() expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) - return expect(atom.project.repositoryPromisesByPath.size).toBe(0) + expect(atom.project.repositoryPromisesByPath.size).toBe(0) }) }) ) @@ -534,12 +533,12 @@ describe('Project', function () { expect(result.getPath()).toBe(path.join(dirPath, '.git')) // Verify that the result is cached. - return expect(atom.project.repositoryForDirectory(directory)).toBe(promise) + expect(atom.project.repositoryForDirectory(directory)).toBe(promise) }) }) ) - return it('creates a new repository if a previous one with the same directory had been destroyed', function () { + it('creates a new repository if a previous one with the same directory had been destroyed', function () { let repository = null const directory = new Directory(path.join(__dirname, '..')) @@ -548,12 +547,12 @@ describe('Project', function () { runs(function () { expect(repository.isDestroyed()).toBe(false) repository.destroy() - return expect(repository.isDestroyed()).toBe(true) + expect(repository.isDestroyed()).toBe(true) }) waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) - return runs(() => expect(repository.isDestroyed()).toBe(false)) + runs(() => expect(repository.isDestroyed()).toBe(false)) }) }) @@ -563,7 +562,7 @@ describe('Project', function () { const filePath = require.resolve('./fixtures/dir/a') atom.project.setPaths([filePath]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) - return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) + expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) }) ) @@ -584,7 +583,7 @@ describe('Project', function () { expect(repo2.getShortHead()).toBe('master') expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) expect(repo3.getShortHead()).toBe('master') - return expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) + expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) }) it('calls callbacks registered with ::onDidChangePaths', function () { @@ -595,10 +594,10 @@ describe('Project', function () { atom.project.setPaths(paths) expect(onDidChangePathsSpy.callCount).toBe(1) - return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) }) - return it('optionally throws an error with any paths that did not exist', function () { + it('optionally throws an error with any paths that did not exist', function () { const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] try { @@ -608,7 +607,7 @@ describe('Project', function () { expect(e.missingProjectPaths).toEqual([paths[1], paths[3]]) } - return expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) + expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) }) }) @@ -616,14 +615,14 @@ describe('Project', function () { it('clears its path', function () { atom.project.setPaths([]) expect(atom.project.getPaths()).toEqual([]) - return expect(atom.project.getDirectories()).toEqual([]) + expect(atom.project.getDirectories()).toEqual([]) }) ) - return it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) - return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) }) }) @@ -638,7 +637,7 @@ describe('Project', function () { atom.project.addPath(newPath) expect(onDidChangePathsSpy.callCount).toBe(1) - return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) }) it("doesn't add redundant paths", function () { @@ -660,16 +659,16 @@ describe('Project', function () { const newPath = path.join(oldPath, 'a-dir') atom.project.addPath(newPath) expect(atom.project.getPaths()).toEqual([oldPath, newPath]) - return expect(onDidChangePathsSpy).toHaveBeenCalled() + expect(onDidChangePathsSpy).toHaveBeenCalled() }) it("doesn't add non-existent directories", function () { const previousPaths = atom.project.getPaths() atom.project.addPath('/this-definitely/does-not-exist') - return expect(atom.project.getPaths()).toEqual(previousPaths) + expect(atom.project.getPaths()).toEqual(previousPaths) }) - return it('optionally throws on non-existent directories', () => + it('optionally throws on non-existent directories', () => expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow() ) }) @@ -679,7 +678,7 @@ describe('Project', function () { beforeEach(function () { onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') - return atom.project.onDidChangePaths(onDidChangePathsSpy) + atom.project.onDidChangePaths(onDidChangePathsSpy) }) it('removes the directory and repository for the path', function () { @@ -688,7 +687,7 @@ describe('Project', function () { expect(atom.project.getRepositories()).toEqual([]) expect(atom.project.getPaths()).toEqual([]) expect(result).toBe(true) - return expect(onDidChangePathsSpy).toHaveBeenCalled() + expect(onDidChangePathsSpy).toHaveBeenCalled() }) it("does nothing if the path is not one of the project's root paths", function () { @@ -696,17 +695,17 @@ describe('Project', function () { const result = atom.project.removePath(originalPaths[0] + 'xyz') expect(result).toBe(false) expect(atom.project.getPaths()).toEqual(originalPaths) - return expect(onDidChangePathsSpy).not.toHaveBeenCalled() + expect(onDidChangePathsSpy).not.toHaveBeenCalled() }) it("doesn't destroy the repository if it is shared by another root directory", function () { atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) atom.project.removePath(__dirname) expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) - return expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) + expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) }) - return it('removes a path that is represented as a URI', function () { + it('removes a path that is represented as a URI', function () { atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return { @@ -725,7 +724,7 @@ describe('Project', function () { expect(atom.project.getPaths()).toEqual([ftpURI]) atom.project.removePath(ftpURI) - return expect(atom.project.getPaths()).toEqual([]) + expect(atom.project.getPaths()).toEqual([]) }) }) @@ -737,7 +736,7 @@ describe('Project', function () { beforeEach(() => sub = atom.project.onDidChangeFiles(function (incoming) { events.push(...incoming || []) - return checkCallback() + checkCallback() }) ) @@ -748,21 +747,21 @@ describe('Project', function () { return new Promise(function (resolve, reject) { checkCallback = function () { for (let event of events) { remaining.delete(event.path) } - if (remaining.size === 0) { return resolve() } + if (remaining.size === 0) { resolve() } } const expire = function () { checkCallback = function () {} console.error('Paths not seen:', remaining) - return reject(new Error('Expired before all expected events were delivered.')) + reject(new Error('Expired before all expected events were delivered.')) } checkCallback() - return setTimeout(expire, 2000) + setTimeout(expire, 2000) }) } - return it('reports filesystem changes within project paths', function () { + it('reports filesystem changes within project paths', function () { const dirOne = temp.mkdirSync('atom-spec-project-one') const fileOne = path.join(dirOne, 'file-one.txt') const fileTwo = path.join(dirOne, 'file-two.txt') @@ -780,12 +779,12 @@ describe('Project', function () { fs.writeFileSync(fileThree, 'three\n') fs.writeFileSync(fileTwo, 'two\n') - return fs.writeFileSync(fileOne, 'one\n') + fs.writeFileSync(fileOne, 'one\n') }) waitsForPromise(() => waitForEvents([fileOne, fileTwo])) - return runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) + runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) }) }) @@ -801,7 +800,7 @@ describe('Project', function () { runs(function () { expect(buffers.length).toBe(1) - return atom.project.onDidAddBuffer(buffer => added.push(buffer)) + atom.project.onDidAddBuffer(buffer => added.push(buffer)) }) waitsForPromise(() => @@ -809,9 +808,9 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - return runs(function () { + runs(function () { expect(buffers.length).toBe(2) - return expect(added).toEqual([buffers[1]]) + expect(added).toEqual([buffers[1]]) }) }) ) @@ -834,7 +833,7 @@ describe('Project', function () { runs(function () { expect(buffers.length).toBe(2) atom.project.observeBuffers(buffer => observed.push(buffer)) - return expect(observed).toEqual(buffers) + expect(observed).toEqual(buffers) }) waitsForPromise(() => @@ -842,10 +841,10 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - return runs(function () { + runs(function () { expect(observed.length).toBe(3) expect(buffers.length).toBe(3) - return expect(observed).toEqual(buffers) + expect(observed).toEqual(buffers) }) }) ) @@ -860,12 +859,12 @@ describe('Project', function () { rootPath = atom.project.getPaths()[1] childPath = path.join(rootPath, 'some', 'child', 'directory') - return expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) }) - return it('returns the given path if it is not in any of the root directories', function () { + it('returns the given path if it is not in any of the root directories', function () { const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.relativize(randomPath)).toBe(randomPath) + expect(atom.project.relativize(randomPath)).toBe(randomPath) }) }) @@ -879,24 +878,24 @@ describe('Project', function () { rootPath = atom.project.getPaths()[1] childPath = path.join(rootPath, 'some', 'child', 'directory') - return expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) }) describe("when the given path isn't inside of any of the project's path", () => it('returns null for the root path, and the given path unchanged', function () { const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) + expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) }) ) describe('when the given path is a URL', () => it('returns null for the root path, and the given path unchanged', function () { const url = 'http://the-path' - return expect(atom.project.relativizePath(url)).toEqual([null, url]) + expect(atom.project.relativizePath(url)).toEqual([null, url]) }) ) - return describe('when the given path is inside more than one root folder', () => + describe('when the given path is inside more than one root folder', () => it('uses the root folder that is closest to the given path', function () { atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) @@ -904,7 +903,7 @@ describe('Project', function () { expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true) expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true) - return expect(atom.project.relativizePath(inputPath)).toEqual([ + expect(atom.project.relativizePath(inputPath)).toEqual([ atom.project.getPaths()[1], path.join('somewhere', 'something.txt') ]) @@ -919,11 +918,11 @@ describe('Project', function () { expect(atom.project.contains(childPath)).toBe(true) const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.contains(randomPath)).toBe(false) + expect(atom.project.contains(randomPath)).toBe(false) }) ) - return describe('.resolvePath(uri)', () => + describe('.resolvePath(uri)', () => it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) ) }) From 498d7c90ebd382ca966b9f1bed32ba8228d225f1 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:45:46 -0400 Subject: [PATCH 004/161] Rewrite code to no longer use __guard__ --- spec/project-spec.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 999b63989..8c45b98b9 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md @@ -16,7 +15,9 @@ const GitRepository = require('../src/git-repository') describe('Project', function () { beforeEach(function () { - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) + const directory = atom.project.getDirectories()[0] + const paths = directory ? [directory.resolve('dir')] : [null] + atom.project.setPaths(paths) // Wait for project's service consumers to be asynchronously added waits(1) @@ -198,7 +199,7 @@ describe('Project', function () { waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) runs(function () { - expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() + expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) @@ -206,7 +207,7 @@ describe('Project', function () { waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) runs(function () { - expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() + expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined() expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) }) @@ -926,7 +927,3 @@ describe('Project', function () { it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) ) }) - -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} From 6e78281a73764d877b7febaf9bacc6a93ceda531 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:48:37 -0400 Subject: [PATCH 005/161] :art: Prefer fat arrow function syntax --- spec/project-spec.js | 224 +++++++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 8c45b98b9..8cd092126 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -13,8 +13,8 @@ const {Directory} = require('pathwatcher') const {stopAllWatchers} = require('../src/path-watcher') const GitRepository = require('../src/git-repository') -describe('Project', function () { - beforeEach(function () { +describe('Project', () => { + beforeEach(() => { const directory = atom.project.getDirectories()[0] const paths = directory ? [directory.resolve('dir')] : [null] atom.project.setPaths(paths) @@ -23,12 +23,12 @@ describe('Project', function () { waits(1) }) - describe('serialization', function () { + describe('serialization', () => { let deserializedProject = null let notQuittingProject = null let quittingProject = null - afterEach(function () { + afterEach(() => { if (deserializedProject != null) { deserializedProject.destroy() } @@ -38,7 +38,7 @@ describe('Project', function () { (quittingProject != null ? quittingProject.destroy() : undefined) }) - it("does not deserialize paths to directories that don't exist", function () { + it("does not deserialize paths to directories that don't exist", () => { deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) const state = atom.project.serialize() state.paths.push('/directory/that/does/not/exist') @@ -49,13 +49,13 @@ describe('Project', function () { .catch(e => err = e) ) - runs(function () { + runs(() => { expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) }) }) - it('does not deserialize paths that are now files', function () { + it('does not deserialize paths that are now files', () => { const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') fs.mkdirSync(childPath) @@ -72,16 +72,16 @@ describe('Project', function () { .catch(e => err = e) ) - runs(function () { + runs(() => { expect(deserializedProject.getPaths()).toEqual([]) expect(err.missingProjectPaths).toEqual([childPath]) }) }) - it('does not include unretained buffers in the serialized state', function () { + it('does not include unretained buffers in the serialized state', () => { waitsForPromise(() => atom.project.bufferForPath('a')) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -92,29 +92,29 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { + it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => { waitsForPromise(() => atom.workspace.open('a')) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(deserializedProject.getBuffers().length).toBe(1) deserializedProject.getBuffers()[0].destroy() expect(deserializedProject.getBuffers().length).toBe(0) }) }) - it('does not deserialize buffers when their path is now a directory', function () { + it('does not deserialize buffers when their path is now a directory', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.mkdirSync(pathToOpen) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -125,14 +125,14 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('does not deserialize buffers when their path is inaccessible', function () { + it('does not deserialize buffers when their path is inaccessible', () => { if (process.platform === 'win32') { return } // chmod not supported on win32 const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') fs.writeFileSync(pathToOpen, '') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.chmodSync(pathToOpen, '000') deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -143,13 +143,13 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('does not deserialize buffers with their path is no longer present', function () { + it('does not deserialize buffers with their path is no longer present', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') fs.writeFileSync(pathToOpen, '') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.unlinkSync(pathToOpen) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -160,12 +160,12 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('deserializes buffers that have never been saved before', function () { + it('deserializes buffers that have never been saved before', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { atom.workspace.getActiveTextEditor().setText('unsaved\n') expect(atom.project.getBuffers().length).toBe(1) @@ -174,21 +174,21 @@ describe('Project', function () { waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(deserializedProject.getBuffers().length).toBe(1) expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') }) }) - it('serializes marker layers and history only if Atom is quitting', function () { + it('serializes marker layers and history only if Atom is quitting', () => { waitsForPromise(() => atom.workspace.open('a')) let bufferA = null let layerA = null let markerA = null - runs(function () { + runs(() => { bufferA = atom.project.getBuffers()[0] layerA = bufferA.addMarkerLayer({persistent: true}) markerA = layerA.markPosition([0, 3]) @@ -198,7 +198,7 @@ describe('Project', function () { waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -206,7 +206,7 @@ describe('Project', function () { waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) - runs(function () { + runs(() => { expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined() expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) @@ -214,7 +214,7 @@ describe('Project', function () { }) describe('when an editor is saved and the project has no path', () => - it("sets the project's path to the saved file's parent directory", function () { + it("sets the project's path to the saved file's parent directory", () => { const tempFile = temp.openSync().path atom.project.setPaths([]) expect(atom.project.getPaths()[0]).toBeUndefined() @@ -228,11 +228,11 @@ describe('Project', function () { }) ) - describe('before and after saving a buffer', function () { + describe('before and after saving a buffer', () => { let buffer beforeEach(() => waitsForPromise(() => - atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { + atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => { buffer = o buffer.retain() }) @@ -241,13 +241,13 @@ describe('Project', function () { afterEach(() => buffer.release()) - it('emits save events on the main process', function () { + it('emits save events on the main process', () => { spyOn(atom.project.applicationDelegate, 'emitDidSavePath') spyOn(atom.project.applicationDelegate, 'emitWillSavePath') waitsForPromise(() => buffer.save()) - runs(function () { + runs(() => { expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) @@ -256,13 +256,13 @@ describe('Project', function () { }) }) - describe('when a watch error is thrown from the TextBuffer', function () { + describe('when a watch error is thrown from the TextBuffer', () => { let editor = null beforeEach(() => waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) ) - it('creates a warning notification', function () { + it('creates a warning notification', () => { let noteSpy atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) @@ -284,10 +284,10 @@ describe('Project', function () { }) }) - describe('when a custom repository-provider service is provided', function () { + describe('when a custom repository-provider service is provided', () => { let fakeRepositoryProvider, fakeRepository - beforeEach(function () { + beforeEach(() => { fakeRepository = {destroy () { return null }} fakeRepositoryProvider = { repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, @@ -295,7 +295,7 @@ describe('Project', function () { } }) - it('uses it to create repositories for any directories that need one', function () { + it('uses it to create repositories for any directories that need one', () => { const projectPath = temp.mkdirSync('atom-project') atom.project.setPaths([projectPath]) expect(atom.project.getRepositories()).toEqual([null]) @@ -305,7 +305,7 @@ describe('Project', function () { runs(() => atom.project.getRepositories()[0] === fakeRepository) }) - it('does not create any new repositories if every directory has a repository', function () { + it('does not create any new repositories if every directory has a repository', () => { const repositories = atom.project.getRepositories() expect(repositories.length).toEqual(1) expect(repositories[0]).toBeTruthy() @@ -315,12 +315,12 @@ describe('Project', function () { runs(() => expect(atom.project.getRepositories()).toBe(repositories)) }) - it('stops using it to create repositories when the service is removed', function () { + it('stops using it to create repositories when the service is removed', () => { atom.project.setPaths([]) const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - runs(function () { + runs(() => { disposable.dispose() atom.project.addPath(temp.mkdirSync('atom-project')) expect(atom.project.getRepositories()).toEqual([null]) @@ -328,7 +328,7 @@ describe('Project', function () { }) }) - describe('when a custom directory-provider service is provided', function () { + describe('when a custom directory-provider service is provided', () => { class DummyDirectory { constructor (path1) { this.path = path1 @@ -343,7 +343,7 @@ describe('Project', function () { let serviceDisposable = null - beforeEach(function () { + beforeEach(() => { serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { if (uri.startsWith('ssh://')) { @@ -357,7 +357,7 @@ describe('Project', function () { waitsFor(() => atom.project.directoryProviders.length > 0) }) - it("uses the provider's custom directories for any paths that it handles", function () { + it("uses the provider's custom directories for any paths that it handles", () => { const localPath = temp.mkdirSync('local-path') const remotePath = 'ssh://foreign-directory:8080/does-exist' @@ -382,28 +382,28 @@ describe('Project', function () { expect(directories[2] instanceof DummyDirectory).toBe(true) }) - it('stops using the provider when the service is removed', function () { + it('stops using the provider when the service is removed', () => { serviceDisposable.dispose() atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) expect(atom.project.getDirectories().length).toBe(0) }) }) - describe('.open(path)', function () { + describe('.open(path)', () => { let absolutePath, newBufferHandler - beforeEach(function () { + beforeEach(() => { absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') atom.project.onDidAddBuffer(newBufferHandler) }) describe("when given an absolute path that isn't currently open", () => - it("returns a new edit session for the given path and emits 'buffer-created'", function () { + it("returns a new edit session for the given path and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -411,11 +411,11 @@ describe('Project', function () { ) describe("when given a relative path that isn't currently opened", () => - it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", function () { + it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -423,7 +423,7 @@ describe('Project', function () { ) describe('when passed the path to a buffer that is currently opened', () => - it('returns a new edit session containing currently opened buffer', function () { + it('returns a new edit session containing currently opened buffer', () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) @@ -435,7 +435,7 @@ describe('Project', function () { ) waitsForPromise(() => - atom.workspace.open('a').then(function ({buffer}) { + atom.workspace.open('a').then(({buffer}) => { expect(buffer).toBe(editor.buffer) expect(newBufferHandler).not.toHaveBeenCalled() }) @@ -444,11 +444,11 @@ describe('Project', function () { ) describe('when not passed a path', () => - it("returns a new edit session and emits 'buffer-created'", function () { + it("returns a new edit session and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open().then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBeUndefined() expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -456,12 +456,12 @@ describe('Project', function () { ) }) - describe('.bufferForPath(path)', function () { + describe('.bufferForPath(path)', () => { let buffer = null beforeEach(() => waitsForPromise(() => - atom.project.bufferForPath('a').then(function (o) { + atom.project.bufferForPath('a').then((o) => { buffer = o buffer.retain() }) @@ -470,8 +470,8 @@ describe('Project', function () { afterEach(() => buffer.release()) - describe('when opening a previously opened path', function () { - it('does not create a new buffer', function () { + describe('when opening a previously opened path', () => { + it('does not create a new buffer', () => { waitsForPromise(() => atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) ) @@ -484,25 +484,25 @@ describe('Project', function () { Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') - ]).then(function ([buffer1, buffer2]) { + ]).then(([buffer1, buffer2]) => { expect(buffer1).toBe(buffer2) }) ) }) - it('retries loading the buffer if it previously failed', function () { - waitsForPromise({shouldReject: true}, function () { + it('retries loading the buffer if it previously failed', () => { + waitsForPromise({shouldReject: true}, () => { spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file'))) return atom.project.bufferForPath('b') }) - waitsForPromise({shouldReject: false}, function () { + waitsForPromise({shouldReject: false}, () => { TextBuffer.load.andCallThrough() return atom.project.bufferForPath('b') }) }) - it('creates a new buffer if the previous buffer was destroyed', function () { + it('creates a new buffer if the previous buffer was destroyed', () => { buffer.release() waitsForPromise(() => @@ -512,11 +512,11 @@ describe('Project', function () { }) }) - describe('.repositoryForDirectory(directory)', function () { + describe('.repositoryForDirectory(directory)', () => { it('resolves to null when the directory does not have a repository', () => - waitsForPromise(function () { + waitsForPromise(() => { const directory = new Directory('/tmp') - return atom.project.repositoryForDirectory(directory).then(function (result) { + return atom.project.repositoryForDirectory(directory).then((result) => { expect(result).toBeNull() expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) expect(atom.project.repositoryPromisesByPath.size).toBe(0) @@ -525,10 +525,10 @@ describe('Project', function () { ) it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => - waitsForPromise(function () { + waitsForPromise(() => { const directory = new Directory(path.join(__dirname, '..')) const promise = atom.project.repositoryForDirectory(directory) - return promise.then(function (result) { + return promise.then((result) => { expect(result).toBeInstanceOf(GitRepository) const dirPath = directory.getRealPathSync() expect(result.getPath()).toBe(path.join(dirPath, '.git')) @@ -539,13 +539,13 @@ describe('Project', function () { }) ) - it('creates a new repository if a previous one with the same directory had been destroyed', function () { + it('creates a new repository if a previous one with the same directory had been destroyed', () => { let repository = null const directory = new Directory(path.join(__dirname, '..')) waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) - runs(function () { + runs(() => { expect(repository.isDestroyed()).toBe(false) repository.destroy() expect(repository.isDestroyed()).toBe(true) @@ -557,9 +557,9 @@ describe('Project', function () { }) }) - describe('.setPaths(paths, options)', function () { + describe('.setPaths(paths, options)', () => { describe('when path is a file', () => - it("sets its path to the file's parent directory and updates the root directory", function () { + it("sets its path to the file's parent directory and updates the root directory", () => { const filePath = require.resolve('./fixtures/dir/a') atom.project.setPaths([filePath]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) @@ -567,8 +567,8 @@ describe('Project', function () { }) ) - describe('when path is a directory', function () { - it('assigns the directories and repositories', function () { + describe('when path is a directory', () => { + it('assigns the directories and repositories', () => { const directory1 = temp.mkdirSync('non-git-repo') const directory2 = temp.mkdirSync('git-repo1') const directory3 = temp.mkdirSync('git-repo2') @@ -587,7 +587,7 @@ describe('Project', function () { expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) }) - it('calls callbacks registered with ::onDidChangePaths', function () { + it('calls callbacks registered with ::onDidChangePaths', () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) @@ -598,7 +598,7 @@ describe('Project', function () { expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) }) - it('optionally throws an error with any paths that did not exist', function () { + it('optionally throws an error with any paths that did not exist', () => { const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] try { @@ -613,22 +613,22 @@ describe('Project', function () { }) describe('when no paths are given', () => - it('clears its path', function () { + it('clears its path', () => { atom.project.setPaths([]) expect(atom.project.getPaths()).toEqual([]) expect(atom.project.getDirectories()).toEqual([]) }) ) - it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + it('normalizes the path to remove consecutive slashes, ., and .. segments', () => { atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) }) }) - describe('.addPath(path, options)', function () { - it('calls callbacks registered with ::onDidChangePaths', function () { + describe('.addPath(path, options)', () => { + it('calls callbacks registered with ::onDidChangePaths', () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) @@ -641,7 +641,7 @@ describe('Project', function () { expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) }) - it("doesn't add redundant paths", function () { + it("doesn't add redundant paths", () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) const [oldPath] = atom.project.getPaths() @@ -663,7 +663,7 @@ describe('Project', function () { expect(onDidChangePathsSpy).toHaveBeenCalled() }) - it("doesn't add non-existent directories", function () { + it("doesn't add non-existent directories", () => { const previousPaths = atom.project.getPaths() atom.project.addPath('/this-definitely/does-not-exist') expect(atom.project.getPaths()).toEqual(previousPaths) @@ -674,15 +674,15 @@ describe('Project', function () { ) }) - describe('.removePath(path)', function () { + describe('.removePath(path)', () => { let onDidChangePathsSpy = null - beforeEach(function () { + beforeEach(() => { onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') atom.project.onDidChangePaths(onDidChangePathsSpy) }) - it('removes the directory and repository for the path', function () { + it('removes the directory and repository for the path', () => { const result = atom.project.removePath(atom.project.getPaths()[0]) expect(atom.project.getDirectories()).toEqual([]) expect(atom.project.getRepositories()).toEqual([]) @@ -691,7 +691,7 @@ describe('Project', function () { expect(onDidChangePathsSpy).toHaveBeenCalled() }) - it("does nothing if the path is not one of the project's root paths", function () { + it("does nothing if the path is not one of the project's root paths", () => { const originalPaths = atom.project.getPaths() const result = atom.project.removePath(originalPaths[0] + 'xyz') expect(result).toBe(false) @@ -699,14 +699,14 @@ describe('Project', function () { expect(onDidChangePathsSpy).not.toHaveBeenCalled() }) - it("doesn't destroy the repository if it is shared by another root directory", function () { + it("doesn't destroy the repository if it is shared by another root directory", () => { atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) atom.project.removePath(__dirname) expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) }) - it('removes a path that is represented as a URI', function () { + it('removes a path that is represented as a URI', () => { atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return { @@ -729,13 +729,13 @@ describe('Project', function () { }) }) - describe('.onDidChangeFiles()', function () { + describe('.onDidChangeFiles()', () => { let sub = [] const events = [] - let checkCallback = function () {} + let checkCallback = () => {} beforeEach(() => - sub = atom.project.onDidChangeFiles(function (incoming) { + sub = atom.project.onDidChangeFiles((incoming) => { events.push(...incoming || []) checkCallback() }) @@ -743,16 +743,16 @@ describe('Project', function () { afterEach(() => sub.dispose()) - const waitForEvents = function (paths) { + const waitForEvents = (paths) => { const remaining = new Set(paths.map((p) => fs.realpathSync(p))) - return new Promise(function (resolve, reject) { - checkCallback = function () { + return new Promise((resolve, reject) => { + checkCallback = () => { for (let event of events) { remaining.delete(event.path) } if (remaining.size === 0) { resolve() } } - const expire = function () { - checkCallback = function () {} + const expire = () => { + checkCallback = () => {} console.error('Paths not seen:', remaining) reject(new Error('Expired before all expected events were delivered.')) } @@ -762,7 +762,7 @@ describe('Project', function () { }) } - it('reports filesystem changes within project paths', function () { + it('reports filesystem changes within project paths', () => { const dirOne = temp.mkdirSync('atom-spec-project-one') const fileOne = path.join(dirOne, 'file-one.txt') const fileTwo = path.join(dirOne, 'file-two.txt') @@ -775,7 +775,7 @@ describe('Project', function () { runs(() => atom.project.setPaths([dirOne])) waitsForPromise(() => atom.project.getWatcherPromise(dirOne)) - runs(function () { + runs(() => { expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined) fs.writeFileSync(fileThree, 'three\n') @@ -790,7 +790,7 @@ describe('Project', function () { }) describe('.onDidAddBuffer()', () => - it('invokes the callback with added text buffers', function () { + it('invokes the callback with added text buffers', () => { const buffers = [] const added = [] @@ -799,7 +799,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(1) atom.project.onDidAddBuffer(buffer => added.push(buffer)) }) @@ -809,7 +809,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(2) expect(added).toEqual([buffers[1]]) }) @@ -817,7 +817,7 @@ describe('Project', function () { ) describe('.observeBuffers()', () => - it('invokes the observer with current and future text buffers', function () { + it('invokes the observer with current and future text buffers', () => { const buffers = [] const observed = [] @@ -831,7 +831,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(2) atom.project.observeBuffers(buffer => observed.push(buffer)) expect(observed).toEqual(buffers) @@ -842,7 +842,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(observed.length).toBe(3) expect(buffers.length).toBe(3) expect(observed).toEqual(buffers) @@ -850,8 +850,8 @@ describe('Project', function () { }) ) - describe('.relativize(path)', function () { - it('returns the path, relative to whichever root directory it is inside of', function () { + describe('.relativize(path)', () => { + it('returns the path, relative to whichever root directory it is inside of', () => { atom.project.addPath(temp.mkdirSync('another-path')) let rootPath = atom.project.getPaths()[0] @@ -863,14 +863,14 @@ describe('Project', function () { expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) }) - it('returns the given path if it is not in any of the root directories', function () { + it('returns the given path if it is not in any of the root directories', () => { const randomPath = path.join('some', 'random', 'path') expect(atom.project.relativize(randomPath)).toBe(randomPath) }) }) - describe('.relativizePath(path)', function () { - it('returns the root path that contains the given path, and the path relativized to that root path', function () { + describe('.relativizePath(path)', () => { + it('returns the root path that contains the given path, and the path relativized to that root path', () => { atom.project.addPath(temp.mkdirSync('another-path')) let rootPath = atom.project.getPaths()[0] @@ -883,21 +883,21 @@ describe('Project', function () { }) describe("when the given path isn't inside of any of the project's path", () => - it('returns null for the root path, and the given path unchanged', function () { + it('returns null for the root path, and the given path unchanged', () => { const randomPath = path.join('some', 'random', 'path') expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) }) ) describe('when the given path is a URL', () => - it('returns null for the root path, and the given path unchanged', function () { + it('returns null for the root path, and the given path unchanged', () => { const url = 'http://the-path' expect(atom.project.relativizePath(url)).toEqual([null, url]) }) ) describe('when the given path is inside more than one root folder', () => - it('uses the root folder that is closest to the given path', function () { + it('uses the root folder that is closest to the given path', () => { atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') @@ -913,7 +913,7 @@ describe('Project', function () { }) describe('.contains(path)', () => - it('returns whether or not the given path is in one of the root directories', function () { + it('returns whether or not the given path is in one of the root directories', () => { const rootPath = atom.project.getPaths()[0] const childPath = path.join(rootPath, 'some', 'child', 'directory') expect(atom.project.contains(childPath)).toBe(true) From 49655a97c84458e88d203d976a3c5d8b4593ae2a Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 18 Oct 2017 20:10:24 -0400 Subject: [PATCH 006/161] :art: --- spec/project-spec.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 8cd092126..747defc3f 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,9 +1,3 @@ -/* - * decaffeinate suggestions: - * DS201: Simplify complex destructure assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const temp = require('temp').track() const TextBuffer = require('text-buffer') const Project = require('../src/project') @@ -35,7 +29,9 @@ describe('Project', () => { if (notQuittingProject != null) { notQuittingProject.destroy() } - (quittingProject != null ? quittingProject.destroy() : undefined) + if (quittingProject != null) { + quittingProject.destroy() + } }) it("does not deserialize paths to directories that don't exist", () => { @@ -330,8 +326,8 @@ describe('Project', () => { describe('when a custom directory-provider service is provided', () => { class DummyDirectory { - constructor (path1) { - this.path = path1 + constructor (aPath) { + this.path = aPath } getPath () { return this.path } getFile () { return {existsSync () { return false }} } @@ -736,7 +732,7 @@ describe('Project', () => { beforeEach(() => sub = atom.project.onDidChangeFiles((incoming) => { - events.push(...incoming || []) + events.push(...incoming) checkCallback() }) ) @@ -924,6 +920,8 @@ describe('Project', () => { ) describe('.resolvePath(uri)', () => - it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) + it('normalizes disk drive letter in passed path on #win32', () => { + expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt') + }) ) }) From 4db60e34b8220c624ce245d8f05d4f0f90ab431c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 18 Oct 2017 20:13:55 -0400 Subject: [PATCH 007/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20linter=20error:=20?= =?UTF-8?q?"Arrow=20function=20should=20not=20return=20assignment."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/project-spec.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 747defc3f..63c065fa6 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -42,7 +42,7 @@ describe('Project', () => { let err = null waitsForPromise(() => deserializedProject.deserialize(state, atom.deserializers) - .catch(e => err = e) + .catch(e => { err = e }) ) runs(() => { @@ -65,7 +65,7 @@ describe('Project', () => { let err = null waitsForPromise(() => deserializedProject.deserialize(state, atom.deserializers) - .catch(e => err = e) + .catch(e => { err = e }) ) runs(() => { @@ -216,7 +216,7 @@ describe('Project', () => { expect(atom.project.getPaths()[0]).toBeUndefined() let editor = null - waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + waitsForPromise(() => atom.workspace.open().then(o => { editor = o })) waitsForPromise(() => editor.saveAs(tempFile)) @@ -255,7 +255,7 @@ describe('Project', () => { describe('when a watch error is thrown from the TextBuffer', () => { let editor = null beforeEach(() => - waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o })) ) it('creates a warning notification', () => { @@ -397,7 +397,7 @@ describe('Project', () => { describe("when given an absolute path that isn't currently open", () => it("returns a new edit session for the given path and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) @@ -409,7 +409,7 @@ describe('Project', () => { describe("when given a relative path that isn't currently opened", () => it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) @@ -422,7 +422,7 @@ describe('Project', () => { it('returns a new edit session containing currently opened buffer', () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => newBufferHandler.reset()) @@ -442,7 +442,7 @@ describe('Project', () => { describe('when not passed a path', () => it("returns a new edit session and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + waitsForPromise(() => atom.workspace.open().then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBeUndefined() @@ -539,7 +539,7 @@ describe('Project', () => { let repository = null const directory = new Directory(path.join(__dirname, '..')) - waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo })) runs(() => { expect(repository.isDestroyed()).toBe(false) @@ -547,7 +547,7 @@ describe('Project', () => { expect(repository.isDestroyed()).toBe(true) }) - waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo })) runs(() => expect(repository.isDestroyed()).toBe(false)) }) @@ -730,12 +730,12 @@ describe('Project', () => { const events = [] let checkCallback = () => {} - beforeEach(() => + beforeEach(() => { sub = atom.project.onDidChangeFiles((incoming) => { events.push(...incoming) checkCallback() }) - ) + }) afterEach(() => sub.dispose()) From d79e6c4b6352e708d97b1d23b29856c9af06c857 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 18 Oct 2017 17:52:03 -0700 Subject: [PATCH 008/161] :arrow_up: tabs@0.108.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc454e3ab..b7e5ddd12 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.7", "symbols-view": "0.118.1", - "tabs": "0.107.4", + "tabs": "0.108.0", "timecop": "0.36.0", "tree-view": "0.219.0", "update-package-dependencies": "0.12.0", From 50243c71f5fec064176c2616290f180f05cd0d18 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Wed, 18 Oct 2017 21:23:08 -0600 Subject: [PATCH 009/161] :arrow_up: autocomplete-snippets@1.11.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7e5ddd12..55b5dfd04 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", "autocomplete-plus": "2.36.7", - "autocomplete-snippets": "1.11.1", + "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", "background-tips": "0.27.1", From 53203e7f1767035e1431000c02e555d6e690b6d8 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 18 Oct 2017 21:27:11 -0700 Subject: [PATCH 010/161] :arrow_up: tree-view@0.220.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55b5dfd04..31c2c917e 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.108.0", "timecop": "0.36.0", - "tree-view": "0.219.0", + "tree-view": "0.220.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From 2289e2b8286fa28404469976599db6db26c07054 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 19 Oct 2017 08:42:20 -0400 Subject: [PATCH 011/161] Decaffeinate src/window-event-handler.coffee --- src/window-event-handler.coffee | 189 ------------------------ src/window-event-handler.js | 253 ++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 189 deletions(-) delete mode 100644 src/window-event-handler.coffee create mode 100644 src/window-event-handler.js diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee deleted file mode 100644 index 6a277b612..000000000 --- a/src/window-event-handler.coffee +++ /dev/null @@ -1,189 +0,0 @@ -{Disposable, CompositeDisposable} = require 'event-kit' -listen = require './delegated-listener' - -# Handles low-level events related to the @window. -module.exports = -class WindowEventHandler - constructor: ({@atomEnvironment, @applicationDelegate}) -> - @reloadRequested = false - @subscriptions = new CompositeDisposable - - @handleNativeKeybindings() - - initialize: (@window, @document) -> - @subscriptions.add @atomEnvironment.commands.add @window, - 'window:toggle-full-screen': @handleWindowToggleFullScreen - 'window:close': @handleWindowClose - 'window:reload': @handleWindowReload - 'window:toggle-dev-tools': @handleWindowToggleDevTools - - if process.platform in ['win32', 'linux'] - @subscriptions.add @atomEnvironment.commands.add @window, - 'window:toggle-menu-bar': @handleWindowToggleMenuBar - - @subscriptions.add @atomEnvironment.commands.add @document, - 'core:focus-next': @handleFocusNext - 'core:focus-previous': @handleFocusPrevious - - @addEventListener(@window, 'beforeunload', @handleWindowBeforeunload) - @addEventListener(@window, 'focus', @handleWindowFocus) - @addEventListener(@window, 'blur', @handleWindowBlur) - - @addEventListener(@document, 'keyup', @handleDocumentKeyEvent) - @addEventListener(@document, 'keydown', @handleDocumentKeyEvent) - @addEventListener(@document, 'drop', @handleDocumentDrop) - @addEventListener(@document, 'dragover', @handleDocumentDragover) - @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu) - @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) - @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - - @subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen)) - @subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen)) - - # Wire commands that should be handled by Chromium for elements with the - # `.native-key-bindings` class. - handleNativeKeybindings: -> - bindCommandToAction = (command, action) => - @subscriptions.add @atomEnvironment.commands.add( - '.native-key-bindings', - command, - ((event) => @applicationDelegate.getCurrentWindow().webContents[action]()), - false - ) - - bindCommandToAction('core:copy', 'copy') - bindCommandToAction('core:paste', 'paste') - bindCommandToAction('core:undo', 'undo') - bindCommandToAction('core:redo', 'redo') - bindCommandToAction('core:select-all', 'selectAll') - bindCommandToAction('core:cut', 'cut') - - unsubscribe: -> - @subscriptions.dispose() - - on: (target, eventName, handler) -> - target.on(eventName, handler) - @subscriptions.add(new Disposable -> - target.removeListener(eventName, handler) - ) - - addEventListener: (target, eventName, handler) -> - target.addEventListener(eventName, handler) - @subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler))) - - handleDocumentKeyEvent: (event) => - @atomEnvironment.keymaps.handleKeyboardEvent(event) - event.stopImmediatePropagation() - - handleDrop: (event) -> - event.preventDefault() - event.stopPropagation() - - handleDragover: (event) -> - event.preventDefault() - event.stopPropagation() - event.dataTransfer.dropEffect = 'none' - - eachTabIndexedElement: (callback) -> - for element in @document.querySelectorAll('[tabindex]') - continue if element.disabled - continue unless element.tabIndex >= 0 - callback(element, element.tabIndex) - return - - handleFocusNext: => - focusedTabIndex = @document.activeElement.tabIndex ? -Infinity - - nextElement = null - nextTabIndex = Infinity - lowestElement = null - lowestTabIndex = Infinity - @eachTabIndexedElement (element, tabIndex) -> - if tabIndex < lowestTabIndex - lowestTabIndex = tabIndex - lowestElement = element - - if focusedTabIndex < tabIndex < nextTabIndex - nextTabIndex = tabIndex - nextElement = element - - if nextElement? - nextElement.focus() - else if lowestElement? - lowestElement.focus() - - handleFocusPrevious: => - focusedTabIndex = @document.activeElement.tabIndex ? Infinity - - previousElement = null - previousTabIndex = -Infinity - highestElement = null - highestTabIndex = -Infinity - @eachTabIndexedElement (element, tabIndex) -> - if tabIndex > highestTabIndex - highestTabIndex = tabIndex - highestElement = element - - if focusedTabIndex > tabIndex > previousTabIndex - previousTabIndex = tabIndex - previousElement = element - - if previousElement? - previousElement.focus() - else if highestElement? - highestElement.focus() - - handleWindowFocus: -> - @document.body.classList.remove('is-blurred') - - handleWindowBlur: => - @document.body.classList.add('is-blurred') - @atomEnvironment.storeWindowDimensions() - - handleEnterFullScreen: => - @document.body.classList.add("fullscreen") - - handleLeaveFullScreen: => - @document.body.classList.remove("fullscreen") - - handleWindowBeforeunload: (event) => - if not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused() - @atomEnvironment.hide() - @reloadRequested = false - @atomEnvironment.storeWindowDimensions() - @atomEnvironment.unloadEditorWindow() - @atomEnvironment.destroy() - - handleWindowToggleFullScreen: => - @atomEnvironment.toggleFullScreen() - - handleWindowClose: => - @atomEnvironment.close() - - handleWindowReload: => - @reloadRequested = true - @atomEnvironment.reload() - - handleWindowToggleDevTools: => - @atomEnvironment.toggleDevTools() - - handleWindowToggleMenuBar: => - @atomEnvironment.config.set('core.autoHideMenuBar', not @atomEnvironment.config.get('core.autoHideMenuBar')) - - if @atomEnvironment.config.get('core.autoHideMenuBar') - detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command" - @atomEnvironment.notifications.addInfo('Menu bar hidden', {detail}) - - handleLinkClick: (event) => - event.preventDefault() - uri = event.currentTarget?.getAttribute('href') - if uri and uri[0] isnt '#' and /^https?:\/\//.test(uri) - @applicationDelegate.openExternal(uri) - - handleFormSubmit: (event) -> - # Prevent form submits from changing the current window's URL - event.preventDefault() - - handleDocumentContextmenu: (event) => - event.preventDefault() - @atomEnvironment.contextMenu.showForEvent(event) diff --git a/src/window-event-handler.js b/src/window-event-handler.js new file mode 100644 index 000000000..6d380819b --- /dev/null +++ b/src/window-event-handler.js @@ -0,0 +1,253 @@ +const {Disposable, CompositeDisposable} = require('event-kit') +const listen = require('./delegated-listener') + +// Handles low-level events related to the `window`. +module.exports = +class WindowEventHandler { + constructor ({atomEnvironment, applicationDelegate}) { + this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this) + this.handleFocusNext = this.handleFocusNext.bind(this) + this.handleFocusPrevious = this.handleFocusPrevious.bind(this) + this.handleWindowBlur = this.handleWindowBlur.bind(this) + this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this) + this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this) + this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this) + this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind(this) + this.handleWindowClose = this.handleWindowClose.bind(this) + this.handleWindowReload = this.handleWindowReload.bind(this) + this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind(this) + this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this) + this.handleLinkClick = this.handleLinkClick.bind(this) + this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this) + this.atomEnvironment = atomEnvironment + this.applicationDelegate = applicationDelegate + this.reloadRequested = false + this.subscriptions = new CompositeDisposable() + + this.handleNativeKeybindings() + } + + initialize (window, document) { + this.window = window + this.document = document + this.subscriptions.add(this.atomEnvironment.commands.add(this.window, { + 'window:toggle-full-screen': this.handleWindowToggleFullScreen, + 'window:close': this.handleWindowClose, + 'window:reload': this.handleWindowReload, + 'window:toggle-dev-tools': this.handleWindowToggleDevTools + })) + + if (['win32', 'linux'].includes(process.platform)) { + this.subscriptions.add(this.atomEnvironment.commands.add(this.window, + {'window:toggle-menu-bar': this.handleWindowToggleMenuBar}) + ) + } + + this.subscriptions.add(this.atomEnvironment.commands.add(this.document, { + 'core:focus-next': this.handleFocusNext, + 'core:focus-previous': this.handleFocusPrevious + })) + + this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload) + this.addEventListener(this.window, 'focus', this.handleWindowFocus) + this.addEventListener(this.window, 'blur', this.handleWindowBlur) + + this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent) + this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent) + this.addEventListener(this.document, 'drop', this.handleDocumentDrop) + this.addEventListener(this.document, 'dragover', this.handleDocumentDragover) + this.addEventListener(this.document, 'contextmenu', this.handleDocumentContextmenu) + this.subscriptions.add(listen(this.document, 'click', 'a', this.handleLinkClick)) + this.subscriptions.add(listen(this.document, 'submit', 'form', this.handleFormSubmit)) + + this.subscriptions.add(this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen)) + this.subscriptions.add(this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen)) + } + + // Wire commands that should be handled by Chromium for elements with the + // `.native-key-bindings` class. + handleNativeKeybindings () { + const bindCommandToAction = (command, action) => { + this.subscriptions.add( + this.atomEnvironment.commands.add( + '.native-key-bindings', + command, + event => this.applicationDelegate.getCurrentWindow().webContents[action](), + false + ) + ) + } + + bindCommandToAction('core:copy', 'copy') + bindCommandToAction('core:paste', 'paste') + bindCommandToAction('core:undo', 'undo') + bindCommandToAction('core:redo', 'redo') + bindCommandToAction('core:select-all', 'selectAll') + bindCommandToAction('core:cut', 'cut') + } + + unsubscribe () { + this.subscriptions.dispose() + } + + on (target, eventName, handler) { + target.on(eventName, handler) + this.subscriptions.add(new Disposable(function () { + target.removeListener(eventName, handler) + })) + } + + addEventListener (target, eventName, handler) { + target.addEventListener(eventName, handler) + this.subscriptions.add(new Disposable(function () { + target.removeEventListener(eventName, handler) + })) + } + + handleDocumentKeyEvent (event) { + this.atomEnvironment.keymaps.handleKeyboardEvent(event) + event.stopImmediatePropagation() + } + + handleDrop (event) { + event.preventDefault() + event.stopPropagation() + } + + handleDragover (event) { + event.preventDefault() + event.stopPropagation() + event.dataTransfer.dropEffect = 'none' + } + + eachTabIndexedElement (callback) { + for (let element of this.document.querySelectorAll('[tabindex]')) { + if (element.disabled) { continue } + if (!(element.tabIndex >= 0)) { continue } + callback(element, element.tabIndex) + } + } + + handleFocusNext () { + const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : -Infinity + + let nextElement = null + let nextTabIndex = Infinity + let lowestElement = null + let lowestTabIndex = Infinity + this.eachTabIndexedElement(function (element, tabIndex) { + if (tabIndex < lowestTabIndex) { + lowestTabIndex = tabIndex + lowestElement = element + } + + if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) { + nextTabIndex = tabIndex + nextElement = element + } + }) + + if (nextElement != null) { + nextElement.focus() + } else if (lowestElement != null) { + lowestElement.focus() + } + } + + handleFocusPrevious () { + const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : Infinity + + let previousElement = null + let previousTabIndex = -Infinity + let highestElement = null + let highestTabIndex = -Infinity + this.eachTabIndexedElement(function (element, tabIndex) { + if (tabIndex > highestTabIndex) { + highestTabIndex = tabIndex + highestElement = element + } + + if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) { + previousTabIndex = tabIndex + previousElement = element + } + }) + + if (previousElement != null) { + previousElement.focus() + } else if (highestElement != null) { + highestElement.focus() + } + } + + handleWindowFocus () { + this.document.body.classList.remove('is-blurred') + } + + handleWindowBlur () { + this.document.body.classList.add('is-blurred') + this.atomEnvironment.storeWindowDimensions() + } + + handleEnterFullScreen () { + this.document.body.classList.add('fullscreen') + } + + handleLeaveFullScreen () { + this.document.body.classList.remove('fullscreen') + } + + handleWindowBeforeunload (event) { + if (!this.reloadRequested && !this.atomEnvironment.inSpecMode() && this.atomEnvironment.getCurrentWindow().isWebViewFocused()) { + this.atomEnvironment.hide() + } + this.reloadRequested = false + this.atomEnvironment.storeWindowDimensions() + this.atomEnvironment.unloadEditorWindow() + this.atomEnvironment.destroy() + } + + handleWindowToggleFullScreen () { + this.atomEnvironment.toggleFullScreen() + } + + handleWindowClose () { + this.atomEnvironment.close() + } + + handleWindowReload () { + this.reloadRequested = true + this.atomEnvironment.reload() + } + + handleWindowToggleDevTools () { + this.atomEnvironment.toggleDevTools() + } + + handleWindowToggleMenuBar () { + this.atomEnvironment.config.set('core.autoHideMenuBar', !this.atomEnvironment.config.get('core.autoHideMenuBar')) + + if (this.atomEnvironment.config.get('core.autoHideMenuBar')) { + const detail = 'To toggle, press the Alt key or execute the window:toggle-menu-bar command' + this.atomEnvironment.notifications.addInfo('Menu bar hidden', {detail}) + } + } + + handleLinkClick (event) { + event.preventDefault() + const uri = event.currentTarget && event.currentTarget.getAttribute('href') + if (uri && (uri[0] !== '#') && /^https?:\/\//.test(uri)) { + this.applicationDelegate.openExternal(uri) + } + } + + handleFormSubmit (event) { + // Prevent form submits from changing the current window's URL + event.preventDefault() + } + + handleDocumentContextmenu (event) { + event.preventDefault() + this.atomEnvironment.contextMenu.showForEvent(event) + } +} From f3dc52c0bd610a7c54971b4a515bed906f0d5d38 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 19 Oct 2017 17:07:29 +0200 Subject: [PATCH 012/161] :arrow_up: language-perl@0.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31c2c917e..64ee0d497 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.37.0", + "language-perl": "0.38.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From fc83739e28f5f6b1044215050d4de70f106ae8fd Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 19 Oct 2017 17:54:22 +0200 Subject: [PATCH 013/161] Revert "Merge pull request #15939 from atom/fk_update_perl" This reverts commit cee38a41d5105b1c34b72338db5f2a49ab2e930c, reversing changes made to 53203e7f1767035e1431000c02e555d6e690b6d8. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64ee0d497..31c2c917e 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.38.0", + "language-perl": "0.37.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From 02b13384437b680d150d3a8a911ba45070acb9ad Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 19 Oct 2017 12:37:12 -0600 Subject: [PATCH 014/161] :arrow_up: autocomplete-plus@2.36.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31c2c917e..e5541ff0e 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.7", + "autocomplete-plus": "2.36.8", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From 9fcc6a9bce21c2404bdce269e02a81f0b7a51682 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 20 Oct 2017 01:27:15 +0200 Subject: [PATCH 015/161] Use endsWith to match modules to exclude from the snapshot --- script/lib/generate-startup-snapshot.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 2905bca1b..333acdc0a 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -27,47 +27,37 @@ module.exports = function (packagedAppPath) { coreModules.has(modulePath) || (relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) || relativePath.startsWith(path.join('..', 'node_modules', 'dugite')) || + relativePath.endsWith(path.join('node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js')) || + relativePath.endsWith(path.join('node_modules', 'fs-extra', 'lib', 'index.js')) || + relativePath.endsWith(path.join('node_modules', 'graceful-fs', 'graceful-fs.js')) || + relativePath.endsWith(path.join('node_modules', 'htmlparser2', 'lib', 'index.js')) || + relativePath.endsWith(path.join('node_modules', 'minimatch', 'minimatch.js')) || relativePath === path.join('..', 'exports', 'atom.js') || relativePath === path.join('..', 'src', 'electron-shims.js') || relativePath === path.join('..', 'src', 'safe-clipboard.js') || relativePath === path.join('..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js') || relativePath === path.join('..', 'node_modules', 'babel-core', 'index.js') || relativePath === path.join('..', 'node_modules', 'cached-run-in-this-context', 'lib', 'main.js') || - relativePath === path.join('..', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') || - relativePath === path.join('..', 'node_modules', 'cson-parser', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') || relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') || relativePath === path.join('..', 'node_modules', 'debug', 'node.js') || - relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') || - relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') || relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') || - relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') || - relativePath === path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') || - relativePath === path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'htmlparser2', 'lib', 'index.js') || - relativePath === path.join('..', 'node_modules', 'roaster', 'node_modules', 'htmlparser2', 'lib', 'index.js') || - relativePath === path.join('..', 'node_modules', 'task-lists', 'node_modules', 'htmlparser2', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'less', 'index.js') || relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') || - relativePath === path.join('..', 'node_modules', 'less', 'node_modules', 'graceful-fs', 'graceful-fs.js') || - relativePath === path.join('..', 'node_modules', 'minimatch', 'minimatch.js') || relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') || - relativePath === path.join('..', 'node_modules', 'nsfw', 'node_modules', 'fs-extra', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') || relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') || relativePath === path.join('..', 'node_modules', 'request', 'index.js') || relativePath === path.join('..', 'node_modules', 'resolve', 'index.js') || relativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') || - relativePath === path.join('..', 'node_modules', 'scandal', 'node_modules', 'minimatch', 'minimatch.js') || relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') || - relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'minimatch', 'minimatch.js') || relativePath === path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') || relativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') || relativePath === path.join('..', 'node_modules', 'tar', 'tar.js') || relativePath === path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') || - relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || - relativePath === path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js') + relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') ) } }).then((snapshotScript) => { From 31cc7251383e50d31d291a0882394eafd783a94f Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Fri, 14 Apr 2017 16:01:12 +0300 Subject: [PATCH 016/161] :arrow_up: coffee-script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..18dff266f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "chai": "3.5.0", "chart.js": "^2.3.0", "clear-cut": "^2.0.2", - "coffee-script": "1.11.1", + "coffee-script": "1.12.7", "color": "^0.7.3", "dedent": "^0.6.0", "devtron": "1.3.0", From 0f89211d55cbcc5f4fdbfa6f76ed6cb709c98783 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Fri, 20 Oct 2017 13:34:15 +0300 Subject: [PATCH 017/161] Prioritize first line matches over bundled/non bundled cirteria --- .../packages/package-with-rb-filetype/grammars/rb.cson | 1 + spec/grammars-spec.coffee | 2 ++ src/grammar-registry.js | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson index 8b4d85412..37aac3d4d 100644 --- a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson +++ b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson @@ -1,5 +1,6 @@ 'name': 'Test Ruby' 'scopeName': 'test.rb' +'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)' 'fileTypes': [ 'rb' ] diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 7b70797ba..db716528d 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -120,6 +120,8 @@ describe "the `grammars` global", -> atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true atom.grammars.grammarForScopeName('test.rb').bundledPackage = false + expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby' + expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb' expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb' describe "when there is no file path", -> diff --git a/src/grammar-registry.js b/src/grammar-registry.js index b1de16ba1..f2994acf1 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -58,10 +58,10 @@ class GrammarRegistry extends FirstMate.GrammarRegistry { let score = this.getGrammarPathScore(grammar, filePath) if ((score > 0) && !grammar.bundledPackage) { - score += 0.25 + score += 0.125 } if (this.grammarMatchesContents(grammar, contents)) { - score += 0.125 + score += 0.25 } return score } From d23510fce97012efd1a7fa5db1a95406e8bbd4d5 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 08:20:40 -0400 Subject: [PATCH 018/161] =?UTF-8?q?=E2=98=A0=E2=98=95=EF=B8=8F=20Decaffein?= =?UTF-8?q?ate=20spec/window-event-handler-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/window-event-handler-spec.coffee | 209 ----------------------- spec/window-event-handler-spec.js | 228 ++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 209 deletions(-) delete mode 100644 spec/window-event-handler-spec.coffee create mode 100644 spec/window-event-handler-spec.js diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee deleted file mode 100644 index 9c9f4a098..000000000 --- a/spec/window-event-handler-spec.coffee +++ /dev/null @@ -1,209 +0,0 @@ -KeymapManager = require 'atom-keymap' -TextEditor = require '../src/text-editor' -WindowEventHandler = require '../src/window-event-handler' -{ipcRenderer} = require 'electron' - -describe "WindowEventHandler", -> - [windowEventHandler] = [] - - beforeEach -> - atom.uninstallWindowEventHandler() - spyOn(atom, 'hide') - initialPath = atom.project.getPaths()[0] - spyOn(atom, 'getLoadSettings').andCallFake -> - loadSettings = atom.getLoadSettings.originalValue.call(atom) - loadSettings.initialPath = initialPath - loadSettings - atom.project.destroy() - windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate}) - windowEventHandler.initialize(window, document) - - afterEach -> - windowEventHandler.unsubscribe() - atom.installWindowEventHandler() - - describe "when the window is loaded", -> - it "doesn't have .is-blurred on the body tag", -> - return if process.platform is 'win32' #Win32TestFailures - can not steal focus - expect(document.body.className).not.toMatch("is-blurred") - - describe "when the window is blurred", -> - beforeEach -> - window.dispatchEvent(new CustomEvent('blur')) - - afterEach -> - document.body.classList.remove('is-blurred') - - it "adds the .is-blurred class on the body", -> - expect(document.body.className).toMatch("is-blurred") - - describe "when the window is focused again", -> - it "removes the .is-blurred class from the body", -> - window.dispatchEvent(new CustomEvent('focus')) - expect(document.body.className).not.toMatch("is-blurred") - - describe "window:close event", -> - it "closes the window", -> - spyOn(atom, 'close') - window.dispatchEvent(new CustomEvent('window:close')) - expect(atom.close).toHaveBeenCalled() - - describe "when a link is clicked", -> - it "opens the http/https links in an external application", -> - {shell} = require 'electron' - spyOn(shell, 'openExternal') - - link = document.createElement('a') - linkChild = document.createElement('span') - link.appendChild(linkChild) - link.href = 'http://github.com' - jasmine.attachToDOM(link) - fakeEvent = {target: linkChild, currentTarget: link, preventDefault: (->)} - - windowEventHandler.handleLinkClick(fakeEvent) - expect(shell.openExternal).toHaveBeenCalled() - expect(shell.openExternal.argsForCall[0][0]).toBe "http://github.com" - shell.openExternal.reset() - - link.href = 'https://github.com' - windowEventHandler.handleLinkClick(fakeEvent) - expect(shell.openExternal).toHaveBeenCalled() - expect(shell.openExternal.argsForCall[0][0]).toBe "https://github.com" - shell.openExternal.reset() - - link.href = '' - windowEventHandler.handleLinkClick(fakeEvent) - expect(shell.openExternal).not.toHaveBeenCalled() - shell.openExternal.reset() - - link.href = '#scroll-me' - windowEventHandler.handleLinkClick(fakeEvent) - expect(shell.openExternal).not.toHaveBeenCalled() - - describe "when a form is submitted", -> - it "prevents the default so that the window's URL isn't changed", -> - form = document.createElement('form') - jasmine.attachToDOM(form) - - defaultPrevented = false - event = new CustomEvent('submit', bubbles: true) - event.preventDefault = -> defaultPrevented = true - form.dispatchEvent(event) - expect(defaultPrevented).toBe(true) - - describe "core:focus-next and core:focus-previous", -> - describe "when there is no currently focused element", -> - it "focuses the element with the lowest/highest tabindex", -> - wrapperDiv = document.createElement('div') - wrapperDiv.innerHTML = """ -
- - -
- """ - elements = wrapperDiv.firstChild - jasmine.attachToDOM(elements) - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 1 - - document.body.focus() - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 2 - - describe "when a tabindex is set on the currently focused element", -> - it "focuses the element with the next highest/lowest tabindex, skipping disabled elements", -> - wrapperDiv = document.createElement('div') - wrapperDiv.innerHTML = """ -
- - - - - - - -
- """ - elements = wrapperDiv.firstChild - jasmine.attachToDOM(elements) - - elements.querySelector('[tabindex="1"]').focus() - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 2 - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 3 - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 5 - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 7 - - elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 1 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 7 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 5 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 3 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 2 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 1 - - elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) - expect(document.activeElement.tabIndex).toBe 7 - - describe "when keydown events occur on the document", -> - it "dispatches the event via the KeymapManager and CommandRegistry", -> - dispatchedCommands = [] - atom.commands.onWillDispatch (command) -> dispatchedCommands.push(command) - atom.commands.add '*', 'foo-command': -> - atom.keymaps.add 'source-name', '*': {'x': 'foo-command'} - - event = KeymapManager.buildKeydownEvent('x', target: document.createElement('div')) - document.dispatchEvent(event) - - expect(dispatchedCommands.length).toBe 1 - expect(dispatchedCommands[0].type).toBe 'foo-command' - - describe "native key bindings", -> - it "correctly dispatches them to active elements with the '.native-key-bindings' class", -> - webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"]) - spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({ - webContents: webContentsSpy - on: -> - }) - - nativeKeyBindingsInput = document.createElement("input") - nativeKeyBindingsInput.classList.add("native-key-bindings") - jasmine.attachToDOM(nativeKeyBindingsInput) - nativeKeyBindingsInput.focus() - - atom.dispatchApplicationMenuCommand("core:copy") - atom.dispatchApplicationMenuCommand("core:paste") - - expect(webContentsSpy.copy).toHaveBeenCalled() - expect(webContentsSpy.paste).toHaveBeenCalled() - - webContentsSpy.copy.reset() - webContentsSpy.paste.reset() - - normalInput = document.createElement("input") - jasmine.attachToDOM(normalInput) - normalInput.focus() - - atom.dispatchApplicationMenuCommand("core:copy") - atom.dispatchApplicationMenuCommand("core:paste") - - expect(webContentsSpy.copy).not.toHaveBeenCalled() - expect(webContentsSpy.paste).not.toHaveBeenCalled() diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js new file mode 100644 index 000000000..a03e168fa --- /dev/null +++ b/spec/window-event-handler-spec.js @@ -0,0 +1,228 @@ +const KeymapManager = require('atom-keymap') +const WindowEventHandler = require('../src/window-event-handler') + +describe('WindowEventHandler', () => { + let windowEventHandler + + beforeEach(() => { + atom.uninstallWindowEventHandler() + spyOn(atom, 'hide') + const initialPath = atom.project.getPaths()[0] + spyOn(atom, 'getLoadSettings').andCallFake(() => { + const loadSettings = atom.getLoadSettings.originalValue.call(atom) + loadSettings.initialPath = initialPath + return loadSettings + }) + atom.project.destroy() + windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate}) + windowEventHandler.initialize(window, document) + }) + + afterEach(() => { + windowEventHandler.unsubscribe() + atom.installWindowEventHandler() + }) + + describe('when the window is loaded', () => + it("doesn't have .is-blurred on the body tag", () => { + if (process.platform === 'win32') { return } // Win32TestFailures - can not steal focus + expect(document.body.className).not.toMatch('is-blurred') + }) + ) + + describe('when the window is blurred', () => { + beforeEach(() => window.dispatchEvent(new CustomEvent('blur'))) + + afterEach(() => document.body.classList.remove('is-blurred')) + + it('adds the .is-blurred class on the body', () => expect(document.body.className).toMatch('is-blurred')) + + describe('when the window is focused again', () => + it('removes the .is-blurred class from the body', () => { + window.dispatchEvent(new CustomEvent('focus')) + expect(document.body.className).not.toMatch('is-blurred') + }) + ) + }) + + describe('window:close event', () => + it('closes the window', () => { + spyOn(atom, 'close') + window.dispatchEvent(new CustomEvent('window:close')) + expect(atom.close).toHaveBeenCalled() + }) + ) + + describe('when a link is clicked', () => + it('opens the http/https links in an external application', () => { + const {shell} = require('electron') + spyOn(shell, 'openExternal') + + const link = document.createElement('a') + const linkChild = document.createElement('span') + link.appendChild(linkChild) + link.href = 'http://github.com' + jasmine.attachToDOM(link) + const fakeEvent = {target: linkChild, currentTarget: link, preventDefault: () => {}} + + windowEventHandler.handleLinkClick(fakeEvent) + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com') + shell.openExternal.reset() + + link.href = 'https://github.com' + windowEventHandler.handleLinkClick(fakeEvent) + expect(shell.openExternal).toHaveBeenCalled() + expect(shell.openExternal.argsForCall[0][0]).toBe('https://github.com') + shell.openExternal.reset() + + link.href = '' + windowEventHandler.handleLinkClick(fakeEvent) + expect(shell.openExternal).not.toHaveBeenCalled() + shell.openExternal.reset() + + link.href = '#scroll-me' + windowEventHandler.handleLinkClick(fakeEvent) + expect(shell.openExternal).not.toHaveBeenCalled() + }) + ) + + describe('when a form is submitted', () => + it("prevents the default so that the window's URL isn't changed", () => { + const form = document.createElement('form') + jasmine.attachToDOM(form) + + let defaultPrevented = false + const event = new CustomEvent('submit', {bubbles: true}) + event.preventDefault = () => { defaultPrevented = true } + form.dispatchEvent(event) + expect(defaultPrevented).toBe(true) + }) + ) + + describe('core:focus-next and core:focus-previous', () => { + describe('when there is no currently focused element', () => + it('focuses the element with the lowest/highest tabindex', () => { + const wrapperDiv = document.createElement('div') + wrapperDiv.innerHTML = ` +
+ + +
+ `.trim() + const elements = wrapperDiv.firstChild + jasmine.attachToDOM(elements) + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(1) + + document.body.focus() + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(2) + }) + ) + + describe('when a tabindex is set on the currently focused element', () => + it('focuses the element with the next highest/lowest tabindex, skipping disabled elements', () => { + const wrapperDiv = document.createElement('div') + wrapperDiv.innerHTML = ` +
+ + + + + + + +
+ `.trim() + const elements = wrapperDiv.firstChild + jasmine.attachToDOM(elements) + + elements.querySelector('[tabindex="1"]').focus() + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(2) + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(3) + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(5) + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(7) + + elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(1) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(7) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(5) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(3) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(2) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(1) + + elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true})) + expect(document.activeElement.tabIndex).toBe(7) + }) + ) + }) + + describe('when keydown events occur on the document', () => + it('dispatches the event via the KeymapManager and CommandRegistry', () => { + const dispatchedCommands = [] + atom.commands.onWillDispatch(command => dispatchedCommands.push(command)) + atom.commands.add('*', {'foo-command': () => {}}) + atom.keymaps.add('source-name', {'*': {'x': 'foo-command'}}) + + const event = KeymapManager.buildKeydownEvent('x', {target: document.createElement('div')}) + document.dispatchEvent(event) + + expect(dispatchedCommands.length).toBe(1) + expect(dispatchedCommands[0].type).toBe('foo-command') + }) + ) + + describe('native key bindings', () => + it("correctly dispatches them to active elements with the '.native-key-bindings' class", () => { + const webContentsSpy = jasmine.createSpyObj('webContents', ['copy', 'paste']) + spyOn(atom.applicationDelegate, 'getCurrentWindow').andReturn({ + webContents: webContentsSpy, + on: () => {} + }) + + const nativeKeyBindingsInput = document.createElement('input') + nativeKeyBindingsInput.classList.add('native-key-bindings') + jasmine.attachToDOM(nativeKeyBindingsInput) + nativeKeyBindingsInput.focus() + + atom.dispatchApplicationMenuCommand('core:copy') + atom.dispatchApplicationMenuCommand('core:paste') + + expect(webContentsSpy.copy).toHaveBeenCalled() + expect(webContentsSpy.paste).toHaveBeenCalled() + + webContentsSpy.copy.reset() + webContentsSpy.paste.reset() + + const normalInput = document.createElement('input') + jasmine.attachToDOM(normalInput) + normalInput.focus() + + atom.dispatchApplicationMenuCommand('core:copy') + atom.dispatchApplicationMenuCommand('core:paste') + + expect(webContentsSpy.copy).not.toHaveBeenCalled() + expect(webContentsSpy.paste).not.toHaveBeenCalled() + }) + ) +}) From 9f73b33b086434eae24cecc09af646f328767df9 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 20 Oct 2017 16:06:43 +0200 Subject: [PATCH 019/161] :arrow_up: language-perl@0.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..dcbeb05c1 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.37.0", + "language-perl": "0.38.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From d0bdbb861ba8b0234135460bd86cc968316a3e14 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 20 Oct 2017 11:30:50 -0600 Subject: [PATCH 020/161] update overlay itself instead of text editor when resize occurs --- src/text-editor-component.js | 76 ++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5ff96eec5..18f53e945 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -804,7 +804,12 @@ class TextEditorComponent { key: overlayProps.element, overlayComponents: this.overlayComponents, measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), - didResize: () => { this.updateSync() } + didResize: (overlayComponent) => { + this.updateOverlayToRender(overlayProps) + overlayComponent.update({ + measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) + }) + } }, overlayProps )) @@ -1339,42 +1344,47 @@ class TextEditorComponent { }) } + updateOverlayToRender (decoration) { + const windowInnerHeight = this.getWindowInnerHeight() + const windowInnerWidth = this.getWindowInnerWidth() + const contentClientRect = this.refs.content.getBoundingClientRect() + + const {element, screenPosition, avoidOverflow} = decoration + const {row, column} = screenPosition + let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() + let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) + const clientRect = element.getBoundingClientRect() + this.overlayDimensionsByElement.set(element, clientRect) + + if (avoidOverflow !== false) { + const computedStyle = window.getComputedStyle(element) + const elementTop = wrapperTop + parseInt(computedStyle.marginTop) + const elementBottom = elementTop + clientRect.height + const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom) + const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft) + const elementRight = elementLeft + clientRect.width + + if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { + wrapperTop -= (elementTop - flippedElementTop) + } + if (elementLeft < 0) { + wrapperLeft -= elementLeft + } else if (elementRight > windowInnerWidth) { + wrapperLeft -= (elementRight - windowInnerWidth) + } + } + + decoration.pixelTop = Math.round(wrapperTop) + decoration.pixelLeft = Math.round(wrapperLeft) + } + updateOverlaysToRender () { const overlayCount = this.decorationsToRender.overlays.length if (overlayCount === 0) return null - const windowInnerHeight = this.getWindowInnerHeight() - const windowInnerWidth = this.getWindowInnerWidth() - const contentClientRect = this.refs.content.getBoundingClientRect() for (let i = 0; i < overlayCount; i++) { const decoration = this.decorationsToRender.overlays[i] - const {element, screenPosition, avoidOverflow} = decoration - const {row, column} = screenPosition - let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() - let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) - const clientRect = element.getBoundingClientRect() - this.overlayDimensionsByElement.set(element, clientRect) - - if (avoidOverflow !== false) { - const computedStyle = window.getComputedStyle(element) - const elementTop = wrapperTop + parseInt(computedStyle.marginTop) - const elementBottom = elementTop + clientRect.height - const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom) - const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft) - const elementRight = elementLeft + clientRect.width - - if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { - wrapperTop -= (elementTop - flippedElementTop) - } - if (elementLeft < 0) { - wrapperLeft -= elementLeft - } else if (elementRight > windowInnerWidth) { - wrapperLeft -= (elementRight - windowInnerWidth) - } - } - - decoration.pixelTop = Math.round(wrapperTop) - decoration.pixelLeft = Math.round(wrapperLeft) + this.updateOverlayToRender(decoration) } } @@ -4202,7 +4212,7 @@ class OverlayComponent { const {contentRect} = entries[0] if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) { this.resizeObserver.disconnect() - this.props.didResize() + this.props.didResize(this) process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } }) @@ -4217,7 +4227,7 @@ class OverlayComponent { update (newProps) { const oldProps = this.props - this.props = newProps + this.props = Object.assign({}, oldProps, newProps) if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px' if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px' if (newProps.className !== oldProps.className) { From 089717cbd3a8743387ee897bc4edea9afafd5db9 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 20 Oct 2017 15:46:27 -0600 Subject: [PATCH 021/161] fix failing test --- spec/text-editor-component-spec.js | 7 +++++-- src/text-editor-component.js | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 41d770212..d46748d91 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1896,6 +1896,9 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) await component.getNextUpdatePromise() + let overlayComponent + component.overlayComponents.forEach(c => overlayComponent = c) + const overlayWrapper = overlayElement.parentElement expect(overlayWrapper.classList.contains('a')).toBe(true) expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) @@ -1926,12 +1929,12 @@ describe('TextEditorComponent', () => { await setScrollTop(component, 20) expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) overlayElement.style.height = 60 + 'px' - await component.getNextUpdatePromise() + await overlayComponent.getNextUpdatePromise() expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4)) // Does not flip the overlay vertically if it would overflow the top of the window overlayElement.style.height = 80 + 'px' - await component.getNextUpdatePromise() + await overlayComponent.getNextUpdatePromise() expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) // Can update overlay wrapper class diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 18f53e945..641cdad02 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -806,9 +806,12 @@ class TextEditorComponent { measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), didResize: (overlayComponent) => { this.updateOverlayToRender(overlayProps) - overlayComponent.update({ - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) - }) + overlayComponent.update(Object.assign( + { + measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) + }, + overlayProps + )) } }, overlayProps @@ -4225,6 +4228,19 @@ class OverlayComponent { this.didDetach() } + getNextUpdatePromise () { + if (!this.nextUpdatePromise) { + this.nextUpdatePromise = new Promise((resolve) => { + this.resolveNextUpdatePromise = () => { + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + resolve() + } + }) + } + return this.nextUpdatePromise + } + update (newProps) { const oldProps = this.props this.props = Object.assign({}, oldProps, newProps) @@ -4234,6 +4250,8 @@ class OverlayComponent { if (oldProps.className != null) this.element.classList.remove(oldProps.className) if (newProps.className != null) this.element.classList.add(newProps.className) } + + if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise() } didAttach () { From cdf3be846be712d79ad901de5a531cc79e8a0bcc Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 20:35:40 -0400 Subject: [PATCH 022/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/view-registry.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.coffee | 201 ------------------------------- src/view-registry.js | 253 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 201 deletions(-) delete mode 100644 src/view-registry.coffee create mode 100644 src/view-registry.js diff --git a/src/view-registry.coffee b/src/view-registry.coffee deleted file mode 100644 index f300cc031..000000000 --- a/src/view-registry.coffee +++ /dev/null @@ -1,201 +0,0 @@ -Grim = require 'grim' -{Disposable} = require 'event-kit' -_ = require 'underscore-plus' - -AnyConstructor = Symbol('any-constructor') - -# Essential: `ViewRegistry` handles the association between model and view -# types in Atom. We call this association a View Provider. As in, for a given -# model, this class can provide a view via {::getView}, as long as the -# model/view association was registered via {::addViewProvider} -# -# If you're adding your own kind of pane item, a good strategy for all but the -# simplest items is to separate the model and the view. The model handles -# application logic and is the primary point of API interaction. The view -# just handles presentation. -# -# Note: Models can be any object, but must implement a `getTitle()` function -# if they are to be displayed in a {Pane} -# -# View providers inform the workspace how your model objects should be -# presented in the DOM. A view provider must always return a DOM node, which -# makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) -# an ideal tool for implementing views in Atom. -# -# You can access the `ViewRegistry` object via `atom.views`. -module.exports = -class ViewRegistry - animationFrameRequest: null - documentReadInProgress: false - - constructor: (@atomEnvironment) -> - @clear() - - clear: -> - @views = new WeakMap - @providers = [] - @clearDocumentRequests() - - # Essential: Add a provider that will be used to construct views in the - # workspace's view layer based on model objects in its model layer. - # - # ## Examples - # - # Text editors are divided into a model and a view layer, so when you interact - # with methods like `atom.workspace.getActiveTextEditor()` you're only going - # to get the model object. We display text editors on screen by teaching the - # workspace what view constructor it should use to represent them: - # - # ```coffee - # atom.views.addViewProvider TextEditor, (textEditor) -> - # textEditorElement = new TextEditorElement - # textEditorElement.initialize(textEditor) - # textEditorElement - # ``` - # - # * `modelConstructor` (optional) Constructor {Function} for your model. If - # a constructor is given, the `createView` function will only be used - # for model objects inheriting from that constructor. Otherwise, it will - # will be called for any object. - # * `createView` Factory {Function} that is passed an instance of your model - # and must return a subclass of `HTMLElement` or `undefined`. If it returns - # `undefined`, then the registry will continue to search for other view - # providers. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # added provider. - addViewProvider: (modelConstructor, createView) -> - if arguments.length is 1 - switch typeof modelConstructor - when 'function' - provider = {createView: modelConstructor, modelConstructor: AnyConstructor} - when 'object' - Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.") - provider = modelConstructor - else - throw new TypeError("Arguments to addViewProvider must be functions") - else - provider = {modelConstructor, createView} - - @providers.push(provider) - new Disposable => - @providers = @providers.filter (p) -> p isnt provider - - getViewProviderCount: -> - @providers.length - - # Essential: Get the view associated with an object in the workspace. - # - # If you're just *using* the workspace, you shouldn't need to access the view - # layer, but view layer access may be necessary if you want to perform DOM - # manipulation that isn't supported via the model API. - # - # ## View Resolution Algorithm - # - # The view associated with the object is resolved using the following - # sequence - # - # 1. Is the object an instance of `HTMLElement`? If true, return the object. - # 2. Does the object have a method named `getElement` that returns an - # instance of `HTMLElement`? If true, return that value. - # 3. Does the object have a property named `element` with a value which is - # an instance of `HTMLElement`? If true, return the property value. - # 4. Is the object a jQuery object, indicated by the presence of a `jquery` - # property? If true, return the root DOM element (i.e. `object[0]`). - # 5. Has a view provider been registered for the object? If true, use the - # provider to create a view associated with the object, and return the - # view. - # - # If no associated view is returned by the sequence an error is thrown. - # - # Returns a DOM element. - getView: (object) -> - return unless object? - - if view = @views.get(object) - view - else - view = @createView(object) - @views.set(object, view) - view - - createView: (object) -> - if object instanceof HTMLElement - return object - - if typeof object?.getElement is 'function' - element = object.getElement() - if element instanceof HTMLElement - return element - - if object?.element instanceof HTMLElement - return object.element - - if object?.jquery - return object[0] - - for provider in @providers - if provider.modelConstructor is AnyConstructor - if element = provider.createView(object, @atomEnvironment) - return element - continue - - if object instanceof provider.modelConstructor - if element = provider.createView?(object, @atomEnvironment) - return element - - if viewConstructor = provider.viewConstructor - element = new viewConstructor - element.initialize?(object) ? element.setModel?(object) - return element - - if viewConstructor = object?.getViewClass?() - view = new viewConstructor(object) - return view[0] - - throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.") - - updateDocument: (fn) -> - @documentWriters.push(fn) - @requestDocumentUpdate() unless @documentReadInProgress - new Disposable => - @documentWriters = @documentWriters.filter (writer) -> writer isnt fn - - readDocument: (fn) -> - @documentReaders.push(fn) - @requestDocumentUpdate() - new Disposable => - @documentReaders = @documentReaders.filter (reader) -> reader isnt fn - - getNextUpdatePromise: -> - @nextUpdatePromise ?= new Promise (resolve) => - @resolveNextUpdatePromise = resolve - - clearDocumentRequests: -> - @documentReaders = [] - @documentWriters = [] - @nextUpdatePromise = null - @resolveNextUpdatePromise = null - if @animationFrameRequest? - cancelAnimationFrame(@animationFrameRequest) - @animationFrameRequest = null - - requestDocumentUpdate: -> - @animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate) - - performDocumentUpdate: => - resolveNextUpdatePromise = @resolveNextUpdatePromise - @animationFrameRequest = null - @nextUpdatePromise = null - @resolveNextUpdatePromise = null - - writer() while writer = @documentWriters.shift() - - @documentReadInProgress = true - reader() while reader = @documentReaders.shift() - @documentReadInProgress = false - - # process updates requested as a result of reads - writer() while writer = @documentWriters.shift() - - resolveNextUpdatePromise?() diff --git a/src/view-registry.js b/src/view-registry.js new file mode 100644 index 000000000..d3167cdc1 --- /dev/null +++ b/src/view-registry.js @@ -0,0 +1,253 @@ +const Grim = require('grim') +const {Disposable} = require('event-kit') + +const AnyConstructor = Symbol('any-constructor') + +// Essential: `ViewRegistry` handles the association between model and view +// types in Atom. We call this association a View Provider. As in, for a given +// model, this class can provide a view via {::getView}, as long as the +// model/view association was registered via {::addViewProvider} +// +// If you're adding your own kind of pane item, a good strategy for all but the +// simplest items is to separate the model and the view. The model handles +// application logic and is the primary point of API interaction. The view +// just handles presentation. +// +// Note: Models can be any object, but must implement a `getTitle()` function +// if they are to be displayed in a {Pane} +// +// View providers inform the workspace how your model objects should be +// presented in the DOM. A view provider must always return a DOM node, which +// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) +// an ideal tool for implementing views in Atom. +// +// You can access the `ViewRegistry` object via `atom.views`. +module.exports = +class ViewRegistry { + constructor (atomEnvironment) { + this.animationFrameRequest = null + this.documentReadInProgress = false + this.performDocumentUpdate = this.performDocumentUpdate.bind(this) + this.atomEnvironment = atomEnvironment + this.clear() + } + + clear () { + this.views = new WeakMap() + this.providers = [] + this.clearDocumentRequests() + } + + // Essential: Add a provider that will be used to construct views in the + // workspace's view layer based on model objects in its model layer. + // + // ## Examples + // + // Text editors are divided into a model and a view layer, so when you interact + // with methods like `atom.workspace.getActiveTextEditor()` you're only going + // to get the model object. We display text editors on screen by teaching the + // workspace what view constructor it should use to represent them: + // + // ```coffee + // atom.views.addViewProvider TextEditor, (textEditor) -> + // textEditorElement = new TextEditorElement + // textEditorElement.initialize(textEditor) + // textEditorElement + // ``` + // + // * `modelConstructor` (optional) Constructor {Function} for your model. If + // a constructor is given, the `createView` function will only be used + // for model objects inheriting from that constructor. Otherwise, it will + // will be called for any object. + // * `createView` Factory {Function} that is passed an instance of your model + // and must return a subclass of `HTMLElement` or `undefined`. If it returns + // `undefined`, then the registry will continue to search for other view + // providers. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // added provider. + addViewProvider (modelConstructor, createView) { + let provider + if (arguments.length === 1) { + switch (typeof modelConstructor) { + case 'function': + provider = {createView: modelConstructor, modelConstructor: AnyConstructor} + break + case 'object': + Grim.deprecate('atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.') + provider = modelConstructor + break + default: + throw new TypeError('Arguments to addViewProvider must be functions') + } + } else { + provider = {modelConstructor, createView} + } + + this.providers.push(provider) + return new Disposable(() => { + this.providers = this.providers.filter(p => p !== provider) + }) + } + + getViewProviderCount () { + return this.providers.length + } + + // Essential: Get the view associated with an object in the workspace. + // + // If you're just *using* the workspace, you shouldn't need to access the view + // layer, but view layer access may be necessary if you want to perform DOM + // manipulation that isn't supported via the model API. + // + // ## View Resolution Algorithm + // + // The view associated with the object is resolved using the following + // sequence + // + // 1. Is the object an instance of `HTMLElement`? If true, return the object. + // 2. Does the object have a method named `getElement` that returns an + // instance of `HTMLElement`? If true, return that value. + // 3. Does the object have a property named `element` with a value which is + // an instance of `HTMLElement`? If true, return the property value. + // 4. Is the object a jQuery object, indicated by the presence of a `jquery` + // property? If true, return the root DOM element (i.e. `object[0]`). + // 5. Has a view provider been registered for the object? If true, use the + // provider to create a view associated with the object, and return the + // view. + // + // If no associated view is returned by the sequence an error is thrown. + // + // Returns a DOM element. + getView (object) { + if (object == null) { return } + + let view + if (view = this.views.get(object)) { + return view + } else { + view = this.createView(object) + this.views.set(object, view) + return view + } + } + + createView (object) { + if (object instanceof HTMLElement) { return object } + + let element + if (object && (typeof object.getElement === 'function')) { + element = object.getElement() + if (element instanceof HTMLElement) { + return element + } + } + + if (object && object.element instanceof HTMLElement) { + return object.element + } + + if (object && object.jquery) { + return object[0] + } + + let viewConstructor + for (let provider of this.providers) { + if (provider.modelConstructor === AnyConstructor) { + if (element = provider.createView(object, this.atomEnvironment)) { + return element + } + continue + } + + if (object instanceof provider.modelConstructor) { + if (element = provider.createView && provider.createView(object, this.atomEnvironment)) { + return element + } + + if (viewConstructor = provider.viewConstructor) { + element = new viewConstructor() + if (element.initialize) { + element.initialize(object) + } else if (element.setModel) { + element.setModel(object) + } + return element + } + } + } + + if (object && object.getViewClass) { + viewConstructor = object.getViewClass() + if (viewConstructor) { + const view = new viewConstructor(object) + return view[0] + } + } + + throw new Error(`Can't create a view for ${object.constructor.name} instance. Please register a view provider.`) + } + + updateDocument (fn) { + this.documentWriters.push(fn) + if (!this.documentReadInProgress) { this.requestDocumentUpdate() } + return new Disposable(() => { + this.documentWriters = this.documentWriters.filter(writer => writer !== fn) + }) + } + + readDocument (fn) { + this.documentReaders.push(fn) + this.requestDocumentUpdate() + return new Disposable(() => { + this.documentReaders = this.documentReaders.filter(reader => reader !== fn) + }) + } + + getNextUpdatePromise () { + if (this.nextUpdatePromise == null) { + this.nextUpdatePromise = new Promise(resolve => { + this.resolveNextUpdatePromise = resolve + }) + } + + return this.nextUpdatePromise + } + + clearDocumentRequests () { + this.documentReaders = [] + this.documentWriters = [] + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + if (this.animationFrameRequest != null) { + cancelAnimationFrame(this.animationFrameRequest) + this.animationFrameRequest = null + } + } + + requestDocumentUpdate () { + if (this.animationFrameRequest == null) { + this.animationFrameRequest = requestAnimationFrame(this.performDocumentUpdate) + } + } + + performDocumentUpdate () { + const { resolveNextUpdatePromise } = this + this.animationFrameRequest = null + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + + let writer + while ((writer = this.documentWriters.shift())) { writer() } + + let reader + this.documentReadInProgress = true + while ((reader = this.documentReaders.shift())) { reader() } + this.documentReadInProgress = false + + // process updates requested as a result of reads + while ((writer = this.documentWriters.shift())) { writer() } + + if (resolveNextUpdatePromise) { resolveNextUpdatePromise() } + } +} From a67272e6fff6094167b0d7bf474973db92bf0e4e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 21:12:53 -0400 Subject: [PATCH 023/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20"Expected=20a=20co?= =?UTF-8?q?nditional=20expression=20&=20instead=20saw=20an=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index d3167cdc1..37849f999 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -122,14 +122,12 @@ class ViewRegistry { getView (object) { if (object == null) { return } - let view - if (view = this.views.get(object)) { - return view - } else { + let view = this.views.get(object) + if (!view) { view = this.createView(object) this.views.set(object, view) - return view } + return view } createView (object) { @@ -154,18 +152,17 @@ class ViewRegistry { let viewConstructor for (let provider of this.providers) { if (provider.modelConstructor === AnyConstructor) { - if (element = provider.createView(object, this.atomEnvironment)) { - return element - } + element = provider.createView(object, this.atomEnvironment) + if (element) { return element } continue } if (object instanceof provider.modelConstructor) { - if (element = provider.createView && provider.createView(object, this.atomEnvironment)) { - return element - } + element = provider.createView && provider.createView(object, this.atomEnvironment) + if (element) { return element } - if (viewConstructor = provider.viewConstructor) { + viewConstructor = provider.viewConstructor + if (viewConstructor) { element = new viewConstructor() if (element.initialize) { element.initialize(object) From dfd1332a016a8af542d2a4f75a14f40341e533db Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 21:16:28 -0400 Subject: [PATCH 024/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20"A=20constructor?= =?UTF-8?q?=20name=20should=20not=20start=20with=20a=20lowercase=20letter"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index 37849f999..dcc1624fc 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -149,7 +149,6 @@ class ViewRegistry { return object[0] } - let viewConstructor for (let provider of this.providers) { if (provider.modelConstructor === AnyConstructor) { element = provider.createView(object, this.atomEnvironment) @@ -161,9 +160,9 @@ class ViewRegistry { element = provider.createView && provider.createView(object, this.atomEnvironment) if (element) { return element } - viewConstructor = provider.viewConstructor - if (viewConstructor) { - element = new viewConstructor() + let ViewConstructor = provider.viewConstructor + if (ViewConstructor) { + element = new ViewConstructor() if (element.initialize) { element.initialize(object) } else if (element.setModel) { @@ -175,9 +174,9 @@ class ViewRegistry { } if (object && object.getViewClass) { - viewConstructor = object.getViewClass() - if (viewConstructor) { - const view = new viewConstructor(object) + let ViewConstructor = object.getViewClass() + if (ViewConstructor) { + const view = new ViewConstructor(object) return view[0] } } From c6d438c5092eb42eae45ca50dbba9dfb5cb950a1 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 09:52:59 -0400 Subject: [PATCH 025/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/view-registry-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.coffee | 163 ------------------------ spec/view-registry-spec.js | 218 +++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 163 deletions(-) delete mode 100644 spec/view-registry-spec.coffee create mode 100644 spec/view-registry-spec.js diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee deleted file mode 100644 index 4bae1d811..000000000 --- a/spec/view-registry-spec.coffee +++ /dev/null @@ -1,163 +0,0 @@ -ViewRegistry = require '../src/view-registry' - -describe "ViewRegistry", -> - registry = null - - beforeEach -> - registry = new ViewRegistry - - afterEach -> - registry.clearDocumentRequests() - - describe "::getView(object)", -> - describe "when passed a DOM node", -> - it "returns the given DOM node", -> - node = document.createElement('div') - expect(registry.getView(node)).toBe node - - describe "when passed an object with an element property", -> - it "returns the element property if it's an instance of HTMLElement", -> - class TestComponent - constructor: -> @element = document.createElement('div') - - component = new TestComponent - expect(registry.getView(component)).toBe component.element - - describe "when passed an object with a getElement function", -> - it "returns the return value of getElement if it's an instance of HTMLElement", -> - class TestComponent - getElement: -> - @myElement ?= document.createElement('div') - - component = new TestComponent - expect(registry.getView(component)).toBe component.myElement - - describe "when passed a model object", -> - describe "when a view provider is registered matching the object's constructor", -> - it "constructs a view element and assigns the model on it", -> - class TestModel - - class TestModelSubclass extends TestModel - - class TestView - initialize: (@model) -> this - - model = new TestModel - - registry.addViewProvider TestModel, (model) -> - new TestView().initialize(model) - - view = registry.getView(model) - expect(view instanceof TestView).toBe true - expect(view.model).toBe model - - subclassModel = new TestModelSubclass - view2 = registry.getView(subclassModel) - expect(view2 instanceof TestView).toBe true - expect(view2.model).toBe subclassModel - - describe "when a view provider is registered generically, and works with the object", -> - it "constructs a view element and assigns the model on it", -> - model = {a: 'b'} - - registry.addViewProvider (model) -> - if model.a is 'b' - element = document.createElement('div') - element.className = 'test-element' - element - - view = registry.getView({a: 'b'}) - expect(view.className).toBe 'test-element' - - expect(-> registry.getView({a: 'c'})).toThrow() - - describe "when no view provider is registered for the object's constructor", -> - it "throws an exception", -> - expect(-> registry.getView(new Object)).toThrow() - - describe "::addViewProvider(providerSpec)", -> - it "returns a disposable that can be used to remove the provider", -> - class TestModel - class TestView - initialize: (@model) -> this - - disposable = registry.addViewProvider TestModel, (model) -> - new TestView().initialize(model) - - expect(registry.getView(new TestModel) instanceof TestView).toBe true - disposable.dispose() - expect(-> registry.getView(new TestModel)).toThrow() - - describe "::updateDocument(fn) and ::readDocument(fn)", -> - frameRequests = null - - beforeEach -> - frameRequests = [] - spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn) - - it "performs all pending writes before all pending reads on the next animation frame", -> - events = [] - - registry.updateDocument -> events.push('write 1') - registry.readDocument -> events.push('read 1') - registry.readDocument -> events.push('read 2') - registry.updateDocument -> events.push('write 2') - - expect(events).toEqual [] - - expect(frameRequests.length).toBe 1 - frameRequests[0]() - expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2'] - - frameRequests = [] - events = [] - disposable = registry.updateDocument -> events.push('write 3') - registry.updateDocument -> events.push('write 4') - registry.readDocument -> events.push('read 3') - - disposable.dispose() - - expect(frameRequests.length).toBe 1 - frameRequests[0]() - expect(events).toEqual ['write 4', 'read 3'] - - it "performs writes requested from read callbacks in the same animation frame", -> - spyOn(window, 'setInterval').andCallFake(fakeSetInterval) - spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) - events = [] - - registry.updateDocument -> events.push('write 1') - registry.readDocument -> - registry.updateDocument -> events.push('write from read 1') - events.push('read 1') - registry.readDocument -> - registry.updateDocument -> events.push('write from read 2') - events.push('read 2') - registry.updateDocument -> events.push('write 2') - - expect(frameRequests.length).toBe 1 - frameRequests[0]() - expect(frameRequests.length).toBe 1 - - expect(events).toEqual [ - 'write 1' - 'write 2' - 'read 1' - 'read 2' - 'write from read 1' - 'write from read 2' - ] - - describe "::getNextUpdatePromise()", -> - it "returns a promise that resolves at the end of the next update cycle", -> - updateCalled = false - readCalled = false - - waitsFor 'getNextUpdatePromise to resolve', (done) -> - registry.getNextUpdatePromise().then -> - expect(updateCalled).toBe true - expect(readCalled).toBe true - done() - - registry.updateDocument -> updateCalled = true - registry.readDocument -> readCalled = true diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js new file mode 100644 index 000000000..984d30718 --- /dev/null +++ b/spec/view-registry-spec.js @@ -0,0 +1,218 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ViewRegistry = require('../src/view-registry') + +describe('ViewRegistry', () => { + let registry = null + + beforeEach(() => { + registry = new ViewRegistry() + }) + + afterEach(() => { + registry.clearDocumentRequests() + }) + + describe('::getView(object)', () => { + describe('when passed a DOM node', () => + it('returns the given DOM node', () => { + const node = document.createElement('div') + expect(registry.getView(node)).toBe(node) + }) + ) + + describe('when passed an object with an element property', () => + it("returns the element property if it's an instance of HTMLElement", () => { + class TestComponent { + constructor () { + this.element = document.createElement('div') + } + } + + const component = new TestComponent() + expect(registry.getView(component)).toBe(component.element) + }) + ) + + describe('when passed an object with a getElement function', () => + it("returns the return value of getElement if it's an instance of HTMLElement", () => { + class TestComponent { + getElement () { + if (this.myElement == null) { + this.myElement = document.createElement('div') + } + return this.myElement + } + } + + const component = new TestComponent() + expect(registry.getView(component)).toBe(component.myElement) + }) + ) + + describe('when passed a model object', () => { + describe("when a view provider is registered matching the object's constructor", () => + it('constructs a view element and assigns the model on it', () => { + class TestModel {} + + class TestModelSubclass extends TestModel {} + + class TestView { + initialize (model) { + this.model = model + return this + } + } + + const model = new TestModel() + + registry.addViewProvider(TestModel, (model) => + new TestView().initialize(model) + ) + + const view = registry.getView(model) + expect(view instanceof TestView).toBe(true) + expect(view.model).toBe(model) + + const subclassModel = new TestModelSubclass() + const view2 = registry.getView(subclassModel) + expect(view2 instanceof TestView).toBe(true) + expect(view2.model).toBe(subclassModel) + }) + ) + + describe('when a view provider is registered generically, and works with the object', () => + it('constructs a view element and assigns the model on it', () => { + const model = {a: 'b'} + + registry.addViewProvider((model) => { + if (model.a === 'b') { + const element = document.createElement('div') + element.className = 'test-element' + return element + } + }) + + const view = registry.getView({a: 'b'}) + expect(view.className).toBe('test-element') + + expect(() => registry.getView({a: 'c'})).toThrow() + }) + ) + + describe("when no view provider is registered for the object's constructor", () => + it('throws an exception', () => { + expect(() => registry.getView(new Object())).toThrow() + }) + ) + }) + }) + + describe('::addViewProvider(providerSpec)', () => + it('returns a disposable that can be used to remove the provider', () => { + class TestModel {} + class TestView { + initialize (model) { + this.model = model + return this + } + } + + const disposable = registry.addViewProvider(TestModel, (model) => + new TestView().initialize(model) + ) + + expect(registry.getView(new TestModel()) instanceof TestView).toBe(true) + disposable.dispose() + expect(() => registry.getView(new TestModel())).toThrow() + }) + ) + + describe('::updateDocument(fn) and ::readDocument(fn)', () => { + let frameRequests = null + + beforeEach(() => { + frameRequests = [] + spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn)) + }) + + it('performs all pending writes before all pending reads on the next animation frame', () => { + let events = [] + + registry.updateDocument(() => events.push('write 1')) + registry.readDocument(() => events.push('read 1')) + registry.readDocument(() => events.push('read 2')) + registry.updateDocument(() => events.push('write 2')) + + expect(events).toEqual([]) + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2']) + + frameRequests = [] + events = [] + const disposable = registry.updateDocument(() => events.push('write 3')) + registry.updateDocument(() => events.push('write 4')) + registry.readDocument(() => events.push('read 3')) + + disposable.dispose() + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(events).toEqual(['write 4', 'read 3']) + }) + + it('performs writes requested from read callbacks in the same animation frame', () => { + spyOn(window, 'setInterval').andCallFake(fakeSetInterval) + spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) + const events = [] + + registry.updateDocument(() => events.push('write 1')) + registry.readDocument(() => { + registry.updateDocument(() => events.push('write from read 1')) + events.push('read 1') + }) + registry.readDocument(() => { + registry.updateDocument(() => events.push('write from read 2')) + events.push('read 2') + }) + registry.updateDocument(() => events.push('write 2')) + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(frameRequests.length).toBe(1) + + expect(events).toEqual([ + 'write 1', + 'write 2', + 'read 1', + 'read 2', + 'write from read 1', + 'write from read 2' + ]) + }) + }) + + describe('::getNextUpdatePromise()', () => + it('returns a promise that resolves at the end of the next update cycle', () => { + let updateCalled = false + let readCalled = false + + waitsFor('getNextUpdatePromise to resolve', (done) => { + registry.getNextUpdatePromise().then(() => { + expect(updateCalled).toBe(true) + expect(readCalled).toBe(true) + done() + }) + + registry.updateDocument(() => updateCalled = true) + registry.readDocument(() => readCalled = true) + }) + }) + ) +}) From 9a6f4b1647a6237c587bdaeb72585a204305bbe2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:05:57 -0400 Subject: [PATCH 026/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20"'model'=20is=20as?= =?UTF-8?q?signed=20a=20value=20but=20never=20used"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index 984d30718..4459af10c 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -87,8 +87,6 @@ describe('ViewRegistry', () => { describe('when a view provider is registered generically, and works with the object', () => it('constructs a view element and assigns the model on it', () => { - const model = {a: 'b'} - registry.addViewProvider((model) => { if (model.a === 'b') { const element = document.createElement('div') From 33aea760588e5126d9c5982c70569b9fc8b85f50 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:07:13 -0400 Subject: [PATCH 027/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20"The=20object=20li?= =?UTF-8?q?teral=20notation=20{}=20is=20preferrable"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index 4459af10c..d29c627bd 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -104,7 +104,7 @@ describe('ViewRegistry', () => { describe("when no view provider is registered for the object's constructor", () => it('throws an exception', () => { - expect(() => registry.getView(new Object())).toThrow() + expect(() => registry.getView({})).toThrow() }) ) }) From 01e7faa988761581392f134ebc64d9c1793b321c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:10:06 -0400 Subject: [PATCH 028/161] =?UTF-8?q?=F0=9F=91=94=20Fix=20"Arrow=20function?= =?UTF-8?q?=20should=20not=20return=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index d29c627bd..db8b077f1 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -208,8 +208,8 @@ describe('ViewRegistry', () => { done() }) - registry.updateDocument(() => updateCalled = true) - registry.readDocument(() => readCalled = true) + registry.updateDocument(() => { updateCalled = true }) + registry.readDocument(() => { readCalled = true }) }) }) ) From 0511c0ae4a5f18b81b3c64ad31f15dbfd8ba3a38 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Mon, 23 Oct 2017 04:11:23 +0300 Subject: [PATCH 029/161] Remove unused argument --- src/text-editor-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 2cbf3093c..d891a5868 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -288,7 +288,7 @@ export default class TextEditorRegistry { let currentScore = this.editorGrammarScores.get(editor) if (currentScore == null || score > currentScore) { - editor.setGrammar(grammar, score) + editor.setGrammar(grammar) this.editorGrammarScores.set(editor, score) } } From 7b76ee3f2593e48ab86fca7e1c602332b4bc8cf7 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 08:47:30 -0400 Subject: [PATCH 030/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/tooltip-manager.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes src/tooltip-manager.coffee src/tooltip-manager.coffee → src/tooltip-manager.js $ standard --fix src/tooltip-manager.js src/tooltip-manager.js:210:25: Unnecessary escape character: \". src/tooltip-manager.js:210:36: Unnecessary escape character: \". --- src/tooltip-manager.coffee | 176 ------------------------------ src/tooltip-manager.js | 212 +++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 176 deletions(-) delete mode 100644 src/tooltip-manager.coffee create mode 100644 src/tooltip-manager.js diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee deleted file mode 100644 index 1a9b6fe44..000000000 --- a/src/tooltip-manager.coffee +++ /dev/null @@ -1,176 +0,0 @@ -_ = require 'underscore-plus' -{Disposable, CompositeDisposable} = require 'event-kit' -Tooltip = null - -# Essential: Associates tooltips with HTML elements. -# -# You can get the `TooltipManager` via `atom.tooltips`. -# -# ## Examples -# -# The essence of displaying a tooltip -# -# ```coffee -# # display it -# disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) -# -# # remove it -# disposable.dispose() -# ``` -# -# In practice there are usually multiple tooltips. So we add them to a -# CompositeDisposable -# -# ```coffee -# {CompositeDisposable} = require 'atom' -# subscriptions = new CompositeDisposable -# -# div1 = document.createElement('div') -# div2 = document.createElement('div') -# subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'}) -# subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'}) -# -# # remove them all -# subscriptions.dispose() -# ``` -# -# You can display a key binding in the tooltip as well with the -# `keyBindingCommand` option. -# -# ```coffee -# disposable = atom.tooltips.add @caseOptionButton, -# title: "Match Case" -# keyBindingCommand: 'find-and-replace:toggle-case-option' -# keyBindingTarget: @findEditor.element -# ``` -module.exports = -class TooltipManager - defaults: - trigger: 'hover' - container: 'body' - html: true - placement: 'auto top' - viewportPadding: 2 - - hoverDefaults: - {delay: {show: 1000, hide: 100}} - - constructor: ({@keymapManager, @viewRegistry}) -> - @tooltips = new Map() - - # Essential: Add a tooltip to the given element. - # - # * `target` An `HTMLElement` - # * `options` An object with one or more of the following options: - # * `title` A {String} or {Function} to use for the text in the tip. If - # a function is passed, `this` will be set to the `target` element. This - # option is mutually exclusive with the `item` option. - # * `html` A {Boolean} affecting the interpretation of the `title` option. - # If `true` (the default), the `title` string will be interpreted as HTML. - # Otherwise it will be interpreted as plain text. - # * `item` A view (object with an `.element` property) or a DOM element - # containing custom content for the tooltip. This option is mutually - # exclusive with the `title` option. - # * `class` A {String} with a class to apply to the tooltip element to - # enable custom styling. - # * `placement` A {String} or {Function} returning a string to indicate - # the position of the tooltip relative to `element`. Can be `'top'`, - # `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is - # specified, it will dynamically reorient the tooltip. For example, if - # placement is `'auto left'`, the tooltip will display to the left when - # possible, otherwise it will display right. - # When a function is used to determine the placement, it is called with - # the tooltip DOM node as its first argument and the triggering element - # DOM node as its second. The `this` context is set to the tooltip - # instance. - # * `trigger` A {String} indicating how the tooltip should be displayed. - # Choose from one of the following options: - # * `'hover'` Show the tooltip when the mouse hovers over the element. - # This is the default. - # * `'click'` Show the tooltip when the element is clicked. The tooltip - # will be hidden after clicking the element again or anywhere else - # outside of the tooltip itself. - # * `'focus'` Show the tooltip when the element is focused. - # * `'manual'` Show the tooltip immediately and only hide it when the - # returned disposable is disposed. - # * `delay` An object specifying the show and hide delay in milliseconds. - # Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and - # otherwise defaults to `0` for both values. - # * `keyBindingCommand` A {String} containing a command name. If you specify - # this option and a key binding exists that matches the command, it will - # be appended to the title or rendered alone if no title is specified. - # * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. - # If this option is not supplied, the first of all matching key bindings - # for the given command will be rendered. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # tooltip. - add: (target, options) -> - if target.jquery - disposable = new CompositeDisposable - disposable.add @add(element, options) for element in target - return disposable - - Tooltip ?= require './tooltip' - - {keyBindingCommand, keyBindingTarget} = options - - if keyBindingCommand? - bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget) - keystroke = getKeystroke(bindings) - if options.title? and keystroke? - options.title += " " + getKeystroke(bindings) - else if keystroke? - options.title = getKeystroke(bindings) - - delete options.selector - options = _.defaults(options, @defaults) - if options.trigger is 'hover' - options = _.defaults(options, @hoverDefaults) - - tooltip = new Tooltip(target, options, @viewRegistry) - - if not @tooltips.has(target) - @tooltips.set(target, []) - @tooltips.get(target).push(tooltip) - - hideTooltip = -> - tooltip.leave(currentTarget: target) - tooltip.hide() - - window.addEventListener('resize', hideTooltip) - - disposable = new Disposable => - window.removeEventListener('resize', hideTooltip) - hideTooltip() - tooltip.destroy() - - if @tooltips.has(target) - tooltipsForTarget = @tooltips.get(target) - index = tooltipsForTarget.indexOf(tooltip) - if index isnt -1 - tooltipsForTarget.splice(index, 1) - if tooltipsForTarget.length is 0 - @tooltips.delete(target) - - disposable - - # Extended: Find the tooltips that have been applied to the given element. - # - # * `target` The `HTMLElement` to find tooltips on. - # - # Returns an {Array} of `Tooltip` objects that match the `target`. - findTooltips: (target) -> - if @tooltips.has(target) - @tooltips.get(target).slice() - else - [] - -humanizeKeystrokes = (keystroke) -> - keystrokes = keystroke.split(' ') - keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes) - keystrokes.join(' ') - -getKeystroke = (bindings) -> - if bindings?.length - "#{humanizeKeystrokes(bindings[0].keystrokes)}" diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js new file mode 100644 index 000000000..c838b6dbc --- /dev/null +++ b/src/tooltip-manager.js @@ -0,0 +1,212 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TooltipManager +const _ = require('underscore-plus') +const {Disposable, CompositeDisposable} = require('event-kit') +let Tooltip = null + +// Essential: Associates tooltips with HTML elements. +// +// You can get the `TooltipManager` via `atom.tooltips`. +// +// ## Examples +// +// The essence of displaying a tooltip +// +// ```coffee +// # display it +// disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) +// +// # remove it +// disposable.dispose() +// ``` +// +// In practice there are usually multiple tooltips. So we add them to a +// CompositeDisposable +// +// ```coffee +// {CompositeDisposable} = require 'atom' +// subscriptions = new CompositeDisposable +// +// div1 = document.createElement('div') +// div2 = document.createElement('div') +// subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'}) +// subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'}) +// +// # remove them all +// subscriptions.dispose() +// ``` +// +// You can display a key binding in the tooltip as well with the +// `keyBindingCommand` option. +// +// ```coffee +// disposable = atom.tooltips.add @caseOptionButton, +// title: "Match Case" +// keyBindingCommand: 'find-and-replace:toggle-case-option' +// keyBindingTarget: @findEditor.element +// ``` +module.exports = +(TooltipManager = (function () { + TooltipManager = class TooltipManager { + static initClass () { + this.prototype.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 + } + + this.prototype.hoverDefaults = + {delay: {show: 1000, hide: 100}} + } + + constructor ({keymapManager, viewRegistry}) { + this.keymapManager = keymapManager + this.viewRegistry = viewRegistry + this.tooltips = new Map() + } + + // Essential: Add a tooltip to the given element. + // + // * `target` An `HTMLElement` + // * `options` An object with one or more of the following options: + // * `title` A {String} or {Function} to use for the text in the tip. If + // a function is passed, `this` will be set to the `target` element. This + // option is mutually exclusive with the `item` option. + // * `html` A {Boolean} affecting the interpretation of the `title` option. + // If `true` (the default), the `title` string will be interpreted as HTML. + // Otherwise it will be interpreted as plain text. + // * `item` A view (object with an `.element` property) or a DOM element + // containing custom content for the tooltip. This option is mutually + // exclusive with the `title` option. + // * `class` A {String} with a class to apply to the tooltip element to + // enable custom styling. + // * `placement` A {String} or {Function} returning a string to indicate + // the position of the tooltip relative to `element`. Can be `'top'`, + // `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is + // specified, it will dynamically reorient the tooltip. For example, if + // placement is `'auto left'`, the tooltip will display to the left when + // possible, otherwise it will display right. + // When a function is used to determine the placement, it is called with + // the tooltip DOM node as its first argument and the triggering element + // DOM node as its second. The `this` context is set to the tooltip + // instance. + // * `trigger` A {String} indicating how the tooltip should be displayed. + // Choose from one of the following options: + // * `'hover'` Show the tooltip when the mouse hovers over the element. + // This is the default. + // * `'click'` Show the tooltip when the element is clicked. The tooltip + // will be hidden after clicking the element again or anywhere else + // outside of the tooltip itself. + // * `'focus'` Show the tooltip when the element is focused. + // * `'manual'` Show the tooltip immediately and only hide it when the + // returned disposable is disposed. + // * `delay` An object specifying the show and hide delay in milliseconds. + // Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and + // otherwise defaults to `0` for both values. + // * `keyBindingCommand` A {String} containing a command name. If you specify + // this option and a key binding exists that matches the command, it will + // be appended to the title or rendered alone if no title is specified. + // * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. + // If this option is not supplied, the first of all matching key bindings + // for the given command will be rendered. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // tooltip. + add (target, options) { + let disposable + if (target.jquery) { + disposable = new CompositeDisposable() + for (let element of target) { disposable.add(this.add(element, options)) } + return disposable + } + + if (Tooltip == null) { Tooltip = require('./tooltip') } + + const {keyBindingCommand, keyBindingTarget} = options + + if (keyBindingCommand != null) { + const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget}) + const keystroke = getKeystroke(bindings) + if ((options.title != null) && (keystroke != null)) { + options.title += ` ${getKeystroke(bindings)}` + } else if (keystroke != null) { + options.title = getKeystroke(bindings) + } + } + + delete options.selector + options = _.defaults(options, this.defaults) + if (options.trigger === 'hover') { + options = _.defaults(options, this.hoverDefaults) + } + + const tooltip = new Tooltip(target, options, this.viewRegistry) + + if (!this.tooltips.has(target)) { + this.tooltips.set(target, []) + } + this.tooltips.get(target).push(tooltip) + + const hideTooltip = function () { + tooltip.leave({currentTarget: target}) + return tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + disposable = new Disposable(() => { + window.removeEventListener('resize', hideTooltip) + hideTooltip() + tooltip.destroy() + + if (this.tooltips.has(target)) { + const tooltipsForTarget = this.tooltips.get(target) + const index = tooltipsForTarget.indexOf(tooltip) + if (index !== -1) { + tooltipsForTarget.splice(index, 1) + } + if (tooltipsForTarget.length === 0) { + return this.tooltips.delete(target) + } + } + }) + + return disposable + } + + // Extended: Find the tooltips that have been applied to the given element. + // + // * `target` The `HTMLElement` to find tooltips on. + // + // Returns an {Array} of `Tooltip` objects that match the `target`. + findTooltips (target) { + if (this.tooltips.has(target)) { + return this.tooltips.get(target).slice() + } else { + return [] + } + } + } + TooltipManager.initClass() + return TooltipManager +})()) + +const humanizeKeystrokes = function (keystroke) { + let keystrokes = keystroke.split(' ') + keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) + return keystrokes.join(' ') +} + +var getKeystroke = function (bindings) { + if (bindings != null ? bindings.length : undefined) { + return `${humanizeKeystrokes(bindings[0].keystrokes)}` + } +} From 034f003705f07e8a8b4d8354ae1f861dc851ab72 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 08:49:27 -0400 Subject: [PATCH 031/161] :shirt: Fix 'Unnecessary escape character: \"' --- src/tooltip-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index c838b6dbc..f127d3f44 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -207,6 +207,6 @@ const humanizeKeystrokes = function (keystroke) { var getKeystroke = function (bindings) { if (bindings != null ? bindings.length : undefined) { - return `${humanizeKeystrokes(bindings[0].keystrokes)}` + return `${humanizeKeystrokes(bindings[0].keystrokes)}` } } From 157c33b5471c6bf3dfb7b361decd6e64a56b8eba Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:27:53 -0400 Subject: [PATCH 032/161] :art: DS206 Rework class to avoid initClass --- src/tooltip-manager.js | 267 ++++++++++++++++++++--------------------- 1 file changed, 130 insertions(+), 137 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index f127d3f44..00e16e405 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,11 +1,9 @@ /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let TooltipManager const _ = require('underscore-plus') const {Disposable, CompositeDisposable} = require('event-kit') let Tooltip = null @@ -52,152 +50,147 @@ let Tooltip = null // keyBindingTarget: @findEditor.element // ``` module.exports = -(TooltipManager = (function () { - TooltipManager = class TooltipManager { - static initClass () { - this.prototype.defaults = { - trigger: 'hover', - container: 'body', - html: true, - placement: 'auto top', - viewportPadding: 2 - } - - this.prototype.hoverDefaults = - {delay: {show: 1000, hide: 100}} +class TooltipManager { + constructor ({keymapManager, viewRegistry}) { + this.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 } - constructor ({keymapManager, viewRegistry}) { - this.keymapManager = keymapManager - this.viewRegistry = viewRegistry - this.tooltips = new Map() + this.hoverDefaults = { + delay: {show: 1000, hide: 100} } - // Essential: Add a tooltip to the given element. - // - // * `target` An `HTMLElement` - // * `options` An object with one or more of the following options: - // * `title` A {String} or {Function} to use for the text in the tip. If - // a function is passed, `this` will be set to the `target` element. This - // option is mutually exclusive with the `item` option. - // * `html` A {Boolean} affecting the interpretation of the `title` option. - // If `true` (the default), the `title` string will be interpreted as HTML. - // Otherwise it will be interpreted as plain text. - // * `item` A view (object with an `.element` property) or a DOM element - // containing custom content for the tooltip. This option is mutually - // exclusive with the `title` option. - // * `class` A {String} with a class to apply to the tooltip element to - // enable custom styling. - // * `placement` A {String} or {Function} returning a string to indicate - // the position of the tooltip relative to `element`. Can be `'top'`, - // `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is - // specified, it will dynamically reorient the tooltip. For example, if - // placement is `'auto left'`, the tooltip will display to the left when - // possible, otherwise it will display right. - // When a function is used to determine the placement, it is called with - // the tooltip DOM node as its first argument and the triggering element - // DOM node as its second. The `this` context is set to the tooltip - // instance. - // * `trigger` A {String} indicating how the tooltip should be displayed. - // Choose from one of the following options: - // * `'hover'` Show the tooltip when the mouse hovers over the element. - // This is the default. - // * `'click'` Show the tooltip when the element is clicked. The tooltip - // will be hidden after clicking the element again or anywhere else - // outside of the tooltip itself. - // * `'focus'` Show the tooltip when the element is focused. - // * `'manual'` Show the tooltip immediately and only hide it when the - // returned disposable is disposed. - // * `delay` An object specifying the show and hide delay in milliseconds. - // Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and - // otherwise defaults to `0` for both values. - // * `keyBindingCommand` A {String} containing a command name. If you specify - // this option and a key binding exists that matches the command, it will - // be appended to the title or rendered alone if no title is specified. - // * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. - // If this option is not supplied, the first of all matching key bindings - // for the given command will be rendered. - // - // Returns a {Disposable} on which `.dispose()` can be called to remove the - // tooltip. - add (target, options) { - let disposable - if (target.jquery) { - disposable = new CompositeDisposable() - for (let element of target) { disposable.add(this.add(element, options)) } - return disposable - } - - if (Tooltip == null) { Tooltip = require('./tooltip') } - - const {keyBindingCommand, keyBindingTarget} = options - - if (keyBindingCommand != null) { - const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget}) - const keystroke = getKeystroke(bindings) - if ((options.title != null) && (keystroke != null)) { - options.title += ` ${getKeystroke(bindings)}` - } else if (keystroke != null) { - options.title = getKeystroke(bindings) - } - } - - delete options.selector - options = _.defaults(options, this.defaults) - if (options.trigger === 'hover') { - options = _.defaults(options, this.hoverDefaults) - } - - const tooltip = new Tooltip(target, options, this.viewRegistry) - - if (!this.tooltips.has(target)) { - this.tooltips.set(target, []) - } - this.tooltips.get(target).push(tooltip) - - const hideTooltip = function () { - tooltip.leave({currentTarget: target}) - return tooltip.hide() - } - - window.addEventListener('resize', hideTooltip) - - disposable = new Disposable(() => { - window.removeEventListener('resize', hideTooltip) - hideTooltip() - tooltip.destroy() - - if (this.tooltips.has(target)) { - const tooltipsForTarget = this.tooltips.get(target) - const index = tooltipsForTarget.indexOf(tooltip) - if (index !== -1) { - tooltipsForTarget.splice(index, 1) - } - if (tooltipsForTarget.length === 0) { - return this.tooltips.delete(target) - } - } - }) + this.keymapManager = keymapManager + this.viewRegistry = viewRegistry + this.tooltips = new Map() + } + // Essential: Add a tooltip to the given element. + // + // * `target` An `HTMLElement` + // * `options` An object with one or more of the following options: + // * `title` A {String} or {Function} to use for the text in the tip. If + // a function is passed, `this` will be set to the `target` element. This + // option is mutually exclusive with the `item` option. + // * `html` A {Boolean} affecting the interpretation of the `title` option. + // If `true` (the default), the `title` string will be interpreted as HTML. + // Otherwise it will be interpreted as plain text. + // * `item` A view (object with an `.element` property) or a DOM element + // containing custom content for the tooltip. This option is mutually + // exclusive with the `title` option. + // * `class` A {String} with a class to apply to the tooltip element to + // enable custom styling. + // * `placement` A {String} or {Function} returning a string to indicate + // the position of the tooltip relative to `element`. Can be `'top'`, + // `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is + // specified, it will dynamically reorient the tooltip. For example, if + // placement is `'auto left'`, the tooltip will display to the left when + // possible, otherwise it will display right. + // When a function is used to determine the placement, it is called with + // the tooltip DOM node as its first argument and the triggering element + // DOM node as its second. The `this` context is set to the tooltip + // instance. + // * `trigger` A {String} indicating how the tooltip should be displayed. + // Choose from one of the following options: + // * `'hover'` Show the tooltip when the mouse hovers over the element. + // This is the default. + // * `'click'` Show the tooltip when the element is clicked. The tooltip + // will be hidden after clicking the element again or anywhere else + // outside of the tooltip itself. + // * `'focus'` Show the tooltip when the element is focused. + // * `'manual'` Show the tooltip immediately and only hide it when the + // returned disposable is disposed. + // * `delay` An object specifying the show and hide delay in milliseconds. + // Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and + // otherwise defaults to `0` for both values. + // * `keyBindingCommand` A {String} containing a command name. If you specify + // this option and a key binding exists that matches the command, it will + // be appended to the title or rendered alone if no title is specified. + // * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. + // If this option is not supplied, the first of all matching key bindings + // for the given command will be rendered. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // tooltip. + add (target, options) { + let disposable + if (target.jquery) { + disposable = new CompositeDisposable() + for (let element of target) { disposable.add(this.add(element, options)) } return disposable } - // Extended: Find the tooltips that have been applied to the given element. - // - // * `target` The `HTMLElement` to find tooltips on. - // - // Returns an {Array} of `Tooltip` objects that match the `target`. - findTooltips (target) { - if (this.tooltips.has(target)) { - return this.tooltips.get(target).slice() - } else { - return [] + if (Tooltip == null) { Tooltip = require('./tooltip') } + + const {keyBindingCommand, keyBindingTarget} = options + + if (keyBindingCommand != null) { + const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget}) + const keystroke = getKeystroke(bindings) + if ((options.title != null) && (keystroke != null)) { + options.title += ` ${getKeystroke(bindings)}` + } else if (keystroke != null) { + options.title = getKeystroke(bindings) } } + + delete options.selector + options = _.defaults(options, this.defaults) + if (options.trigger === 'hover') { + options = _.defaults(options, this.hoverDefaults) + } + + const tooltip = new Tooltip(target, options, this.viewRegistry) + + if (!this.tooltips.has(target)) { + this.tooltips.set(target, []) + } + this.tooltips.get(target).push(tooltip) + + const hideTooltip = function () { + tooltip.leave({currentTarget: target}) + return tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + disposable = new Disposable(() => { + window.removeEventListener('resize', hideTooltip) + hideTooltip() + tooltip.destroy() + + if (this.tooltips.has(target)) { + const tooltipsForTarget = this.tooltips.get(target) + const index = tooltipsForTarget.indexOf(tooltip) + if (index !== -1) { + tooltipsForTarget.splice(index, 1) + } + if (tooltipsForTarget.length === 0) { + return this.tooltips.delete(target) + } + } + }) + + return disposable } - TooltipManager.initClass() - return TooltipManager -})()) + + // Extended: Find the tooltips that have been applied to the given element. + // + // * `target` The `HTMLElement` to find tooltips on. + // + // Returns an {Array} of `Tooltip` objects that match the `target`. + findTooltips (target) { + if (this.tooltips.has(target)) { + return this.tooltips.get(target).slice() + } else { + return [] + } + } +} const humanizeKeystrokes = function (keystroke) { let keystrokes = keystroke.split(' ') From 028d419ce778651124c504470f9515671c58a88c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:31:32 -0400 Subject: [PATCH 033/161] :art: DS102 Remove unnecessary code created because of implicit returns --- src/tooltip-manager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 00e16e405..89849020c 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ @@ -153,7 +152,7 @@ class TooltipManager { const hideTooltip = function () { tooltip.leave({currentTarget: target}) - return tooltip.hide() + tooltip.hide() } window.addEventListener('resize', hideTooltip) @@ -170,7 +169,7 @@ class TooltipManager { tooltipsForTarget.splice(index, 1) } if (tooltipsForTarget.length === 0) { - return this.tooltips.delete(target) + this.tooltips.delete(target) } } }) From 4179b11cb9142ef69d4b0d4464fda06f0a992641 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:32:20 -0400 Subject: [PATCH 034/161] :art: DS207 Use shorter variations of null checks --- src/tooltip-manager.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 89849020c..a27b860b0 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const _ = require('underscore-plus') const {Disposable, CompositeDisposable} = require('event-kit') let Tooltip = null @@ -198,7 +193,7 @@ const humanizeKeystrokes = function (keystroke) { } var getKeystroke = function (bindings) { - if (bindings != null ? bindings.length : undefined) { + if (bindings && bindings.length) { return `${humanizeKeystrokes(bindings[0].keystrokes)}` } } From 74137446e79d74afac2aa7ca4b9e45581180496c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:34:20 -0400 Subject: [PATCH 035/161] =?UTF-8?q?:memo:=E2=98=A0=E2=98=95=20Decaffeinate?= =?UTF-8?q?=20TooltipManager=20API=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tooltip-manager.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index a27b860b0..73a58d1d6 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -10,38 +10,39 @@ let Tooltip = null // // The essence of displaying a tooltip // -// ```coffee -// # display it -// disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) +// ```javascript +// // display it +// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) // -// # remove it +// // remove it // disposable.dispose() // ``` // // In practice there are usually multiple tooltips. So we add them to a // CompositeDisposable // -// ```coffee -// {CompositeDisposable} = require 'atom' -// subscriptions = new CompositeDisposable +// ```javascript +// const {CompositeDisposable} = require('atom') +// const subscriptions = new CompositeDisposable() // -// div1 = document.createElement('div') -// div2 = document.createElement('div') -// subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'}) -// subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'}) +// const div1 = document.createElement('div') +// const div2 = document.createElement('div') +// subscriptions.add(atom.tooltips.add(div1, {title: 'This is a tooltip'})) +// subscriptions.add(atom.tooltips.add(div2, {title: 'Another tooltip'})) // -// # remove them all +// // remove them all // subscriptions.dispose() // ``` // // You can display a key binding in the tooltip as well with the // `keyBindingCommand` option. // -// ```coffee -// disposable = atom.tooltips.add @caseOptionButton, -// title: "Match Case" -// keyBindingCommand: 'find-and-replace:toggle-case-option' -// keyBindingTarget: @findEditor.element +// ```javascript +// disposable = atom.tooltips.add(this.caseOptionButton, { +// title: 'Match Case', +// keyBindingCommand: 'find-and-replace:toggle-case-option', +// keyBindingTarget: this.findEditor.element +// }) // ``` module.exports = class TooltipManager { From 5e587e88a982e81ac8b96421fef75078e722579f Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:34:35 -0400 Subject: [PATCH 036/161] :art: --- src/tooltip-manager.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 73a58d1d6..937f831d1 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -112,10 +112,9 @@ class TooltipManager { // Returns a {Disposable} on which `.dispose()` can be called to remove the // tooltip. add (target, options) { - let disposable if (target.jquery) { - disposable = new CompositeDisposable() - for (let element of target) { disposable.add(this.add(element, options)) } + const disposable = new CompositeDisposable() + for (const element of target) { disposable.add(this.add(element, options)) } return disposable } @@ -153,7 +152,7 @@ class TooltipManager { window.addEventListener('resize', hideTooltip) - disposable = new Disposable(() => { + const disposable = new Disposable(() => { window.removeEventListener('resize', hideTooltip) hideTooltip() tooltip.destroy() @@ -187,13 +186,13 @@ class TooltipManager { } } -const humanizeKeystrokes = function (keystroke) { +function humanizeKeystrokes (keystroke) { let keystrokes = keystroke.split(' ') keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) return keystrokes.join(' ') } -var getKeystroke = function (bindings) { +function getKeystroke (bindings) { if (bindings && bindings.length) { return `${humanizeKeystrokes(bindings[0].keystrokes)}` } From 8d532e77806703dae7ec4b80f84bc4e970b0b4fd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 10:20:45 -0700 Subject: [PATCH 037/161] Fix exception when trying to fold non-foldable row --- spec/text-editor-spec.js | 20 ++++++++++++++++++++ src/text-editor.coffee | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index c81df8089..b766a8ac9 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -173,6 +173,26 @@ describe('TextEditor', () => { }) }) + describe('.foldCurrentRow()', () => { + it('creates a fold at the location of the last cursor', async () => { + editor = await atom.workspace.open() + editor.setText('\nif (x) {\n y()\n}') + editor.setCursorBufferPosition([1, 0]) + expect(editor.getScreenLineCount()).toBe(4) + editor.foldCurrentRow() + expect(editor.getScreenLineCount()).toBe(3) + }) + + it('does nothing when the current row cannot be folded', async () => { + editor = await atom.workspace.open() + editor.setText('var x;\nx++\nx++') + editor.setCursorBufferPosition([0, 0]) + expect(editor.getScreenLineCount()).toBe(3) + editor.foldCurrentRow() + expect(editor.getScreenLineCount()).toBe(3) + }) + }) + describe('.foldAllAtIndentLevel(indentLevel)', () => { it('folds blocks of text at the given indentation level', async () => { editor = await atom.workspace.open('sample.js', {autoIndent: false}) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index c00508f09..6700af089 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3310,8 +3310,8 @@ class TextEditor extends Model # level. foldCurrentRow: -> {row} = @getCursorBufferPosition() - range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) - @displayLayer.foldBufferRange(range) + if range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + @displayLayer.foldBufferRange(range) # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> From 6ccc807aebbdbfea1a3b147d4d1bfc09a4362e9f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 10:45:08 -0700 Subject: [PATCH 038/161] :arrow_up: season --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..2fba03420 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "scandal": "^3.1.0", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", - "season": "^6.0.1", + "season": "^6.0.2", "semver": "^4.3.3", "service-hub": "^0.7.4", "sinon": "1.17.4", From ef6b5ee07c42cf7fd31c56b83168496e6eeda8ad Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:14:46 -0700 Subject: [PATCH 039/161] :arrow_up: language-gfm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fba03420..9c9a988db 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "language-coffee-script": "0.49.1", "language-csharp": "0.14.3", "language-css": "0.42.6", - "language-gfm": "0.90.1", + "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.2", "language-html": "0.48.1", From 8318b7207e5fcdf3f81425d61cc95cb15c974a1a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:22:34 -0700 Subject: [PATCH 040/161] :arrow_up: language-less --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c9a988db..077b8d46f 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.4", "language-javascript": "0.127.5", "language-json": "0.19.1", - "language-less": "0.33.0", + "language-less": "0.33.1", "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", From 9e21931b91e48b71d01675a5b24850ab49cf86aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 12:23:06 -0700 Subject: [PATCH 041/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 077b8d46f..af75ffd96 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.7", + "text-buffer": "13.5.8", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From ed94726fab201200be8a33098dd91b0d54f1e1aa Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 23 Oct 2017 14:32:34 -0600 Subject: [PATCH 042/161] fix overlayComponent access syntax in test --- spec/text-editor-component-spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d46748d91..5f0a28883 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1896,8 +1896,7 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) await component.getNextUpdatePromise() - let overlayComponent - component.overlayComponents.forEach(c => overlayComponent = c) + const overlayComponent = component.overlayComponents.values().next().value const overlayWrapper = overlayElement.parentElement expect(overlayWrapper.classList.contains('a')).toBe(true) From 5465830dbe04a43bc98eafccacef7f6824c7bb23 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:23:20 -0700 Subject: [PATCH 043/161] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af75ffd96..2e98cc797 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.0", - "snippets": "1.1.5", + "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", "styleguide": "0.49.7", From aa4796e7d614b8b58db1f11dc6cf1b162b96eeb6 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:24:41 -0700 Subject: [PATCH 044/161] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e98cc797..8b90346a4 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.252.0", + "settings-view": "0.252.1", "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", From adcbb7ab2c12a524cb5b91be9777c7f9f7afe0a7 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 12:35:38 -0700 Subject: [PATCH 045/161] :arrow_up: first-mate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b90346a4..910f569fc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.9", + "first-mate": "7.0.10", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From f31bbc58829003a85bb4c0c6bf818ef083c6e571 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 14:04:04 -0700 Subject: [PATCH 046/161] :arrow_up: atom-keymap --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 910f569fc..2790f4c8f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", - "atom-keymap": "8.2.7", + "atom-keymap": "8.2.8", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From d03bedd8cff531cbcf3c62e9da377e99a469467d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 15:03:56 -0700 Subject: [PATCH 047/161] :arrow_up: styleguide --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2790f4c8f..51f6732d2 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", - "styleguide": "0.49.7", + "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.108.0", "timecop": "0.36.0", From d1844eccec173a16f030f2f90a08350da56fb300 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 15:13:05 -0700 Subject: [PATCH 048/161] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51f6732d2..8d8b98ccc 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.15", + "markdown-preview": "0.159.16", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From bbbf09ecf274d0665c70fb200b284fc425265dea Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 16:18:01 -0600 Subject: [PATCH 049/161] Add preserveTrailingLineIndentation option to Selection.insertText We can use this to support a new command that preserves all formatting when pasting. --- spec/selection-spec.coffee | 5 +++++ src/selection.coffee | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index cb070310a..b0e65be30 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -103,6 +103,11 @@ describe "Selection", -> selection.insertText("\r\n", autoIndent: true) expect(buffer.lineForRow(2)).toBe " " + it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", -> + selection.setBufferRange [[5, 0], [5, 0]] + selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1) + expect(buffer.lineForRow(6)).toBe(' bar') + describe ".fold()", -> it "folds the buffer range spanned by the selection", -> selection.setBufferRange([[0, 3], [1, 6]]) diff --git a/src/selection.coffee b/src/selection.coffee index 4d3fe8882..6fcf8dd36 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -356,13 +356,19 @@ class Selection extends Model # # * `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 + # * `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). + # * `preserveTrailingLineIndentation` By default, when pasting multiple + # lines, Atom attempts to preserve the relative indent level between the + # first line and trailing lines, even if the indent level of the first + # line has changed from the copied text. If this option is `true`, this + # behavior is suppressed. + # level between the first lines and the trailing lines. # * `normalizeLineEndings` (optional) {Boolean} (default: true) - # * `undo` if `skip`, skips the undo stack for this operation. + # * `undo` If `skip`, skips the undo stack for this operation. insertText: (text, options={}) -> oldBufferRange = @getBufferRange() wasReversed = @isReversed() @@ -373,7 +379,7 @@ class Selection extends Model remainingLines = text.split('\n') firstInsertedLine = remainingLines.shift() - if options.indentBasis? + if options.indentBasis? and not options.preserveTrailingLineIndentation indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis @adjustIndent(remainingLines, indentAdjustment) From 6701644bbd9c983f804dc0596bc880695e09971d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 17:02:41 -0600 Subject: [PATCH 050/161] Respect format-preserving options in TextEditor.pasteText --- spec/text-editor-spec.coffee | 13 +++++++++++++ src/text-editor.coffee | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 53011fdcc..bc74cd443 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4222,6 +4222,19 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + it "respects options that preserve the formatting of the pasted text", -> + editor.update({autoIndentOnPaste: true}) + atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0) + editor.setCursorBufferPosition([5, 0]) + editor.insertText(' ') + editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false}) + + expect(editor.lineTextForBufferRow(5)).toBe " a(x);" + expect(editor.lineTextForBufferRow(6)).toBe " b(x);" + expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n" + expect(editor.lineTextForBufferRow(7)).toBe "c(x);" + expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" + describe ".indentSelectedRows()", -> describe "when nothing is selected", -> describe "when softTabs is enabled", -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6700af089..32dd49a18 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3247,12 +3247,13 @@ class TextEditor extends Model # corresponding clipboard selection text. # # * `options` (optional) See {Selection::insertText}. - pasteText: (options={}) -> + pasteText: (options) -> + options = Object.assign({}, options) {text: clipboardText, metadata} = @constructor.clipboard.readWithMetadata() return false unless @emitWillInsertTextEvent(clipboardText) metadata ?= {} - options.autoIndent = @shouldAutoIndentOnPaste() + options.autoIndent ?= @shouldAutoIndentOnPaste() @mutateSelectedText (selection, index) => if metadata.selections?.length is @getSelections().length From 40ed5838a5a80f5681b2a43dbfa5d036c491a29d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 16:07:41 -0700 Subject: [PATCH 051/161] :arrow_up: dedent --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d8b98ccc..4056b0d71 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "clear-cut": "^2.0.2", "coffee-script": "1.11.1", "color": "^0.7.3", - "dedent": "^0.6.0", + "dedent": "^0.7.0", "devtron": "1.3.0", "etch": "^0.12.6", "event-kit": "^2.4.0", From fd85c1bb5abec895bd780f2ed69033f5d89b3439 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 17:14:41 -0600 Subject: [PATCH 052/161] Add `Paste without reformatting` command It is bound to cmd-shift-V on macOS and ctrl-shift-V on Windows and Linux. It is also available in the edit menu. --- keymaps/darwin.cson | 1 + keymaps/linux.cson | 1 + keymaps/win32.cson | 1 + menus/darwin.cson | 1 + menus/linux.cson | 1 + menus/win32.cson | 1 + src/register-default-commands.coffee | 5 +++++ 7 files changed, 11 insertions(+) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index fa942d97c..7161a8478 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -132,6 +132,7 @@ 'ctrl-shift-w': 'editor:select-word' 'cmd-ctrl-left': 'editor:move-selection-left' 'cmd-ctrl-right': 'editor:move-selection-right' + 'cmd-shift-V': 'editor:paste-without-reformatting' # Emacs 'alt-f': 'editor:move-to-end-of-word' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index d6ded1f90..9d3e4dbb1 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -105,6 +105,7 @@ 'alt-shift-right': 'editor:select-to-next-subword-boundary' 'alt-backspace': 'editor:delete-to-beginning-of-subword' 'alt-delete': 'editor:delete-to-end-of-subword' + 'ctrl-shift-V': 'editor:paste-without-reformatting' # Sublime Parity 'ctrl-a': 'core:select-all' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 14f5a4283..8a8e92249 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -110,6 +110,7 @@ 'alt-shift-right': 'editor:select-to-next-subword-boundary' 'alt-backspace': 'editor:delete-to-beginning-of-subword' 'alt-delete': 'editor:delete-to-end-of-subword' + 'ctrl-shift-V': 'editor:paste-without-reformatting' # Sublime Parity 'ctrl-a': 'core:select-all' diff --git a/menus/darwin.cson b/menus/darwin.cson index 055cd2405..2dffda1ef 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -65,6 +65,7 @@ { label: 'Copy', command: 'core:copy' } { label: 'Copy Path', command: 'editor:copy-path' } { label: 'Paste', command: 'core:paste' } + { label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' } { label: 'Select All', command: 'core:select-all' } { type: 'separator' } { label: 'Toggle Comments', command: 'editor:toggle-line-comments' } diff --git a/menus/linux.cson b/menus/linux.cson index 2a1ca47f8..b44900398 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -38,6 +38,7 @@ { label: 'C&opy', command: 'core:copy' } { label: 'Copy Pat&h', command: 'editor:copy-path' } { label: '&Paste', command: 'core:paste' } + { label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' } { label: 'Select &All', command: 'core:select-all' } { type: 'separator' } { label: '&Toggle Comments', command: 'editor:toggle-line-comments' } diff --git a/menus/win32.cson b/menus/win32.cson index 553b6017e..a921bae74 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -46,6 +46,7 @@ { label: '&Copy', command: 'core:copy' } { label: 'Copy Pat&h', command: 'editor:copy-path' } { label: '&Paste', command: 'core:paste' } + { label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' } { label: 'Select &All', command: 'core:select-all' } { type: 'separator' } { label: '&Toggle Comments', command: 'editor:toggle-line-comments' } diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index d5b741c40..7dc0d3298 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -174,6 +174,11 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage 'core:cut': -> @cutSelectedText() 'core:copy': -> @copySelectedText() 'core:paste': -> @pasteText() + 'editor:paste-without-reformatting': -> @pasteText({ + normalizeLineEndings: false, + autoIndent: false, + preserveTrailingLineIndentation: true + }) 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() From 311567ecec887c937d45287b92f667e827fcb1db Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 16:45:12 -0700 Subject: [PATCH 053/161] Simplify .toggleLineComments method to avoid using oniguruma --- spec/tokenized-buffer-spec.js | 18 ++++---- src/tokenized-buffer.js | 78 +++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index ba43f9ff3..9dc636bef 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -692,38 +692,38 @@ describe('TokenizedBuffer', () => { it('comments/uncomments lines in the given range', () => { tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe('/*body {') - expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/') + expect(buffer.lineForRow(0)).toBe('/* body {') + expect(buffer.lineForRow(1)).toBe(' font-size: 1234px; */') expect(buffer.lineForRow(2)).toBe(' width: 110%;') expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(0)).toBe('/*body {') - expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/') - expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/') + expect(buffer.lineForRow(0)).toBe('/* body {') + expect(buffer.lineForRow(1)).toBe(' font-size: 1234px; */') + expect(buffer.lineForRow(2)).toBe(' /* width: 110%; */') expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) expect(buffer.lineForRow(0)).toBe('body {') expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;') - expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/') + expect(buffer.lineForRow(2)).toBe(' /* width: 110%; */') expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') }) it('uncomments lines with leading whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /* width: 110%; */') tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe(' width: 110%;') }) it('uncomments lines with trailing whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') + buffer.setTextInRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ') tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe('width: 110%; ') }) it('uncomments lines with leading and trailing whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ') tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe(' width: 110%; ') }) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index b4bc0d41c..13a1b17fa 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -165,37 +165,32 @@ class TokenizedBuffer { toggleLineCommentsForBufferRows (start, end) { const scope = this.scopeDescriptorForPosition([start, 0]) - const commentStrings = this.commentStringsForScopeDescriptor(scope) - if (!commentStrings) return - const {commentStartString, commentEndString} = commentStrings + let {commentStartString, commentEndString} = this.commentStringsForScopeDescriptor(scope) if (!commentStartString) return - - const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`) + commentStartString = commentStartString.trim() if (commentEndString) { - const shouldUncomment = commentStartRegex.testSync(this.buffer.lineForRow(start)) - if (shouldUncomment) { - const commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') - const commentEndRegex = new OnigRegExp(`(${commentEndRegexString})(\\s*)$`) - const startMatch = commentStartRegex.searchSync(this.buffer.lineForRow(start)) - const endMatch = commentEndRegex.searchSync(this.buffer.lineForRow(end)) - if (startMatch && endMatch) { + commentEndString = commentEndString.trim() + const startDelimiterColumnRange = this.columnRangeForStartDelimiter( + this.buffer.lineForRow(start), + commentStartString + ) + if (startDelimiterColumnRange) { + const endDelimiterColumnRange = this.columnRangeForEndDelimiter( + this.buffer.lineForRow(end), + commentEndString + ) + if (endDelimiterColumnRange) { this.buffer.transact(() => { - const columnStart = startMatch[1].length - const columnEnd = columnStart + startMatch[2].length - this.buffer.setTextInRange([[start, columnStart], [start, columnEnd]], '') - - const endLength = this.buffer.lineLengthForRow(end) - endMatch[2].length - const endColumn = endLength - endMatch[1].length - return this.buffer.setTextInRange([[end, endColumn], [end, endLength]], '') + this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) + this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) }) } } else { this.buffer.transact(() => { const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length - this.buffer.insert([start, indentLength], commentStartString) - this.buffer.insert([end, this.buffer.lineLengthForRow(end)], commentEndString) + this.buffer.insert([start, indentLength], commentStartString + ' ') + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) }) } } else { @@ -204,7 +199,7 @@ class TokenizedBuffer { for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { - if (commentStartRegex.testSync(line)) { + if (this.columnRangeForStartDelimiter(line, commentStartString)) { hasCommentedLines = true } else { hasUncommentedLines = true @@ -216,12 +211,11 @@ class TokenizedBuffer { if (shouldUncomment) { for (let row = start; row <= end; row++) { - const match = commentStartRegex.searchSync(this.buffer.lineForRow(row)) - if (match) { - const columnStart = match[1].length - const columnEnd = columnStart + match[2].length - this.buffer.setTextInRange([[row, columnStart], [row, columnEnd]], '') - } + const columnRange = this.columnRangeForStartDelimiter( + this.buffer.lineForRow(row), + commentStartString + ) + if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) } } else { let minIndentLevel = Infinity @@ -247,11 +241,11 @@ class TokenizedBuffer { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { const indentColumn = this.columnForIndentLevel(line, minIndentLevel) - this.buffer.insert(Point(row, indentColumn), commentStartString) + this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') } else { this.buffer.setTextInRange( new Range(new Point(row, 0), new Point(row, Infinity)), - indentString + commentStartString + indentString + commentStartString + ' ' ) } } @@ -259,6 +253,26 @@ class TokenizedBuffer { } } + columnRangeForStartDelimiter (line, delimiter) { + const startColumn = line.search(NON_WHITESPACE_REGEX) + if (startColumn === -1) return null + if (!line.startsWith(delimiter, startColumn)) return null + + let endColumn = startColumn + delimiter.length + if (line[endColumn] === ' ') endColumn++ + return [startColumn, endColumn] + } + + columnRangeForEndDelimiter (line, delimiter) { + let startColumn = line.lastIndexOf(delimiter) + if (startColumn === -1) return null + + const endColumn = startColumn + delimiter.length + if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null + if (line[startColumn - 1] === ' ') startColumn-- + return [startColumn, endColumn] + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -844,6 +858,8 @@ class TokenizedBuffer { commentStringsForScopeDescriptor (scopes) { if (this.scopedSettingsDelegate) { return this.scopedSettingsDelegate.getCommentStrings(scopes) + } else { + return {} } } From 7637bc32d154f6f92b750eb26481853e94129a86 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 16:57:34 -0700 Subject: [PATCH 054/161] :arrow_up: atom-package-manager --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 5391c9972..e759a39d2 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.8" + "atom-package-manager": "1.18.9" } } From 079f4d901cdd006263d2844992eca6a52b898a0f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 17:00:05 -0700 Subject: [PATCH 055/161] Move all .toggleLineComments tests to text-editor-spec.js --- spec/text-editor-spec.coffee | 102 ------------- spec/text-editor-spec.js | 272 ++++++++++++++++++++++++++++++++++ spec/tokenized-buffer-spec.js | 180 ---------------------- 3 files changed, 272 insertions(+), 282 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 53011fdcc..de2f9fe8d 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4363,108 +4363,6 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(4)).toBe " }" expect(editor.lineTextForBufferRow(5)).toBe " i=1" - describe ".toggleLineCommentsInSelection()", -> - it "toggles comments on the selected lines", -> - editor.setSelectedBufferRange([[4, 5], [7, 5]]) - editor.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - expect(editor.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]] - - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " current = items.shift();" - expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " }" - - it "does not comment the last line of a non-empty selection if it ends at column 0", -> - editor.setSelectedBufferRange([[4, 5], [7, 0]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " }" - - it "uncomments lines if all lines match the comment regex", -> - editor.setSelectedBufferRange([[0, 0], [0, 1]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - - editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;" - - editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - - editor.setSelectedBufferRange([[0, 0], [0, Infinity]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "uncomments commented lines separated by an empty line", -> - editor.setSelectedBufferRange([[0, 0], [1, Infinity]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {" - - buffer.insert([0, Infinity], '\n') - - editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "" - expect(buffer.lineForRow(2)).toBe " var sort = function(items) {" - - it "preserves selection emptiness", -> - editor.setCursorBufferPosition([4, 0]) - editor.toggleLineCommentsInSelection() - expect(editor.getLastSelection().isEmpty()).toBeTruthy() - - it "does not explode if the current language mode has no comment regex", -> - editor = new TextEditor(buffer: new TextBuffer(text: 'hello')) - editor.setSelectedBufferRange([[0, 0], [0, 5]]) - editor.toggleLineCommentsInSelection() - expect(editor.lineTextForBufferRow(0)).toBe "hello" - - it "does nothing for empty lines and null grammar", -> - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar')) - editor.setCursorBufferPosition([10, 0]) - editor.toggleLineCommentsInSelection() - expect(editor.buffer.lineForRow(10)).toBe "" - - it "uncomments when the line lacks the trailing whitespace in the comment regex", -> - editor.setCursorBufferPosition([10, 0]) - editor.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(10)).toBe "// " - expect(editor.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]] - editor.backspace() - expect(buffer.lineForRow(10)).toBe "//" - - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(10)).toBe "" - expect(editor.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]] - - it "uncomments when the line has leading whitespace", -> - editor.setCursorBufferPosition([10, 0]) - editor.toggleLineCommentsInSelection() - - expect(buffer.lineForRow(10)).toBe "// " - editor.moveToBeginningOfLine() - editor.insertText(" ") - editor.setSelectedBufferRange([[10, 0], [10, 0]]) - editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(10)).toBe " " - describe ".undo() and .redo()", -> it "undoes/redoes the last change", -> editor.insertText("foo") diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index b766a8ac9..d10efa695 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -2,6 +2,8 @@ const fs = require('fs') const temp = require('temp').track() const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') +const TextBuffer = require('text-buffer') +const TextEditor = require('../src/text-editor') describe('TextEditor', () => { let editor @@ -58,6 +60,276 @@ describe('TextEditor', () => { }) }) + describe('.toggleLineCommentsInSelection()', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-javascript') + editor = await atom.workspace.open('sample.js') + }) + + it('toggles comments on the selected lines', () => { + editor.setSelectedBufferRange([[4, 5], [7, 5]]) + editor.toggleLineCommentsInSelection() + + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + expect(editor.getSelectedBufferRange()).toEqual([[4, 8], [7, 8]]) + + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' }') + }) + + it('does not comment the last line of a non-empty selection if it ends at column 0', () => { + editor.setSelectedBufferRange([[4, 5], [7, 0]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' }') + }) + + it('uncomments lines if all lines match the comment regex', () => { + editor.setSelectedBufferRange([[0, 0], [0, 1]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {') + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('// // var quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {') + expect(editor.lineTextForBufferRow(2)).toBe('// if (items.length <= 1) return items;') + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + + editor.setSelectedBufferRange([[0, 0], [0, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + }) + + it('uncomments commented lines separated by an empty line', () => { + editor.setSelectedBufferRange([[0, 0], [1, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {') + + editor.getBuffer().insert([0, Infinity], '\n') + + editor.setSelectedBufferRange([[0, 0], [2, Infinity]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe('') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + }) + + it('preserves selection emptiness', () => { + editor.setCursorBufferPosition([4, 0]) + editor.toggleLineCommentsInSelection() + expect(editor.getLastSelection().isEmpty()).toBeTruthy() + }) + + it('does not explode if the current language mode has no comment regex', () => { + const editor = new TextEditor({buffer: new TextBuffer({text: 'hello'})}) + editor.setSelectedBufferRange([[0, 0], [0, 5]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe('hello') + }) + + it('does nothing for empty lines and null grammar', () => { + editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar')) + editor.setCursorBufferPosition([10, 0]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(10)).toBe('') + }) + + it('uncomments when the line lacks the trailing whitespace in the comment regex', () => { + editor.setCursorBufferPosition([10, 0]) + editor.toggleLineCommentsInSelection() + + expect(editor.lineTextForBufferRow(10)).toBe('// ') + expect(editor.getSelectedBufferRange()).toEqual([[10, 3], [10, 3]]) + editor.backspace() + expect(editor.lineTextForBufferRow(10)).toBe('//') + + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(10)).toBe('') + expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]]) + }) + + it('uncomments when the line has leading whitespace', () => { + editor.setCursorBufferPosition([10, 0]) + editor.toggleLineCommentsInSelection() + + expect(editor.lineTextForBufferRow(10)).toBe('// ') + editor.moveToBeginningOfLine() + editor.insertText(' ') + editor.setSelectedBufferRange([[10, 0], [10, 0]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(10)).toBe(' ') + }) + }) + + describe('.toggleLineCommentsForBufferRows', () => { + describe('xml', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-xml') + editor = await atom.workspace.open('test.xml') + editor.setText('') + }) + + it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') + }) + }) + + describe('less', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + editor = await atom.workspace.open('sample.less') + }) + + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + }) + }) + + describe('css', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-css') + editor = await atom.workspace.open('css.css') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('/* body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */') + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(0)).toBe('/* body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */') + expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') + expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + }) + + it('uncomments lines with leading whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + }) + + it('uncomments lines with trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') + }) + + it('uncomments lines with leading and trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + }) + }) + + describe('coffeescript', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-coffee-script') + editor = await atom.workspace.open('coffee.coffee') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 6) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + }) + + it('comments/uncomments empty lines', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + }) + }) + + describe('javascript', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-javascript') + editor = await atom.workspace.open('sample.js') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.setText('\tvar i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + + editor.setText('var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + + editor.setText(' var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + + editor.setText(' ') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // ') + + editor.setText(' a\n \n b') + editor.toggleLineCommentsForBufferRows(0, 2) + expect(editor.lineTextForBufferRow(0)).toBe(' // a') + expect(editor.lineTextForBufferRow(1)).toBe(' // ') + expect(editor.lineTextForBufferRow(2)).toBe(' // b') + + editor.setText(' \n // var i;') + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + }) + }) + }) + describe('folding', () => { beforeEach(async () => { await atom.packages.activatePackage('language-javascript') diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 9dc636bef..b1574673a 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -643,186 +643,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('.toggleLineCommentsForBufferRows', () => { - describe('xml', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-xml') - buffer = new TextBuffer('') - tokenizedBuffer = new TokenizedBuffer({ - buffer, - grammar: atom.grammars.grammarForScopeName('text.xml'), - scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) - }) - }) - - it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('test') - }) - }) - - describe('less', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-less') - await atom.packages.activatePackage('language-css') - buffer = await TextBuffer.load(require.resolve('./fixtures/sample.less')) - tokenizedBuffer = new TokenizedBuffer({ - buffer, - grammar: atom.grammars.grammarForScopeName('source.css.less'), - scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) - }) - }) - - it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('// @color: #4D926F;') - }) - }) - - describe('css', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-css') - buffer = await TextBuffer.load(require.resolve('./fixtures/css.css')) - tokenizedBuffer = new TokenizedBuffer({ - buffer, - grammar: atom.grammars.grammarForScopeName('source.css'), - scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) - }) - }) - - it('comments/uncomments lines in the given range', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe('/* body {') - expect(buffer.lineForRow(1)).toBe(' font-size: 1234px; */') - expect(buffer.lineForRow(2)).toBe(' width: 110%;') - expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') - - tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(0)).toBe('/* body {') - expect(buffer.lineForRow(1)).toBe(' font-size: 1234px; */') - expect(buffer.lineForRow(2)).toBe(' /* width: 110%; */') - expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') - - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe('body {') - expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;') - expect(buffer.lineForRow(2)).toBe(' /* width: 110%; */') - expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') - }) - - it('uncomments lines with leading whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], ' /* width: 110%; */') - tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe(' width: 110%;') - }) - - it('uncomments lines with trailing whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ') - tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe('width: 110%; ') - }) - - it('uncomments lines with leading and trailing whitespace', () => { - buffer.setTextInRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ') - tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe(' width: 110%; ') - }) - }) - - describe('coffeescript', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-coffee-script') - buffer = await TextBuffer.load(require.resolve('./fixtures/coffee.coffee')) - tokenizedBuffer = new TokenizedBuffer({ - buffer, - tabLength: 2, - grammar: atom.grammars.grammarForScopeName('source.coffee'), - scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) - }) - }) - - it('comments/uncomments lines in the given range', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 6) - expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()') - expect(buffer.lineForRow(5)).toBe(' # left = []') - expect(buffer.lineForRow(6)).toBe(' # right = []') - - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()') - expect(buffer.lineForRow(5)).toBe(' left = []') - expect(buffer.lineForRow(6)).toBe(' # right = []') - }) - - it('comments/uncomments empty lines', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()') - expect(buffer.lineForRow(5)).toBe(' # left = []') - expect(buffer.lineForRow(6)).toBe(' # right = []') - expect(buffer.lineForRow(7)).toBe(' # ') - - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()') - expect(buffer.lineForRow(5)).toBe(' left = []') - expect(buffer.lineForRow(6)).toBe(' # right = []') - expect(buffer.lineForRow(7)).toBe(' # ') - }) - }) - - describe('javascript', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-javascript') - buffer = await TextBuffer.load(require.resolve('./fixtures/sample.js')) - tokenizedBuffer = new TokenizedBuffer({ - buffer, - tabLength: 2, - grammar: atom.grammars.grammarForScopeName('source.js'), - scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) - }) - }) - - it('comments/uncomments lines in the given range', () => { - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe(' // while(items.length > 0) {') - expect(buffer.lineForRow(5)).toBe(' // current = items.shift();') - expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(buffer.lineForRow(7)).toBe(' // }') - - tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe(' while(items.length > 0) {') - expect(buffer.lineForRow(5)).toBe(' current = items.shift();') - expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(buffer.lineForRow(7)).toBe(' // }') - - buffer.setText('\tvar i;') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('\t// var i;') - - buffer.setText('var i;') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('// var i;') - - buffer.setText(' var i;') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe(' // var i;') - - buffer.setText(' ') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe(' // ') - - buffer.setText(' a\n \n b') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2) - expect(buffer.lineForRow(0)).toBe(' // a') - expect(buffer.lineForRow(1)).toBe(' // ') - expect(buffer.lineForRow(2)).toBe(' // b') - - buffer.setText(' \n // var i;') - tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe(' ') - expect(buffer.lineForRow(1)).toBe(' var i;') - }) - }) - }) - describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') From cfe5cfce766fcdec4cd342e1b4c7c72f475c4693 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 17:44:45 -0700 Subject: [PATCH 056/161] Move .toggleLineComments method from TokenizedBuffer to TextEditor --- src/text-editor-utils.js | 139 +++++++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 9 ++- src/tokenized-buffer.js | 137 ++------------------------------------ 3 files changed, 148 insertions(+), 137 deletions(-) create mode 100644 src/text-editor-utils.js diff --git a/src/text-editor-utils.js b/src/text-editor-utils.js new file mode 100644 index 000000000..ab1104144 --- /dev/null +++ b/src/text-editor-utils.js @@ -0,0 +1,139 @@ +// This file is temporary. We should gradually convert methods in `text-editor.coffee` +// from CoffeeScript to JavaScript and move them here, so that we can eventually convert +// the entire class to JavaScript. + +const {Point, Range} = require('text-buffer') + +const NON_WHITESPACE_REGEX = /\S/ + +module.exports = { + toggleLineCommentsForBufferRows (start, end) { + let { + commentStartString, + commentEndString + } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0)) + if (!commentStartString) return + commentStartString = commentStartString.trim() + + if (commentEndString) { + commentEndString = commentEndString.trim() + const startDelimiterColumnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(start), + commentStartString + ) + if (startDelimiterColumnRange) { + const endDelimiterColumnRange = columnRangeForEndDelimiter( + this.buffer.lineForRow(end), + commentEndString + ) + if (endDelimiterColumnRange) { + this.buffer.transact(() => { + this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) + this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) + }) + } + } else { + this.buffer.transact(() => { + const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length + this.buffer.insert([start, indentLength], commentStartString + ' ') + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) + }) + } + } else { + let hasCommentedLines = false + let hasUncommentedLines = false + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEX.test(line)) { + if (columnRangeForStartDelimiter(line, commentStartString)) { + hasCommentedLines = true + } else { + hasUncommentedLines = true + } + } + } + + const shouldUncomment = hasCommentedLines && !hasUncommentedLines + + if (shouldUncomment) { + for (let row = start; row <= end; row++) { + const columnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(row), + commentStartString + ) + if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) + } + } else { + let minIndentLevel = Infinity + let minBlankIndentLevel = Infinity + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const indentLevel = this.indentLevelForLine(line) + if (NON_WHITESPACE_REGEX.test(line)) { + if (indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else { + if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel + } + } + minIndentLevel = Number.isFinite(minIndentLevel) + ? minIndentLevel + : Number.isFinite(minBlankIndentLevel) + ? minBlankIndentLevel + : 0 + + const tabLength = this.getTabLength() + const indentString = ' '.repeat(tabLength * minIndentLevel) + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEX.test(line)) { + const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength()) + this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') + } else { + this.buffer.setTextInRange( + new Range(new Point(row, 0), new Point(row, Infinity)), + indentString + commentStartString + ' ' + ) + } + } + } + } + } +} + +function columnForIndentLevel (line, indentLevel, tabLength) { + let column = 0 + let indentLength = 0 + const goalIndentLength = indentLevel * tabLength + while (indentLength < goalIndentLength) { + const char = line[column] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + column++ + } + return column +} + +function columnRangeForStartDelimiter (line, delimiter) { + const startColumn = line.search(NON_WHITESPACE_REGEX) + if (startColumn === -1) return null + if (!line.startsWith(delimiter, startColumn)) return null + + let endColumn = startColumn + delimiter.length + if (line[endColumn] === ' ') endColumn++ + return [startColumn, endColumn] +} + +function columnRangeForEndDelimiter (line, delimiter) { + let startColumn = line.lastIndexOf(delimiter) + if (startColumn === -1) return null + + const endColumn = startColumn + delimiter.length + if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null + if (line[startColumn - 1] === ' ') startColumn-- + return [startColumn, endColumn] +} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6700af089..f75822d77 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -9,6 +9,8 @@ TokenizedBuffer = require './tokenized-buffer' Cursor = require './cursor' Model = require './model' Selection = require './selection' +TextEditorUtils = require './text-editor-utils' + TextMateScopeSelector = require('first-mate').ScopeSelector GutterContainer = require './gutter-container' TextEditorComponent = null @@ -123,6 +125,8 @@ class TextEditor extends Model Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) + Object.assign(@prototype, TextEditorUtils) + @deserialize: (state, atomEnvironment) -> # TODO: Return null on version mismatch when 1.8.0 has been out for a while if state.version isnt @prototype.serializationVersion and state.displayBuffer? @@ -3621,9 +3625,6 @@ class TextEditor extends Model getNonWordCharacters: (scopes) -> @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - getCommentStrings: (scopes) -> - @scopedSettingsDelegate?.getCommentStrings?(scopes) - ### Section: Event Handlers ### @@ -3886,8 +3887,6 @@ class TextEditor extends Model toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - toggleLineCommentsForBufferRows: (start, end) -> @tokenizedBuffer.toggleLineCommentsForBufferRows(start, end) - rowRangeForParagraphAtBufferRow: (bufferRow) -> return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 13a1b17fa..2a9446256 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -163,116 +163,15 @@ class TokenizedBuffer { Section - Comments */ - toggleLineCommentsForBufferRows (start, end) { - const scope = this.scopeDescriptorForPosition([start, 0]) - let {commentStartString, commentEndString} = this.commentStringsForScopeDescriptor(scope) - if (!commentStartString) return - commentStartString = commentStartString.trim() - - if (commentEndString) { - commentEndString = commentEndString.trim() - const startDelimiterColumnRange = this.columnRangeForStartDelimiter( - this.buffer.lineForRow(start), - commentStartString - ) - if (startDelimiterColumnRange) { - const endDelimiterColumnRange = this.columnRangeForEndDelimiter( - this.buffer.lineForRow(end), - commentEndString - ) - if (endDelimiterColumnRange) { - this.buffer.transact(() => { - this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) - this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) - }) - } - } else { - this.buffer.transact(() => { - const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length - this.buffer.insert([start, indentLength], commentStartString + ' ') - this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) - }) - } + commentStringsForPosition (position) { + if (this.scopedSettingsDelegate) { + const scope = this.scopeDescriptorForPosition(position) + return this.scopedSettingsDelegate.getCommentStrings(scope) } else { - let hasCommentedLines = false - let hasUncommentedLines = false - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - if (this.columnRangeForStartDelimiter(line, commentStartString)) { - hasCommentedLines = true - } else { - hasUncommentedLines = true - } - } - } - - const shouldUncomment = hasCommentedLines && !hasUncommentedLines - - if (shouldUncomment) { - for (let row = start; row <= end; row++) { - const columnRange = this.columnRangeForStartDelimiter( - this.buffer.lineForRow(row), - commentStartString - ) - if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) - } - } else { - let minIndentLevel = Infinity - let minBlankIndentLevel = Infinity - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - const indentLevel = this.indentLevelForLine(line) - if (NON_WHITESPACE_REGEX.test(line)) { - if (indentLevel < minIndentLevel) minIndentLevel = indentLevel - } else { - if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel - } - } - minIndentLevel = Number.isFinite(minIndentLevel) - ? minIndentLevel - : Number.isFinite(minBlankIndentLevel) - ? minBlankIndentLevel - : 0 - - const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * minIndentLevel) - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - const indentColumn = this.columnForIndentLevel(line, minIndentLevel) - this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') - } else { - this.buffer.setTextInRange( - new Range(new Point(row, 0), new Point(row, Infinity)), - indentString + commentStartString + ' ' - ) - } - } - } + return {} } } - columnRangeForStartDelimiter (line, delimiter) { - const startColumn = line.search(NON_WHITESPACE_REGEX) - if (startColumn === -1) return null - if (!line.startsWith(delimiter, startColumn)) return null - - let endColumn = startColumn + delimiter.length - if (line[endColumn] === ' ') endColumn++ - return [startColumn, endColumn] - } - - columnRangeForEndDelimiter (line, delimiter) { - let startColumn = line.lastIndexOf(delimiter) - if (startColumn === -1) return null - - const endColumn = startColumn + delimiter.length - if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null - if (line[startColumn - 1] === ' ') startColumn-- - return [startColumn, endColumn] - } - buildIterator () { return new TokenizedBufferIterator(this) } @@ -608,24 +507,6 @@ class TokenizedBuffer { return scopes } - columnForIndentLevel (line, indentLevel, tabLength = this.tabLength) { - let column = 0 - let indentLength = 0 - const goalIndentLength = indentLevel * tabLength - while (indentLength < goalIndentLength) { - const char = line[column] - if (char === '\t') { - indentLength += tabLength - (indentLength % tabLength) - } else if (char === ' ') { - indentLength++ - } else { - break - } - column++ - } - return column - } - indentLevelForLine (line, tabLength = this.tabLength) { let indentLength = 0 for (let i = 0, {length} = line; i < length; i++) { @@ -855,14 +736,6 @@ class TokenizedBuffer { } } - commentStringsForScopeDescriptor (scopes) { - if (this.scopedSettingsDelegate) { - return this.scopedSettingsDelegate.getCommentStrings(scopes) - } else { - return {} - } - } - regexForPattern (pattern) { if (pattern) { if (!this.regexesByPattern[pattern]) { From f771cf9d1ae4ca47c941dee52e7266f0e2bf41ab Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:47:44 -0400 Subject: [PATCH 057/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/tooltip-manager-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes spec/tooltip-manager-spec.coffee $ standard --fix spec/tooltip-manager-spec.js --- spec/tooltip-manager-spec.coffee | 213 ------------------------- spec/tooltip-manager-spec.js | 260 +++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 213 deletions(-) delete mode 100644 spec/tooltip-manager-spec.coffee create mode 100644 spec/tooltip-manager-spec.js diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee deleted file mode 100644 index 95182853e..000000000 --- a/spec/tooltip-manager-spec.coffee +++ /dev/null @@ -1,213 +0,0 @@ -{CompositeDisposable} = require 'atom' -TooltipManager = require '../src/tooltip-manager' -Tooltip = require '../src/tooltip' -_ = require 'underscore-plus' - -describe "TooltipManager", -> - [manager, element] = [] - - ctrlX = _.humanizeKeystroke("ctrl-x") - ctrlY = _.humanizeKeystroke("ctrl-y") - - beforeEach -> - manager = new TooltipManager(keymapManager: atom.keymaps, viewRegistry: atom.views) - element = createElement 'foo' - - createElement = (className) -> - el = document.createElement('div') - el.classList.add(className) - jasmine.attachToDOM(el) - el - - mouseEnter = (element) -> - element.dispatchEvent(new CustomEvent('mouseenter', bubbles: false)) - element.dispatchEvent(new CustomEvent('mouseover', bubbles: true)) - - mouseLeave = (element) -> - element.dispatchEvent(new CustomEvent('mouseleave', bubbles: false)) - element.dispatchEvent(new CustomEvent('mouseout', bubbles: true)) - - hover = (element, fn) -> - mouseEnter(element) - advanceClock(manager.hoverDefaults.delay.show) - fn() - mouseLeave(element) - advanceClock(manager.hoverDefaults.delay.hide) - - describe "::add(target, options)", -> - describe "when the trigger is 'hover' (the default)", -> - it "creates a tooltip when hovering over the target element", -> - manager.add element, title: "Title" - hover element, -> - expect(document.body.querySelector(".tooltip")).toHaveText("Title") - - it "displays tooltips immediately when hovering over new elements once a tooltip has been displayed once", -> - disposables = new CompositeDisposable - element1 = createElement('foo') - disposables.add(manager.add element1, title: 'Title') - element2 = createElement('bar') - disposables.add(manager.add element2, title: 'Title') - element3 = createElement('baz') - disposables.add(manager.add element3, title: 'Title') - - hover element1, -> - expect(document.body.querySelector(".tooltip")).toBeNull() - - mouseEnter(element2) - expect(document.body.querySelector(".tooltip")).not.toBeNull() - mouseLeave(element2) - advanceClock(manager.hoverDefaults.delay.hide) - expect(document.body.querySelector(".tooltip")).toBeNull() - - advanceClock(Tooltip.FOLLOW_THROUGH_DURATION) - mouseEnter(element3) - expect(document.body.querySelector(".tooltip")).toBeNull() - advanceClock(manager.hoverDefaults.delay.show) - expect(document.body.querySelector(".tooltip")).not.toBeNull() - - disposables.dispose() - - describe "when the trigger is 'manual'", -> - it "creates a tooltip immediately and only hides it on dispose", -> - disposable = manager.add element, title: "Title", trigger: "manual" - expect(document.body.querySelector(".tooltip")).toHaveText("Title") - disposable.dispose() - expect(document.body.querySelector(".tooltip")).toBeNull() - - describe "when the trigger is 'click'", -> - it "shows and hides the tooltip when the target element is clicked", -> - disposable = manager.add element, title: "Title", trigger: "click" - expect(document.body.querySelector(".tooltip")).toBeNull() - element.click() - expect(document.body.querySelector(".tooltip")).not.toBeNull() - element.click() - expect(document.body.querySelector(".tooltip")).toBeNull() - - # Hide the tooltip when clicking anywhere but inside the tooltip element - element.click() - expect(document.body.querySelector(".tooltip")).not.toBeNull() - document.body.querySelector(".tooltip").click() - expect(document.body.querySelector(".tooltip")).not.toBeNull() - document.body.querySelector(".tooltip").firstChild.click() - expect(document.body.querySelector(".tooltip")).not.toBeNull() - document.body.click() - expect(document.body.querySelector(".tooltip")).toBeNull() - - # Tooltip can show again after hiding due to clicking outside of the tooltip - element.click() - expect(document.body.querySelector(".tooltip")).not.toBeNull() - element.click() - expect(document.body.querySelector(".tooltip")).toBeNull() - - it "allows a custom item to be specified for the content of the tooltip", -> - tooltipElement = document.createElement('div') - manager.add element, item: {element: tooltipElement} - hover element, -> - expect(tooltipElement.closest(".tooltip")).not.toBeNull() - - it "allows a custom class to be specified for the tooltip", -> - tooltipElement = document.createElement('div') - manager.add element, title: 'Title', class: 'custom-tooltip-class' - hover element, -> - expect(document.body.querySelector(".tooltip").classList.contains('custom-tooltip-class')).toBe(true) - - it "allows jQuery elements to be passed as the target", -> - element2 = document.createElement('div') - jasmine.attachToDOM(element2) - - fakeJqueryWrapper = [element, element2] - fakeJqueryWrapper.jquery = 'any-version' - disposable = manager.add fakeJqueryWrapper, title: "Title" - - hover element, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title") - expect(document.body.querySelector(".tooltip")).toBeNull() - hover element2, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title") - expect(document.body.querySelector(".tooltip")).toBeNull() - - disposable.dispose() - - hover element, -> expect(document.body.querySelector(".tooltip")).toBeNull() - hover element2, -> expect(document.body.querySelector(".tooltip")).toBeNull() - - describe "when a keyBindingCommand is specified", -> - describe "when a title is specified", -> - it "appends the key binding corresponding to the command to the title", -> - atom.keymaps.add 'test', - '.foo': 'ctrl-x ctrl-y': 'test-command' - '.bar': 'ctrl-x ctrl-z': 'test-command' - - manager.add element, title: "Title", keyBindingCommand: 'test-command' - - hover element, -> - tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "Title #{ctrlX} #{ctrlY}" - - describe "when no title is specified", -> - it "shows the key binding corresponding to the command alone", -> - atom.keymaps.add 'test', '.foo': 'ctrl-x ctrl-y': 'test-command' - - manager.add element, keyBindingCommand: 'test-command' - - hover element, -> - tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}" - - describe "when a keyBindingTarget is specified", -> - it "looks up the key binding relative to the target", -> - atom.keymaps.add 'test', - '.bar': 'ctrl-x ctrl-z': 'test-command' - '.foo': 'ctrl-x ctrl-y': 'test-command' - - manager.add element, keyBindingCommand: 'test-command', keyBindingTarget: element - - hover element, -> - tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}" - - it "does not display the keybinding if there is nothing mapped to the specified keyBindingCommand", -> - manager.add element, title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element - - hover element, -> - tooltipElement = document.body.querySelector(".tooltip") - expect(tooltipElement.textContent).toBe "A Title" - - describe "when .dispose() is called on the returned disposable", -> - it "no longer displays the tooltip on hover", -> - disposable = manager.add element, title: "Title" - - hover element, -> - expect(document.body.querySelector(".tooltip")).toHaveText("Title") - - disposable.dispose() - - hover element, -> - expect(document.body.querySelector(".tooltip")).toBeNull() - - describe "when the window is resized", -> - it "hides the tooltips", -> - disposable = manager.add element, title: "Title" - hover element, -> - expect(document.body.querySelector(".tooltip")).not.toBeNull() - window.dispatchEvent(new CustomEvent('resize')) - expect(document.body.querySelector(".tooltip")).toBeNull() - disposable.dispose() - - describe "findTooltips", -> - it "adds and remove tooltips correctly", -> - expect(manager.findTooltips(element).length).toBe(0) - disposable1 = manager.add element, title: "elem1" - expect(manager.findTooltips(element).length).toBe(1) - disposable2 = manager.add element, title: "elem2" - expect(manager.findTooltips(element).length).toBe(2) - disposable1.dispose() - expect(manager.findTooltips(element).length).toBe(1) - disposable2.dispose() - expect(manager.findTooltips(element).length).toBe(0) - - it "lets us hide tooltips programmatically", -> - disposable = manager.add element, title: "Title" - hover element, -> - expect(document.body.querySelector(".tooltip")).not.toBeNull() - manager.findTooltips(element)[0].hide() - expect(document.body.querySelector(".tooltip")).toBeNull() - disposable.dispose() diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js new file mode 100644 index 000000000..c022db44a --- /dev/null +++ b/spec/tooltip-manager-spec.js @@ -0,0 +1,260 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {CompositeDisposable} = require('atom') +const TooltipManager = require('../src/tooltip-manager') +const Tooltip = require('../src/tooltip') +const _ = require('underscore-plus') + +describe('TooltipManager', function () { + let [manager, element] = Array.from([]) + + const ctrlX = _.humanizeKeystroke('ctrl-x') + const ctrlY = _.humanizeKeystroke('ctrl-y') + + beforeEach(function () { + manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) + return element = createElement('foo') + }) + + var createElement = function (className) { + const el = document.createElement('div') + el.classList.add(className) + jasmine.attachToDOM(el) + return el + } + + const mouseEnter = function (element) { + element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) + return element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) + } + + const mouseLeave = function (element) { + element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) + return element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) + } + + const hover = function (element, fn) { + mouseEnter(element) + advanceClock(manager.hoverDefaults.delay.show) + fn() + mouseLeave(element) + return advanceClock(manager.hoverDefaults.delay.hide) + } + + return describe('::add(target, options)', function () { + describe("when the trigger is 'hover' (the default)", function () { + it('creates a tooltip when hovering over the target element', function () { + manager.add(element, {title: 'Title'}) + return hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + }) + + return it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { + const disposables = new CompositeDisposable() + const element1 = createElement('foo') + disposables.add(manager.add(element1, {title: 'Title'})) + const element2 = createElement('bar') + disposables.add(manager.add(element2, {title: 'Title'})) + const element3 = createElement('baz') + disposables.add(manager.add(element3, {title: 'Title'})) + + hover(element1, function () {}) + expect(document.body.querySelector('.tooltip')).toBeNull() + + mouseEnter(element2) + expect(document.body.querySelector('.tooltip')).not.toBeNull() + mouseLeave(element2) + advanceClock(manager.hoverDefaults.delay.hide) + expect(document.body.querySelector('.tooltip')).toBeNull() + + advanceClock(Tooltip.FOLLOW_THROUGH_DURATION) + mouseEnter(element3) + expect(document.body.querySelector('.tooltip')).toBeNull() + advanceClock(manager.hoverDefaults.delay.show) + expect(document.body.querySelector('.tooltip')).not.toBeNull() + + return disposables.dispose() + }) + }) + + describe("when the trigger is 'manual'", () => + it('creates a tooltip immediately and only hides it on dispose', function () { + const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) + expect(document.body.querySelector('.tooltip')).toHaveText('Title') + disposable.dispose() + return expect(document.body.querySelector('.tooltip')).toBeNull() + }) + ) + + describe("when the trigger is 'click'", () => + it('shows and hides the tooltip when the target element is clicked', function () { + const disposable = manager.add(element, {title: 'Title', trigger: 'click'}) + expect(document.body.querySelector('.tooltip')).toBeNull() + element.click() + expect(document.body.querySelector('.tooltip')).not.toBeNull() + element.click() + expect(document.body.querySelector('.tooltip')).toBeNull() + + // Hide the tooltip when clicking anywhere but inside the tooltip element + element.click() + expect(document.body.querySelector('.tooltip')).not.toBeNull() + document.body.querySelector('.tooltip').click() + expect(document.body.querySelector('.tooltip')).not.toBeNull() + document.body.querySelector('.tooltip').firstChild.click() + expect(document.body.querySelector('.tooltip')).not.toBeNull() + document.body.click() + expect(document.body.querySelector('.tooltip')).toBeNull() + + // Tooltip can show again after hiding due to clicking outside of the tooltip + element.click() + expect(document.body.querySelector('.tooltip')).not.toBeNull() + element.click() + return expect(document.body.querySelector('.tooltip')).toBeNull() + }) + ) + + it('allows a custom item to be specified for the content of the tooltip', function () { + const tooltipElement = document.createElement('div') + manager.add(element, {item: {element: tooltipElement}}) + return hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) + }) + + it('allows a custom class to be specified for the tooltip', function () { + const tooltipElement = document.createElement('div') + manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) + return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) + }) + + it('allows jQuery elements to be passed as the target', function () { + const element2 = document.createElement('div') + jasmine.attachToDOM(element2) + + const fakeJqueryWrapper = [element, element2] + fakeJqueryWrapper.jquery = 'any-version' + const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'}) + + hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + expect(document.body.querySelector('.tooltip')).toBeNull() + hover(element2, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + expect(document.body.querySelector('.tooltip')).toBeNull() + + disposable.dispose() + + hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + return hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + }) + + describe('when a keyBindingCommand is specified', function () { + describe('when a title is specified', () => + it('appends the key binding corresponding to the command to the title', function () { + atom.keymaps.add('test', { + '.foo': { 'ctrl-x ctrl-y': 'test-command' + }, + '.bar': { 'ctrl-x ctrl-z': 'test-command' + } + } + ) + + manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) + }) + }) + ) + + describe('when no title is specified', () => + it('shows the key binding corresponding to the command alone', function () { + atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}}) + + manager.add(element, {keyBindingCommand: 'test-command'}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + }) + }) + ) + + return describe('when a keyBindingTarget is specified', function () { + it('looks up the key binding relative to the target', function () { + atom.keymaps.add('test', { + '.bar': { 'ctrl-x ctrl-z': 'test-command' + }, + '.foo': { 'ctrl-x ctrl-y': 'test-command' + } + } + ) + + manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + }) + }) + + return it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement.textContent).toBe('A Title') + }) + }) + }) + }) + + describe('when .dispose() is called on the returned disposable', () => + it('no longer displays the tooltip on hover', function () { + const disposable = manager.add(element, {title: 'Title'}) + + hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + + disposable.dispose() + + return hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + }) + ) + + describe('when the window is resized', () => + it('hides the tooltips', function () { + const disposable = manager.add(element, {title: 'Title'}) + return hover(element, function () { + expect(document.body.querySelector('.tooltip')).not.toBeNull() + window.dispatchEvent(new CustomEvent('resize')) + expect(document.body.querySelector('.tooltip')).toBeNull() + return disposable.dispose() + }) + }) + ) + + return describe('findTooltips', function () { + it('adds and remove tooltips correctly', function () { + expect(manager.findTooltips(element).length).toBe(0) + const disposable1 = manager.add(element, {title: 'elem1'}) + expect(manager.findTooltips(element).length).toBe(1) + const disposable2 = manager.add(element, {title: 'elem2'}) + expect(manager.findTooltips(element).length).toBe(2) + disposable1.dispose() + expect(manager.findTooltips(element).length).toBe(1) + disposable2.dispose() + return expect(manager.findTooltips(element).length).toBe(0) + }) + + return it('lets us hide tooltips programmatically', function () { + const disposable = manager.add(element, {title: 'Title'}) + return hover(element, function () { + expect(document.body.querySelector('.tooltip')).not.toBeNull() + manager.findTooltips(element)[0].hide() + expect(document.body.querySelector('.tooltip')).toBeNull() + return disposable.dispose() + }) + }) + }) + }) +}) From 7f75a46b97dfbae0ade622e8edf91f4afa72623c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:51:15 -0400 Subject: [PATCH 058/161] =?UTF-8?q?=F0=9F=91=95=20Fix=20"Return=20statemen?= =?UTF-8?q?t=20should=20not=20contain=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/tooltip-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index c022db44a..222b5a766 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -17,7 +17,7 @@ describe('TooltipManager', function () { beforeEach(function () { manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) - return element = createElement('foo') + element = createElement('foo') }) var createElement = function (className) { From f976c93d5ad52beebdc6a4e975a1ab20a226b281 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:51:40 -0400 Subject: [PATCH 059/161] :shirt: Fix "'disposable' is assigned a value but never used" --- spec/tooltip-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 222b5a766..35988c24e 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -91,7 +91,7 @@ describe('TooltipManager', function () { describe("when the trigger is 'click'", () => it('shows and hides the tooltip when the target element is clicked', function () { - const disposable = manager.add(element, {title: 'Title', trigger: 'click'}) + manager.add(element, {title: 'Title', trigger: 'click'}) expect(document.body.querySelector('.tooltip')).toBeNull() element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() From 90cfb69c7cd291d5c920e739275a735edc17fc17 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:53:24 -0400 Subject: [PATCH 060/161] :shirt: Fix "'tooltipElement' is assigned a value but never used" --- spec/tooltip-manager-spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 35988c24e..2380d2dc9 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -123,7 +123,6 @@ describe('TooltipManager', function () { }) it('allows a custom class to be specified for the tooltip', function () { - const tooltipElement = document.createElement('div') manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) From 76eb993e7e507ffbe88d2f60046f62d8e2bd69c2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:56:20 -0400 Subject: [PATCH 061/161] :art: DS101 Remove unnecessary use of Array.from --- spec/tooltip-manager-spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 2380d2dc9..683e08e45 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ @@ -10,7 +9,7 @@ const Tooltip = require('../src/tooltip') const _ = require('underscore-plus') describe('TooltipManager', function () { - let [manager, element] = Array.from([]) + let manager, element const ctrlX = _.humanizeKeystroke('ctrl-x') const ctrlY = _.humanizeKeystroke('ctrl-y') From 706f7e3d4408285fc6efb86504d54c7543909e60 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:58:59 -0400 Subject: [PATCH 062/161] :art: DS102 Remove unnecessary code created because of implicit returns --- spec/tooltip-manager-spec.js | 65 +++++++++++++++++------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 683e08e45..0edcd646f 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const {CompositeDisposable} = require('atom') const TooltipManager = require('../src/tooltip-manager') const Tooltip = require('../src/tooltip') @@ -28,12 +23,12 @@ describe('TooltipManager', function () { const mouseEnter = function (element) { element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) - return element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) + element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) } const mouseLeave = function (element) { element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) - return element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) + element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) } const hover = function (element, fn) { @@ -41,17 +36,17 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.show) fn() mouseLeave(element) - return advanceClock(manager.hoverDefaults.delay.hide) + advanceClock(manager.hoverDefaults.delay.hide) } - return describe('::add(target, options)', function () { + describe('::add(target, options)', function () { describe("when the trigger is 'hover' (the default)", function () { it('creates a tooltip when hovering over the target element', function () { manager.add(element, {title: 'Title'}) - return hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) }) - return it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { + it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { const disposables = new CompositeDisposable() const element1 = createElement('foo') disposables.add(manager.add(element1, {title: 'Title'})) @@ -75,7 +70,7 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.show) expect(document.body.querySelector('.tooltip')).not.toBeNull() - return disposables.dispose() + disposables.dispose() }) }) @@ -84,7 +79,7 @@ describe('TooltipManager', function () { const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) expect(document.body.querySelector('.tooltip')).toHaveText('Title') disposable.dispose() - return expect(document.body.querySelector('.tooltip')).toBeNull() + expect(document.body.querySelector('.tooltip')).toBeNull() }) ) @@ -111,19 +106,19 @@ describe('TooltipManager', function () { element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() element.click() - return expect(document.body.querySelector('.tooltip')).toBeNull() + expect(document.body.querySelector('.tooltip')).toBeNull() }) ) it('allows a custom item to be specified for the content of the tooltip', function () { const tooltipElement = document.createElement('div') manager.add(element, {item: {element: tooltipElement}}) - return hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) + hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) }) it('allows a custom class to be specified for the tooltip', function () { manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) - return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) + hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) it('allows jQuery elements to be passed as the target', function () { @@ -142,7 +137,7 @@ describe('TooltipManager', function () { disposable.dispose() hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) - return hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) describe('when a keyBindingCommand is specified', function () { @@ -158,9 +153,9 @@ describe('TooltipManager', function () { manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) }) }) ) @@ -171,14 +166,14 @@ describe('TooltipManager', function () { manager.add(element, {keyBindingCommand: 'test-command'}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) ) - return describe('when a keyBindingTarget is specified', function () { + describe('when a keyBindingTarget is specified', function () { it('looks up the key binding relative to the target', function () { atom.keymaps.add('test', { '.bar': { 'ctrl-x ctrl-z': 'test-command' @@ -190,18 +185,18 @@ describe('TooltipManager', function () { manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) - return it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement.textContent).toBe('A Title') + expect(tooltipElement.textContent).toBe('A Title') }) }) }) @@ -215,23 +210,23 @@ describe('TooltipManager', function () { disposable.dispose() - return hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) ) describe('when the window is resized', () => it('hides the tooltips', function () { const disposable = manager.add(element, {title: 'Title'}) - return hover(element, function () { + hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() window.dispatchEvent(new CustomEvent('resize')) expect(document.body.querySelector('.tooltip')).toBeNull() - return disposable.dispose() + disposable.dispose() }) }) ) - return describe('findTooltips', function () { + describe('findTooltips', function () { it('adds and remove tooltips correctly', function () { expect(manager.findTooltips(element).length).toBe(0) const disposable1 = manager.add(element, {title: 'elem1'}) @@ -241,16 +236,16 @@ describe('TooltipManager', function () { disposable1.dispose() expect(manager.findTooltips(element).length).toBe(1) disposable2.dispose() - return expect(manager.findTooltips(element).length).toBe(0) + expect(manager.findTooltips(element).length).toBe(0) }) - return it('lets us hide tooltips programmatically', function () { + it('lets us hide tooltips programmatically', function () { const disposable = manager.add(element, {title: 'Title'}) - return hover(element, function () { + hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() manager.findTooltips(element)[0].hide() expect(document.body.querySelector('.tooltip')).toBeNull() - return disposable.dispose() + disposable.dispose() }) }) }) From aa69409b1b576ea50eba2bbe3306a9d3d3979980 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 09:04:11 -0400 Subject: [PATCH 063/161] :art: Prefer arrow function syntax --- spec/tooltip-manager-spec.js | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 0edcd646f..2f95299f3 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -3,7 +3,7 @@ const TooltipManager = require('../src/tooltip-manager') const Tooltip = require('../src/tooltip') const _ = require('underscore-plus') -describe('TooltipManager', function () { +describe('TooltipManager', () => { let manager, element const ctrlX = _.humanizeKeystroke('ctrl-x') @@ -39,14 +39,14 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.hide) } - describe('::add(target, options)', function () { - describe("when the trigger is 'hover' (the default)", function () { - it('creates a tooltip when hovering over the target element', function () { + describe('::add(target, options)', () => { + describe("when the trigger is 'hover' (the default)", () => { + it('creates a tooltip when hovering over the target element', () => { manager.add(element, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) }) - it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { + it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', () => { const disposables = new CompositeDisposable() const element1 = createElement('foo') disposables.add(manager.add(element1, {title: 'Title'})) @@ -55,7 +55,7 @@ describe('TooltipManager', function () { const element3 = createElement('baz') disposables.add(manager.add(element3, {title: 'Title'})) - hover(element1, function () {}) + hover(element1, () => {}) expect(document.body.querySelector('.tooltip')).toBeNull() mouseEnter(element2) @@ -75,7 +75,7 @@ describe('TooltipManager', function () { }) describe("when the trigger is 'manual'", () => - it('creates a tooltip immediately and only hides it on dispose', function () { + it('creates a tooltip immediately and only hides it on dispose', () => { const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) expect(document.body.querySelector('.tooltip')).toHaveText('Title') disposable.dispose() @@ -84,7 +84,7 @@ describe('TooltipManager', function () { ) describe("when the trigger is 'click'", () => - it('shows and hides the tooltip when the target element is clicked', function () { + it('shows and hides the tooltip when the target element is clicked', () => { manager.add(element, {title: 'Title', trigger: 'click'}) expect(document.body.querySelector('.tooltip')).toBeNull() element.click() @@ -110,18 +110,18 @@ describe('TooltipManager', function () { }) ) - it('allows a custom item to be specified for the content of the tooltip', function () { + it('allows a custom item to be specified for the content of the tooltip', () => { const tooltipElement = document.createElement('div') manager.add(element, {item: {element: tooltipElement}}) hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) }) - it('allows a custom class to be specified for the tooltip', function () { + it('allows a custom class to be specified for the tooltip', () => { manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) - it('allows jQuery elements to be passed as the target', function () { + it('allows jQuery elements to be passed as the target', () => { const element2 = document.createElement('div') jasmine.attachToDOM(element2) @@ -140,9 +140,9 @@ describe('TooltipManager', function () { hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) - describe('when a keyBindingCommand is specified', function () { + describe('when a keyBindingCommand is specified', () => { describe('when a title is specified', () => - it('appends the key binding corresponding to the command to the title', function () { + it('appends the key binding corresponding to the command to the title', () => { atom.keymaps.add('test', { '.foo': { 'ctrl-x ctrl-y': 'test-command' }, @@ -161,7 +161,7 @@ describe('TooltipManager', function () { ) describe('when no title is specified', () => - it('shows the key binding corresponding to the command alone', function () { + it('shows the key binding corresponding to the command alone', () => { atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}}) manager.add(element, {keyBindingCommand: 'test-command'}) @@ -173,8 +173,8 @@ describe('TooltipManager', function () { }) ) - describe('when a keyBindingTarget is specified', function () { - it('looks up the key binding relative to the target', function () { + describe('when a keyBindingTarget is specified', () => { + it('looks up the key binding relative to the target', () => { atom.keymaps.add('test', { '.bar': { 'ctrl-x ctrl-z': 'test-command' }, @@ -191,7 +191,7 @@ describe('TooltipManager', function () { }) }) - it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => { manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) hover(element, function () { @@ -203,7 +203,7 @@ describe('TooltipManager', function () { }) describe('when .dispose() is called on the returned disposable', () => - it('no longer displays the tooltip on hover', function () { + it('no longer displays the tooltip on hover', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) @@ -215,7 +215,7 @@ describe('TooltipManager', function () { ) describe('when the window is resized', () => - it('hides the tooltips', function () { + it('hides the tooltips', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() @@ -226,8 +226,8 @@ describe('TooltipManager', function () { }) ) - describe('findTooltips', function () { - it('adds and remove tooltips correctly', function () { + describe('findTooltips', () => { + it('adds and remove tooltips correctly', () => { expect(manager.findTooltips(element).length).toBe(0) const disposable1 = manager.add(element, {title: 'elem1'}) expect(manager.findTooltips(element).length).toBe(1) @@ -239,7 +239,7 @@ describe('TooltipManager', function () { expect(manager.findTooltips(element).length).toBe(0) }) - it('lets us hide tooltips programmatically', function () { + it('lets us hide tooltips programmatically', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() From fc620b9e80d67ca99f962431461b8fc4d085d9df Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 09:06:50 -0400 Subject: [PATCH 064/161] :art: Move helper functions outside of `describe` block --- spec/tooltip-manager-spec.js | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 2f95299f3..65587839f 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -9,28 +9,6 @@ describe('TooltipManager', () => { const ctrlX = _.humanizeKeystroke('ctrl-x') const ctrlY = _.humanizeKeystroke('ctrl-y') - beforeEach(function () { - manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) - element = createElement('foo') - }) - - var createElement = function (className) { - const el = document.createElement('div') - el.classList.add(className) - jasmine.attachToDOM(el) - return el - } - - const mouseEnter = function (element) { - element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) - element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) - } - - const mouseLeave = function (element) { - element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) - element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) - } - const hover = function (element, fn) { mouseEnter(element) advanceClock(manager.hoverDefaults.delay.show) @@ -39,6 +17,11 @@ describe('TooltipManager', () => { advanceClock(manager.hoverDefaults.delay.hide) } + beforeEach(function () { + manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) + element = createElement('foo') + }) + describe('::add(target, options)', () => { describe("when the trigger is 'hover' (the default)", () => { it('creates a tooltip when hovering over the target element', () => { @@ -251,3 +234,20 @@ describe('TooltipManager', () => { }) }) }) + +function createElement (className) { + const el = document.createElement('div') + el.classList.add(className) + jasmine.attachToDOM(el) + return el +} + +function mouseEnter (element) { + element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) + element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) +} + +function mouseLeave (element) { + element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) + element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) +} From e42435208f933d8f119a3e3d6abd97dd944a89a5 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 00:01:42 +0200 Subject: [PATCH 065/161] :arrow_up: apm@1.18.10 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index e759a39d2..336544d3e 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.9" + "atom-package-manager": "1.18.10" } } From 54a67b60cbcad3eb001c4e7dc6f4235cb7cfeaca Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:49:04 +0200 Subject: [PATCH 066/161] :arrow_up: language-ruby@0.71.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4056b0d71..783076c91 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", - "language-ruby": "0.71.3", + "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.1", "language-shellscript": "0.25.3", From 51349a79fb44c450f14f6067e415cbe6cbef59fc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:49:54 +0200 Subject: [PATCH 067/161] :arrow_up: language-hyperlink@0.16.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 783076c91..25f4e0c5a 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "language-git": "0.19.1", "language-go": "0.44.2", "language-html": "0.48.1", - "language-hyperlink": "0.16.2", + "language-hyperlink": "0.16.3", "language-java": "0.27.4", "language-javascript": "0.127.5", "language-json": "0.19.1", From c1ec73602f55871ffb87edf653f246223d2677e8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:50:26 +0200 Subject: [PATCH 068/161] :arrow_up: language-todo@0.29.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25f4e0c5a..a865c288d 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", - "language-todo": "0.29.2", + "language-todo": "0.29.3", "language-toml": "0.18.1", "language-typescript": "0.2.2", "language-xml": "0.35.2", From 287d98b321db215033eeaea47b479d4a356ba416 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:51:08 +0200 Subject: [PATCH 069/161] :arrow_up: language-javascript@0.127.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a865c288d..3fe5b5235 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "language-html": "0.48.1", "language-hyperlink": "0.16.3", "language-java": "0.27.4", - "language-javascript": "0.127.5", + "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.1", "language-make": "0.22.3", From fff1c06c50bbf8964ed9e1df4a3a74d2318e1b27 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:51:39 +0200 Subject: [PATCH 070/161] :arrow_up: language-go@0.44.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fe5b5235..3aba6812c 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "language-css": "0.42.6", "language-gfm": "0.90.2", "language-git": "0.19.1", - "language-go": "0.44.2", + "language-go": "0.44.3", "language-html": "0.48.1", "language-hyperlink": "0.16.3", "language-java": "0.27.4", From b525b7212bd6399aeb8de4cc945e05cf4e7b179b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:52:30 +0200 Subject: [PATCH 071/161] :arrow_up: language-php@0.42.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3aba6812c..498e17b2b 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-mustache": "0.14.3", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.42.1", + "language-php": "0.42.2", "language-property-list": "0.9.1", "language-python": "0.45.4", "language-ruby": "0.71.4", From 2aca4268a5466fc7ea77e7f391b20ed778d0d840 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:52:57 +0200 Subject: [PATCH 072/161] :arrow_up: language-yaml@0.31.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 498e17b2b..384b4902f 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "language-toml": "0.18.1", "language-typescript": "0.2.2", "language-xml": "0.35.2", - "language-yaml": "0.31.0" + "language-yaml": "0.31.1" }, "private": true, "scripts": { From 5173b8f23fd38cfba452a2e6ad3838fa0368dbfa Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:53:51 +0200 Subject: [PATCH 073/161] :arrow_up: language-css@0.42.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 384b4902f..1e6352564 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-clojure": "0.22.4", "language-coffee-script": "0.49.1", "language-csharp": "0.14.3", - "language-css": "0.42.6", + "language-css": "0.42.7", "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.3", From 946b4be5cfd3659876f6963a55899b55f9d0ddd2 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:54:32 +0200 Subject: [PATCH 074/161] :arrow_up: language-sass@0.61.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e6352564..d82792259 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "language-python": "0.45.4", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.61.1", + "language-sass": "0.61.2", "language-shellscript": "0.25.3", "language-source": "0.9.0", "language-sql": "0.25.8", From f83a8f7e7e631b944ea336e0b7173494ccce6168 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:55:06 +0200 Subject: [PATCH 075/161] :arrow_up: language-shellscript@0.25.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d82792259..ba5140094 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.2", - "language-shellscript": "0.25.3", + "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", From d474ddcd746a8bbe84bfe5385f7fc5a1e43e155e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:55:46 +0200 Subject: [PATCH 076/161] :arrow_up: language-coffee-script@0.49.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba5140094..629019363 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.49.1", + "language-coffee-script": "0.49.2", "language-csharp": "0.14.3", "language-css": "0.42.7", "language-gfm": "0.90.2", From e76eee10e3d40a5f5b1653d913aaaba226715123 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:56:30 +0200 Subject: [PATCH 077/161] :arrow_up: language-python@0.45.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 629019363..a83d0b694 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "language-perl": "0.37.0", "language-php": "0.42.2", "language-property-list": "0.9.1", - "language-python": "0.45.4", + "language-python": "0.45.5", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.2", From 6dd28c0b37e3d6440bffcc5f6f4fe2ee695f226f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:56:58 +0200 Subject: [PATCH 078/161] :arrow_up: language-java@0.27.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a83d0b694..ea9b9b8e6 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-go": "0.44.3", "language-html": "0.48.1", "language-hyperlink": "0.16.3", - "language-java": "0.27.4", + "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.1", From 13ecc8a2280e47cdf5491962afe4d754cdf4d388 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:58:42 +0200 Subject: [PATCH 079/161] :arrow_up: language-mustache@0.14.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea9b9b8e6..2919dcceb 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-json": "0.19.1", "language-less": "0.33.1", "language-make": "0.22.3", - "language-mustache": "0.14.3", + "language-mustache": "0.14.4", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.42.2", From dfd1e715bf6e845c955a1b4a3d616ede953b2598 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:59:25 +0200 Subject: [PATCH 080/161] :arrow_up: language-html@0.48.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2919dcceb..3902ffa30 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.3", - "language-html": "0.48.1", + "language-html": "0.48.2", "language-hyperlink": "0.16.3", "language-java": "0.27.5", "language-javascript": "0.127.6", From 7b7ddb9eb989afa7ed15efcbf1da0f5021eb6906 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 15:00:00 +0200 Subject: [PATCH 081/161] :arrow_up: language-less@0.34.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3902ffa30..2887ec8bf 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", - "language-less": "0.33.1", + "language-less": "0.34.0", "language-make": "0.22.3", "language-mustache": "0.14.4", "language-objective-c": "0.15.1", From 2bf9e4b0c7b1a4a9ba45b6ce78a69a4f06023ac6 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 17:36:21 +0200 Subject: [PATCH 082/161] Use scope names rather than names Some languages are not guaranteed to have names --- spec/workspace-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 43a04eba9..1bde0e6fe 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1585,15 +1585,15 @@ i = /test/; #FIXME\ atom2.project.deserialize(atom.project.serialize()) atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) - expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ - 'CoffeeScript', - 'CoffeeScript (Literate)', - 'JSDoc', - 'JavaScript', - 'Null Grammar', - 'Regular Expression Replacement (JavaScript)', - 'Regular Expressions (JavaScript)', - 'TODO' + expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([ + 'source.coffee', + 'source.js', + 'source.js.regexp', + 'source.js.regexp.replacement', + 'source.jsdoc', + 'source.litcoffee', + 'text.plain.null-grammar', + 'text.todo' ]) atom2.destroy() From 5fc8563fe56c2962a1267cdc0fa786db0b74352d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 18:03:45 +0200 Subject: [PATCH 083/161] :arrow_up: grammar-selector@0.49.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2887ec8bf..acf21aca8 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.6", + "grammar-selector": "0.49.7", "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", From 364964ea0a1bf0a5b5ca612e2ee8be10bbdf8db2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 10:34:43 -0600 Subject: [PATCH 084/161] Always assign a project path outside of bundle for legacy package specs This prevents package specs that don't have a fixtures directory from attempting to read files out of a non-existent directory inside the ASAR bundle, which causes ENOTDIR errors in superstring. If the spec does not have a parent folder containing a fixtures directory, we now set the default project path to `os.tmpdir()`. --- spec/spec-helper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c20bfc827..7621f9cae 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -58,7 +58,7 @@ if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json') if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures') specProjectPath = path.join(specDirectory, 'fixtures') else - specProjectPath = path.join(__dirname, 'fixtures') + specProjectPath = require('os').tmpdir() beforeEach -> atom.project.setPaths([specProjectPath]) From 00242541aed5edb03cfdaa7570c6f84011b28b32 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 12:15:33 -0600 Subject: [PATCH 085/161] Don't destroy folds that are completely contained within a selection --- package.json | 2 +- spec/text-editor-spec.coffee | 5 ++++- src/selection.coffee | 2 +- src/text-editor.coffee | 7 ++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4056b0d71..4e1216846 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.8", + "text-buffer": "13.6.0-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index bc74cd443..5bb010321 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1871,7 +1871,7 @@ describe "TextEditor", -> expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] describe "when the 'preserveFolds' option is false (the default)", -> - it "removes folds that contain the selections", -> + it "removes folds that contain one or both of the selection's end points", -> editor.setSelectedBufferRange([[0, 0], [0, 0]]) editor.foldBufferRowRange(1, 4) editor.foldBufferRowRange(2, 3) @@ -1884,6 +1884,9 @@ describe "TextEditor", -> expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + editor.setSelectedBufferRange([[10, 0], [12, 0]]) + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + describe "when the 'preserveFolds' option is true", -> it "does not remove folds that contain the selections", -> editor.setSelectedBufferRange([[0, 0], [0, 0]]) diff --git a/src/selection.coffee b/src/selection.coffee index 6fcf8dd36..0907888d6 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -87,7 +87,7 @@ class Selection extends Model setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) options.reversed ?= @isReversed() - @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds + @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 32dd49a18..400d48f97 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2495,8 +2495,9 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> + bufferRange = Range.fromObject(bufferRange) unless options.preserveFolds - @destroyFoldsIntersectingBufferRange(bufferRange) + @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3446,6 +3447,10 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) + # Remove any {Fold}s found that intersect the given array of buffer positions. + destroyFoldsContainingBufferPositions: (bufferPositions) -> + @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions) + ### Section: Gutters ### From 2189bd502c73a79d30ba8f8712213ce264b59fe6 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 20:33:52 +0200 Subject: [PATCH 086/161] :arrow_up: grammar-selector@0.49.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index acf21aca8..f09178dff 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.7", + "grammar-selector": "0.49.8", "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", From 577911179969cc8fc51e36f416eda05765d03962 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 12:48:20 -0600 Subject: [PATCH 087/161] Use destroyFoldsContainingBufferPosition in more cases --- package.json | 2 +- src/selection.coffee | 2 +- src/text-editor-component.js | 2 +- src/text-editor.coffee | 13 ++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4e1216846..a447d53b5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-0", + "text-buffer": "13.6.0-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/selection.coffee b/src/selection.coffee index 0907888d6..cb45286b8 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -87,7 +87,7 @@ class Selection extends Model setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) options.reversed ?= @isReversed() - @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) unless options.preserveFolds + @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 641cdad02..f19b7e31c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1771,7 +1771,7 @@ class TextEditorComponent { if (target && target.matches('.fold-marker')) { const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) - model.destroyFoldsIntersectingBufferRange(Range(bufferPosition, bufferPosition)) + model.destroyFoldsContainingBufferPositions([bufferPosition], false) return } diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 400d48f97..dd359df9e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2497,7 +2497,7 @@ class TextEditor extends Model addSelectionForBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) unless options.preserveFolds - @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) + @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3318,8 +3318,7 @@ class TextEditor extends Model # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> {row} = @getCursorBufferPosition() - position = Point(row, Infinity) - @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) + @displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) # Essential: Fold the given row in buffer coordinates based on its indentation # level. @@ -3348,7 +3347,7 @@ class TextEditor extends Model # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> position = Point(bufferRow, Infinity) - @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) + @displayLayer.destroyFoldsContainingBufferPositions([position]) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> @@ -3447,9 +3446,9 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - # Remove any {Fold}s found that intersect the given array of buffer positions. - destroyFoldsContainingBufferPositions: (bufferPositions) -> - @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions) + # Remove any {Fold}s found that contain the given array of buffer positions. + destroyFoldsContainingBufferPositions: (bufferPositions, excludeEndpoints) -> + @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) ### Section: Gutters From 1722273630bf8b306ac34bb7b4b38ffa40135cab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 12:52:31 -0700 Subject: [PATCH 088/161] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f09178dff..6c1d675cc 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.8", + "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From ba7c3e57f51d2e290390715981a1b3648727625b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 13:19:13 -0700 Subject: [PATCH 089/161] :arrow_up: text-buffer for new onWillChange behavior --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c1d675cc..afba9de19 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.8", + "text-buffer": "13.6.0-will-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 197425ffe42ac1ded431694b5d99907a8bb36ff8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 14:47:29 -0700 Subject: [PATCH 090/161] :arrow_up: text-buffer (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afba9de19..78c4847b3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-will-change-event-1", + "text-buffer": "13.6.0-will-change-event-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From d715f3ee2743c48c830ca9b2859efea9fedc71df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 16:35:02 -0600 Subject: [PATCH 091/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a447d53b5..43446f242 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-2", + "text-buffer": "13.6.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 3e9c6601a252435be2488074a2742162c6bb9a93 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 20:30:42 -0600 Subject: [PATCH 092/161] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f4c4bbce..6578f7392 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.16", + "markdown-preview": "0.159.17", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 6ad37f08d3b4f38cbb9c025b1e07be1399b02d1c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:50:59 +0200 Subject: [PATCH 093/161] :arrow_up: autocomplete-css@0.17.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6578f7392..b34cce317 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "about": "1.7.8", "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", - "autocomplete-css": "0.17.3", + "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.2", "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", From 1f5565fec748c2d2a24967e9cb885a5ac4ff87ad Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:55:03 +0200 Subject: [PATCH 094/161] :arrow_up: autocomplete-html@0.8.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b34cce317..d5bea7833 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.4", - "autocomplete-html": "0.8.2", + "autocomplete-html": "0.8.3", "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", From 9ae36efc2fa2a5ad01aabf948ffffa8cf52f73e4 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:59:05 +0200 Subject: [PATCH 095/161] :arrow_up: autocomplete-atom-api@0.10.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5bea7833..6ad2f68b8 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "solarized-light-syntax": "1.1.2", "about": "1.7.8", "archive-view": "0.63.4", - "autocomplete-atom-api": "0.10.3", + "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", "autocomplete-plus": "2.37.0", From a32f1c3684a9de77af1b6dbd9e133a8b3b30c904 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 Oct 2017 06:52:23 -0600 Subject: [PATCH 096/161] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6578f7392..eaf1951a5 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.252.1", + "settings-view": "0.252.2", "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", From f21ede2da253c0f4c32bba5e8706daf459bf2336 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 09:59:11 -0700 Subject: [PATCH 097/161] :arrow_up: text-buffer for new onDidChange behavior --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78e75f11b..6cae9962e 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.1", + "text-buffer": "13.7.0-did-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 32e9547558c60c283225e461d3b588a76f15a344 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 Oct 2017 14:36:42 -0600 Subject: [PATCH 098/161] :arrow_up: tree-view /cc @Alhadis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 418a9da02..96e7330fd 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.108.0", "timecop": "0.36.0", - "tree-view": "0.220.0", + "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From 590302572621abd6a84b70f6b5dc3a2d5a268e8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 14:29:24 -0700 Subject: [PATCH 099/161] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cae9962e..494654b87 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", - "autocomplete-plus": "2.37.0", + "autocomplete-plus": "2.37.1", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From ada645aaa16c51b49357b161d81af51903d07cc3 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 26 Oct 2017 15:31:43 -0600 Subject: [PATCH 100/161] fix optimizer bailing on performDocumentUpdate --- src/view-registry.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index dcc1624fc..87bf8620f 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -233,16 +233,26 @@ class ViewRegistry { this.nextUpdatePromise = null this.resolveNextUpdatePromise = null - let writer - while ((writer = this.documentWriters.shift())) { writer() } + var writer = this.documentWriters.shift() + while (writer) { + writer() + writer = this.documentWriters.shift() + } - let reader + var reader = this.documentReaders.shift() this.documentReadInProgress = true - while ((reader = this.documentReaders.shift())) { reader() } + while (reader) { + reader() + reader = this.documentReaders.shift() + } this.documentReadInProgress = false // process updates requested as a result of reads - while ((writer = this.documentWriters.shift())) { writer() } + writer = this.documentWriters.shift() + while (writer) { + writer() + writer = this.documentWriters.shift() + } if (resolveNextUpdatePromise) { resolveNextUpdatePromise() } } From ab07a6ec63b37719f1b5454ef485643728889ee5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 14:56:17 -0700 Subject: [PATCH 101/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 494654b87..8ea46bba2 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.0-did-change-event-1", + "text-buffer": "13.7.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 2f0fb1798240c1c2468a37be87610d67e640cf2d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 15:39:35 -0700 Subject: [PATCH 102/161] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5343381f..491795d55 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", - "autocomplete-plus": "2.37.1", + "autocomplete-plus": "2.37.2", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From 67dc7c745ecc6ec3c09dbdb346bee05170b8a065 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 16:53:16 -0700 Subject: [PATCH 103/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 491795d55..f800b1b3f 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.0", + "text-buffer": "13.7.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e4044699dc18903a50cfffa45f733544fa60a165 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 27 Oct 2017 21:49:27 +0200 Subject: [PATCH 104/161] :memo: [ci skip] --- src/workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 80dfc47cb..dcaf06006 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -659,7 +659,7 @@ module.exports = class Workspace extends Model { // changing or closing tabs and ensures critical UI feedback, like changing the // highlighted tab, gets priority over work that can be done asynchronously. // - // * `callback` {Function} to be called when the active pane item stopts + // * `callback` {Function} to be called when the active pane item stops // changing. // * `item` The active pane item. // From 06ca120efe7850aee43b80235a1e659b0a237257 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 27 Oct 2017 13:52:58 -0700 Subject: [PATCH 105/161] :arrow_up: status-bar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f800b1b3f..362de6ac3 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.252.2", "snippets": "1.1.6", "spell-check": "0.72.3", - "status-bar": "1.8.13", + "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.108.0", From e695e6565fc70cc159ea206e499cb1169bb0313e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 Oct 2017 16:46:48 -0600 Subject: [PATCH 106/161] Switch to fork of nsfw to fix symlink loops on Linux --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 362de6ac3..ab8346925 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "MIT", "electronVersion": "1.6.15", "dependencies": { + "@atom/nsfw": "^1.0.17", "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.8", @@ -53,7 +54,6 @@ "mocha-multi-reporters": "^1.1.4", "mock-spawn": "^0.2.6", "normalize-package-data": "^2.0.0", - "nsfw": "^1.0.15", "nslog": "^3", "oniguruma": "6.2.1", "pathwatcher": "8.0.1", From ab79a2d2b29fe51af9d9b73cc58df91beaaffaf9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 28 Oct 2017 00:53:01 +0200 Subject: [PATCH 107/161] :memo: [ci skip] --- src/command-registry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/command-registry.js b/src/command-registry.js index 30089b7f1..ba75918ab 100644 --- a/src/command-registry.js +++ b/src/command-registry.js @@ -89,7 +89,7 @@ module.exports = class CommandRegistry { // DOM element, the command will be associated with just that element. // * `commandName` A {String} containing the name of a command you want to // handle such as `user:insert-date`. - // * `listener` A listener which handles the event. Either A {Function} to + // * `listener` A listener which handles the event. Either a {Function} to // call when the given command is invoked on an element matching the // selector, or an {Object} with a `didDispatch` property which is such a // function. @@ -97,7 +97,7 @@ module.exports = class CommandRegistry { // The function (`listener` itself if it is a function, or the `didDispatch` // method if `listener` is an object) will be called with `this` referencing // the matching DOM node and the following argument: - // * `event` A standard DOM event instance. Call `stopPropagation` or + // * `event`: A standard DOM event instance. Call `stopPropagation` or // `stopImmediatePropagation` to terminate bubbling early. // // Additionally, `listener` may have additional properties which are returned From 02d348e56ed98f67b0ab7483d2444d6854f878ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 Oct 2017 17:10:59 -0600 Subject: [PATCH 108/161] :arrow_up: @atom/nsfw to public version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab8346925..e063e25ce 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "license": "MIT", "electronVersion": "1.6.15", "dependencies": { - "@atom/nsfw": "^1.0.17", + "@atom/nsfw": "^1.0.18", "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.8", From 781b87144e79bc32959953154bf2db83970081d2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 27 Oct 2017 20:46:54 -0400 Subject: [PATCH 109/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/theme-package.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-package.coffee | 37 --------------------------- src/theme-package.js | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 37 deletions(-) delete mode 100644 src/theme-package.coffee create mode 100644 src/theme-package.js diff --git a/src/theme-package.coffee b/src/theme-package.coffee deleted file mode 100644 index 053132d61..000000000 --- a/src/theme-package.coffee +++ /dev/null @@ -1,37 +0,0 @@ -path = require 'path' -Package = require './package' - -module.exports = -class ThemePackage extends Package - getType: -> 'theme' - - getStyleSheetPriority: -> 1 - - enable: -> - @config.unshiftAtKeyPath('core.themes', @name) - - disable: -> - @config.removeAtKeyPath('core.themes', @name) - - preload: -> - @loadTime = 0 - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - - finishLoading: -> - @path = path.join(@packageManager.resourcePath, @path) - - load: -> - @loadTime = 0 - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - this - - activate: -> - @activationPromise ?= new Promise (resolve, reject) => - @resolveActivationPromise = resolve - @rejectActivationPromise = reject - @measure 'activateTime', => - try - @loadStylesheets() - @activateNow() - catch error - @handleError("Failed to activate the #{@name} theme", error) diff --git a/src/theme-package.js b/src/theme-package.js new file mode 100644 index 000000000..7ac01bd97 --- /dev/null +++ b/src/theme-package.js @@ -0,0 +1,55 @@ +const path = require('path') +const Package = require('./package') + +module.exports = +class ThemePackage extends Package { + getType () { + return 'theme' + } + + getStyleSheetPriority () { + return 1 + } + + enable () { + this.config.unshiftAtKeyPath('core.themes', this.name) + } + + disable () { + this.config.removeAtKeyPath('core.themes', this.name) + } + + preload () { + this.loadTime = 0 + this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata() + } + + finishLoading () { + this.path = path.join(this.packageManager.resourcePath, this.path) + } + + load () { + this.loadTime = 0 + this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata() + return this + } + + activate () { + if (this.activationPromise == null) { + this.activationPromise = new Promise((resolve, reject) => { + this.resolveActivationPromise = resolve + this.rejectActivationPromise = reject + this.measure('activateTime', () => { + try { + this.loadStylesheets() + this.activateNow() + } catch (error) { + this.handleError(`Failed to activate the ${this.name} theme`, error) + } + }) + }) + } + + return this.activationPromise + } +} From 2f4d6ae3177c4005ef6ec1c9bd9bc9abfe1e2a13 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 28 Oct 2017 17:04:07 +0200 Subject: [PATCH 110/161] :arrow_up: snippets@1.1.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 362de6ac3..f945df5b3 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.6", + "snippets": "1.1.7", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", From 042c22f4321fb3a35e3d8d0c2562966c12a6fc35 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 29 Oct 2017 19:01:16 +0100 Subject: [PATCH 111/161] :arrow_up: first-mate@7.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63d729a3a..0df753e69 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.10", + "first-mate": "7.1.0", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 8ef74222c43794120cdb56f307037fe42c6c6fef Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 29 Oct 2017 10:21:34 -0400 Subject: [PATCH 112/161] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/theme-manager.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-manager.coffee | 322 ------------------------------- src/theme-manager.js | 399 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+), 322 deletions(-) delete mode 100644 src/theme-manager.coffee create mode 100644 src/theme-manager.js diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee deleted file mode 100644 index d5a2cb0d1..000000000 --- a/src/theme-manager.coffee +++ /dev/null @@ -1,322 +0,0 @@ -path = require 'path' -_ = require 'underscore-plus' -{Emitter, CompositeDisposable} = require 'event-kit' -{File} = require 'pathwatcher' -fs = require 'fs-plus' -LessCompileCache = require './less-compile-cache' - -# Extended: Handles loading and activating available themes. -# -# An instance of this class is always available as the `atom.themes` global. -module.exports = -class ThemeManager - constructor: ({@packageManager, @config, @styleManager, @notificationManager, @viewRegistry}) -> - @emitter = new Emitter - @styleSheetDisposablesBySourcePath = {} - @lessCache = null - @initialLoadComplete = false - @packageManager.registerPackageActivator(this, ['theme']) - @packageManager.onDidActivateInitialPackages => - @onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets() - - initialize: ({@resourcePath, @configDirPath, @safeMode, devMode}) -> - @lessSourcesByRelativeFilePath = null - if devMode or typeof snapshotAuxiliaryData is 'undefined' - @lessSourcesByRelativeFilePath = {} - @importedFilePathsByRelativeImportPath = {} - else - @lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath - @importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath - - ### - Section: Event Subscription - ### - - # Essential: Invoke `callback` when style sheet changes associated with - # updating the list of active themes have completed. - # - # * `callback` {Function} - onDidChangeActiveThemes: (callback) -> - @emitter.on 'did-change-active-themes', callback - - ### - Section: Accessing Available Themes - ### - - getAvailableNames: -> - # TODO: Maybe should change to list all the available themes out there? - @getLoadedNames() - - ### - Section: Accessing Loaded Themes - ### - - # Public: Returns an {Array} of {String}s of all the loaded theme names. - getLoadedThemeNames: -> - theme.name for theme in @getLoadedThemes() - - # Public: Returns an {Array} of all the loaded themes. - getLoadedThemes: -> - pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() - - ### - Section: Accessing Active Themes - ### - - # Public: Returns an {Array} of {String}s all the active theme names. - getActiveThemeNames: -> - theme.name for theme in @getActiveThemes() - - # Public: Returns an {Array} of all the active themes. - getActiveThemes: -> - pack for pack in @packageManager.getActivePackages() when pack.isTheme() - - activatePackages: -> @activateThemes() - - ### - Section: Managing Enabled Themes - ### - - warnForNonExistentThemes: -> - themeNames = @config.get('core.themes') ? [] - themeNames = [themeNames] unless _.isArray(themeNames) - for themeName in themeNames - unless themeName and typeof themeName is 'string' and @packageManager.resolvePackagePath(themeName) - console.warn("Enabled theme '#{themeName}' is not installed.") - - # Public: Get the enabled theme names from the config. - # - # Returns an array of theme names in the order that they should be activated. - getEnabledThemeNames: -> - themeNames = @config.get('core.themes') ? [] - themeNames = [themeNames] unless _.isArray(themeNames) - themeNames = themeNames.filter (themeName) => - if themeName and typeof themeName is 'string' - return true if @packageManager.resolvePackagePath(themeName) - false - - # Use a built-in syntax and UI theme any time the configured themes are not - # available. - if themeNames.length < 2 - builtInThemeNames = [ - 'atom-dark-syntax' - 'atom-dark-ui' - 'atom-light-syntax' - 'atom-light-ui' - 'base16-tomorrow-dark-theme' - 'base16-tomorrow-light-theme' - 'solarized-dark-syntax' - 'solarized-light-syntax' - ] - themeNames = _.intersection(themeNames, builtInThemeNames) - if themeNames.length is 0 - themeNames = ['atom-dark-syntax', 'atom-dark-ui'] - else if themeNames.length is 1 - if _.endsWith(themeNames[0], '-ui') - themeNames.unshift('atom-dark-syntax') - else - themeNames.push('atom-dark-ui') - - # Reverse so the first (top) theme is loaded after the others. We want - # the first/top theme to override later themes in the stack. - themeNames.reverse() - - ### - Section: Private - ### - - # Resolve and apply the stylesheet specified by the path. - # - # This supports both CSS and Less stylesheets. - # - # * `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 a {Disposable} on which `.dispose()` can be called to remove the - # required stylesheet. - requireStylesheet: (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) -> - if fullPath = @resolveStylesheet(stylesheetPath) - content = @loadStylesheet(fullPath) - @applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation) - else - throw new Error("Could not find a file at path '#{stylesheetPath}'") - - unwatchUserStylesheet: -> - @userStylesheetSubscriptions?.dispose() - @userStylesheetSubscriptions = null - @userStylesheetFile = null - @userStyleSheetDisposable?.dispose() - @userStyleSheetDisposable = null - - loadUserStylesheet: -> - @unwatchUserStylesheet() - - userStylesheetPath = @styleManager.getUserStyleSheetPath() - return unless fs.isFileSync(userStylesheetPath) - - try - @userStylesheetFile = new File(userStylesheetPath) - @userStylesheetSubscriptions = new CompositeDisposable() - reloadStylesheet = => @loadUserStylesheet() - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet)) - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet)) - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet)) - catch error - message = """ - Unable to watch path: `#{path.basename(userStylesheetPath)}`. Make sure - you have permissions to `#{userStylesheetPath}`. - - On linux there are currently problems with watch sizes. See - [this document][watches] for more info. - [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path - """ - @notificationManager.addError(message, dismissable: true) - - try - userStylesheetContents = @loadStylesheet(userStylesheetPath, true) - catch - return - - @userStyleSheetDisposable = @styleManager.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2) - - loadBaseStylesheets: -> - @reloadBaseStylesheets() - - reloadBaseStylesheets: -> - @requireStylesheet('../static/atom', -2, true) - - stylesheetElementForId: (id) -> - escapedId = id.replace(/\\/g, '\\\\') - document.head.querySelector("atom-styles style[source-path=\"#{escapedId}\"]") - - resolveStylesheet: (stylesheetPath) -> - if path.extname(stylesheetPath).length > 0 - fs.resolveOnLoadPath(stylesheetPath) - else - fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) - - loadStylesheet: (stylesheetPath, importFallbackVariables) -> - if path.extname(stylesheetPath) is '.less' - @loadLessStylesheet(stylesheetPath, importFallbackVariables) - else - fs.readFileSync(stylesheetPath, 'utf8') - - loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> - @lessCache ?= new LessCompileCache({ - @resourcePath, - @lessSourcesByRelativeFilePath, - @importedFilePathsByRelativeImportPath, - importPaths: @getImportPaths() - }) - - try - if importFallbackVariables - baseVarImports = """ - @import "variables/ui-variables"; - @import "variables/syntax-variables"; - """ - relativeFilePath = path.relative(@resourcePath, lessStylesheetPath) - lessSource = @lessSourcesByRelativeFilePath[relativeFilePath] - if lessSource? - content = lessSource.content - digest = lessSource.digest - else - content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8') - digest = null - - @lessCache.cssForFile(lessStylesheetPath, content, digest) - else - @lessCache.read(lessStylesheetPath) - catch error - error.less = true - if error.line? - # Adjust line numbers for import fallbacks - error.line -= 2 if importFallbackVariables - - message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`" - detail = """ - Line number: #{error.line} - #{error.message} - """ - else - message = "Error loading Less stylesheet: `#{lessStylesheetPath}`" - detail = error.message - - @notificationManager.addError(message, {detail, dismissable: true}) - throw error - - removeStylesheet: (stylesheetPath) -> - @styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose() - - applyStylesheet: (path, text, priority, skipDeprecatedSelectorsTransformation) -> - @styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet( - text, - { - priority, - skipDeprecatedSelectorsTransformation, - sourcePath: path - } - ) - - activateThemes: -> - new Promise (resolve) => - # @config.observe runs the callback once, then on subsequent changes. - @config.observe 'core.themes', => - @deactivateThemes().then => - @warnForNonExistentThemes() - @refreshLessCache() # Update cache for packages in core.themes config - - promises = [] - for themeName in @getEnabledThemeNames() - if @packageManager.resolvePackagePath(themeName) - promises.push(@packageManager.activatePackage(themeName)) - else - console.warn("Failed to activate theme '#{themeName}' because it isn't installed.") - - Promise.all(promises).then => - @addActiveThemeClasses() - @refreshLessCache() # Update cache again now that @getActiveThemes() is populated - @loadUserStylesheet() - @reloadBaseStylesheets() - @initialLoadComplete = true - @emitter.emit 'did-change-active-themes' - resolve() - - deactivateThemes: -> - @removeActiveThemeClasses() - @unwatchUserStylesheet() - results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name)) - Promise.all(results.filter((r) -> typeof r?.then is 'function')) - - isInitialLoadComplete: -> @initialLoadComplete - - addActiveThemeClasses: -> - if workspaceElement = @viewRegistry.getView(@workspace) - for pack in @getActiveThemes() - workspaceElement.classList.add("theme-#{pack.name}") - return - - removeActiveThemeClasses: -> - workspaceElement = @viewRegistry.getView(@workspace) - for pack in @getActiveThemes() - workspaceElement.classList.remove("theme-#{pack.name}") - return - - refreshLessCache: -> - @lessCache?.setImportPaths(@getImportPaths()) - - getImportPaths: -> - activeThemes = @getActiveThemes() - if activeThemes.length > 0 - themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme) - else - themePaths = [] - for themeName in @getEnabledThemeNames() - if themePath = @packageManager.resolvePackagePath(themeName) - deprecatedPath = path.join(themePath, 'stylesheets') - if fs.isDirectorySync(deprecatedPath) - themePaths.push(deprecatedPath) - else - themePaths.push(path.join(themePath, 'styles')) - - themePaths.filter (themePath) -> fs.isDirectorySync(themePath) diff --git a/src/theme-manager.js b/src/theme-manager.js new file mode 100644 index 000000000..b46fb1ada --- /dev/null +++ b/src/theme-manager.js @@ -0,0 +1,399 @@ +const path = require('path') +const _ = require('underscore-plus') +const {Emitter, CompositeDisposable} = require('event-kit') +const {File} = require('pathwatcher') +const fs = require('fs-plus') +const LessCompileCache = require('./less-compile-cache') + +// Extended: Handles loading and activating available themes. +// +// An instance of this class is always available as the `atom.themes` global. +module.exports = +class ThemeManager { + constructor ({packageManager, config, styleManager, notificationManager, viewRegistry}) { + this.packageManager = packageManager + this.config = config + this.styleManager = styleManager + this.notificationManager = notificationManager + this.viewRegistry = viewRegistry + this.emitter = new Emitter() + this.styleSheetDisposablesBySourcePath = {} + this.lessCache = null + this.initialLoadComplete = false + this.packageManager.registerPackageActivator(this, ['theme']) + this.packageManager.onDidActivateInitialPackages(() => { + this.onDidChangeActiveThemes(() => this.packageManager.reloadActivePackageStyleSheets()) + }) + } + + initialize ({resourcePath, configDirPath, safeMode, devMode}) { + this.resourcePath = resourcePath + this.configDirPath = configDirPath + this.safeMode = safeMode + this.lessSourcesByRelativeFilePath = null + if (devMode || (typeof snapshotAuxiliaryData === 'undefined')) { + this.lessSourcesByRelativeFilePath = {} + this.importedFilePathsByRelativeImportPath = {} + } else { + this.lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath + this.importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath + } + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke `callback` when style sheet changes associated with + // updating the list of active themes have completed. + // + // * `callback` {Function} + onDidChangeActiveThemes (callback) { + return this.emitter.on('did-change-active-themes', callback) + } + + /* + Section: Accessing Available Themes + */ + + getAvailableNames () { + // TODO: Maybe should change to list all the available themes out there? + return this.getLoadedNames() + } + + /* + Section: Accessing Loaded Themes + */ + + // Public: Returns an {Array} of {String}s of all the loaded theme names. + getLoadedThemeNames () { + return this.getLoadedThemes().map((theme) => theme.name) + } + + // Public: Returns an {Array} of all the loaded themes. + getLoadedThemes () { + return this.packageManager.getLoadedPackages().filter((pack) => pack.isTheme()) + } + + /* + Section: Accessing Active Themes + */ + + // Public: Returns an {Array} of {String}s of all the active theme names. + getActiveThemeNames () { + return this.getActiveThemes().map((theme) => theme.name) + } + + // Public: Returns an {Array} of all the active themes. + getActiveThemes () { + return this.packageManager.getActivePackages().filter((pack) => pack.isTheme()) + } + + activatePackages () { + return this.activateThemes() + } + + /* + Section: Managing Enabled Themes + */ + + warnForNonExistentThemes () { + let themeNames = this.config.get('core.themes') || [] + if (!_.isArray(themeNames)) { themeNames = [themeNames] } + for (let themeName of themeNames) { + if (!themeName || (typeof themeName !== 'string') || !this.packageManager.resolvePackagePath(themeName)) { + console.warn(`Enabled theme '${themeName}' is not installed.`) + } + } + } + + // Public: Get the enabled theme names from the config. + // + // Returns an array of theme names in the order that they should be activated. + getEnabledThemeNames () { + let themeNames = this.config.get('core.themes') || [] + if (!_.isArray(themeNames)) { themeNames = [themeNames] } + themeNames = themeNames.filter((themeName) => + (typeof themeName === 'string') && this.packageManager.resolvePackagePath(themeName) + ) + + // Use a built-in syntax and UI theme any time the configured themes are not + // available. + if (themeNames.length < 2) { + const builtInThemeNames = [ + 'atom-dark-syntax', + 'atom-dark-ui', + 'atom-light-syntax', + 'atom-light-ui', + 'base16-tomorrow-dark-theme', + 'base16-tomorrow-light-theme', + 'solarized-dark-syntax', + 'solarized-light-syntax' + ] + themeNames = _.intersection(themeNames, builtInThemeNames) + if (themeNames.length === 0) { + themeNames = ['atom-dark-syntax', 'atom-dark-ui'] + } else if (themeNames.length === 1) { + if (_.endsWith(themeNames[0], '-ui')) { + themeNames.unshift('atom-dark-syntax') + } else { + themeNames.push('atom-dark-ui') + } + } + } + + // Reverse so the first (top) theme is loaded after the others. We want + // the first/top theme to override later themes in the stack. + return themeNames.reverse() + } + + /* + Section: Private + */ + + // Resolve and apply the stylesheet specified by the path. + // + // This supports both CSS and Less stylesheets. + // + // * `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 a {Disposable} on which `.dispose()` can be called to remove the + // required stylesheet. + requireStylesheet (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) { + let fullPath = this.resolveStylesheet(stylesheetPath) + if (fullPath) { + const content = this.loadStylesheet(fullPath) + return this.applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation) + } else { + throw new Error(`Could not find a file at path '${stylesheetPath}'`) + } + } + + unwatchUserStylesheet () { + if (this.userStylesheetSubscriptions != null) this.userStylesheetSubscriptions.dispose() + this.userStylesheetSubscriptions = null + this.userStylesheetFile = null + if (this.userStyleSheetDisposable != null) this.userStyleSheetDisposable.dispose() + this.userStyleSheetDisposable = null + } + + loadUserStylesheet () { + this.unwatchUserStylesheet() + + const userStylesheetPath = this.styleManager.getUserStyleSheetPath() + if (!fs.isFileSync(userStylesheetPath)) { return } + + try { + this.userStylesheetFile = new File(userStylesheetPath) + this.userStylesheetSubscriptions = new CompositeDisposable() + const reloadStylesheet = () => this.loadUserStylesheet() + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidChange(reloadStylesheet)) + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidRename(reloadStylesheet)) + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidDelete(reloadStylesheet)) + } catch (error) { + const message = `\ +Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure +you have permissions to \`${userStylesheetPath}\`. + +On linux there are currently problems with watch sizes. See +[this document][watches] for more info. +[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ +` + this.notificationManager.addError(message, {dismissable: true}) + } + + let userStylesheetContents + try { + userStylesheetContents = this.loadStylesheet(userStylesheetPath, true) + } catch (error) { + return + } + + this.userStyleSheetDisposable = this.styleManager.addStyleSheet(userStylesheetContents, {sourcePath: userStylesheetPath, priority: 2}) + } + + loadBaseStylesheets () { + this.reloadBaseStylesheets() + } + + reloadBaseStylesheets () { + this.requireStylesheet('../static/atom', -2, true) + } + + stylesheetElementForId (id) { + const escapedId = id.replace(/\\/g, '\\\\') + return document.head.querySelector(`atom-styles style[source-path="${escapedId}"]`) + } + + resolveStylesheet (stylesheetPath) { + if (path.extname(stylesheetPath).length > 0) { + return fs.resolveOnLoadPath(stylesheetPath) + } else { + return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) + } + } + + loadStylesheet (stylesheetPath, importFallbackVariables) { + if (path.extname(stylesheetPath) === '.less') { + return this.loadLessStylesheet(stylesheetPath, importFallbackVariables) + } else { + return fs.readFileSync(stylesheetPath, 'utf8') + } + } + + loadLessStylesheet (lessStylesheetPath, importFallbackVariables = false) { + if (this.lessCache == null) { + this.lessCache = new LessCompileCache({ + resourcePath: this.resourcePath, + lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath, + importedFilePathsByRelativeImportPath: this.importedFilePathsByRelativeImportPath, + importPaths: this.getImportPaths() + }) + } + + try { + if (importFallbackVariables) { + const baseVarImports = `\ +@import "variables/ui-variables"; +@import "variables/syntax-variables";\ +` + const relativeFilePath = path.relative(this.resourcePath, lessStylesheetPath) + const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath] + + let content, digest + if (lessSource != null) { + ({ content } = lessSource); + ({ digest } = lessSource) + } else { + content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8') + digest = null + } + + return this.lessCache.cssForFile(lessStylesheetPath, content, digest) + } else { + return this.lessCache.read(lessStylesheetPath) + } + } catch (error) { + let detail, message + error.less = true + if (error.line != null) { + // Adjust line numbers for import fallbacks + if (importFallbackVariables) { error.line -= 2 } + + message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\`` + detail = `Line number: ${error.line}\n${error.message}` + } else { + message = `Error loading Less stylesheet: \`${lessStylesheetPath}\`` + detail = error.message + } + + this.notificationManager.addError(message, {detail, dismissable: true}) + throw error + } + } + + removeStylesheet (stylesheetPath) { + if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) { + this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose() + } + } + + applyStylesheet (path, text, priority, skipDeprecatedSelectorsTransformation) { + this.styleSheetDisposablesBySourcePath[path] = this.styleManager.addStyleSheet( + text, + { + priority, + skipDeprecatedSelectorsTransformation, + sourcePath: path + } + ) + + return this.styleSheetDisposablesBySourcePath[path] + } + + activateThemes () { + return new Promise(resolve => { + // @config.observe runs the callback once, then on subsequent changes. + this.config.observe('core.themes', () => { + this.deactivateThemes().then(() => { + this.warnForNonExistentThemes() + this.refreshLessCache() // Update cache for packages in core.themes config + + const promises = [] + for (const themeName of this.getEnabledThemeNames()) { + if (this.packageManager.resolvePackagePath(themeName)) { + promises.push(this.packageManager.activatePackage(themeName)) + } else { + console.warn(`Failed to activate theme '${themeName}' because it isn't installed.`) + } + } + + return Promise.all(promises).then(() => { + this.addActiveThemeClasses() + this.refreshLessCache() // Update cache again now that @getActiveThemes() is populated + this.loadUserStylesheet() + this.reloadBaseStylesheets() + this.initialLoadComplete = true + this.emitter.emit('did-change-active-themes') + resolve() + }) + }) + }) + }) + } + + deactivateThemes () { + this.removeActiveThemeClasses() + this.unwatchUserStylesheet() + const results = this.getActiveThemes().map(pack => this.packageManager.deactivatePackage(pack.name)) + return Promise.all(results.filter((r) => (r != null) && (typeof r.then === 'function'))) + } + + isInitialLoadComplete () { + return this.initialLoadComplete + } + + addActiveThemeClasses () { + const workspaceElement = this.viewRegistry.getView(this.workspace) + if (workspaceElement) { + for (const pack of this.getActiveThemes()) { + workspaceElement.classList.add(`theme-${pack.name}`) + } + } + } + + removeActiveThemeClasses () { + const workspaceElement = this.viewRegistry.getView(this.workspace) + for (const pack of this.getActiveThemes()) { + workspaceElement.classList.remove(`theme-${pack.name}`) + } + } + + refreshLessCache () { + if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths()) + } + + getImportPaths () { + let themePaths + const activeThemes = this.getActiveThemes() + if (activeThemes.length > 0) { + themePaths = (activeThemes.filter((theme) => theme).map((theme) => theme.getStylesheetsPath())) + } else { + themePaths = [] + for (const themeName of this.getEnabledThemeNames()) { + const themePath = this.packageManager.resolvePackagePath(themeName) + if (themePath) { + const deprecatedPath = path.join(themePath, 'stylesheets') + if (fs.isDirectorySync(deprecatedPath)) { + themePaths.push(deprecatedPath) + } else { + themePaths.push(path.join(themePath, 'styles')) + } + } + } + } + + return themePaths.filter(themePath => fs.isDirectorySync(themePath)) + } +} From c06745f098a2c462c3bacc569b4aa53c868ed62b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 29 Oct 2017 14:59:12 -0400 Subject: [PATCH 113/161] =?UTF-8?q?=F0=9F=91=95=20Suppress=20"'snapshotAux?= =?UTF-8?q?iliaryData'=20is=20not=20defined"=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/theme-manager.js b/src/theme-manager.js index b46fb1ada..6abf0fc74 100644 --- a/src/theme-manager.js +++ b/src/theme-manager.js @@ -1,3 +1,5 @@ +/* global snapshotAuxiliaryData */ + const path = require('path') const _ = require('underscore-plus') const {Emitter, CompositeDisposable} = require('event-kit') From 4eea63c50b4f8061f633392bf581e3d3a4fb3e5b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 30 Oct 2017 10:31:41 +0100 Subject: [PATCH 114/161] :memo: --- src/workspace.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index dcaf06006..defb43df0 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1050,10 +1050,10 @@ module.exports = class Workspace extends Model { // Essential: Search the workspace for items matching the given URI and hide them. // - // * `itemOrURI` (optional) The item to hide or a {String} containing the URI + // * `itemOrURI` The item to hide or a {String} containing the URI // of the item to hide. // - // Returns a {boolean} indicating whether any items were found (and hidden). + // Returns a {Boolean} indicating whether any items were found (and hidden). hide (itemOrURI) { let foundItems = false From 9eb9cb1a4a7fd9c5270e40a2a50684d5a466ecf3 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 30 Oct 2017 15:09:53 +0100 Subject: [PATCH 115/161] :arrow_up: language-perl@0.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcbeb05c1..777e96513 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.38.0", + "language-perl": "0.38.1", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From d035e41f378497cc2d23b9f7983adebc1bd1820e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 13:27:47 -0600 Subject: [PATCH 116/161] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0df753e69..777a2be49 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.1", + "fuzzy-finder": "1.7.0", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 8284995f5f1a56691d2581e773a9ba32b4e9d642 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 13:35:08 -0600 Subject: [PATCH 117/161] Revert ":arrow_up: fuzzy-finder" This reverts commit d035e41f378497cc2d23b9f7983adebc1bd1820e. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 777a2be49..0df753e69 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.7.0", + "fuzzy-finder": "1.6.1", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 058a42f7c2ff1a5ac35578b9b28ef32862f0d158 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 14:03:18 -0600 Subject: [PATCH 118/161] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0df753e69..4ccff5107 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.1", + "fuzzy-finder": "1.7.1", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 09c25baf6caa34774acbf04f1217bbcffd2d985e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Oct 2017 13:34:45 -0700 Subject: [PATCH 119/161] :arrow_up: text-buffer for DisplayLayer.onDidChangeSync change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ccff5107..678ec53e3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.1", + "text-buffer": "13.8.0-display-layer-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From b30b1e36aba1adada4e036efb0c8de13c2e0ef23 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 30 Oct 2017 17:17:52 -0600 Subject: [PATCH 120/161] :arrow_up: text-buffer@13.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ccff5107..348bb2d41 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.1", + "text-buffer": "13.7.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 482824047c9c92a15d429577ed1eec8d8c49c16a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Oct 2017 17:26:21 -0700 Subject: [PATCH 121/161] :arrow_up: text-buffer --- package.json | 2 +- src/text-editor-component.js | 8 ++++++-- src/text-editor.coffee | 21 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 678ec53e3..8dfed5b41 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0-display-layer-change-event-1", + "text-buffer": "13.8.0-display-layer-change-event-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/text-editor-component.js b/src/text-editor-component.js index f19b7e31c..b67b45f83 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2456,8 +2456,12 @@ class TextEditorComponent { didChangeDisplayLayer (changes) { for (let i = 0; i < changes.length; i++) { - const {start, oldExtent, newExtent} = changes[i] - this.spliceLineTopIndex(start.row, oldExtent.row, newExtent.row) + const {oldRange, newRange} = changes[i] + this.spliceLineTopIndex( + newRange.start.row, + oldRange.end.row - oldRange.start.row, + newRange.end.row - newRange.start.row + ) } this.scheduleUpdate() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 79e00e31a..3bc5fa34e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -468,10 +468,10 @@ class TextEditor extends Model subscribeToDisplayLayer: -> @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayLayer.onDidChangeSync (e) => + @disposables.add @displayLayer.onDidChange (changes) => @mergeIntersectingSelections() - @component?.didChangeDisplayLayer(e) - @emitter.emit 'did-change', e + @component?.didChangeDisplayLayer(changes) + @emitter.emit 'did-change', changes.map (change) -> new ChangeEvent(change) @disposables.add @displayLayer.onDidReset => @mergeIntersectingSelections() @component?.didResetDisplayLayer() @@ -3911,3 +3911,18 @@ class TextEditor extends Model endRow++ new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) + +class ChangeEvent + constructor: ({@oldRange, @newRange}) -> + + Object.defineProperty @prototype, 'start', { + get: -> @oldRange.start + } + + Object.defineProperty @prototype, 'oldExtent', { + get: -> @oldRange.getExtent() + } + + Object.defineProperty @prototype, 'newExtent', { + get: -> @newRange.getExtent() + } From 1395e69fe007efa7c11efe21a251cb8897c875fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 Oct 2017 10:10:07 -0700 Subject: [PATCH 122/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf9d08db9..80677b540 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0-display-layer-change-event-2", + "text-buffer": "13.8.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From a88d453b4ad34752d453ac164dfb90d174d05b1e Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 31 Oct 2017 11:47:55 -0600 Subject: [PATCH 123/161] :arrow_up: snippets@1.1.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80677b540..6fb7f3394 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.7", + "snippets": "1.1.8", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", From 5af83435abbebd4e40f20b841a8e68198bfed8b4 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Tue, 31 Oct 2017 18:48:58 +0100 Subject: [PATCH 124/161] :arrow_up: exception-reporting@0.41.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6fb7f3394..de9a49468 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", - "exception-reporting": "0.41.4", + "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", "fuzzy-finder": "1.7.1", "github": "0.7.0", From 080137f377d439ffa2bd23c391dad31953427325 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 09:36:29 -0600 Subject: [PATCH 125/161] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de9a49468..56619128a 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.7.1", + "fuzzy-finder": "1.7.2", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From cffa433267111317bebd2254386fd282bfb0aae4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:15:14 -0600 Subject: [PATCH 126/161] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56619128a..98ccece1a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", - "tabs": "0.108.0", + "tabs": "0.109.0", "timecop": "0.36.0", "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", From c3adde5846e44828ceb3eaa4880e58af3bde71e0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:46:15 -0600 Subject: [PATCH 127/161] :arrow_up: archive-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98ccece1a..de6813c2d 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.63.4", + "archive-view": "0.64.0", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 70e9ebd545df3216e4152dd9989af25c3348ca82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:53:10 -0600 Subject: [PATCH 128/161] Revert ":arrow_up: archive-view" This reverts commit c3adde5846e44828ceb3eaa4880e58af3bde71e0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de6813c2d..98ccece1a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.64.0", + "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 25cb5c2c2779d74eca03573f8f908baffdc4edad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 13:14:01 -0600 Subject: [PATCH 129/161] :arrow_up: archive-view This version should be able to be packaged cleanly. /cc @Alhadis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98ccece1a..72250311d 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.63.4", + "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 252a98b231eac7b94581cc640a7e61f043729d60 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 13:55:40 -0600 Subject: [PATCH 130/161] Prevent the browser from auto-scrolling the scroll container on spacebar --- src/text-editor-component.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index b67b45f83..4c639e532 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1616,11 +1616,23 @@ class TextEditorComponent { if (this.isInputEnabled()) { event.stopPropagation() - // WARNING: If we call preventDefault on the input of a space character, - // then the browser interprets the spacebar keypress as a page-down command, - // causing spaces to scroll elements containing editors. This is impossible - // to test. - if (event.data !== ' ') event.preventDefault() + // WARNING: If we call preventDefault on the input of a space + // character, then the browser interprets the spacebar keypress as a + // page-down command, causing spaces to scroll elements containing + // editors. This means typing space will actually change the contents + // of the hidden input, which will cause the browser to autoscroll the + // scroll container to reveal the input if it is off screen (See + // https://github.com/atom/atom/issues/16046). To correct for this + // situation, we automatically reset the scroll position to 0,0 after + // typing a space. None of this can really be tested. + if (event.data === ' ') { + window.setImmediate(() => { + this.refs.scrollContainer.scrollTop = 0 + this.refs.scrollContainer.scrollLeft = 0 + }) + } else { + event.preventDefault() + } // If the input event is fired while the accented character menu is open it // means that the user has chosen one of the accented alternatives. Thus, we From cbc2bdb20ba79787572b2cf948e4a43b802855be Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 31 Oct 2017 15:31:10 -0700 Subject: [PATCH 131/161] :arrow_up: github@0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72250311d..5f2b8dfa1 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", "fuzzy-finder": "1.7.2", - "github": "0.7.0", + "github": "0.8.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From e76be3839ee0f1485768211eb92d7b47784340ea Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:22:11 +0100 Subject: [PATCH 132/161] :arrow_up: find-and-replace@0.212.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f2b8dfa1..dcbaf9c0d 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", - "find-and-replace": "0.212.3", + "find-and-replace": "0.212.4", "fuzzy-finder": "1.7.2", "github": "0.8.0", "git-diff": "1.3.6", From a66dcc41a6266634994dfe34c9df896189721091 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:23:49 +0100 Subject: [PATCH 133/161] :arrow_up: markdown-preview@0.159.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcbaf9c0d..17c9dc9e1 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.17", + "markdown-preview": "0.159.18", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 60aa93846e201c1597b5695a59a6e3144f647163 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:25:01 +0100 Subject: [PATCH 134/161] :arrow_up: keybinding-resolver@0.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17c9dc9e1..1a9194951 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "grammar-selector": "0.49.8", "image-view": "0.62.4", "incompatible-packages": "0.27.3", - "keybinding-resolver": "0.38.0", + "keybinding-resolver": "0.38.1", "line-ending-selector": "0.7.4", "link": "0.31.3", "markdown-preview": "0.159.18", From 9a3d98cf9b2b3fd86af6a7c2a678f9fbdf0628c1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:28:45 +0100 Subject: [PATCH 135/161] :arrow_down: language-less, language-sass --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1a9194951..1e92bd287 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", - "language-less": "0.34.0", + "language-less": "0.33.0", "language-make": "0.22.3", "language-mustache": "0.14.4", "language-objective-c": "0.15.1", @@ -159,7 +159,7 @@ "language-python": "0.45.5", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.61.2", + "language-sass": "0.61.1", "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.8", From 449fcfbd24e3795f3bc9dba8b7aae6b54265e468 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:30:16 +0100 Subject: [PATCH 136/161] :arrow_up: language-java@0.27.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e92bd287..64b17206a 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-go": "0.44.3", "language-html": "0.48.2", "language-hyperlink": "0.16.3", - "language-java": "0.27.5", + "language-java": "0.27.6", "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.0", From 138524db115233c9db051644922f324fdc12f103 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:31:23 +0100 Subject: [PATCH 137/161] :arrow_up: language-coffee-script@0.49.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64b17206a..1c88caa54 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.49.2", + "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.7", "language-gfm": "0.90.2", From 4c02e96f2ae03d35aa148079431cf2c80774d4db Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:59:02 +0100 Subject: [PATCH 138/161] Preserve whitespace --- static/jasmine.less | 1 + 1 file changed, 1 insertion(+) diff --git a/static/jasmine.less b/static/jasmine.less index ab2695179..dcd467c71 100644 --- a/static/jasmine.less +++ b/static/jasmine.less @@ -165,6 +165,7 @@ body { font-weight: bold; color: #d9534f; padding: 5px 0 5px 0; + white-space: pre-wrap; } .result-message.deprecation-message { From b6c804d637803d4d494e77de12aeb2fcc6114802 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:59:21 +0100 Subject: [PATCH 139/161] Do not modify menus --- spec/menu-manager-spec.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index 798aa3766..3bbd8b9da 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -6,6 +6,7 @@ describe "MenuManager", -> beforeEach -> menu = new MenuManager({keymapManager: atom.keymaps, packageManager: atom.packages}) + spyOn(menu, 'sendToBrowserProcess') # Do not modify Atom's actual menus menu.initialize({resourcePath: atom.getLoadSettings().resourcePath}) describe "::add(items)", -> @@ -54,7 +55,6 @@ describe "MenuManager", -> afterEach -> Object.defineProperty process, 'platform', value: originalPlatform it "sends the current menu template and associated key bindings to the browser process", -> - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b' menu.update() @@ -66,7 +66,6 @@ describe "MenuManager", -> it "omits key bindings that are mapped to unset! in any context", -> # it would be nice to be smarter about omitting, but that would require a much # more dynamic interaction between the currently focused element and the menu - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b' atom.keymaps.add 'test', 'atom-text-editor': 'ctrl-b': 'unset!' @@ -77,7 +76,6 @@ describe "MenuManager", -> it "omits key bindings that could conflict with AltGraph characters on macOS", -> Object.defineProperty process, 'platform', value: 'darwin' - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", command: "c"} @@ -98,7 +96,6 @@ describe "MenuManager", -> it "omits key bindings that could conflict with AltGraph characters on Windows", -> Object.defineProperty process, 'platform', value: 'win32' - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", command: "c"} From 11511f27d5d8cfe9c23919c8a8bfaa9167f3a1b5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 09:21:13 -0600 Subject: [PATCH 140/161] Don't terminate selection dragging when a modifier key is pressed This preserves the ability to add selections via ctrl- or cmd-click. --- spec/text-editor-component-spec.js | 21 +++++++++++++++++---- src/text-editor-component.js | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 5f0a28883..97fdf45c7 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4428,11 +4428,14 @@ describe('TextEditorComponent', () => { const {component, editor} = buildComponent() let dragging = false - component.handleMouseDragUntilMouseUp({ - didDrag: (event) => { dragging = true }, - didStopDragging: () => { dragging = false } - }) + function startDragging () { + component.handleMouseDragUntilMouseUp({ + didDrag: (event) => { dragging = true }, + didStopDragging: () => { dragging = false } + }) + } + startDragging() window.dispatchEvent(new MouseEvent('mousemove')) await getNextAnimationFramePromise() expect(dragging).toBe(true) @@ -4448,6 +4451,16 @@ describe('TextEditorComponent', () => { window.dispatchEvent(new MouseEvent('mousemove')) await getNextAnimationFramePromise() expect(dragging).toBe(false) + + // Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse) + startDragging() + window.dispatchEvent(new MouseEvent('mousemove')) + await getNextAnimationFramePromise() + expect(dragging).toBe(true) + component.didKeydown({key: 'Control'}) + component.didKeydown({key: 'Alt'}) + component.didKeydown({key: 'Meta'}) + expect(dragging).toBe(true) }) function getNextAnimationFramePromise () { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 4c639e532..bbb02bb7f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1666,7 +1666,7 @@ class TextEditorComponent { // Stop dragging when user interacts with the keyboard. This prevents // unwanted selections in the case edits are performed while selecting text // at the same time. - if (this.stopDragging) this.stopDragging() + if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') this.stopDragging() if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { From afc341ed4674d53e86779ef49926a0c539638ec1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 09:50:45 -0600 Subject: [PATCH 141/161] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c88caa54..a0ae60d00 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", - "find-and-replace": "0.212.4", + "find-and-replace": "0.213.0", "fuzzy-finder": "1.7.2", "github": "0.8.0", "git-diff": "1.3.6", From f0f0ba2296ccf49474490d3394a898129a12c050 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 10:13:37 -0600 Subject: [PATCH 142/161] :arrow_up: event-kit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0ae60d00..2716fac9f 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.109.0", "timecop": "0.36.0", - "tree-view": "0.221.0", + "tree-view": "0.221.1", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From f25570f135ef75dc518f02db1dc3619dd75d9f54 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 14:00:43 -0600 Subject: [PATCH 143/161] Exclude Shift from keydown events that terminate selection drags --- spec/text-editor-component-spec.js | 1 + src/text-editor-component.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 97fdf45c7..992785d6e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4459,6 +4459,7 @@ describe('TextEditorComponent', () => { expect(dragging).toBe(true) component.didKeydown({key: 'Control'}) component.didKeydown({key: 'Alt'}) + component.didKeydown({key: 'Shift'}) component.didKeydown({key: 'Meta'}) expect(dragging).toBe(true) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index bbb02bb7f..2a77e30f8 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1665,8 +1665,11 @@ class TextEditorComponent { didKeydown (event) { // Stop dragging when user interacts with the keyboard. This prevents // unwanted selections in the case edits are performed while selecting text - // at the same time. - if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') this.stopDragging() + // at the same time. Modifier keys are exempt to preserve the ability to + // add selections, shift-scroll horizontally while selecting. + if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift') { + this.stopDragging() + } if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { From 61b4fc7d2921a7b11daee0f044091add9731d9f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 15:30:21 -0600 Subject: [PATCH 144/161] Actually require @atom/nsfw dependency in path-watcher.js :facepalm: --- src/path-watcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 2dfece46e..5a2d10bde 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -4,7 +4,7 @@ const fs = require('fs') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') -const nsfw = require('nsfw') +const nsfw = require('@atom/nsfw') const {NativeWatcherRegistry} = require('./native-watcher-registry') // Private: Associate native watcher action flags with descriptive String equivalents. From e9e23a2d09832f578874020ed10deada2c954eb6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:19:52 -0700 Subject: [PATCH 145/161] Convert text-editor.coffee to JS Signed-off-by: Nathan Sobo --- src/selection.coffee | 2 +- src/text-editor-utils.js | 139 -- src/text-editor.coffee | 3928 -------------------------------- src/text-editor.js | 4587 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 4588 insertions(+), 4068 deletions(-) delete mode 100644 src/text-editor-utils.js delete mode 100644 src/text-editor.coffee create mode 100644 src/text-editor.js diff --git a/src/selection.coffee b/src/selection.coffee index cb45286b8..e55f17e88 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -735,7 +735,7 @@ class Selection extends Model # # * `otherSelection` A {Selection} to merge with. # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - merge: (otherSelection, options) -> + merge: (otherSelection, options = {}) -> myGoalScreenRange = @getGoalScreenRange() otherGoalScreenRange = otherSelection.getGoalScreenRange() diff --git a/src/text-editor-utils.js b/src/text-editor-utils.js deleted file mode 100644 index ab1104144..000000000 --- a/src/text-editor-utils.js +++ /dev/null @@ -1,139 +0,0 @@ -// This file is temporary. We should gradually convert methods in `text-editor.coffee` -// from CoffeeScript to JavaScript and move them here, so that we can eventually convert -// the entire class to JavaScript. - -const {Point, Range} = require('text-buffer') - -const NON_WHITESPACE_REGEX = /\S/ - -module.exports = { - toggleLineCommentsForBufferRows (start, end) { - let { - commentStartString, - commentEndString - } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0)) - if (!commentStartString) return - commentStartString = commentStartString.trim() - - if (commentEndString) { - commentEndString = commentEndString.trim() - const startDelimiterColumnRange = columnRangeForStartDelimiter( - this.buffer.lineForRow(start), - commentStartString - ) - if (startDelimiterColumnRange) { - const endDelimiterColumnRange = columnRangeForEndDelimiter( - this.buffer.lineForRow(end), - commentEndString - ) - if (endDelimiterColumnRange) { - this.buffer.transact(() => { - this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) - this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) - }) - } - } else { - this.buffer.transact(() => { - const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length - this.buffer.insert([start, indentLength], commentStartString + ' ') - this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) - }) - } - } else { - let hasCommentedLines = false - let hasUncommentedLines = false - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - if (columnRangeForStartDelimiter(line, commentStartString)) { - hasCommentedLines = true - } else { - hasUncommentedLines = true - } - } - } - - const shouldUncomment = hasCommentedLines && !hasUncommentedLines - - if (shouldUncomment) { - for (let row = start; row <= end; row++) { - const columnRange = columnRangeForStartDelimiter( - this.buffer.lineForRow(row), - commentStartString - ) - if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) - } - } else { - let minIndentLevel = Infinity - let minBlankIndentLevel = Infinity - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - const indentLevel = this.indentLevelForLine(line) - if (NON_WHITESPACE_REGEX.test(line)) { - if (indentLevel < minIndentLevel) minIndentLevel = indentLevel - } else { - if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel - } - } - minIndentLevel = Number.isFinite(minIndentLevel) - ? minIndentLevel - : Number.isFinite(minBlankIndentLevel) - ? minBlankIndentLevel - : 0 - - const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * minIndentLevel) - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength()) - this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') - } else { - this.buffer.setTextInRange( - new Range(new Point(row, 0), new Point(row, Infinity)), - indentString + commentStartString + ' ' - ) - } - } - } - } - } -} - -function columnForIndentLevel (line, indentLevel, tabLength) { - let column = 0 - let indentLength = 0 - const goalIndentLength = indentLevel * tabLength - while (indentLength < goalIndentLength) { - const char = line[column] - if (char === '\t') { - indentLength += tabLength - (indentLength % tabLength) - } else if (char === ' ') { - indentLength++ - } else { - break - } - column++ - } - return column -} - -function columnRangeForStartDelimiter (line, delimiter) { - const startColumn = line.search(NON_WHITESPACE_REGEX) - if (startColumn === -1) return null - if (!line.startsWith(delimiter, startColumn)) return null - - let endColumn = startColumn + delimiter.length - if (line[endColumn] === ' ') endColumn++ - return [startColumn, endColumn] -} - -function columnRangeForEndDelimiter (line, delimiter) { - let startColumn = line.lastIndexOf(delimiter) - if (startColumn === -1) return null - - const endColumn = startColumn + delimiter.length - if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null - if (line[startColumn - 1] === ' ') startColumn-- - return [startColumn, endColumn] -} diff --git a/src/text-editor.coffee b/src/text-editor.coffee deleted file mode 100644 index 3bc5fa34e..000000000 --- a/src/text-editor.coffee +++ /dev/null @@ -1,3928 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -fs = require 'fs-plus' -Grim = require 'grim' -{CompositeDisposable, Disposable, Emitter} = require 'event-kit' -{Point, Range} = TextBuffer = require 'text-buffer' -DecorationManager = require './decoration-manager' -TokenizedBuffer = require './tokenized-buffer' -Cursor = require './cursor' -Model = require './model' -Selection = require './selection' -TextEditorUtils = require './text-editor-utils' - -TextMateScopeSelector = require('first-mate').ScopeSelector -GutterContainer = require './gutter-container' -TextEditorComponent = null -TextEditorElement = null -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' - -NON_WHITESPACE_REGEXP = /\S/ -ZERO_WIDTH_NBSP = '\ufeff' - -# Essential: This class represents all essential editing state for a single -# {TextBuffer}, including cursor and selection positions, folds, and soft wraps. -# If you're manipulating the state of an editor, use this class. -# -# A single {TextBuffer} can belong to multiple editors. For example, if the -# same file is open in two different panes, Atom creates a separate editor for -# each pane. If the buffer is manipulated the changes are reflected in both -# editors, but each maintains its own cursor position, folded lines, etc. -# -# ## Accessing TextEditor Instances -# -# The easiest way to get hold of `TextEditor` objects is by registering a callback -# with `::observeTextEditors` on the `atom.workspace` global. Your callback will -# then be called with all current editor instances and also when any editor is -# created in the future. -# -# ```coffee -# atom.workspace.observeTextEditors (editor) -> -# editor.insertText('Hello World') -# ``` -# -# ## Buffer vs. Screen Coordinates -# -# Because editors support folds and soft-wrapping, the lines on screen don't -# always match the lines in the buffer. For example, a long line that soft wraps -# twice renders as three lines on screen, but only represents one line in the -# buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds -# to row 11 in the buffer. -# -# Your choice of coordinates systems will depend on what you're trying to -# achieve. For example, if you're writing a command that jumps the cursor up or -# down by 10 lines, you'll want to use screen coordinates because the user -# probably wants to skip lines *on screen*. However, if you're writing a package -# that jumps between method definitions, you'll want to work in buffer -# coordinates. -# -# **When in doubt, just default to buffer coordinates**, then experiment with -# soft wraps and folds to ensure your code interacts with them correctly. -module.exports = -class TextEditor extends Model - @setClipboard: (clipboard) -> - @clipboard = clipboard - - @setScheduler: (scheduler) -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.setScheduler(scheduler) - - @didUpdateStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateStyles() - - @didUpdateScrollbarStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateScrollbarStyles() - - @viewForItem: (item) -> item.element ? item - - serializationVersion: 1 - - buffer: null - cursors: null - showCursorOnSelection: null - selections: null - suppressSelectionMerging: false - selectionFlashDuration: 500 - gutterContainer: null - editorElement: null - verticalScrollMargin: 2 - horizontalScrollMargin: 6 - softWrapped: null - editorWidthInChars: null - lineHeightInPixels: null - defaultCharWidth: null - height: null - width: null - registered: false - atomicSoftTabs: true - invisibles: null - - Object.defineProperty @prototype, "element", - get: -> @getElement() - - Object.defineProperty @prototype, "editorElement", - get: -> - Grim.deprecate(""" - `TextEditor.prototype.editorElement` has always been private, but now - it is gone. Reading the `editorElement` property still returns a - reference to the editor element but this field will be removed in a - later version of Atom, so we recommend using the `element` property instead. - """) - - @getElement() - - Object.defineProperty(@prototype, 'displayBuffer', get: -> - Grim.deprecate(""" - `TextEditor.prototype.displayBuffer` has always been private, but now - it is gone. Reading the `displayBuffer` property now returns a reference - to the containing `TextEditor`, which now provides *some* of the API of - the defunct `DisplayBuffer` class. - """) - this - ) - - Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) - - Object.assign(@prototype, TextEditorUtils) - - @deserialize: (state, atomEnvironment) -> - # TODO: Return null on version mismatch when 1.8.0 has been out for a while - if state.version isnt @prototype.serializationVersion and state.displayBuffer? - state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer - - try - tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) - return null unless tokenizedBuffer? - - state.tokenizedBuffer = tokenizedBuffer - state.tabLength = state.tokenizedBuffer.getTabLength() - catch error - if error.syscall is 'read' - return # Error reading the file, don't deserialize an editor for it - else - throw error - - state.buffer = state.tokenizedBuffer.buffer - state.assert = atomEnvironment.assert.bind(atomEnvironment) - editor = new this(state) - if state.registered - disposable = atomEnvironment.textEditors.add(editor) - editor.onDidDestroy -> disposable.dispose() - editor - - constructor: (params={}) -> - unless @constructor.clipboard? - throw new Error("Must call TextEditor.setClipboard at least once before creating TextEditor instances") - - super - - { - @softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength, - @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, @showLineNumbers, @largeFileMode, - @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @scrollSensitivity, @editorWidthInChars, - @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, - @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, - @showCursorOnSelection, @maxScreenLineLength - } = params - - @assert ?= (condition) -> condition - @emitter = new Emitter - @disposables = new CompositeDisposable - @cursors = [] - @cursorsByMarkerId = new Map - @selections = [] - @hasTerminatedPendingState = false - - @mini ?= false - @scrollPastEnd ?= false - @scrollSensitivity ?= 40 - @showInvisibles ?= true - @softTabs ?= true - tabLength ?= 2 - @autoIndent ?= true - @autoIndentOnPaste ?= true - @showCursorOnSelection ?= true - @undoGroupingInterval ?= 300 - @nonWordCharacters ?= "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" - @softWrapped ?= false - @softWrapAtPreferredLineLength ?= false - @preferredLineLength ?= 80 - @maxScreenLineLength ?= 500 - @showLineNumbers ?= true - - @buffer ?= new TextBuffer({ - shouldDestroyOnFileDelete: -> atom.config.get('core.closeDeletedFileTabs') - }) - @tokenizedBuffer ?= new TokenizedBuffer({ - grammar, tabLength, @buffer, @largeFileMode, @assert - }) - - unless @displayLayer? - displayLayerParams = { - invisibles: @getInvisibles(), - softWrapColumn: @getSoftWrapColumn(), - showIndentGuides: @doesShowIndentGuide(), - atomicSoftTabs: params.atomicSoftTabs ? true, - tabLength: tabLength, - ratioForCharacter: @ratioForCharacter.bind(this), - isWrapBoundary: isWrapBoundary, - foldCharacter: ZERO_WIDTH_NBSP, - softWrapHangingIndent: params.softWrapHangingIndentLength ? 0 - } - - if @displayLayer = @buffer.getDisplayLayer(params.displayLayerId) - @displayLayer.reset(displayLayerParams) - @selectionsMarkerLayer = @displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) - else - @displayLayer = @buffer.addDisplayLayer(displayLayerParams) - - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - @disposables.add new Disposable => - cancelIdleCallback(@backgroundWorkHandle) if @backgroundWorkHandle? - - @displayLayer.setTextDecorationLayer(@tokenizedBuffer) - @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @disposables.add(@defaultMarkerLayer.onDidDestroy => - @assert(false, "defaultMarkerLayer destroyed at an unexpected time") - ) - @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) - @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - - @decorationManager = new DecorationManager(this) - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor') - @decorateCursorLine() unless @isMini() - - @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) - - for marker in @selectionsMarkerLayer.getMarkers() - @addSelection(marker) - - @subscribeToBuffer() - @subscribeToDisplayLayer() - - if @cursors.length is 0 and not suppressCursorCreation - initialLine = Math.max(parseInt(initialLine) or 0, 0) - initialColumn = Math.max(parseInt(initialColumn) or 0, 0) - @addCursorAtBufferPosition([initialLine, initialColumn]) - - @gutterContainer = new GutterContainer(this) - @lineNumberGutter = @gutterContainer.addGutter - name: 'line-number' - priority: 0 - visible: lineNumberGutterVisible - - decorateCursorLine: -> - @cursorLineDecorations = [ - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) - ] - - doBackgroundWork: (deadline) => - previousLongestRow = @getApproximateLongestScreenRow() - if @displayLayer.doBackgroundWork(deadline) - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - else - @backgroundWorkHandle = null - - if @getApproximateLongestScreenRow() isnt previousLongestRow - @component?.scheduleUpdate() - - update: (params) -> - displayLayerParams = {} - - for param in Object.keys(params) - value = params[param] - - switch param - when 'autoIndent' - @autoIndent = value - - when 'autoIndentOnPaste' - @autoIndentOnPaste = value - - when 'undoGroupingInterval' - @undoGroupingInterval = value - - when 'nonWordCharacters' - @nonWordCharacters = value - - when 'scrollSensitivity' - @scrollSensitivity = value - - when 'encoding' - @buffer.setEncoding(value) - - when 'softTabs' - if value isnt @softTabs - @softTabs = value - - when 'atomicSoftTabs' - if value isnt @displayLayer.atomicSoftTabs - displayLayerParams.atomicSoftTabs = value - - when 'tabLength' - if value? and value isnt @tokenizedBuffer.getTabLength() - @tokenizedBuffer.setTabLength(value) - displayLayerParams.tabLength = value - - when 'softWrapped' - if value isnt @softWrapped - @softWrapped = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - @emitter.emit 'did-change-soft-wrapped', @isSoftWrapped() - - when 'softWrapHangingIndentLength' - if value isnt @displayLayer.softWrapHangingIndent - displayLayerParams.softWrapHangingIndent = value - - when 'softWrapAtPreferredLineLength' - if value isnt @softWrapAtPreferredLineLength - @softWrapAtPreferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'preferredLineLength' - if value isnt @preferredLineLength - @preferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'maxScreenLineLength' - if value isnt @maxScreenLineLength - @maxScreenLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'mini' - if value isnt @mini - @mini = value - @emitter.emit 'did-change-mini', value - displayLayerParams.invisibles = @getInvisibles() - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - if @mini - decoration.destroy() for decoration in @cursorLineDecorations - @cursorLineDecorations = null - else - @decorateCursorLine() - @component?.scheduleUpdate() - - when 'placeholderText' - if value isnt @placeholderText - @placeholderText = value - @emitter.emit 'did-change-placeholder-text', value - - when 'lineNumberGutterVisible' - if value isnt @lineNumberGutterVisible - if value - @lineNumberGutter.show() - else - @lineNumberGutter.hide() - @emitter.emit 'did-change-line-number-gutter-visible', @lineNumberGutter.isVisible() - - when 'showIndentGuide' - if value isnt @showIndentGuide - @showIndentGuide = value - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - - when 'showLineNumbers' - if value isnt @showLineNumbers - @showLineNumbers = value - @component?.scheduleUpdate() - - when 'showInvisibles' - if value isnt @showInvisibles - @showInvisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'invisibles' - if not _.isEqual(value, @invisibles) - @invisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'editorWidthInChars' - if value > 0 and value isnt @editorWidthInChars - @editorWidthInChars = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'width' - if value isnt @width - @width = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'scrollPastEnd' - if value isnt @scrollPastEnd - @scrollPastEnd = value - @component?.scheduleUpdate() - - when 'autoHeight' - if value isnt @autoHeight - @autoHeight = value - - when 'autoWidth' - if value isnt @autoWidth - @autoWidth = value - - when 'showCursorOnSelection' - if value isnt @showCursorOnSelection - @showCursorOnSelection = value - @component?.scheduleUpdate() - - else - if param isnt 'ref' and param isnt 'key' - throw new TypeError("Invalid TextEditor parameter: '#{param}'") - - @displayLayer.reset(displayLayerParams) - - if @component? - @component.getNextUpdatePromise() - else - Promise.resolve() - - scheduleComponentUpdate: -> - @component?.scheduleUpdate() - - serialize: -> - tokenizedBufferState = @tokenizedBuffer.serialize() - - { - deserializer: 'TextEditor' - version: @serializationVersion - - # TODO: Remove this forward-compatible fallback once 1.8 reaches stable. - displayBuffer: {tokenizedBuffer: tokenizedBufferState} - - tokenizedBuffer: tokenizedBufferState - displayLayerId: @displayLayer.id - selectionsMarkerLayerId: @selectionsMarkerLayer.id - - initialScrollTopRow: @getScrollTopRow() - initialScrollLeftColumn: @getScrollLeftColumn() - - atomicSoftTabs: @displayLayer.atomicSoftTabs - softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent - - @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @maxScreenLineLength, - @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth - } - - subscribeToBuffer: -> - @buffer.retain() - @disposables.add @buffer.onDidChangePath => - @emitter.emit 'did-change-title', @getTitle() - @emitter.emit 'did-change-path', @getPath() - @disposables.add @buffer.onDidChangeEncoding => - @emitter.emit 'did-change-encoding', @getEncoding() - @disposables.add @buffer.onDidDestroy => @destroy() - @disposables.add @buffer.onDidChangeModified => - @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() - - terminatePendingState: -> - @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState - @hasTerminatedPendingState = true - - onDidTerminatePendingState: (callback) -> - @emitter.on 'did-terminate-pending-state', callback - - subscribeToDisplayLayer: -> - @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayLayer.onDidChange (changes) => - @mergeIntersectingSelections() - @component?.didChangeDisplayLayer(changes) - @emitter.emit 'did-change', changes.map (change) -> new ChangeEvent(change) - @disposables.add @displayLayer.onDidReset => - @mergeIntersectingSelections() - @component?.didResetDisplayLayer() - @emitter.emit 'did-change', {} - @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) - @disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections() - - destroyed: -> - @disposables.dispose() - @displayLayer.destroy() - @tokenizedBuffer.destroy() - selection.destroy() for selection in @selections.slice() - @buffer.release() - @gutterContainer.destroy() - @emitter.emit 'did-destroy' - @emitter.clear() - @component?.element.component = null - @component = null - @lineNumberGutter.element = null - - ### - Section: Event Subscription - ### - - # Essential: Calls your `callback` when the buffer's title has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeTitle: (callback) -> - @emitter.on 'did-change-title', callback - - # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePath: (callback) -> - @emitter.on 'did-change-path', callback - - # Essential: Invoke the given callback synchronously when the content of the - # buffer changes. - # - # Because observers are invoked synchronously, it's important not to perform - # any expensive operations via this method. Consider {::onDidStopChanging} to - # delay expensive operations until after changes stop occurring. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - # Essential: Invoke `callback` 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. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChanging: (callback) -> - @getBuffer().onDidStopChanging(callback) - - # Essential: Calls your `callback` when a {Cursor} is moved. If there are - # multiple cursors, your callback will be called for each cursor. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferPosition` {Point} - # * `oldScreenPosition` {Point} - # * `newBufferPosition` {Point} - # * `newScreenPosition` {Point} - # * `textChanged` {Boolean} - # * `cursor` {Cursor} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeCursorPosition: (callback) -> - @emitter.on 'did-change-cursor-position', callback - - # Essential: Calls your `callback` when a selection's screen range changes. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferRange` {Range} - # * `oldScreenRange` {Range} - # * `newBufferRange` {Range} - # * `newScreenRange` {Range} - # * `selection` {Selection} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSelectionRange: (callback) -> - @emitter.on 'did-change-selection-range', callback - - # Extended: Calls your `callback` when soft wrap was enabled or disabled. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSoftWrapped: (callback) -> - @emitter.on 'did-change-soft-wrapped', callback - - # Extended: Calls your `callback` when the buffer's encoding has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeEncoding: (callback) -> - @emitter.on 'did-change-encoding', callback - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. Immediately calls your callback with - # the current grammar. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGrammar: (callback) -> - callback(@getGrammar()) - @onDidChangeGrammar(callback) - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeGrammar: (callback) -> - @emitter.on 'did-change-grammar', callback - - # Extended: Calls your `callback` when the result of {::isModified} changes. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeModified: (callback) -> - @getBuffer().onDidChangeModified(callback) - - # Extended: Calls your `callback` when the buffer's underlying file changes on - # disk at a moment when the result of {::isModified} is true. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidConflict: (callback) -> - @getBuffer().onDidConflict(callback) - - # Extended: Calls your `callback` before text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # * `cancel` {Function} Call to prevent the text from being inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillInsertText: (callback) -> - @emitter.on 'will-insert-text', callback - - # Extended: Calls your `callback` after text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidInsertText: (callback) -> - @emitter.on 'did-insert-text', callback - - # Essential: Invoke the given callback after the buffer is saved to disk. - # - # * `callback` {Function} to be called after the buffer is saved. - # * `event` {Object} with the following keys: - # * `path` The path to which the buffer was saved. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidSave: (callback) -> - @getBuffer().onDidSave(callback) - - # Essential: Invoke the given callback when the editor is destroyed. - # - # * `callback` {Function} to be called when the editor is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # Immediately calls your callback for each existing cursor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeCursors: (callback) -> - callback(cursor) for cursor in @getCursors() - @onDidAddCursor(callback) - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddCursor: (callback) -> - @emitter.on 'did-add-cursor', callback - - # Extended: Calls your `callback` when a {Cursor} is removed from the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveCursor: (callback) -> - @emitter.on 'did-remove-cursor', callback - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # Immediately calls your callback for each existing selection. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeSelections: (callback) -> - callback(selection) for selection in @getSelections() - @onDidAddSelection(callback) - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddSelection: (callback) -> - @emitter.on 'did-add-selection', callback - - # Extended: Calls your `callback` when a {Selection} is removed from the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveSelection: (callback) -> - @emitter.on 'did-remove-selection', callback - - # Extended: Calls your `callback` with each {Decoration} added to the editor. - # Calls your `callback` immediately for any existing decorations. - # - # * `callback` {Function} - # * `decoration` {Decoration} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeDecorations: (callback) -> - @decorationManager.observeDecorations(callback) - - # Extended: Calls your `callback` when a {Decoration} is added to the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddDecoration: (callback) -> - @decorationManager.onDidAddDecoration(callback) - - # Extended: Calls your `callback` when a {Decoration} is removed from the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveDecoration: (callback) -> - @decorationManager.onDidRemoveDecoration(callback) - - # Called by DecorationManager when a decoration is added. - didAddDecoration: (decoration) -> - if decoration.isType('block') - @component?.addBlockDecoration(decoration) - - # Extended: Calls your `callback` when the placeholder text is changed. - # - # * `callback` {Function} - # * `placeholderText` {String} new text - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePlaceholderText: (callback) -> - @emitter.on 'did-change-placeholder-text', callback - - onDidChangeScrollTop: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - - @getElement().onDidChangeScrollTop(callback) - - onDidChangeScrollLeft: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - - @getElement().onDidChangeScrollLeft(callback) - - onDidRequestAutoscroll: (callback) -> - @emitter.on 'did-request-autoscroll', callback - - # TODO Remove once the tabs package no longer uses .on subscriptions - onDidChangeIcon: (callback) -> - @emitter.on 'did-change-icon', callback - - onDidUpdateDecorations: (callback) -> - @decorationManager.onDidUpdateDecorations(callback) - - # Essential: Retrieves the current {TextBuffer}. - getBuffer: -> @buffer - - # Retrieves the current buffer's URI. - getURI: -> @buffer.getUri() - - # Create an {TextEditor} with its initial state based on this object - copy: -> - displayLayer = @displayLayer.copy() - selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) - softTabs = @getSoftTabs() - new TextEditor({ - @buffer, selectionsMarkerLayer, softTabs, - suppressCursorCreation: true, - tabLength: @tokenizedBuffer.getTabLength(), - initialScrollTopRow: @getScrollTopRow(), - initialScrollLeftColumn: @getScrollLeftColumn(), - @assert, displayLayer, grammar: @getGrammar(), - @autoWidth, @autoHeight, @showCursorOnSelection - }) - - # Controls visibility based on the given {Boolean}. - setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) - - setMini: (mini) -> - @update({mini}) - @mini - - isMini: -> @mini - - onDidChangeMini: (callback) -> - @emitter.on 'did-change-mini', callback - - setLineNumberGutterVisible: (lineNumberGutterVisible) -> @update({lineNumberGutterVisible}) - - isLineNumberGutterVisible: -> @lineNumberGutter.isVisible() - - onDidChangeLineNumberGutterVisible: (callback) -> - @emitter.on 'did-change-line-number-gutter-visible', callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # Immediately calls your callback for each existing gutter. - # - # * `callback` {Function} - # * `gutter` {Gutter} that currently exists/was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGutters: (callback) -> - @gutterContainer.observeGutters callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # - # * `callback` {Function} - # * `gutter` {Gutter} that was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddGutter: (callback) -> - @gutterContainer.onDidAddGutter callback - - # Essential: Calls your `callback` when a {Gutter} is removed from the editor. - # - # * `callback` {Function} - # * `name` The name of the {Gutter} that was removed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveGutter: (callback) -> - @gutterContainer.onDidRemoveGutter callback - - # Set the number of characters that can be displayed horizontally in the - # editor. - # - # * `editorWidthInChars` A {Number} representing the width of the - # {TextEditorElement} in characters. - setEditorWidthInChars: (editorWidthInChars) -> @update({editorWidthInChars}) - - # Returns the editor width in characters. - getEditorWidthInChars: -> - if @width? and @defaultCharWidth > 0 - Math.max(0, Math.floor(@width / @defaultCharWidth)) - else - @editorWidthInChars - - ### - Section: File Details - ### - - # Essential: Get the editor's title for display in other parts of the - # UI such as the tabs. - # - # If the editor's buffer is saved, its title is the file name. If it is - # unsaved, its title is "untitled". - # - # Returns a {String}. - getTitle: -> - @getFileName() ? 'untitled' - - # Essential: Get unique title for display in other parts of the UI, such as - # the window title. - # - # If the editor's buffer is unsaved, its title is "untitled" - # If the editor's buffer is saved, its unique title is formatted as one - # of the following, - # * "" when it is the only editing buffer with this file name. - # * " — " when other buffers have this file name. - # - # Returns a {String} - getLongTitle: -> - if @getPath() - fileName = @getFileName() - - allPathSegments = [] - for textEditor in atom.workspace.getTextEditors() when textEditor isnt this - if textEditor.getFileName() is fileName - directoryPath = fs.tildify(textEditor.getDirectoryPath()) - allPathSegments.push(directoryPath.split(path.sep)) - - if allPathSegments.length is 0 - return fileName - - ourPathSegments = fs.tildify(@getDirectoryPath()).split(path.sep) - allPathSegments.push ourPathSegments - - loop - firstSegment = ourPathSegments[0] - - commonBase = _.all(allPathSegments, (pathSegments) -> pathSegments.length > 1 and pathSegments[0] is firstSegment) - if commonBase - pathSegments.shift() for pathSegments in allPathSegments - else - break - - "#{fileName} \u2014 #{path.join(pathSegments...)}" - else - 'untitled' - - # Essential: Returns the {String} path of this editor's text buffer. - getPath: -> @buffer.getPath() - - getFileName: -> - if fullPath = @getPath() - path.basename(fullPath) - else - null - - getDirectoryPath: -> - if fullPath = @getPath() - path.dirname(fullPath) - else - null - - # Extended: Returns the {String} character set encoding of this editor's text - # buffer. - getEncoding: -> @buffer.getEncoding() - - # Extended: Set the character set encoding to use in this editor's text - # buffer. - # - # * `encoding` The {String} character set encoding name such as 'utf8' - setEncoding: (encoding) -> @buffer.setEncoding(encoding) - - # Essential: Returns {Boolean} `true` if this editor has been modified. - isModified: -> @buffer.isModified() - - # Essential: Returns {Boolean} `true` if this editor has no content. - isEmpty: -> @buffer.isEmpty() - - ### - Section: File Operations - ### - - # Essential: Saves the editor's text buffer. - # - # See {TextBuffer::save} for more details. - save: -> @buffer.save() - - # Essential: Saves the editor's text buffer as the given path. - # - # See {TextBuffer::saveAs} for more details. - # - # * `filePath` A {String} path. - saveAs: (filePath) -> @buffer.saveAs(filePath) - - # Determine whether the user should be prompted to save before closing - # this editor. - shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> - if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() - @buffer.isInConflict() - else - @isModified() and not @buffer.hasMultipleEditors() - - # Returns an {Object} to configure dialog shown when this editor is saved - # via {Pane::saveItemAs}. - getSaveDialogOptions: -> {} - - ### - Section: Reading Text - ### - - # Essential: Returns a {String} representing the entire contents of the editor. - getText: -> @buffer.getText() - - # Essential: 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) - - # Essential: Returns a {Number} representing the number of lines in the buffer. - getLineCount: -> @buffer.getLineCount() - - # Essential: Returns a {Number} representing the number of screen lines in the - # editor. This accounts for folds. - getScreenLineCount: -> @displayLayer.getScreenLineCount() - - getApproximateScreenLineCount: -> @displayLayer.getApproximateScreenLineCount() - - # Essential: Returns a {Number} representing the last zero-indexed buffer row - # number of the editor. - getLastBufferRow: -> @buffer.getLastRow() - - # Essential: Returns a {Number} representing the last zero-indexed screen row - # number of the editor. - getLastScreenRow: -> @getScreenLineCount() - 1 - - # Essential: Returns a {String} representing the contents of the line at the - # given buffer row. - # - # * `bufferRow` A {Number} representing a zero-indexed buffer row. - lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow) - - # Essential: Returns a {String} representing the contents of the line at the - # given screen row. - # - # * `screenRow` A {Number} representing a zero-indexed screen row. - lineTextForScreenRow: (screenRow) -> - @screenLineForScreenRow(screenRow)?.lineText - - logScreenLines: (start=0, end=@getLastScreenRow()) -> - for row in [start..end] - line = @lineTextForScreenRow(row) - console.log row, @bufferRowForScreenRow(row), line, line.length - return - - tokensForScreenRow: (screenRow) -> - tokens = [] - lineTextIndex = 0 - currentTokenScopes = [] - {lineText, tags} = @screenLineForScreenRow(screenRow) - for tag in tags - if @displayLayer.isOpenTag(tag) - currentTokenScopes.push(@displayLayer.classNameForTag(tag)) - else if @displayLayer.isCloseTag(tag) - currentTokenScopes.pop() - else - tokens.push({ - text: lineText.substr(lineTextIndex, tag) - scopes: currentTokenScopes.slice() - }) - lineTextIndex += tag - tokens - - screenLineForScreenRow: (screenRow) -> - @displayLayer.getScreenLine(screenRow) - - bufferRowForScreenRow: (screenRow) -> - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - - bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - @displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) - - screenRowForBufferRow: (row) -> - @displayLayer.translateBufferPosition(Point(row, 0)).row - - getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() - - getApproximateRightmostScreenPosition: -> @displayLayer.getApproximateRightmostScreenPosition() - - getMaxScreenLineLength: -> @getRightmostScreenPosition().column - - getLongestScreenRow: -> @getRightmostScreenPosition().row - - getApproximateLongestScreenRow: -> @getApproximateRightmostScreenPosition().row - - lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow) - - # Returns the range for the given buffer row. - # - # * `row` A row {Number}. - # * `options` (optional) An options hash with an `includeNewline` key. - # - # Returns a {Range}. - bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) - - # Get the text in the given {Range}. - # - # Returns a {String}. - getTextInRange: (range) -> @buffer.getTextInRange(range) - - # {Delegates to: TextBuffer.isRowBlank} - isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - - # {Delegates to: TextBuffer.nextNonBlankRow} - nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) - - # {Delegates to: TextBuffer.getEndPosition} - getEofBufferPosition: -> @buffer.getEndPosition() - - # Essential: Get the {Range} of the paragraph surrounding the most recently added - # cursor. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @getLastCursor().getCurrentParagraphBufferRange() - - - ### - Section: Mutating Text - ### - - # Essential: Replaces the entire contents of the buffer with the given {String}. - # - # * `text` A {String} to replace with - setText: (text) -> @buffer.setText(text) - - # Essential: Set the text in the given {Range} in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `text` A {String} - # * `options` (optional) {Object} - # * `normalizeLineEndings` (optional) {Boolean} (default: true) - # * `undo` (optional) {String} 'skip' will skip the undo system - # - # Returns the {Range} of the newly-inserted text. - setTextInBufferRange: (range, text, options) -> @getBuffer().setTextInRange(range, text, options) - - # Essential: 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 {Boolean} false when the text has not been inserted - insertText: (text, options={}) -> - return false unless @emitWillInsertTextEvent(text) - - groupingInterval = if options.groupUndo - @undoGroupingInterval - else - 0 - - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText( - (selection) => - range = selection.insertText(text, options) - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - range - , groupingInterval - ) - - # Essential: For each selection, replace the selected text with a newline. - insertNewline: (options) -> - @insertText('\n', options) - - # Essential: For each selection, if the selection is empty, delete the character - # following the cursor. Otherwise delete the selected text. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Essential: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() - - # Extended: 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, groupingInterval=0) -> - @mergeIntersectingSelections => - @transact groupingInterval, => - fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition() - - # Move lines intersecting the most recent selection or multiple selections - # up by one row in screen coordinates. - moveLineUp: -> - selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b)) - - if selections[0].start.row is 0 - return - - if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is '' - return - - @transact => - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - while selection.end.row is selections[0]?.start.row - selectionsToMove.push(selections[0]) - selection.end.row = selections[0].end.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is preceded by a fold, one line above on screen - # could be multiple lines in the buffer. - precedingRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) - insertDelta = linesRange.start.row - precedingRow - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([-insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the preceding buffer row - lines = @buffer.getTextInRange(linesRange) - lines += @buffer.lineEndingForRow(linesRange.end.row - 2) unless lines[lines.length - 1] is '\n' - @buffer.delete(linesRange) - @buffer.insert([precedingRow, 0], lines) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([-insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) - - # Move lines intersecting the most recent selection or multiple selections - # down by one row in screen coordinates. - moveLineDown: -> - selections = @getSelectedBufferRanges() - selections.sort (a, b) -> a.compare(b) - selections = selections.reverse() - - @transact => - @consolidateSelections() - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - # if the current selection start row matches the next selections' end row - make them one selection - while selection.start.row is selections[0]?.end.row - selectionsToMove.push(selections[0]) - selection.start.row = selections[0].start.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is followed by a fold, one line below on screen - # could be multiple lines in the buffer. But at the same time, if the - # next buffer row is wrapped, one line in the buffer can represent many - # screen rows. - followingRow = Math.min(@buffer.getLineCount(), @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) - insertDelta = followingRow - linesRange.end.row - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the following correct buffer row - lines = @buffer.getTextInRange(linesRange) - if followingRow - 1 is @buffer.getLastRow() - lines = "\n#{lines}" - - @buffer.insert([followingRow, 0], lines) - @buffer.delete(linesRange) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) - - # Move any active selections one column to the left. - moveSelectionLeft: -> - selections = @getSelectedBufferRanges() - noSelectionAtStartOfLine = selections.every((selection) -> - selection.start.column isnt 0 - ) - - translationDelta = [0, -1] - translatedRanges = [] - - if noSelectionAtStartOfLine - @transact => - for selection in selections - charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) - charTextToLeftOfSelection = @buffer.getTextInRange(charToLeftOfSelection) - - @buffer.insert(selection.end, charTextToLeftOfSelection) - @buffer.delete(charToLeftOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - # Move any active selections one column to the right. - moveSelectionRight: -> - selections = @getSelectedBufferRanges() - noSelectionAtEndOfLine = selections.every((selection) => - selection.end.column isnt @buffer.lineLengthForRow(selection.end.row) - ) - - translationDelta = [0, 1] - translatedRanges = [] - - if noSelectionAtEndOfLine - @transact => - for selection in selections - charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) - charTextToRightOfSelection = @buffer.getTextInRange(charToRightOfSelection) - - @buffer.delete(charToRightOfSelection) - @buffer.insert(selection.start, charTextToRightOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - duplicateLines: -> - @transact => - selections = @getSelectionsOrderedByBufferPosition() - previousSelectionRanges = [] - - i = selections.length - 1 - while i >= 0 - j = i - previousSelectionRanges[i] = selections[i].getBufferRange() - if selections[i].isEmpty() - {start} = selections[i].getScreenRange() - selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) - [startRow, endRow] = selections[i].getBufferRowRange() - endRow++ - while i > 0 - [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() - if previousSelectionEndRow is startRow - startRow = previousSelectionStartRow - previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() - i-- - else - break - - intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = @getTextInBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() - @buffer.insert([endRow, 0], textToDuplicate) - - insertedRowCount = endRow - startRow - - for k in [i..j] by 1 - selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) - - for fold in intersectingFolds - foldRange = @displayLayer.bufferRangeForFold(fold) - @displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) - - i-- - - replaceSelectedText: (options={}, fn) -> - {selectWordIfEmpty} = options - @mutateSelectedText (selection) -> - selection.getBufferRange() - if selectWordIfEmpty and selection.isEmpty() - selection.selectWord() - text = selection.getText() - selection.deleteSelectedText() - range = selection.insertText(fn(text)) - selection.setBufferRange(range) - - # 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: -> - @mergeIntersectingSelections => - for selection in @getSelections() - range = selection.getBufferRange() - continue if range.isSingleLine() - - {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 - selection.destroy() - return - - # Extended: 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('') - - # Extended: 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() - - # Extended: Convert the selected text to lower case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - lowerCase: -> - @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase() - - # Extended: Toggle line comments for rows intersecting selections. - # - # If the current grammar doesn't support comments, does nothing. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - # 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() - - # Extended: For each cursor, insert a newline at beginning the following line. - insertNewlineBelow: -> - @transact => - @moveToEndOfLine() - @insertNewline() - - # Extended: 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 - - @moveToBeginningOfLine() - @moveLeft() - @insertNewline() - - if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel - @setIndentationForBufferRow(bufferRow, indentLevel) - - if onFirstLine - @moveUp() - @moveToEndOfLine() - - # Extended: 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() - - # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the - # previous word boundary. - deleteToPreviousWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary() - - # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the - # next word boundary. - deleteToNextWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing subword following the cursor. Otherwise delete the selected - # text. - deleteToBeginningOfSubword: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfSubword() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing subword following the cursor. Otherwise delete the selected - # text. - deleteToEndOfSubword: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfSubword() - - # Extended: 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() - - # Extended: 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() - - # Extended: 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() - - # Extended: Delete all lines intersecting selections. - deleteLine: -> - @mergeSelectionsOnSameRows() - @mutateSelectedText (selection) -> selection.deleteLine() - - ### - Section: History - ### - - # Essential: Undo the last change. - undo: -> - @avoidMergingSelections => @buffer.undo() - @getLastSelection().autoscroll() - - # Essential: Redo the last change. - redo: -> - @avoidMergingSelections => @buffer.redo() - @getLastSelection().autoscroll() - - # Extended: 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. - # - # * `groupingInterval` (optional) The {Number} of milliseconds for which this - # transaction should be considered 'groupable' after it begins. If a transaction - # with a positive `groupingInterval` is committed while the previous transaction is - # still 'groupable', the two transactions are merged with respect to undo and redo. - # * `fn` A {Function} to call inside the transaction. - transact: (groupingInterval, fn) -> - @buffer.transact(groupingInterval, fn) - - # Deprecated: Start an open-ended transaction. - beginTransaction: (groupingInterval) -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.beginTransaction(groupingInterval) - - # Deprecated: Commit an open-ended transaction started with {::beginTransaction}. - commitTransaction: -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.commitTransaction() - - # Extended: Abort an open transaction, undoing any operations performed so far - # within the transaction. - abortTransaction: -> @buffer.abortTransaction() - - # Extended: Create a pointer to the current state of the buffer for use - # with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. - # - # Returns a checkpoint value. - createCheckpoint: -> @buffer.createCheckpoint() - - # Extended: Revert the buffer to the state it was in when the given - # checkpoint was created. - # - # The redo stack will be empty following this operation, so changes since the - # checkpoint will be lost. If the given checkpoint is no longer present in the - # undo history, no changes will be made to the buffer and this method will - # return `false`. - # - # * `checkpoint` The checkpoint to revert to. - # - # Returns a {Boolean} indicating whether the operation succeeded. - revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint) - - # Extended: Group all changes since the given checkpoint into a single - # transaction for purposes of undo/redo. - # - # If the given checkpoint is no longer present in the undo history, no - # grouping will be performed and this method will return `false`. - # - # * `checkpoint` The checkpoint from which to group changes. - # - # Returns a {Boolean} indicating whether the operation succeeded. - groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint) - - ### - Section: TextEditor Coordinates - ### - - # Essential: 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) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateBufferPosition(bufferPosition, options) - - # Essential: 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) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateScreenPosition(screenPosition, options) - - # Essential: Convert a range in buffer-coordinates to screen-coordinates. - # - # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. - # - # Returns a {Range}. - screenRangeForBufferRange: (bufferRange, options) -> - bufferRange = Range.fromObject(bufferRange) - start = @screenPositionForBufferPosition(bufferRange.start, options) - end = @screenPositionForBufferPosition(bufferRange.end, options) - new Range(start, end) - - # Essential: Convert a range in screen-coordinates to buffer-coordinates. - # - # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. - # - # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> - screenRange = Range.fromObject(screenRange) - start = @bufferPositionForScreenPosition(screenRange.start) - end = @bufferPositionForScreenPosition(screenRange.end) - new Range(start, end) - - # Extended: 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) - - # Extended: 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) - - # Extended: 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]` - # ``` - # - # * `screenPosition` The {Point} representing the position to clip. - # * `options` (optional) {Object} - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.clipScreenPosition(screenPosition, options) - - # Extended: Clip the start and end of the given range to valid positions on screen. - # See {::clipScreenPosition} for more information. - # - # * `range` The {Range} to clip. - # * `options` (optional) See {::clipScreenPosition} `options`. - # - # Returns a {Range}. - clipScreenRange: (screenRange, options) -> - screenRange = Range.fromObject(screenRange) - start = @displayLayer.clipScreenPosition(screenRange.start, options) - end = @displayLayer.clipScreenPosition(screenRange.end, options) - Range(start, end) - - ### - Section: Decorations - ### - - # Essential: Add a decoration that tracks a {DisplayMarker}. When the - # marker moves, is invalidated, or is destroyed, the decoration will be - # updated to reflect the marker's state. - # - # The following are the supported decorations types: - # - # * __line__: Adds your CSS `class` to the line nodes within the range - # marked by the marker - # * __line-number__: 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 - # 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 - # ```html - #
- # - #
- #
- # ``` - # * __overlay__: Positions the view associated with the given item at the head - # or tail of the given `DisplayMarker`. - # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter - # decorations are created by calling {Gutter::decorateMarker} on the - # desired `Gutter` instance. - # * __block__: Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`. - # - # ## Arguments - # - # * `marker` A {DisplayMarker} you want this decoration to follow. - # * `decorationParams` An {Object} representing the decoration e.g. - # `{type: 'line-number', class: 'linter-error'}` - # * `type` There are several supported decoration types. The behavior of the - # types are as follows: - # * `line` Adds the given `class` to the lines overlapping the rows - # spanned by the `DisplayMarker`. - # * `line-number` Adds the given `class` to the line numbers overlapping - # the rows spanned by the `DisplayMarker`. - # * `text` Injects spans into all text overlapping the marked range, - # then adds the given `class` or `style` properties to these spans. - # Use this to manipulate the foreground color or styling of text in - # a given range. - # * `highlight` Creates an absolutely-positioned `.highlight` div - # containing nested divs to cover the marked region. For example, this - # is used to implement selections. - # * `overlay` Positions the view associated with the given item at the - # head or tail of the given `DisplayMarker`, depending on the `position` - # property. - # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling - # {Gutter::decorateMarker} on the desired `Gutter` instance. - # * `block` Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`, depending on the `position` - # property. - # * `cursor` Renders a cursor at the head of the given marker. If multiple - # decorations are created for the same marker, their class strings and - # style objects are combined into a single cursor. You can use this - # decoration type to style existing cursors by passing in their markers - # or render artificial cursors that don't actually exist in the model - # by passing a marker that isn't actually associated with a cursor. - # * `class` This CSS class will be applied to the decorated line number, - # line, text spans, highlight regions, cursors, or overlay. - # * `style` An {Object} containing CSS style properties to apply to the - # relevant DOM node. Currently this only works with a `type` of `cursor` - # or `text`. - # * `item` (optional) An {HTMLElement} or a model {Object} with a - # corresponding view registered. Only applicable to the `gutter`, - # `overlay` and `block` decoration types. - # * `onlyHead` (optional) If `true`, the decoration will only be applied to - # the head of the `DisplayMarker`. Only applicable to the `line` and - # `line-number` decoration types. - # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if - # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, - # `line`, and `line-number` decoration types. - # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied - # if the associated `DisplayMarker` is non-empty. Only applicable to the - # `gutter`, `line`, and `line-number` decoration types. - # * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied - # to the last row of a non-empty range, even if it ends at column 0. - # Defaults to `true`. Only applicable to the `gutter`, `line`, and - # `line-number` decoration types. - # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. - # Controls where the view is positioned relative to the `TextEditorMarker`. - # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and - # `'before'` (the default) or `'after'` for block decorations. - # * `avoidOverflow` (optional) Only applicable to decorations of type - # `overlay`. Determines whether the decoration adjusts its horizontal or - # vertical position to remain fully visible when it would otherwise - # overflow the editor. Defaults to `true`. - # - # Returns a {Decoration} object - decorateMarker: (marker, decorationParams) -> - @decorationManager.decorateMarker(marker, decorationParams) - - # Essential: Add a decoration to every marker in the given marker layer. Can - # be used to decorate a large number of markers without having to create and - # manage many individual decorations. - # - # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. - # * `decorationParams` The same parameters that are passed to - # {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. - # - # Returns a {LayerDecoration}. - decorateMarkerLayer: (markerLayer, decorationParams) -> - @decorationManager.decorateMarkerLayer(markerLayer, decorationParams) - - # Deprecated: Get all the decorations within a screen row range on the default - # layer. - # - # * `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: 'line-number', class: 'someclass'}], 2: ...}` - # where the keys are {DisplayMarker} IDs, and the values are an array of decoration - # params objects attached to the marker. - # Returns an empty object when no decorations are found - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) - - decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) - - # Extended: Get all decorations. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getDecorations: (propertyFilter) -> - @decorationManager.getDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineDecorations: (propertyFilter) -> - @decorationManager.getLineDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line-number'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineNumberDecorations: (propertyFilter) -> - @decorationManager.getLineNumberDecorations(propertyFilter) - - # Extended: Get all decorations of type 'highlight'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getHighlightDecorations: (propertyFilter) -> - @decorationManager.getHighlightDecorations(propertyFilter) - - # Extended: Get all decorations of type 'overlay'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getOverlayDecorations: (propertyFilter) -> - @decorationManager.getOverlayDecorations(propertyFilter) - - ### - Section: Markers - ### - - # Essential: Create a marker on the default marker layer with the given range - # in buffer coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferRange: (bufferRange, options) -> - @defaultMarkerLayer.markBufferRange(bufferRange, options) - - # Essential: Create a marker on the default marker layer with the given range - # in screen coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markScreenRange: (screenRange, options) -> - @defaultMarkerLayer.markScreenRange(screenRange, options) - - # Essential: Create a marker on the default marker layer with the given buffer - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `bufferPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferPosition: (bufferPosition, options) -> - @defaultMarkerLayer.markBufferPosition(bufferPosition, options) - - # Essential: Create a marker on the default marker layer with the given screen - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `screenPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {DisplayMarker}. - markScreenPosition: (screenPosition, options) -> - @defaultMarkerLayer.markScreenPosition(screenPosition, options) - - # Essential: Find all {DisplayMarker}s on the default marker layer that - # match the given properties. - # - # This method finds markers based on the given properties. Markers can be - # associated with custom properties that will be compared with basic equality. - # 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 - # 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 - # coordinates. - # * `endBufferRow` Only include markers ending at this row in buffer - # coordinates. - # * `containsBufferRange` Only include markers containing this {Range} or - # in range-compatible {Array} in buffer coordinates. - # * `containsBufferPosition` Only include markers containing this {Point} - # or {Array} of `[row, column]` in buffer coordinates. - # - # Returns an {Array} of {DisplayMarker}s - findMarkers: (params) -> - @defaultMarkerLayer.findMarkers(params) - - # Extended: Get the {DisplayMarker} on the default layer for the given - # marker id. - # - # * `id` {Number} id of the marker - getMarker: (id) -> - @defaultMarkerLayer.getMarker(id) - - # Extended: Get all {DisplayMarker}s on the default marker layer. Consider - # using {::findMarkers} - getMarkers: -> - @defaultMarkerLayer.getMarkers() - - # Extended: Get the number of markers in the default marker layer. - # - # Returns a {Number}. - getMarkerCount: -> - @defaultMarkerLayer.getMarkerCount() - - destroyMarker: (id) -> - @getMarker(id)?.destroy() - - # Essential: Create a marker layer to group related markers. - # - # * `options` An {Object} containing the following keys: - # * `maintainHistory` A {Boolean} indicating whether marker state should be - # restored on undo/redo. Defaults to `false`. - # * `persistent` A {Boolean} indicating whether or not this marker layer - # should be serialized and deserialized along with the rest of the - # buffer. Defaults to `false`. If `true`, the marker layer's id will be - # maintained across the serialization boundary, allowing you to retrieve - # it via {::getMarkerLayer}. - # - # Returns a {DisplayMarkerLayer}. - addMarkerLayer: (options) -> - @displayLayer.addMarkerLayer(options) - - # Essential: Get a {DisplayMarkerLayer} by id. - # - # * `id` The id of the marker layer to retrieve. - # - # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the - # given id. - getMarkerLayer: (id) -> - @displayLayer.getMarkerLayer(id) - - # Essential: Get the default {DisplayMarkerLayer}. - # - # All marker APIs not tied to an explicit layer interact with this default - # layer. - # - # Returns a {DisplayMarkerLayer}. - getDefaultMarkerLayer: -> - @defaultMarkerLayer - - ### - Section: Cursors - ### - - # Essential: Get the position of the most recently added cursor in buffer - # coordinates. - # - # Returns a {Point} - getCursorBufferPosition: -> - @getLastCursor().getBufferPosition() - - # Essential: Get the position of all the cursor positions in buffer coordinates. - # - # Returns {Array} of {Point}s in the order they were added - getCursorBufferPositions: -> - cursor.getBufferPosition() for cursor in @getCursors() - - # Essential: Move the cursor to the given position in buffer coordinates. - # - # If there are multiple cursors, they will be consolidated to a single cursor. - # - # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} containing the following keys: - # * `autoscroll` Determines whether the editor scrolls to the new cursor's - # position. Defaults to true. - setCursorBufferPosition: (position, options) -> - @moveCursors (cursor) -> cursor.setBufferPosition(position, options) - - # Essential: Get a {Cursor} at given screen coordinates {Point} - # - # * `position` A {Point} or {Array} of `[row, column]` - # - # Returns the first matched {Cursor} or undefined - getCursorAtScreenPosition: (position) -> - if selection = @getSelectionAtScreenPosition(position) - if selection.getHeadScreenPosition().isEqual(position) - selection.cursor - - # Essential: Get the position of the most recently added cursor in screen - # coordinates. - # - # Returns a {Point}. - getCursorScreenPosition: -> - @getLastCursor().getScreenPosition() - - # Essential: Get the position of all the cursor positions in screen coordinates. - # - # Returns {Array} of {Point}s in the order the cursors were added - getCursorScreenPositions: -> - cursor.getScreenPosition() for cursor in @getCursors() - - # Essential: 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` (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) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @moveCursors (cursor) -> cursor.setScreenPosition(position, options) - - # Essential: Add a cursor at the given position in buffer coordinates. - # - # * `bufferPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Add a cursor at the position in screen coordinates. - # - # * `screenPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtScreenPosition: (screenPosition, options) -> - @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Returns {Boolean} indicating whether or not there are multiple cursors. - hasMultipleCursors: -> - @getCursors().length > 1 - - # Essential: Move every cursor up one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveUp: (lineCount) -> - @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor down one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveDown: (lineCount) -> - @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor left one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveLeft: (columnCount) -> - @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor right one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveRight: (columnCount) -> - @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor to the beginning of its line in buffer coordinates. - moveToBeginningOfLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - - # Essential: Move every cursor to the beginning of its line in screen coordinates. - moveToBeginningOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine() - - # Essential: Move every cursor to the first non-whitespace character of its line. - moveToFirstCharacterOfLine: -> - @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - - # Essential: Move every cursor to the end of its line in buffer coordinates. - moveToEndOfLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfLine() - - # Essential: Move every cursor to the end of its line in screen coordinates. - moveToEndOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfScreenLine() - - # Essential: Move every cursor to the beginning of its surrounding word. - moveToBeginningOfWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - - # Essential: Move every cursor to the end of its surrounding word. - moveToEndOfWord: -> - @moveCursors (cursor) -> cursor.moveToEndOfWord() - - # Cursor Extended - - # Extended: Move every cursor to the top of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToTop: -> - @moveCursors (cursor) -> cursor.moveToTop() - - # Extended: Move every cursor to the bottom of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToBottom: -> - @moveCursors (cursor) -> cursor.moveToBottom() - - # Extended: Move every cursor to the beginning of the next word. - moveToBeginningOfNextWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - - # Extended: Move every cursor to the previous word boundary. - moveToPreviousWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - - # Extended: Move every cursor to the next word boundary. - moveToNextWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - - # Extended: Move every cursor to the previous subword boundary. - moveToPreviousSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousSubwordBoundary() - - # Extended: Move every cursor to the next subword boundary. - moveToNextSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextSubwordBoundary() - - # Extended: Move every cursor to the beginning of the next paragraph. - moveToBeginningOfNextParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() - - # Extended: Move every cursor to the beginning of the previous paragraph. - moveToBeginningOfPreviousParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() - - # Extended: Returns the most recently added {Cursor} - getLastCursor: -> - @createLastSelectionIfNeeded() - _.last(@cursors) - - # Extended: Returns the word surrounding the most recently added cursor. - # - # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. - getWordUnderCursor: (options) -> - @getTextInBufferRange(@getLastCursor().getCurrentWordBufferRange(options)) - - # Extended: Get an Array of all {Cursor}s. - getCursors: -> - @createLastSelectionIfNeeded() - @cursors.slice() - - # Extended: Get all {Cursors}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getCursorsOrderedByBufferPosition: -> - @getCursors().sort (a, b) -> a.compare(b) - - cursorsForScreenRowRange: (startScreenRow, endScreenRow) -> - cursors = [] - for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if cursor = @cursorsByMarkerId.get(marker.id) - cursors.push(cursor) - cursors - - # Add a cursor based on the given {DisplayMarker}. - addCursor: (marker) -> - cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection) - @cursors.push(cursor) - @cursorsByMarkerId.set(marker.id, cursor) - cursor - - moveCursors: (fn) -> - @transact => - fn(cursor) for cursor in @getCursors() - @mergeCursors() - - cursorMoved: (event) -> - @emitter.emit 'did-change-cursor-position', event - - # Merge cursors that have the same screen position - mergeCursors: -> - positions = {} - for cursor in @getCursors() - position = cursor.getBufferPosition().toString() - if positions.hasOwnProperty(position) - cursor.destroy() - else - positions[position] = true - return - - ### - Section: Selections - ### - - # Essential: Get the selected text of the most recently added selection. - # - # Returns a {String}. - getSelectedText: -> - @getLastSelection().getText() - - # Essential: Get the {Range} of the most recently added selection in buffer - # coordinates. - # - # Returns a {Range}. - getSelectedBufferRange: -> - @getLastSelection().getBufferRange() - - # Essential: Get the {Range}s of all selections in buffer coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getSelections() - - # Essential: 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. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - setSelectedBufferRange: (bufferRange, options) -> - @setSelectedBufferRanges([bufferRange], options) - - # Essential: 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. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - 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) - return - - # Essential: Get the {Range} of the most recently added selection in screen - # coordinates. - # - # Returns a {Range}. - getSelectedScreenRange: -> - @getLastSelection().getScreenRange() - - # Essential: Get the {Range}s of all selections in screen coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedScreenRanges: -> - selection.getScreenRange() for selection in @getSelections() - - # Essential: 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) - - # Essential: Set the selected ranges in screen coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # * `screenRanges` 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. - setSelectedScreenRanges: (screenRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedScreenRanges") unless screenRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[screenRanges.length...] - - @mergeIntersectingSelections options, => - for screenRange, i in screenRanges - screenRange = Range.fromObject(screenRange) - if selections[i] - selections[i].setScreenRange(screenRange, options) - else - @addSelectionForScreenRange(screenRange, options) - return - - # Essential: 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. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # - # Returns the added {Selection}. - addSelectionForBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - unless options.preserveFolds - @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) - @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) - @getLastSelection().autoscroll() unless options.autoscroll is false - @getLastSelection() - - # Essential: Add a selection for the given range in screen coordinates. - # - # * `screenRange` A {Range} - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # Returns the added {Selection}. - addSelectionForScreenRange: (screenRange, options={}) -> - @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options) - - # Essential: Select from the current cursor position to the given position in - # buffer coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToBufferPosition: (position) -> - lastSelection = @getLastSelection() - lastSelection.selectToBufferPosition(position) - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Select from the current cursor position to the given position in - # screen coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, options) -> - lastSelection = @getLastSelection() - lastSelection.selectToScreenPosition(position, options) - unless options?.suppressSelectionMerge - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Move the cursor of each selection one character upward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectUp: (rowCount) -> - @expandSelectionsBackward (selection) -> selection.selectUp(rowCount) - - # Essential: Move the cursor of each selection one character downward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectDown: (rowCount) -> - @expandSelectionsForward (selection) -> selection.selectDown(rowCount) - - # Essential: Move the cursor of each selection one character leftward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectLeft: (columnCount) -> - @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) - - # Essential: Move the cursor of each selection one character rightward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectRight: (columnCount) -> - @expandSelectionsForward (selection) -> selection.selectRight(columnCount) - - # Essential: Select from the top of the buffer to the end of the last selection - # in the buffer. - # - # This method merges multiple selections into a single selection. - selectToTop: -> - @expandSelectionsBackward (selection) -> selection.selectToTop() - - # Essential: Selects from the top of the first selection in the buffer to the end - # of the buffer. - # - # This method merges multiple selections into a single selection. - selectToBottom: -> - @expandSelectionsForward (selection) -> selection.selectToBottom() - - # Essential: Select all text in the buffer. - # - # This method merges multiple selections into a single selection. - selectAll: -> - @expandSelectionsForward (selection) -> selection.selectAll() - - # Essential: Move the cursor of each selection to the beginning of its line - # while preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToBeginningOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine() - - # Essential: Move the cursor of each selection to the first non-whitespace - # character of its line while preserving the selection's tail position. If the - # cursor is already on the first character of the line, move it to the - # beginning of the line. - # - # This method may merge selections that end up intersecting. - selectToFirstCharacterOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToFirstCharacterOfLine() - - # Essential: Move the cursor of each selection to the end of its line while - # preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToEndOfLine: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfLine() - - # Essential: Expand selections to the beginning of their containing word. - # - # Operates on all selections. Moves the cursor to the beginning of the - # containing word while preserving the selection's tail position. - selectToBeginningOfWord: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfWord() - - # Essential: Expand selections to the end of their containing word. - # - # Operates on all selections. Moves the cursor to the end of the containing - # word while preserving the selection's tail position. - selectToEndOfWord: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfWord() - - # Extended: For each selection, move its cursor to the preceding subword - # boundary while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousSubwordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousSubwordBoundary() - - # Extended: For each selection, move its cursor to the next subword boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextSubwordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextSubwordBoundary() - - # Essential: For each cursor, select the containing line. - # - # This method merges selections on successive lines. - selectLinesContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectLine() - - # Essential: Select the word surrounding each cursor. - selectWordsContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectWord() - - # Selection Extended - - # Extended: For each selection, move its cursor to the preceding word boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousWordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousWordBoundary() - - # Extended: For each selection, move its cursor to the next word boundary while - # maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextWordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextWordBoundary() - - # Extended: Expand selections to the beginning of the next word. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # word while preserving the selection's tail position. - selectToBeginningOfNextWord: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextWord() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfNextParagraph: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextParagraph() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfPreviousParagraph: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfPreviousParagraph() - - # Extended: Select the range of the given marker if it is valid. - # - # * `marker` A {DisplayMarker} - # - # Returns the selected {Range} or `undefined` if the marker is invalid. - selectMarker: (marker) -> - if marker.isValid() - range = marker.getBufferRange() - @setSelectedBufferRange(range) - range - - # Extended: Get the most recently added {Selection}. - # - # Returns a {Selection}. - getLastSelection: -> - @createLastSelectionIfNeeded() - _.last(@selections) - - getSelectionAtScreenPosition: (position) -> - markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position) - if markers.length > 0 - @cursorsByMarkerId.get(markers[0].id).selection - - # Extended: Get current {Selection}s. - # - # Returns: An {Array} of {Selection}s. - getSelections: -> - @createLastSelectionIfNeeded() - @selections.slice() - - # Extended: 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) - - # Extended: 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) - - # Selections Private - - # Add a similarly-shaped selection to the next eligible line below - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next following non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionBelow: -> - @expandSelectionsForward (selection) -> selection.addSelectionBelow() - - # Add a similarly-shaped selection to the next eligible line above - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next preceding non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionAbove: -> - @expandSelectionsBackward (selection) -> selection.addSelectionAbove() - - # Calls the given function with each selection, then merges selections - expandSelectionsForward: (fn) -> - @mergeIntersectingSelections => - fn(selection) for selection in @getSelections() - return - - # Calls the given function with each selection, then merges selections in the - # reversed orientation - expandSelectionsBackward: (fn) -> - @mergeIntersectingSelections reversed: true, => - fn(selection) for selection in @getSelections() - return - - finalizeSelections: -> - selection.finalize() for selection in @getSelections() - return - - selectionsForScreenRows: (startRow, endRow) -> - @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) - - # Merges intersecting selections. If passed a function, it executes - # the function with merging suppressed, then merges intersecting selections - # afterward. - mergeIntersectingSelections: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty() - - previousSelection.intersectsWith(currentSelection, exclusive) - - mergeSelectionsOnSameRows: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - screenRange = currentSelection.getScreenRange() - - previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) - - avoidMergingSelections: (args...) -> - @mergeSelections args..., -> false - - mergeSelections: (args...) -> - mergePredicate = args.pop() - fn = args.pop() if _.isFunction(_.last(args)) - options = args.pop() ? {} - - return fn?() if @suppressSelectionMerging - - if fn? - @suppressSelectionMerging = true - result = fn() - @suppressSelectionMerging = false - - reducer = (disjointSelections, selection) -> - adjacentSelection = _.last(disjointSelections) - if mergePredicate(adjacentSelection, selection) - adjacentSelection.merge(selection, options) - disjointSelections - else - disjointSelections.concat([selection]) - - [head, tail...] = @getSelectionsOrderedByBufferPosition() - _.reduce(tail, reducer, [head]) - return result if fn? - - # Add a {Selection} based on the given {DisplayMarker}. - # - # * `marker` The {DisplayMarker} to highlight - # * `options` (optional) An {Object} that pertains to the {Selection} constructor. - # - # Returns the new {Selection}. - addSelection: (marker, options={}) -> - cursor = @addCursor(marker) - selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) - @selections.push(selection) - selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections(preserveFolds: options.preserveFolds) - - if selection.destroyed - for selection in @getSelections() - if selection.intersectsBufferRange(selectionBufferRange) - return selection - else - @emitter.emit 'did-add-cursor', cursor - @emitter.emit 'did-add-selection', selection - selection - - # Remove the given selection. - removeSelection: (selection) -> - _.remove(@cursors, selection.cursor) - _.remove(@selections, selection) - @cursorsByMarkerId.delete(selection.cursor.marker.id) - @emitter.emit 'did-remove-cursor', selection.cursor - @emitter.emit 'did-remove-selection', selection - - # Reduce one or more selections to a single empty selection based on the most - # recently added cursor. - clearSelections: (options) -> - @consolidateSelections() - @getLastSelection().clear(options) - - # Reduce multiple selections to the least recently added selection. - consolidateSelections: -> - selections = @getSelections() - if selections.length > 1 - selection.destroy() for selection in selections[1...(selections.length)] - selections[0].autoscroll(center: true) - true - else - false - - # Called by the selection - selectionRangeChanged: (event) -> - @component?.didChangeSelectionRange() - @emitter.emit 'did-change-selection-range', event - - createLastSelectionIfNeeded: -> - if @selections.length is 0 - @addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true) - - ### - Section: Searching and Replacing - ### - - # Essential: Scan regular expression matches in the entire buffer, calling the - # given iterator function on each match. - # - # `::scan` functions as the replace method as well via the `replace` - # - # If you're programmatically modifying the results, you may want to try - # {::backwardsScanInBufferRange} to avoid tripping over your own changes. - # - # * `regex` A {RegExp} to search for. - # * `options` (optional) {Object} - # * `leadingContextLineCount` {Number} default `0`; The number of lines - # before the matched line to include in the results object. - # * `trailingContextLineCount` {Number} default `0`; The number of lines - # after the matched line to include in the results object. - # * `iterator` A {Function} that's called on each match - # * `object` {Object} - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - @buffer.scan(regex, options, iterator) - - # Essential: Scan regular expression matches in a given range, calling the given - # iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator) - - # Essential: Scan regular expression matches in a given range in reverse order, - # calling the given iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator) - - ### - Section: Tab Behavior - ### - - # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Essential: Enable or disable soft tabs for this editor. - # - # * `softTabs` A {Boolean} - setSoftTabs: (@softTabs) -> @update({@softTabs}) - - # Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. - hasAtomicSoftTabs: -> @displayLayer.atomicSoftTabs - - # Essential: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Essential: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @tokenizedBuffer.getTabLength() - - # Essential: Set the on-screen length of tab characters. Setting this to a - # {Number} This will override the `editor.tabLength` setting. - # - # * `tabLength` {Number} length of a single tab. Setting to `null` will - # fallback to using the `editor.tabLength` config setting - setTabLength: (tabLength) -> @update({tabLength}) - - # Returns an {Object} representing the current invisible character - # substitutions for this editor. See {::setInvisibles}. - getInvisibles: -> - if not @mini and @showInvisibles and @invisibles? - @invisibles - else - {} - - doesShowIndentGuide: -> @showIndentGuide and not @mini - - getSoftWrapHangingIndentLength: -> @displayLayer.softWrapHangingIndent - - # Extended: 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} or undefined if no non-comment lines had leading - # whitespace. - usesSoftTabs: -> - for bufferRow in [0..Math.min(1000, @buffer.getLastRow())] - continue if @tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - line = @buffer.lineForRow(bufferRow) - return true if line[0] is ' ' - return false if line[0] is '\t' - - undefined - - # Extended: 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) - - # 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 - ### - - # Essential: Determine whether lines in this editor are soft-wrapped. - # - # Returns a {Boolean}. - isSoftWrapped: -> @softWrapped - - # Essential: Enable or disable soft wrapping for this editor. - # - # * `softWrapped` A {Boolean} - # - # Returns a {Boolean}. - setSoftWrapped: (softWrapped) -> - @update({softWrapped}) - @isSoftWrapped() - - getPreferredLineLength: -> @preferredLineLength - - # Essential: Toggle soft wrapping for this editor - # - # Returns a {Boolean}. - toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) - - # Essential: Gets the column at which column will soft wrap - getSoftWrapColumn: -> - if @isSoftWrapped() and not @mini - if @softWrapAtPreferredLineLength - Math.min(@getEditorWidthInChars(), @preferredLineLength) - else - @getEditorWidthInChars() - else - @maxScreenLineLength - - ### - Section: Indentation - ### - - # Essential: Get the indentation level of the given buffer row. - # - # Determines 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(@lineTextForBufferRow(bufferRow)) - - # Essential: 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 = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length - newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - - # Extended: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Extended: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Extended: Get the indentation level of the given line of text. - # - # Determines 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) -> - @tokenizedBuffer.indentLevelForLine(line) - - # Extended: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # Indent all lines intersecting selections. See {Selection::indent} for more - # information. - indent: (options={}) -> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Constructs the string used for indents. - buildIndentString: (level, column=0) -> - if @getSoftTabs() - tabStopViolation = column % @getTabLength() - _.multiplyString(" ", Math.floor(level * @getTabLength()) - tabStopViolation) - else - excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * @getTabLength())) - _.multiplyString("\t", Math.floor(level)) + excessWhitespace - - ### - Section: Grammars - ### - - # Essential: Get the current {Grammar} of this editor. - getGrammar: -> - @tokenizedBuffer.grammar - - # Essential: Set the current {Grammar} of this editor. - # - # Assigning a grammar will cause the editor to re-tokenize based on the new - # grammar. - # - # * `grammar` {Grammar} - setGrammar: (grammar) -> - @tokenizedBuffer.setGrammar(grammar) - - # Reload the grammar based on the file name. - reloadGrammar: -> - @tokenizedBuffer.reloadGrammar() - - # Experimental: Get a notification when async tokenization is completed. - onDidTokenize: (callback) -> - @tokenizedBuffer.onDidTokenize(callback) - - ### - Section: Managing Syntax Scopes - ### - - # Essential: Returns a {ScopeDescriptor} that includes this editor's language. - # e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with - # {Config::get} to get language specific config values. - getRootScopeDescriptor: -> - @tokenizedBuffer.rootScopeDescriptor - - # Essential: Get the syntactic scopeDescriptor for the given position in buffer - # coordinates. Useful with {Config::get}. - # - # 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 a {ScopeDescriptor}. - scopeDescriptorForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) - - # Extended: 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")`. - # - # * `scopeSelector` {String} selector. e.g. `'.source.ruby'` - # - # Returns a {Range}. - bufferRangeForScopeAtCursor: (scopeSelector) -> - @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) - - bufferRangeForScopeAtPosition: (scopeSelector, position) -> - @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) - - # Extended: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineTextForBufferRow(bufferRow).match(/\S/) - @commentScopeSelector ?= new TextMateScopeSelector('comment.*') - @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) - - # Get the scope descriptor at the cursor. - getCursorScope: -> - @getLastCursor().getScopeDescriptor() - - tokenForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.tokenForPosition(bufferPosition) - - ### - Section: Clipboard Operations - ### - - # Essential: For each selection, copy the selected text. - copySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if selection.isEmpty() - previousRange = selection.getBufferRange() - selection.selectLine() - selection.copy(maintainClipboard, true) - selection.setBufferRange(previousRange) - else - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Private: For each selection, only copy highlighted text. - copyOnlySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if not selection.isEmpty() - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Essential: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectLine() - selection.cut(maintainClipboard, true) - else - selection.cut(maintainClipboard, false) - maintainClipboard = true - - # Essential: 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) -> - options = Object.assign({}, options) - {text: clipboardText, metadata} = @constructor.clipboard.readWithMetadata() - return false unless @emitWillInsertTextEvent(clipboardText) - - metadata ?= {} - options.autoIndent ?= @shouldAutoIndentOnPaste() - - @mutateSelectedText (selection, index) => - if metadata.selections?.length is @getSelections().length - {text, indentBasis, fullLine} = metadata.selections[index] - else - {indentBasis, fullLine} = metadata - text = clipboardText - - delete options.indentBasis - {cursor} = selection - if indentBasis? - containsNewlines = text.indexOf('\n') isnt -1 - if containsNewlines or not cursor.hasPrecedingCharactersOnLine() - options.indentBasis ?= indentBasis - - range = null - if fullLine and selection.isEmpty() - oldPosition = selection.getBufferRange().start - selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) - range = selection.insertText(text, options) - newPosition = oldPosition.translate([1, 0]) - selection.setBufferRange([newPosition, newPosition]) - else - range = selection.insertText(text, options) - - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - - # Essential: For each selection, if the selection is empty, cut all characters - # of the containing screen line following the cursor. Otherwise cut the selected - # text. - cutToEndOfLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainClipboard) - maintainClipboard = true - - # Essential: For each selection, if the selection is empty, cut all characters - # of the containing buffer line following the cursor. Otherwise cut the - # selected text. - cutToEndOfBufferLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfBufferLine(maintainClipboard) - maintainClipboard = true - - ### - Section: Folds - ### - - # Essential: 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: -> - {row} = @getCursorBufferPosition() - if range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) - @displayLayer.foldBufferRange(range) - - # Essential: Unfold the most recent cursor's row by one level. - unfoldCurrentRow: -> - {row} = @getCursorBufferPosition() - @displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) - - # Essential: 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) -> - position = Point(bufferRow, Infinity) - loop - foldableRange = @tokenizedBuffer.getFoldableRangeContainingPoint(position, @getTabLength()) - if foldableRange - existingFolds = @displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) - if existingFolds.length is 0 - @displayLayer.foldBufferRange(foldableRange) - else - firstExistingFoldRange = @displayLayer.bufferRangeForFold(existingFolds[0]) - if firstExistingFoldRange.start.isLessThan(position) - position = Point(firstExistingFoldRange.start.row, 0) - continue - return - - # Essential: Unfold all folds containing the given row in buffer coordinates. - # - # * `bufferRow` A {Number} - unfoldBufferRow: (bufferRow) -> - position = Point(bufferRow, Infinity) - @displayLayer.destroyFoldsContainingBufferPositions([position]) - - # Extended: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - return - - # Extended: Fold all foldable lines. - foldAll: -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: Unfold all existing folds. - unfoldAll: -> - result = @displayLayer.destroyAllFolds() - @scrollToCursorPosition() - result - - # Extended: Fold all foldable lines at the given indent level. - # - # * `level` A {Number}. - foldAllAtIndentLevel: (level) -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRangesAtIndentLevel(level, @getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: 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) -> - @tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Extended: Determine whether the given row in screen coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtScreenRow: (screenRow) -> - @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Extended: 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) - - # Extended: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtBufferRow(@getCursorBufferPosition().row) - - # Extended: Determine whether the given row in buffer coordinates is folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - range = Range( - Point(bufferRow, 0), - Point(bufferRow, @buffer.lineLengthForRow(bufferRow)) - ) - @displayLayer.foldsIntersectingBufferRange(range).length > 0 - - # Extended: Determine whether the given row in screen coordinates is folded. - # - # * `screenRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Creates a new fold between two row numbers. - # - # startRow - The row {Number} to start folding at - # endRow - The row {Number} to end the fold - # - # Returns the new {Fold}. - foldBufferRowRange: (startRow, endRow) -> - @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) - - foldBufferRange: (range) -> - @displayLayer.foldBufferRange(range) - - # Remove any {Fold}s found that intersect the given buffer range. - destroyFoldsIntersectingBufferRange: (bufferRange) -> - @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - - # Remove any {Fold}s found that contain the given array of buffer positions. - destroyFoldsContainingBufferPositions: (bufferPositions, excludeEndpoints) -> - @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) - - ### - Section: Gutters - ### - - # Essential: Add a custom {Gutter}. - # - # * `options` An {Object} with the following fields: - # * `name` (required) A unique {String} to identify this gutter. - # * `priority` (optional) A {Number} that determines stacking order between - # gutters. Lower priority items are forced closer to the edges of the - # window. (default: -100) - # * `visible` (optional) {Boolean} specifying whether the gutter is visible - # initially after being created. (default: true) - # - # Returns the newly-created {Gutter}. - addGutter: (options) -> - @gutterContainer.addGutter(options) - - # Essential: Get this editor's gutters. - # - # Returns an {Array} of {Gutter}s. - getGutters: -> - @gutterContainer.getGutters() - - getLineNumberGutter: -> - @lineNumberGutter - - # Essential: Get the gutter with the given name. - # - # Returns a {Gutter}, or `null` if no gutter exists for the given name. - gutterWithName: (name) -> - @gutterContainer.gutterWithName(name) - - ### - Section: Scrolling the TextEditor - ### - - # Essential: Scroll the editor to reveal the most recently added cursor if it is - # off-screen. - # - # * `options` (optional) {Object} - # * `center` Center the editor around the cursor if possible. (default: true) - scrollToCursorPosition: (options) -> - @getLastCursor().autoscroll(center: options?.center ? true) - - # Essential: 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` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) - - # Essential: Scrolls the editor to the given screen position. - # - # * `screenPosition` An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToScreenPosition: (screenPosition, options) -> - @scrollToScreenRange(new Range(screenPosition, screenPosition), options) - - scrollToTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToTop() - - scrollToBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToBottom() - - scrollToScreenRange: (screenRange, options = {}) -> - screenRange = @clipScreenRange(screenRange) if options.clip isnt false - scrollEvent = {screenRange, options} - @component?.didRequestAutoscroll(scrollEvent) - @emitter.emit "did-request-autoscroll", scrollEvent - - getHorizontalScrollbarHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - - @getElement().getHorizontalScrollbarHeight() - - getVerticalScrollbarWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - - @getElement().getVerticalScrollbarWidth() - - pageUp: -> - @moveUp(@getRowsPerPage()) - - pageDown: -> - @moveDown(@getRowsPerPage()) - - selectPageUp: -> - @selectUp(@getRowsPerPage()) - - selectPageDown: -> - @selectDown(@getRowsPerPage()) - - # Returns the number of rows per page - getRowsPerPage: -> - if @component? - clientHeight = @component.getScrollContainerClientHeight() - lineHeight = @component.getLineHeight() - Math.max(1, Math.ceil(clientHeight / lineHeight)) - else - 1 - - Object.defineProperty(@prototype, 'rowsPerPage', { - get: -> @getRowsPerPage() - }) - - ### - Section: Config - ### - - # Experimental: Supply an object that will provide the editor with settings - # for specific syntactic scopes. See the `ScopedSettingsDelegate` in - # `text-editor-registry.js` for an example implementation. - setScopedSettingsDelegate: (@scopedSettingsDelegate) -> - @tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate - - # Experimental: Retrieve the {Object} that provides the editor with settings - # for specific syntactic scopes. - getScopedSettingsDelegate: -> @scopedSettingsDelegate - - # Experimental: Is auto-indentation enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndent: -> @autoIndent - - # Experimental: Is auto-indentation on paste enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndentOnPaste: -> @autoIndentOnPaste - - # Experimental: Does this editor allow scrolling past the last line? - # - # Returns a {Boolean}. - getScrollPastEnd: -> - if @getAutoHeight() - false - else - @scrollPastEnd - - # Experimental: How fast does the editor scroll in response to mouse wheel - # movements? - # - # Returns a positive {Number}. - getScrollSensitivity: -> @scrollSensitivity - - # Experimental: Does this editor show cursors while there is a selection? - # - # Returns a positive {Boolean}. - getShowCursorOnSelection: -> @showCursorOnSelection - - # Experimental: Are line numbers enabled for this editor? - # - # Returns a {Boolean} - doesShowLineNumbers: -> @showLineNumbers - - # Experimental: Get the time interval within which text editing operations - # are grouped together in the editor's undo history. - # - # Returns the time interval {Number} in milliseconds. - getUndoGroupingInterval: -> @undoGroupingInterval - - # Experimental: Get the characters that are *not* considered part of words, - # for the purpose of word-based cursor movements. - # - # Returns a {String} containing the non-word characters. - getNonWordCharacters: (scopes) -> - @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - - ### - Section: Event Handlers - ### - - handleGrammarChange: -> - @unfoldAll() - @emitter.emit 'did-change-grammar', @getGrammar() - - ### - Section: TextEditor Rendering - ### - - # Get the Element for the editor. - getElement: -> - if @component? - @component.element - else - TextEditorComponent ?= require('./text-editor-component') - TextEditorElement ?= require('./text-editor-element') - new TextEditorComponent({ - model: this, - updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, - @initialScrollTopRow, @initialScrollLeftColumn - }) - @component.element - - getAllowedLocations: -> - ['center'] - - # Essential: Retrieves the greyed out placeholder of a mini editor. - # - # Returns a {String}. - getPlaceholderText: -> @placeholderText - - # Essential: Set the greyed out placeholder of a mini editor. Placeholder text - # will be displayed when the editor has no content. - # - # * `placeholderText` {String} text that is displayed when the editor has no content. - setPlaceholderText: (placeholderText) -> @update({placeholderText}) - - pixelPositionForBufferPosition: (bufferPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @getElement().pixelPositionForBufferPosition(bufferPosition) - - pixelPositionForScreenPosition: (screenPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @getElement().pixelPositionForScreenPosition(screenPosition) - - getVerticalScrollMargin: -> - maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2) - Math.min(@verticalScrollMargin, maxScrollMargin) - - setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2)) - setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - - getLineHeightInPixels: -> @lineHeightInPixels - setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels - - getKoreanCharWidth: -> @koreanCharWidth - getHalfWidthCharWidth: -> @halfWidthCharWidth - getDoubleWidthCharWidth: -> @doubleWidthCharWidth - getDefaultCharWidth: -> @defaultCharWidth - - ratioForCharacter: (character) -> - if isKoreanCharacter(character) - @getKoreanCharWidth() / @getDefaultCharWidth() - else if isHalfWidthCharacter(character) - @getHalfWidthCharWidth() / @getDefaultCharWidth() - else if isDoubleWidthCharacter(character) - @getDoubleWidthCharWidth() / @getDefaultCharWidth() - else - 1 - - setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - doubleWidthCharWidth ?= defaultCharWidth - halfWidthCharWidth ?= defaultCharWidth - koreanCharWidth ?= defaultCharWidth - if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth - @defaultCharWidth = defaultCharWidth - @doubleWidthCharWidth = doubleWidthCharWidth - @halfWidthCharWidth = halfWidthCharWidth - @koreanCharWidth = koreanCharWidth - if @isSoftWrapped() - @displayLayer.reset({ - softWrapColumn: @getSoftWrapColumn() - }) - defaultCharWidth - - setHeight: (height) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @getElement().setHeight(height) - - getHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") - @getElement().getHeight() - - getAutoHeight: -> @autoHeight ? true - - getAutoWidth: -> @autoWidth ? false - - setWidth: (width) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @getElement().setWidth(width) - - getWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") - @getElement().getWidth() - - # Use setScrollTopRow instead of this method - setFirstVisibleScreenRow: (screenRow) -> - @setScrollTopRow(screenRow) - - getFirstVisibleScreenRow: -> - @getElement().component.getFirstVisibleRow() - - getLastVisibleScreenRow: -> - @getElement().component.getLastVisibleRow() - - getVisibleRowRange: -> - [@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()] - - # Use setScrollLeftColumn instead of this method - setFirstVisibleScreenColumn: (column) -> - @setScrollLeftColumn(column) - - getFirstVisibleScreenColumn: -> - @getElement().component.getFirstVisibleColumn() - - getScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - - @getElement().getScrollTop() - - setScrollTop: (scrollTop) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - - @getElement().setScrollTop(scrollTop) - - getScrollBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - - @getElement().getScrollBottom() - - setScrollBottom: (scrollBottom) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - - @getElement().setScrollBottom(scrollBottom) - - getScrollLeft: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - - @getElement().getScrollLeft() - - setScrollLeft: (scrollLeft) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - - @getElement().setScrollLeft(scrollLeft) - - getScrollRight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - - @getElement().getScrollRight() - - setScrollRight: (scrollRight) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - - @getElement().setScrollRight(scrollRight) - - getScrollHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - - @getElement().getScrollHeight() - - getScrollWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - - @getElement().getScrollWidth() - - getMaxScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") - - @getElement().getMaxScrollTop() - - getScrollTopRow: -> - @getElement().component.getScrollTopRow() - - setScrollTopRow: (scrollTopRow) -> - @getElement().component.setScrollTopRow(scrollTopRow) - - getScrollLeftColumn: -> - @getElement().component.getScrollLeftColumn() - - setScrollLeftColumn: (scrollLeftColumn) -> - @getElement().component.setScrollLeftColumn(scrollLeftColumn) - - intersectsVisibleRowRange: (startRow, endRow) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - - @getElement().intersectsVisibleRowRange(startRow, endRow) - - selectionIntersectsVisibleRowRange: (selection) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - - @getElement().selectionIntersectsVisibleRowRange(selection) - - screenPositionForPixelPosition: (pixelPosition) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - - @getElement().screenPositionForPixelPosition(pixelPosition) - - pixelRectForScreenRange: (screenRange) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - - @getElement().pixelRectForScreenRange(screenRange) - - ### - Section: Utility - ### - - inspect: -> - "" - - emitWillInsertTextEvent: (text) -> - result = true - cancel = -> result = false - willInsertEvent = {cancel, text} - @emitter.emit 'will-insert-text', willInsertEvent - result - - ### - Section: Language Mode Delegated Methods - ### - - suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) - - # Given a buffer row, indent it. - # - # * bufferRow - The row {Number}. - # * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. - autoIndentBufferRow: (bufferRow, options) -> - indentLevel = @suggestedIndentForBufferRow(bufferRow, options) - @setIndentationForBufferRow(bufferRow, indentLevel, options) - - # Indents all the rows between two buffer row numbers. - # - # * startRow - The row {Number} to start at - # * endRow - The row {Number} to end at - autoIndentBufferRows: (startRow, endRow) -> - row = startRow - while row <= endRow - @autoIndentBufferRow(row) - row++ - return - - autoDecreaseIndentForBufferRow: (bufferRow) -> - indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) - @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel? - - toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - - rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) - - isCommented = @tokenizedBuffer.isRowCommented(bufferRow) - - startRow = bufferRow - while startRow > 0 - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(startRow - 1)) - break if @tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented - startRow-- - - endRow = bufferRow - rowCount = @getLineCount() - while endRow < rowCount - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(endRow + 1)) - break if @tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented - endRow++ - - new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) - -class ChangeEvent - constructor: ({@oldRange, @newRange}) -> - - Object.defineProperty @prototype, 'start', { - get: -> @oldRange.start - } - - Object.defineProperty @prototype, 'oldExtent', { - get: -> @oldRange.getExtent() - } - - Object.defineProperty @prototype, 'newExtent', { - get: -> @newRange.getExtent() - } diff --git a/src/text-editor.js b/src/text-editor.js new file mode 100644 index 000000000..4d7d94de0 --- /dev/null +++ b/src/text-editor.js @@ -0,0 +1,4587 @@ +const _ = require('underscore-plus') +const path = require('path') +const fs = require('fs-plus') +const Grim = require('grim') +const dedent = require('dedent') +const {CompositeDisposable, Disposable, Emitter} = require('event-kit') +const TextBuffer = require('text-buffer') +const {Point, Range} = TextBuffer +const DecorationManager = require('./decoration-manager') +const TokenizedBuffer = require('./tokenized-buffer') +const Cursor = require('./cursor') +const Selection = require('./selection') + +const TextMateScopeSelector = require('first-mate').ScopeSelector +const GutterContainer = require('./gutter-container') +let TextEditorComponent = null +let TextEditorElement = null +const {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require('./text-utils') + +const SERIALIZATION_VERSION = 1 +const NON_WHITESPACE_REGEXP = /\S/ +const ZERO_WIDTH_NBSP = '\ufeff' +let nextId = 0 + +// Essential: This class represents all essential editing state for a single +// {TextBuffer}, including cursor and selection positions, folds, and soft wraps. +// If you're manipulating the state of an editor, use this class. +// +// A single {TextBuffer} can belong to multiple editors. For example, if the +// same file is open in two different panes, Atom creates a separate editor for +// each pane. If the buffer is manipulated the changes are reflected in both +// editors, but each maintains its own cursor position, folded lines, etc. +// +// ## Accessing TextEditor Instances +// +// The easiest way to get hold of `TextEditor` objects is by registering a callback +// with `::observeTextEditors` on the `atom.workspace` global. Your callback will +// then be called with all current editor instances and also when any editor is +// created in the future. +// +// ```coffee +// atom.workspace.observeTextEditors (editor) -> +// editor.insertText('Hello World') +// ``` +// +// ## Buffer vs. Screen Coordinates +// +// Because editors support folds and soft-wrapping, the lines on screen don't +// always match the lines in the buffer. For example, a long line that soft wraps +// twice renders as three lines on screen, but only represents one line in the +// buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds +// to row 11 in the buffer. +// +// Your choice of coordinates systems will depend on what you're trying to +// achieve. For example, if you're writing a command that jumps the cursor up or +// down by 10 lines, you'll want to use screen coordinates because the user +// probably wants to skip lines *on screen*. However, if you're writing a package +// that jumps between method definitions, you'll want to work in buffer +// coordinates. +// +// **When in doubt, just default to buffer coordinates**, then experiment with +// soft wraps and folds to ensure your code interacts with them correctly. +module.exports = +class TextEditor { + static setClipboard (clipboard) { + this.clipboard = clipboard + } + + static setScheduler (scheduler) { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.setScheduler(scheduler) + } + + static didUpdateStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateStyles() + } + + static didUpdateScrollbarStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateScrollbarStyles() + } + + static viewForItem (item) { return item.element || item } + + static deserialize (state, atomEnvironment) { + if (state.version !== SERIALIZATION_VERSION) return null + + try { + const tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + if (!tokenizedBuffer) return null + + state.tokenizedBuffer = tokenizedBuffer + state.tabLength = state.tokenizedBuffer.getTabLength() + } catch (error) { + if (error.syscall === 'read') { + return // Error reading the file, don't deserialize an editor for it + } else { + throw error + } + } + + state.buffer = state.tokenizedBuffer.buffer + state.assert = atomEnvironment.assert.bind(atomEnvironment) + const editor = new TextEditor(state) + if (state.registered) { + const disposable = atomEnvironment.textEditors.add(editor) + editor.onDidDestroy(() => disposable.dispose()) + } + return editor + } + + constructor (params = {}) { + if (this.constructor.clipboard == null) { + throw new Error('Must call TextEditor.setClipboard at least once before creating TextEditor instances') + } + + this.id = params.id != null ? params.id : nextId++ + this.initialScrollTopRow = params.initialScrollTopRow + this.initialScrollLeftColumn = params.initialScrollLeftColumn + this.decorationManager = params.decorationManager + this.selectionsMarkerLayer = params.selectionsMarkerLayer + this.mini = (params.mini != null) ? params.mini : false + this.placeholderText = params.placeholderText + this.showLineNumbers = params.showLineNumbers + this.largeFileMode = params.largeFileMode + this.assert = params.assert || (condition => condition) + this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true + this.autoHeight = params.autoHeight + this.autoWidth = params.autoWidth + this.scrollPastEnd = (params.scrollPastEnd != null) ? params.scrollPastEnd : false + this.scrollSensitivity = (params.scrollSensitivity != null) ? params.scrollSensitivity : 40 + this.editorWidthInChars = params.editorWidthInChars + this.invisibles = params.invisibles + this.showIndentGuide = params.showIndentGuide + this.softWrapped = params.softWrapped + this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength + this.preferredLineLength = params.preferredLineLength + this.showCursorOnSelection = (params.showCursorOnSelection != null) ? params.showCursorOnSelection : true + this.maxScreenLineLength = params.maxScreenLineLength + this.softTabs = (params.softTabs != null) ? params.softTabs : true + this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true + this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true + this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300 + this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" + this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false + this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false + this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80 + this.maxScreenLineLength = (params.maxScreenLineLength != null) ? params.maxScreenLineLength : 500 + this.showLineNumbers = (params.showLineNumbers != null) ? params.showLineNumbers : true + const {tabLength = 2} = params + + this.alive = true + this.doBackgroundWork = this.doBackgroundWork.bind(this) + this.serializationVersion = 1 + this.suppressSelectionMerging = false + this.selectionFlashDuration = 500 + this.gutterContainer = null + this.verticalScrollMargin = 2 + this.horizontalScrollMargin = 6 + this.lineHeightInPixels = null + this.defaultCharWidth = null + this.height = null + this.width = null + this.registered = false + this.atomicSoftTabs = true + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.cursors = [] + this.cursorsByMarkerId = new Map() + this.selections = [] + this.hasTerminatedPendingState = false + + this.buffer = params.buffer || new TextBuffer({ + shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') } + }) + + this.tokenizedBuffer = params.tokenizedBuffer || new TokenizedBuffer({ + grammar: params.grammar, + tabLength, + buffer: this.buffer, + largeFileMode: this.largeFileMode, + assert: this.assert + }) + + if (params.displayLayer) { + this.displayLayer = params.displayLayer + } else { + const displayLayerParams = { + invisibles: this.getInvisibles(), + softWrapColumn: this.getSoftWrapColumn(), + showIndentGuides: this.doesShowIndentGuide(), + atomicSoftTabs: params.atomicSoftTabs != null ? params.atomicSoftTabs : true, + tabLength, + ratioForCharacter: this.ratioForCharacter.bind(this), + isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP, + softWrapHangingIndent: params.softWrapHangingIndentLength != null ? params.softWrapHangingIndentLength : 0 + } + + this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId) + if (this.displayLayer) { + this.displayLayer.reset(displayLayerParams) + this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) + } else { + this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams) + } + } + + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + this.disposables.add(new Disposable(() => { + if (this.backgroundWorkHandle != null) return cancelIdleCallback(this.backgroundWorkHandle) + })) + + this.defaultMarkerLayer = this.displayLayer.addMarkerLayer() + if (!this.selectionsMarkerLayer) { + this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true}) + } + + this.displayLayer.setTextDecorationLayer(this.tokenizedBuffer) + + this.decorationManager = new DecorationManager(this) + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'}) + if (!this.isMini()) this.decorateCursorLine() + + this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) + + for (let marker of this.selectionsMarkerLayer.getMarkers()) { + this.addSelection(marker) + } + + this.subscribeToBuffer() + this.subscribeToDisplayLayer() + + if (this.cursors.length === 0 && !params.suppressCursorCreation) { + const initialLine = Math.max(parseInt(params.initialLine) || 0, 0) + const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0) + this.addCursorAtBufferPosition([initialLine, initialColumn]) + } + + this.gutterContainer = new GutterContainer(this) + this.lineNumberGutter = this.gutterContainer.addGutter({ + name: 'line-number', + priority: 0, + visible: params.lineNumberGutterVisible + }) + } + + get element () { + return this.getElement() + } + + get editorElement () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.editorElement\` has always been private, but now + it is gone. Reading the \`editorElement\` property still returns a + reference to the editor element but this field will be removed in a + later version of Atom, so we recommend using the \`element\` property instead.\ + `) + + return this.getElement() + } + + get displayBuffer () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.displayBuffer\` has always been private, but now + it is gone. Reading the \`displayBuffer\` property now returns a reference + to the containing \`TextEditor\`, which now provides *some* of the API of + the defunct \`DisplayBuffer\` class.\ + `) + return this + } + + get languageMode () { + return this.tokenizedBuffer + } + + get rowsPerPage () { + return this.getRowsPerPage() + } + + decorateCursorLine () { + this.cursorLineDecorations = [ + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line', class: 'cursor-line', onlyEmpty: true}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line'}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true}) + ] + } + + doBackgroundWork (deadline) { + const previousLongestRow = this.getApproximateLongestScreenRow() + if (this.displayLayer.doBackgroundWork(deadline)) { + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + } else { + this.backgroundWorkHandle = null + } + + if (this.component && this.getApproximateLongestScreenRow() !== previousLongestRow) { + this.component.scheduleUpdate() + } + } + + update (params) { + const displayLayerParams = {} + + for (let param of Object.keys(params)) { + const value = params[param] + + switch (param) { + case 'autoIndent': + this.autoIndent = value + break + + case 'autoIndentOnPaste': + this.autoIndentOnPaste = value + break + + case 'undoGroupingInterval': + this.undoGroupingInterval = value + break + + case 'nonWordCharacters': + this.nonWordCharacters = value + break + + case 'scrollSensitivity': + this.scrollSensitivity = value + break + + case 'encoding': + this.buffer.setEncoding(value) + break + + case 'softTabs': + if (value !== this.softTabs) { + this.softTabs = value + } + break + + case 'atomicSoftTabs': + if (value !== this.displayLayer.atomicSoftTabs) { + displayLayerParams.atomicSoftTabs = value + } + break + + case 'tabLength': + if (value > 0 && value !== this.tokenizedBuffer.getTabLength()) { + this.tokenizedBuffer.setTabLength(value) + displayLayerParams.tabLength = value + } + break + + case 'softWrapped': + if (value !== this.softWrapped) { + this.softWrapped = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped()) + } + break + + case 'softWrapHangingIndentLength': + if (value !== this.displayLayer.softWrapHangingIndent) { + displayLayerParams.softWrapHangingIndent = value + } + break + + case 'softWrapAtPreferredLineLength': + if (value !== this.softWrapAtPreferredLineLength) { + this.softWrapAtPreferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'preferredLineLength': + if (value !== this.preferredLineLength) { + this.preferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'maxScreenLineLength': + if (value !== this.maxScreenLineLength) { + this.maxScreenLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'mini': + if (value !== this.mini) { + this.mini = value + this.emitter.emit('did-change-mini', value) + displayLayerParams.invisibles = this.getInvisibles() + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + if (this.mini) { + for (let decoration of this.cursorLineDecorations) { decoration.destroy() } + this.cursorLineDecorations = null + } else { + this.decorateCursorLine() + } + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'placeholderText': + if (value !== this.placeholderText) { + this.placeholderText = value + this.emitter.emit('did-change-placeholder-text', value) + } + break + + case 'lineNumberGutterVisible': + if (value !== this.lineNumberGutterVisible) { + if (value) { + this.lineNumberGutter.show() + } else { + this.lineNumberGutter.hide() + } + this.emitter.emit('did-change-line-number-gutter-visible', this.lineNumberGutter.isVisible()) + } + break + + case 'showIndentGuide': + if (value !== this.showIndentGuide) { + this.showIndentGuide = value + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + } + break + + case 'showLineNumbers': + if (value !== this.showLineNumbers) { + this.showLineNumbers = value + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'showInvisibles': + if (value !== this.showInvisibles) { + this.showInvisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'invisibles': + if (!_.isEqual(value, this.invisibles)) { + this.invisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'editorWidthInChars': + if (value > 0 && value !== this.editorWidthInChars) { + this.editorWidthInChars = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'width': + if (value !== this.width) { + this.width = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'scrollPastEnd': + if (value !== this.scrollPastEnd) { + this.scrollPastEnd = value + if (this.component) this.component.scheduleUpdate() + } + break + + case 'autoHeight': + if (value !== this.autoHeight) { + this.autoHeight = value + } + break + + case 'autoWidth': + if (value !== this.autoWidth) { + this.autoWidth = value + } + break + + case 'showCursorOnSelection': + if (value !== this.showCursorOnSelection) { + this.showCursorOnSelection = value + if (this.component) this.component.scheduleUpdate() + } + break + + default: + if (param !== 'ref' && param !== 'key') { + throw new TypeError(`Invalid TextEditor parameter: '${param}'`) + } + } + } + + this.displayLayer.reset(displayLayerParams) + + if (this.component) { + return this.component.getNextUpdatePromise() + } else { + return Promise.resolve() + } + } + + scheduleComponentUpdate () { + if (this.component) this.component.scheduleUpdate() + } + + serialize () { + const tokenizedBufferState = this.tokenizedBuffer.serialize() + + return { + deserializer: 'TextEditor', + version: SERIALIZATION_VERSION, + + // TODO: Remove this forward-compatible fallback once 1.8 reaches stable. + displayBuffer: {tokenizedBuffer: tokenizedBufferState}, + + tokenizedBuffer: tokenizedBufferState, + displayLayerId: this.displayLayer.id, + selectionsMarkerLayerId: this.selectionsMarkerLayer.id, + + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + + atomicSoftTabs: this.displayLayer.atomicSoftTabs, + softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent, + + id: this.id, + softTabs: this.softTabs, + softWrapped: this.softWrapped, + softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength, + preferredLineLength: this.preferredLineLength, + mini: this.mini, + editorWidthInChars: this.editorWidthInChars, + width: this.width, + largeFileMode: this.largeFileMode, + maxScreenLineLength: this.maxScreenLineLength, + registered: this.registered, + invisibles: this.invisibles, + showInvisibles: this.showInvisibles, + showIndentGuide: this.showIndentGuide, + autoHeight: this.autoHeight, + autoWidth: this.autoWidth + } + } + + subscribeToBuffer () { + this.buffer.retain() + this.disposables.add(this.buffer.onDidChangePath(() => { + this.emitter.emit('did-change-title', this.getTitle()) + this.emitter.emit('did-change-path', this.getPath()) + })) + this.disposables.add(this.buffer.onDidChangeEncoding(() => { + this.emitter.emit('did-change-encoding', this.getEncoding()) + })) + this.disposables.add(this.buffer.onDidDestroy(() => this.destroy())) + this.disposables.add(this.buffer.onDidChangeModified(() => { + if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState() + })) + } + + terminatePendingState () { + if (!this.hasTerminatedPendingState) this.emitter.emit('did-terminate-pending-state') + this.hasTerminatedPendingState = true + } + + onDidTerminatePendingState (callback) { + return this.emitter.on('did-terminate-pending-state', callback) + } + + subscribeToDisplayLayer () { + this.disposables.add(this.tokenizedBuffer.onDidChangeGrammar(this.handleGrammarChange.bind(this))) + this.disposables.add(this.displayLayer.onDidChange(changes => { + this.mergeIntersectingSelections() + if (this.component) this.component.didChangeDisplayLayer(changes) + this.emitter.emit('did-change', changes.map(change => new ChangeEvent(change))) + })) + this.disposables.add(this.displayLayer.onDidReset(() => { + this.mergeIntersectingSelections() + if (this.component) this.component.didResetDisplayLayer() + this.emitter.emit('did-change', {}) + })) + this.disposables.add(this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this))) + return this.disposables.add(this.selectionsMarkerLayer.onDidUpdate(() => (this.component != null ? this.component.didUpdateSelections() : undefined))) + } + + destroy () { + if (!this.alive) return + this.alive = false + this.disposables.dispose() + this.displayLayer.destroy() + this.tokenizedBuffer.destroy() + for (let selection of this.selections.slice()) { + selection.destroy() + } + this.buffer.release() + this.gutterContainer.destroy() + this.emitter.emit('did-destroy') + this.emitter.clear() + if (this.component) this.component.element.component = null + this.component = null + this.lineNumberGutter.element = null + } + + isAlive () { return this.alive } + + isDestroyed () { return !this.alive } + + /* + Section: Event Subscription + */ + + // Essential: Calls your `callback` when the buffer's title has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeTitle (callback) { + return this.emitter.on('did-change-title', callback) + } + + // Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePath (callback) { + return this.emitter.on('did-change-path', callback) + } + + // Essential: Invoke the given callback synchronously when the content of the + // buffer changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider {::onDidStopChanging} to + // delay expensive operations until after changes stop occurring. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange (callback) { + return this.emitter.on('did-change', callback) + } + + // Essential: Invoke `callback` 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. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChanging (callback) { + return this.getBuffer().onDidStopChanging(callback) + } + + // Essential: Calls your `callback` when a {Cursor} is moved. If there are + // multiple cursors, your callback will be called for each cursor. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferPosition` {Point} + // * `oldScreenPosition` {Point} + // * `newBufferPosition` {Point} + // * `newScreenPosition` {Point} + // * `textChanged` {Boolean} + // * `cursor` {Cursor} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeCursorPosition (callback) { + return this.emitter.on('did-change-cursor-position', callback) + } + + // Essential: Calls your `callback` when a selection's screen range changes. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferRange` {Range} + // * `oldScreenRange` {Range} + // * `newBufferRange` {Range} + // * `newScreenRange` {Range} + // * `selection` {Selection} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSelectionRange (callback) { + return this.emitter.on('did-change-selection-range', callback) + } + + // Extended: Calls your `callback` when soft wrap was enabled or disabled. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSoftWrapped (callback) { + return this.emitter.on('did-change-soft-wrapped', callback) + } + + // Extended: Calls your `callback` when the buffer's encoding has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeEncoding (callback) { + return this.emitter.on('did-change-encoding', callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. Immediately calls your callback with + // the current grammar. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGrammar (callback) { + callback(this.getGrammar()) + return this.onDidChangeGrammar(callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeGrammar (callback) { + return this.emitter.on('did-change-grammar', callback) + } + + // Extended: Calls your `callback` when the result of {::isModified} changes. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeModified (callback) { + return this.getBuffer().onDidChangeModified(callback) + } + + // Extended: Calls your `callback` when the buffer's underlying file changes on + // disk at a moment when the result of {::isModified} is true. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidConflict (callback) { + return this.getBuffer().onDidConflict(callback) + } + + // Extended: Calls your `callback` before text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // * `cancel` {Function} Call to prevent the text from being inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillInsertText (callback) { + return this.emitter.on('will-insert-text', callback) + } + + // Extended: Calls your `callback` after text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidInsertText (callback) { + return this.emitter.on('did-insert-text', callback) + } + + // Essential: Invoke the given callback after the buffer is saved to disk. + // + // * `callback` {Function} to be called after the buffer is saved. + // * `event` {Object} with the following keys: + // * `path` The path to which the buffer was saved. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidSave (callback) { + return this.getBuffer().onDidSave(callback) + } + + // Essential: Invoke the given callback when the editor is destroyed. + // + // * `callback` {Function} to be called when the editor is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // Immediately calls your callback for each existing cursor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeCursors (callback) { + this.getCursors().forEach(callback) + return this.onDidAddCursor(callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddCursor (callback) { + return this.emitter.on('did-add-cursor', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is removed from the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveCursor (callback) { + return this.emitter.on('did-remove-cursor', callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // Immediately calls your callback for each existing selection. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeSelections (callback) { + this.getSelections().forEach(callback) + return this.onDidAddSelection(callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddSelection (callback) { + return this.emitter.on('did-add-selection', callback) + } + + // Extended: Calls your `callback` when a {Selection} is removed from the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveSelection (callback) { + return this.emitter.on('did-remove-selection', callback) + } + + // Extended: Calls your `callback` with each {Decoration} added to the editor. + // Calls your `callback` immediately for any existing decorations. + // + // * `callback` {Function} + // * `decoration` {Decoration} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeDecorations (callback) { + return this.decorationManager.observeDecorations(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is added to the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddDecoration (callback) { + return this.decorationManager.onDidAddDecoration(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is removed from the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveDecoration (callback) { + return this.decorationManager.onDidRemoveDecoration(callback) + } + + // Called by DecorationManager when a decoration is added. + didAddDecoration (decoration) { + if (this.component && decoration.isType('block')) { + this.component.addBlockDecoration(decoration) + } + } + + // Extended: Calls your `callback` when the placeholder text is changed. + // + // * `callback` {Function} + // * `placeholderText` {String} new text + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePlaceholderText (callback) { + return this.emitter.on('did-change-placeholder-text', callback) + } + + onDidChangeScrollTop (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.') + return this.getElement().onDidChangeScrollTop(callback) + } + + onDidChangeScrollLeft (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.') + return this.getElement().onDidChangeScrollLeft(callback) + } + + onDidRequestAutoscroll (callback) { + return this.emitter.on('did-request-autoscroll', callback) + } + + // TODO Remove once the tabs package no longer uses .on subscriptions + onDidChangeIcon (callback) { + return this.emitter.on('did-change-icon', callback) + } + + onDidUpdateDecorations (callback) { + return this.decorationManager.onDidUpdateDecorations(callback) + } + + // Essential: Retrieves the current {TextBuffer}. + getBuffer () { return this.buffer } + + // Retrieves the current buffer's URI. + getURI () { return this.buffer.getUri() } + + // Create an {TextEditor} with its initial state based on this object + copy () { + const displayLayer = this.displayLayer.copy() + const selectionsMarkerLayer = displayLayer.getMarkerLayer(this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id) + const softTabs = this.getSoftTabs() + return new TextEditor({ + buffer: this.buffer, + selectionsMarkerLayer, + softTabs, + suppressCursorCreation: true, + tabLength: this.tokenizedBuffer.getTabLength(), + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + assert: this.assert, + displayLayer, + grammar: this.getGrammar(), + autoWidth: this.autoWidth, + autoHeight: this.autoHeight, + showCursorOnSelection: this.showCursorOnSelection + }) + } + + // Controls visibility based on the given {Boolean}. + setVisible (visible) { this.tokenizedBuffer.setVisible(visible) } + + setMini (mini) { + this.update({mini}) + } + + isMini () { return this.mini } + + onDidChangeMini (callback) { + return this.emitter.on('did-change-mini', callback) + } + + setLineNumberGutterVisible (lineNumberGutterVisible) { this.update({lineNumberGutterVisible}) } + + isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() } + + onDidChangeLineNumberGutterVisible (callback) { + return this.emitter.on('did-change-line-number-gutter-visible', callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // Immediately calls your callback for each existing gutter. + // + // * `callback` {Function} + // * `gutter` {Gutter} that currently exists/was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGutters (callback) { + return this.gutterContainer.observeGutters(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // + // * `callback` {Function} + // * `gutter` {Gutter} that was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddGutter (callback) { + return this.gutterContainer.onDidAddGutter(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is removed from the editor. + // + // * `callback` {Function} + // * `name` The name of the {Gutter} that was removed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveGutter (callback) { + return this.gutterContainer.onDidRemoveGutter(callback) + } + + // Set the number of characters that can be displayed horizontally in the + // editor. + // + // * `editorWidthInChars` A {Number} representing the width of the + // {TextEditorElement} in characters. + setEditorWidthInChars (editorWidthInChars) { this.update({editorWidthInChars}) } + + // Returns the editor width in characters. + getEditorWidthInChars () { + if (this.width != null && this.defaultCharWidth > 0) { + return Math.max(0, Math.floor(this.width / this.defaultCharWidth)) + } else { + return this.editorWidthInChars + } + } + + /* + Section: File Details + */ + + // Essential: Get the editor's title for display in other parts of the + // UI such as the tabs. + // + // If the editor's buffer is saved, its title is the file name. If it is + // unsaved, its title is "untitled". + // + // Returns a {String}. + getTitle () { + return this.getFileName() || 'untitled' + } + + // Essential: Get unique title for display in other parts of the UI, such as + // the window title. + // + // If the editor's buffer is unsaved, its title is "untitled" + // If the editor's buffer is saved, its unique title is formatted as one + // of the following, + // * "" when it is the only editing buffer with this file name. + // * " — " when other buffers have this file name. + // + // Returns a {String} + getLongTitle () { + if (this.getPath()) { + const fileName = this.getFileName() + + let myPathSegments + const openEditorPathSegmentsWithSameFilename = [] + for (const textEditor of atom.workspace.getTextEditors()) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) + if (textEditor.getFileName() === fileName) { + openEditorPathSegmentsWithSameFilename.push(pathSegments) + } + if (textEditor === this) myPathSegments = pathSegments + } + + if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + + let commonPathSegmentCount + for (let i = 0, {length} = myPathSegments; i < length; i++) { + const myPathSegment = myPathSegments[i] + if (openEditorPathSegmentsWithSameFilename.some(segments => (segments.length === i + 1) || (segments[i] !== myPathSegment))) { + commonPathSegmentCount = i + break + } + } + + return `${fileName} \u2014 ${path.join(...myPathSegments.slice(commonPathSegmentCount))}` + } else { + return 'untitled' + } + } + + // Essential: Returns the {String} path of this editor's text buffer. + getPath () { + return this.buffer.getPath() + } + + getFileName () { + const fullPath = this.getPath() + if (fullPath) return path.basename(fullPath) + } + + getDirectoryPath () { + const fullPath = this.getPath() + if (fullPath) return path.dirname(fullPath) + } + + // Extended: Returns the {String} character set encoding of this editor's text + // buffer. + getEncoding () { return this.buffer.getEncoding() } + + // Extended: Set the character set encoding to use in this editor's text + // buffer. + // + // * `encoding` The {String} character set encoding name such as 'utf8' + setEncoding (encoding) { this.buffer.setEncoding(encoding) } + + // Essential: Returns {Boolean} `true` if this editor has been modified. + isModified () { return this.buffer.isModified() } + + // Essential: Returns {Boolean} `true` if this editor has no content. + isEmpty () { return this.buffer.isEmpty() } + + /* + Section: File Operations + */ + + // Essential: Saves the editor's text buffer. + // + // See {TextBuffer::save} for more details. + save () { return this.buffer.save() } + + // Essential: Saves the editor's text buffer as the given path. + // + // See {TextBuffer::saveAs} for more details. + // + // * `filePath` A {String} path. + saveAs (filePath) { return this.buffer.saveAs(filePath) } + + // Determine whether the user should be prompted to save before closing + // this editor. + shouldPromptToSave ({windowCloseRequested, projectHasPaths} = {}) { + if (windowCloseRequested && projectHasPaths && atom.stateStore.isConnected()) { + return this.buffer.isInConflict() + } else { + return this.isModified() && !this.buffer.hasMultipleEditors() + } + } + + // Returns an {Object} to configure dialog shown when this editor is saved + // via {Pane::saveItemAs}. + getSaveDialogOptions () { return {} } + + /* + Section: Reading Text + */ + + // Essential: Returns a {String} representing the entire contents of the editor. + getText () { return this.buffer.getText() } + + // Essential: Get the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // + // Returns a {String}. + getTextInBufferRange (range) { + return this.buffer.getTextInRange(range) + } + + // Essential: Returns a {Number} representing the number of lines in the buffer. + getLineCount () { return this.buffer.getLineCount() } + + // Essential: Returns a {Number} representing the number of screen lines in the + // editor. This accounts for folds. + getScreenLineCount () { return this.displayLayer.getScreenLineCount() } + + getApproximateScreenLineCount () { return this.displayLayer.getApproximateScreenLineCount() } + + // Essential: Returns a {Number} representing the last zero-indexed buffer row + // number of the editor. + getLastBufferRow () { return this.buffer.getLastRow() } + + // Essential: Returns a {Number} representing the last zero-indexed screen row + // number of the editor. + getLastScreenRow () { return this.getScreenLineCount() - 1 } + + // Essential: Returns a {String} representing the contents of the line at the + // given buffer row. + // + // * `bufferRow` A {Number} representing a zero-indexed buffer row. + lineTextForBufferRow (bufferRow) { return this.buffer.lineForRow(bufferRow) } + + // Essential: Returns a {String} representing the contents of the line at the + // given screen row. + // + // * `screenRow` A {Number} representing a zero-indexed screen row. + lineTextForScreenRow (screenRow) { + const screenLine = this.screenLineForScreenRow(screenRow) + if (screenLine) return screenLine.lineText + } + + logScreenLines (start = 0, end = this.getLastScreenRow()) { + for (let row = start; row <= end; row++) { + const line = this.lineTextForScreenRow(row) + console.log(row, this.bufferRowForScreenRow(row), line, line.length) + } + } + + tokensForScreenRow (screenRow) { + const tokens = [] + let lineTextIndex = 0 + const currentTokenScopes = [] + const {lineText, tags} = this.screenLineForScreenRow(screenRow) + for (const tag of tags) { + if (this.displayLayer.isOpenTag(tag)) { + currentTokenScopes.push(this.displayLayer.classNameForTag(tag)) + } else if (this.displayLayer.isCloseTag(tag)) { + currentTokenScopes.pop() + } else { + tokens.push({ + text: lineText.substr(lineTextIndex, tag), + scopes: currentTokenScopes.slice() + }) + lineTextIndex += tag + } + } + return tokens + } + + screenLineForScreenRow (screenRow) { + return this.displayLayer.getScreenLine(screenRow) + } + + bufferRowForScreenRow (screenRow) { + return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row + } + + bufferRowsForScreenRows (startScreenRow, endScreenRow) { + return this.displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) + } + + screenRowForBufferRow (row) { + return this.displayLayer.translateBufferPosition(Point(row, 0)).row + } + + getRightmostScreenPosition () { return this.displayLayer.getRightmostScreenPosition() } + + getApproximateRightmostScreenPosition () { return this.displayLayer.getApproximateRightmostScreenPosition() } + + getMaxScreenLineLength () { return this.getRightmostScreenPosition().column } + + getLongestScreenRow () { return this.getRightmostScreenPosition().row } + + getApproximateLongestScreenRow () { return this.getApproximateRightmostScreenPosition().row } + + lineLengthForScreenRow (screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow) } + + // Returns the range for the given buffer row. + // + // * `row` A row {Number}. + // * `options` (optional) An options hash with an `includeNewline` key. + // + // Returns a {Range}. + bufferRangeForBufferRow (row, options) { + return this.buffer.rangeForRow(row, options && options.includeNewline) + } + + // Get the text in the given {Range}. + // + // Returns a {String}. + getTextInRange (range) { return this.buffer.getTextInRange(range) } + + // {Delegates to: TextBuffer.isRowBlank} + isBufferRowBlank (bufferRow) { return this.buffer.isRowBlank(bufferRow) } + + // {Delegates to: TextBuffer.nextNonBlankRow} + nextNonBlankBufferRow (bufferRow) { return this.buffer.nextNonBlankRow(bufferRow) } + + // {Delegates to: TextBuffer.getEndPosition} + getEofBufferPosition () { return this.buffer.getEndPosition() } + + // Essential: Get the {Range} of the paragraph surrounding the most recently added + // cursor. + // + // Returns a {Range}. + getCurrentParagraphBufferRange () { + return this.getLastCursor().getCurrentParagraphBufferRange() + } + + /* + Section: Mutating Text + */ + + // Essential: Replaces the entire contents of the buffer with the given {String}. + // + // * `text` A {String} to replace with + setText (text) { return this.buffer.setText(text) } + + // Essential: Set the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // * `text` A {String} + // * `options` (optional) {Object} + // * `normalizeLineEndings` (optional) {Boolean} (default: true) + // * `undo` (optional) {String} 'skip' will skip the undo system + // + // Returns the {Range} of the newly-inserted text. + setTextInBufferRange (range, text, options) { + return this.getBuffer().setTextInRange(range, text, options) + } + + // Essential: 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 {Boolean} false when the text has not been inserted + insertText (text, options = {}) { + if (!this.emitWillInsertTextEvent(text)) return false + + const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0 + if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent() + if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent() + return this.mutateSelectedText(selection => { + const range = selection.insertText(text, options) + const didInsertEvent = {text, range} + this.emitter.emit('did-insert-text', didInsertEvent) + return range + }, groupingInterval) + } + + // Essential: For each selection, replace the selected text with a newline. + insertNewline (options) { + return this.insertText('\n', options) + } + + // Essential: For each selection, if the selection is empty, delete the character + // following the cursor. Otherwise delete the selected text. + delete () { + return this.mutateSelectedText(selection => selection.delete()) + } + + // Essential: For each selection, if the selection is empty, delete the character + // preceding the cursor. Otherwise delete the selected text. + backspace () { + return this.mutateSelectedText(selection => selection.backspace()) + } + + // Extended: 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, groupingInterval = 0) { + return this.mergeIntersectingSelections(() => { + return this.transact(groupingInterval, () => { + return this.getSelectionsOrderedByBufferPosition().map((selection, index) => fn(selection, index)) + }) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // up by one row in screen coordinates. + moveLineUp () { + const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b)) + + if (selections[0].start.row === 0) return + if (selections[selections.length - 1].start.row === this.getLastBufferRow() && this.buffer.getLastLine() === '') return + + this.transact(() => { + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + while (selection.end.row === (selections[0] != null ? selections[0].start.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.end.row = selections[0].end.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is preceded by a fold, one line above on screen + // could be multiple lines in the buffer. + const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) + const insertDelta = linesRange.start.row - precedingRow + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([-insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the preceding buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (lines[lines.length - 1] !== '\n') { lines += this.buffer.lineEndingForRow(linesRange.end.row - 2) } + this.buffer.delete(linesRange) + this.buffer.insert([precedingRow, 0], lines) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // down by one row in screen coordinates. + moveLineDown () { + const selections = this.getSelectedBufferRanges() + selections.sort((a, b) => b.compare(a)) + + this.transact(() => { + this.consolidateSelections() + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + // if the current selection start row matches the next selections' end row - make them one selection + while (selection.start.row === (selections[0] != null ? selections[0].end.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.start.row = selections[0].start.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is followed by a fold, one line below on screen + // could be multiple lines in the buffer. But at the same time, if the + // next buffer row is wrapped, one line in the buffer can represent many + // screen rows. + const followingRow = Math.min(this.buffer.getLineCount(), this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) + const insertDelta = followingRow - linesRange.end.row + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the following correct buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (followingRow - 1 === this.buffer.getLastRow()) { + lines = `\n${lines}` + } + + this.buffer.insert([followingRow, 0], lines) + this.buffer.delete(linesRange) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) + }) + } + + // Move any active selections one column to the left. + moveSelectionLeft () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0) + + const translationDelta = [0, -1] + const translatedRanges = [] + + if (noSelectionAtStartOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) + const charTextToLeftOfSelection = this.buffer.getTextInRange(charToLeftOfSelection) + + this.buffer.insert(selection.end, charTextToLeftOfSelection) + this.buffer.delete(charToLeftOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + // Move any active selections one column to the right. + moveSelectionRight () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtEndOfLine = selections.every(selection => { + return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row) + }) + + const translationDelta = [0, 1] + const translatedRanges = [] + + if (noSelectionAtEndOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) + const charTextToRightOfSelection = this.buffer.getTextInRange(charToRightOfSelection) + + this.buffer.delete(charToRightOfSelection) + this.buffer.insert(selection.start, charTextToRightOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + duplicateLines () { + this.transact(() => { + const selections = this.getSelectionsOrderedByBufferPosition() + const previousSelectionRanges = [] + + let i = selections.length - 1 + while (i >= 0) { + const j = i + previousSelectionRanges[i] = selections[i].getBufferRange() + if (selections[i].isEmpty()) { + const {start} = selections[i].getScreenRange() + selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], {preserveFolds: true}) + } + let [startRow, endRow] = selections[i].getBufferRowRange() + endRow++ + while (i > 0) { + const [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() + if (previousSelectionEndRow === startRow) { + startRow = previousSelectionStartRow + previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() + i-- + } else { + break + } + } + + const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) + let textToDuplicate = this.getTextInBufferRange([[startRow, 0], [endRow, 0]]) + if (endRow > this.getLastBufferRow()) textToDuplicate = `\n${textToDuplicate}` + this.buffer.insert([endRow, 0], textToDuplicate) + + const insertedRowCount = endRow - startRow + + for (let k = i; k <= j; k++) { + selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) + } + + for (const fold of intersectingFolds) { + const foldRange = this.displayLayer.bufferRangeForFold(fold) + this.displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) + } + + i-- + } + }) + } + + replaceSelectedText (options, fn) { + this.mutateSelectedText((selection) => { + selection.getBufferRange() + if (options && options.selectWordIfEmpty && selection.isEmpty()) { + selection.selectWord() + } + const text = selection.getText() + selection.deleteSelectedText() + const range = selection.insertText(fn(text)) + selection.setBufferRange(range) + }) + } + + // 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 () { + this.mergeIntersectingSelections(() => { + for (const selection of this.getSelections()) { + const range = selection.getBufferRange() + if (range.isSingleLine()) continue + + const {start, end} = range + this.addSelectionForBufferRange([start, [start.row, Infinity]]) + let {row} = start + while (++row < end.row) { + this.addSelectionForBufferRange([[row, 0], [row, Infinity]]) + } + if (end.column !== 0) this.addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) + selection.destroy() + } + }) + } + + // Extended: 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 () { + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectRight() + const text = selection.getText() + selection.delete() + selection.cursor.moveLeft() + selection.insertText(text) + } else { + selection.insertText(selection.getText().split('').reverse().join('')) + } + }) + } + + // Extended: 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 () { + this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase()) + } + + // Extended: Convert the selected text to lower case. + // + // For each selection, if the selection is empty, converts the containing word + // to upper case. Otherwise convert the selected text to upper case. + lowerCase () { + this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase()) + } + + // Extended: Toggle line comments for rows intersecting selections. + // + // If the current grammar doesn't support comments, does nothing. + toggleLineCommentsInSelection () { + this.mutateSelectedText(selection => selection.toggleLineComments()) + } + + // 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 () { + this.mutateSelectedText(selection => selection.joinLines()) + } + + // Extended: For each cursor, insert a newline at beginning the following line. + insertNewlineBelow () { + this.transact(() => { + this.moveToEndOfLine() + this.insertNewline() + }) + } + + // Extended: For each cursor, insert a newline at the end of the preceding line. + insertNewlineAbove () { + this.transact(() => { + const bufferRow = this.getCursorBufferPosition().row + const indentLevel = this.indentationForBufferRow(bufferRow) + const onFirstLine = bufferRow === 0 + + this.moveToBeginningOfLine() + this.moveLeft() + this.insertNewline() + + if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) { + this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + if (onFirstLine) { + this.moveUp() + this.moveToEndOfLine() + } + }) + } + + // Extended: 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 () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfWord()) + } + + // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the + // previous word boundary. + deleteToPreviousWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary()) + } + + // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the + // next word boundary. + deleteToNextWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToNextWordBoundary()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing subword following the cursor. Otherwise delete the selected + // text. + deleteToBeginningOfSubword () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing subword following the cursor. Otherwise delete the selected + // text. + deleteToEndOfSubword () { + this.mutateSelectedText(selection => selection.deleteToEndOfSubword()) + } + + // Extended: 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 () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfLine()) + } + + // Extended: 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 () { + this.mutateSelectedText(selection => selection.deleteToEndOfLine()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing word following the cursor. Otherwise delete the selected + // text. + deleteToEndOfWord () { + this.mutateSelectedText(selection => selection.deleteToEndOfWord()) + } + + // Extended: Delete all lines intersecting selections. + deleteLine () { + this.mergeSelectionsOnSameRows() + this.mutateSelectedText(selection => selection.deleteLine()) + } + + /* + Section: History + */ + + // Essential: Undo the last change. + undo () { + this.avoidMergingSelections(() => this.buffer.undo()) + this.getLastSelection().autoscroll() + } + + // Essential: Redo the last change. + redo () { + this.avoidMergingSelections(() => this.buffer.redo()) + this.getLastSelection().autoscroll() + } + + // Extended: 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. + // + // * `groupingInterval` (optional) The {Number} of milliseconds for which this + // transaction should be considered 'groupable' after it begins. If a transaction + // with a positive `groupingInterval` is committed while the previous transaction is + // still 'groupable', the two transactions are merged with respect to undo and redo. + // * `fn` A {Function} to call inside the transaction. + transact (groupingInterval, fn) { + return this.buffer.transact(groupingInterval, fn) + } + + // Extended: Abort an open transaction, undoing any operations performed so far + // within the transaction. + abortTransaction () { return this.buffer.abortTransaction() } + + // Extended: Create a pointer to the current state of the buffer for use + // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. + // + // Returns a checkpoint value. + createCheckpoint () { return this.buffer.createCheckpoint() } + + // Extended: Revert the buffer to the state it was in when the given + // checkpoint was created. + // + // The redo stack will be empty following this operation, so changes since the + // checkpoint will be lost. If the given checkpoint is no longer present in the + // undo history, no changes will be made to the buffer and this method will + // return `false`. + // + // * `checkpoint` The checkpoint to revert to. + // + // Returns a {Boolean} indicating whether the operation succeeded. + revertToCheckpoint (checkpoint) { return this.buffer.revertToCheckpoint(checkpoint) } + + // Extended: Group all changes since the given checkpoint into a single + // transaction for purposes of undo/redo. + // + // If the given checkpoint is no longer present in the undo history, no + // grouping will be performed and this method will return `false`. + // + // * `checkpoint` The checkpoint from which to group changes. + // + // Returns a {Boolean} indicating whether the operation succeeded. + groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) } + + /* + Section: TextEditor Coordinates + */ + + // Essential: 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) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateBufferPosition(bufferPosition, options) + } + + // Essential: 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) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateScreenPosition(screenPosition, options) + } + + // Essential: Convert a range in buffer-coordinates to screen-coordinates. + // + // * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. + // + // Returns a {Range}. + screenRangeForBufferRange (bufferRange, options) { + bufferRange = Range.fromObject(bufferRange) + const start = this.screenPositionForBufferPosition(bufferRange.start, options) + const end = this.screenPositionForBufferPosition(bufferRange.end, options) + return new Range(start, end) + } + + // Essential: Convert a range in screen-coordinates to buffer-coordinates. + // + // * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. + // + // Returns a {Range}. + bufferRangeForScreenRange (screenRange) { + screenRange = Range.fromObject(screenRange) + const start = this.bufferPositionForScreenPosition(screenRange.start) + const end = this.bufferPositionForScreenPosition(screenRange.end) + return new Range(start, end) + } + + // Extended: 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) { return this.buffer.clipPosition(bufferPosition) } + + // Extended: 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) { return this.buffer.clipRange(range) } + + // Extended: 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]` + // ``` + // + // * `screenPosition` The {Point} representing the position to clip. + // * `options` (optional) {Object} + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {Point}. + clipScreenPosition (screenPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.clipScreenPosition(screenPosition, options) + } + + // Extended: Clip the start and end of the given range to valid positions on screen. + // See {::clipScreenPosition} for more information. + // + // * `range` The {Range} to clip. + // * `options` (optional) See {::clipScreenPosition} `options`. + // + // Returns a {Range}. + clipScreenRange (screenRange, options) { + screenRange = Range.fromObject(screenRange) + const start = this.displayLayer.clipScreenPosition(screenRange.start, options) + const end = this.displayLayer.clipScreenPosition(screenRange.end, options) + return Range(start, end) + } + + /* + Section: Decorations + */ + + // Essential: Add a decoration that tracks a {DisplayMarker}. When the + // marker moves, is invalidated, or is destroyed, the decoration will be + // updated to reflect the marker's state. + // + // The following are the supported decorations types: + // + // * __line__: Adds your CSS `class` to the line nodes within the range + // marked by the marker + // * __line-number__: 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 + // 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 + // ```html + //
+ // + //
+ //
+ // ``` + // * __overlay__: Positions the view associated with the given item at the head + // or tail of the given `DisplayMarker`. + // * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter + // decorations are created by calling {Gutter::decorateMarker} on the + // desired `Gutter` instance. + // * __block__: Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`. + // + // ## Arguments + // + // * `marker` A {DisplayMarker} you want this decoration to follow. + // * `decorationParams` An {Object} representing the decoration e.g. + // `{type: 'line-number', class: 'linter-error'}` + // * `type` There are several supported decoration types. The behavior of the + // types are as follows: + // * `line` Adds the given `class` to the lines overlapping the rows + // spanned by the `DisplayMarker`. + // * `line-number` Adds the given `class` to the line numbers overlapping + // the rows spanned by the `DisplayMarker`. + // * `text` Injects spans into all text overlapping the marked range, + // then adds the given `class` or `style` properties to these spans. + // Use this to manipulate the foreground color or styling of text in + // a given range. + // * `highlight` Creates an absolutely-positioned `.highlight` div + // containing nested divs to cover the marked region. For example, this + // is used to implement selections. + // * `overlay` Positions the view associated with the given item at the + // head or tail of the given `DisplayMarker`, depending on the `position` + // property. + // * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling + // {Gutter::decorateMarker} on the desired `Gutter` instance. + // * `block` Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`, depending on the `position` + // property. + // * `cursor` Renders a cursor at the head of the given marker. If multiple + // decorations are created for the same marker, their class strings and + // style objects are combined into a single cursor. You can use this + // decoration type to style existing cursors by passing in their markers + // or render artificial cursors that don't actually exist in the model + // by passing a marker that isn't actually associated with a cursor. + // * `class` This CSS class will be applied to the decorated line number, + // line, text spans, highlight regions, cursors, or overlay. + // * `style` An {Object} containing CSS style properties to apply to the + // relevant DOM node. Currently this only works with a `type` of `cursor` + // or `text`. + // * `item` (optional) An {HTMLElement} or a model {Object} with a + // corresponding view registered. Only applicable to the `gutter`, + // `overlay` and `block` decoration types. + // * `onlyHead` (optional) If `true`, the decoration will only be applied to + // the head of the `DisplayMarker`. Only applicable to the `line` and + // `line-number` decoration types. + // * `onlyEmpty` (optional) If `true`, the decoration will only be applied if + // the associated `DisplayMarker` is empty. Only applicable to the `gutter`, + // `line`, and `line-number` decoration types. + // * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied + // if the associated `DisplayMarker` is non-empty. Only applicable to the + // `gutter`, `line`, and `line-number` decoration types. + // * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied + // to the last row of a non-empty range, even if it ends at column 0. + // Defaults to `true`. Only applicable to the `gutter`, `line`, and + // `line-number` decoration types. + // * `position` (optional) Only applicable to decorations of type `overlay` and `block`. + // Controls where the view is positioned relative to the `TextEditorMarker`. + // Values can be `'head'` (the default) or `'tail'` for overlay decorations, and + // `'before'` (the default) or `'after'` for block decorations. + // * `avoidOverflow` (optional) Only applicable to decorations of type + // `overlay`. Determines whether the decoration adjusts its horizontal or + // vertical position to remain fully visible when it would otherwise + // overflow the editor. Defaults to `true`. + // + // Returns a {Decoration} object + decorateMarker (marker, decorationParams) { + return this.decorationManager.decorateMarker(marker, decorationParams) + } + + // Essential: Add a decoration to every marker in the given marker layer. Can + // be used to decorate a large number of markers without having to create and + // manage many individual decorations. + // + // * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. + // * `decorationParams` The same parameters that are passed to + // {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. + // + // Returns a {LayerDecoration}. + decorateMarkerLayer (markerLayer, decorationParams) { + return this.decorationManager.decorateMarkerLayer(markerLayer, decorationParams) + } + + // Deprecated: Get all the decorations within a screen row range on the default + // layer. + // + // * `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: 'line-number', class: 'someclass'}], 2: ...}` + // where the keys are {DisplayMarker} IDs, and the values are an array of decoration + // params objects attached to the marker. + // Returns an empty object when no decorations are found + decorationsForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) + } + + decorationsStateForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) + } + + // Extended: Get all decorations. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getDecorations (propertyFilter) { + return this.decorationManager.getDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineDecorations (propertyFilter) { + return this.decorationManager.getLineDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line-number'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineNumberDecorations (propertyFilter) { + return this.decorationManager.getLineNumberDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'highlight'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getHighlightDecorations (propertyFilter) { + return this.decorationManager.getHighlightDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'overlay'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getOverlayDecorations (propertyFilter) { + return this.decorationManager.getOverlayDecorations(propertyFilter) + } + + /* + Section: Markers + */ + + // Essential: Create a marker on the default marker layer with the given range + // in buffer coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferRange (bufferRange, options) { + return this.defaultMarkerLayer.markBufferRange(bufferRange, options) + } + + // Essential: Create a marker on the default marker layer with the given range + // in screen coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markScreenRange (screenRange, options) { + return this.defaultMarkerLayer.markScreenRange(screenRange, options) + } + + // Essential: Create a marker on the default marker layer with the given buffer + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `bufferPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferPosition (bufferPosition, options) { + return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options) + } + + // Essential: Create a marker on the default marker layer with the given screen + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `screenPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {DisplayMarker}. + markScreenPosition (screenPosition, options) { + return this.defaultMarkerLayer.markScreenPosition(screenPosition, options) + } + + // Essential: Find all {DisplayMarker}s on the default marker layer that + // match the given properties. + // + // This method finds markers based on the given properties. Markers can be + // associated with custom properties that will be compared with basic equality. + // 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 + // 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 + // coordinates. + // * `endBufferRow` Only include markers ending at this row in buffer + // coordinates. + // * `containsBufferRange` Only include markers containing this {Range} or + // in range-compatible {Array} in buffer coordinates. + // * `containsBufferPosition` Only include markers containing this {Point} + // or {Array} of `[row, column]` in buffer coordinates. + // + // Returns an {Array} of {DisplayMarker}s + findMarkers (params) { + return this.defaultMarkerLayer.findMarkers(params) + } + + // Extended: Get the {DisplayMarker} on the default layer for the given + // marker id. + // + // * `id` {Number} id of the marker + getMarker (id) { + return this.defaultMarkerLayer.getMarker(id) + } + + // Extended: Get all {DisplayMarker}s on the default marker layer. Consider + // using {::findMarkers} + getMarkers () { + return this.defaultMarkerLayer.getMarkers() + } + + // Extended: Get the number of markers in the default marker layer. + // + // Returns a {Number}. + getMarkerCount () { + return this.defaultMarkerLayer.getMarkerCount() + } + + destroyMarker (id) { + const marker = this.getMarker(id) + if (marker) marker.destroy() + } + + // Essential: Create a marker layer to group related markers. + // + // * `options` An {Object} containing the following keys: + // * `maintainHistory` A {Boolean} indicating whether marker state should be + // restored on undo/redo. Defaults to `false`. + // * `persistent` A {Boolean} indicating whether or not this marker layer + // should be serialized and deserialized along with the rest of the + // buffer. Defaults to `false`. If `true`, the marker layer's id will be + // maintained across the serialization boundary, allowing you to retrieve + // it via {::getMarkerLayer}. + // + // Returns a {DisplayMarkerLayer}. + addMarkerLayer (options) { + return this.displayLayer.addMarkerLayer(options) + } + + // Essential: Get a {DisplayMarkerLayer} by id. + // + // * `id` The id of the marker layer to retrieve. + // + // Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the + // given id. + getMarkerLayer (id) { + return this.displayLayer.getMarkerLayer(id) + } + + // Essential: Get the default {DisplayMarkerLayer}. + // + // All marker APIs not tied to an explicit layer interact with this default + // layer. + // + // Returns a {DisplayMarkerLayer}. + getDefaultMarkerLayer () { + return this.defaultMarkerLayer + } + + /* + Section: Cursors + */ + + // Essential: Get the position of the most recently added cursor in buffer + // coordinates. + // + // Returns a {Point} + getCursorBufferPosition () { + return this.getLastCursor().getBufferPosition() + } + + // Essential: Get the position of all the cursor positions in buffer coordinates. + // + // Returns {Array} of {Point}s in the order they were added + getCursorBufferPositions () { + return this.getCursors().map((cursor) => cursor.getBufferPosition()) + } + + // Essential: Move the cursor to the given position in buffer coordinates. + // + // If there are multiple cursors, they will be consolidated to a single cursor. + // + // * `position` A {Point} or {Array} of `[row, column]` + // * `options` (optional) An {Object} containing the following keys: + // * `autoscroll` Determines whether the editor scrolls to the new cursor's + // position. Defaults to true. + setCursorBufferPosition (position, options) { + return this.moveCursors(cursor => cursor.setBufferPosition(position, options)) + } + + // Essential: Get a {Cursor} at given screen coordinates {Point} + // + // * `position` A {Point} or {Array} of `[row, column]` + // + // Returns the first matched {Cursor} or undefined + getCursorAtScreenPosition (position) { + const selection = this.getSelectionAtScreenPosition(position) + if (selection && selection.getHeadScreenPosition().isEqual(position)) { + return selection.cursor + } + } + + // Essential: Get the position of the most recently added cursor in screen + // coordinates. + // + // Returns a {Point}. + getCursorScreenPosition () { + return this.getLastCursor().getScreenPosition() + } + + // Essential: Get the position of all the cursor positions in screen coordinates. + // + // Returns {Array} of {Point}s in the order the cursors were added + getCursorScreenPositions () { + return this.getCursors().map((cursor) => cursor.getScreenPosition()) + } + + // Essential: 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` (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) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.moveCursors(cursor => cursor.setScreenPosition(position, options)) + } + + // Essential: Add a cursor at the given position in buffer coordinates. + // + // * `bufferPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtBufferPosition (bufferPosition, options) { + this.selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Add a cursor at the position in screen coordinates. + // + // * `screenPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtScreenPosition (screenPosition, options) { + this.selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Returns {Boolean} indicating whether or not there are multiple cursors. + hasMultipleCursors () { + return this.getCursors().length > 1 + } + + // Essential: Move every cursor up one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveUp (lineCount) { + return this.moveCursors(cursor => cursor.moveUp(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor down one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveDown (lineCount) { + return this.moveCursors(cursor => cursor.moveDown(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor left one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveLeft (columnCount) { + return this.moveCursors(cursor => cursor.moveLeft(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor right one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveRight (columnCount) { + return this.moveCursors(cursor => cursor.moveRight(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor to the beginning of its line in buffer coordinates. + moveToBeginningOfLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfLine()) + } + + // Essential: Move every cursor to the beginning of its line in screen coordinates. + moveToBeginningOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine()) + } + + // Essential: Move every cursor to the first non-whitespace character of its line. + moveToFirstCharacterOfLine () { + return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine()) + } + + // Essential: Move every cursor to the end of its line in buffer coordinates. + moveToEndOfLine () { + return this.moveCursors(cursor => cursor.moveToEndOfLine()) + } + + // Essential: Move every cursor to the end of its line in screen coordinates. + moveToEndOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToEndOfScreenLine()) + } + + // Essential: Move every cursor to the beginning of its surrounding word. + moveToBeginningOfWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfWord()) + } + + // Essential: Move every cursor to the end of its surrounding word. + moveToEndOfWord () { + return this.moveCursors(cursor => cursor.moveToEndOfWord()) + } + + // Cursor Extended + + // Extended: Move every cursor to the top of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToTop () { + return this.moveCursors(cursor => cursor.moveToTop()) + } + + // Extended: Move every cursor to the bottom of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToBottom () { + return this.moveCursors(cursor => cursor.moveToBottom()) + } + + // Extended: Move every cursor to the beginning of the next word. + moveToBeginningOfNextWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord()) + } + + // Extended: Move every cursor to the previous word boundary. + moveToPreviousWordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary()) + } + + // Extended: Move every cursor to the next word boundary. + moveToNextWordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextWordBoundary()) + } + + // Extended: Move every cursor to the previous subword boundary. + moveToPreviousSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary()) + } + + // Extended: Move every cursor to the next subword boundary. + moveToNextSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary()) + } + + // Extended: Move every cursor to the beginning of the next paragraph. + moveToBeginningOfNextParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph()) + } + + // Extended: Move every cursor to the beginning of the previous paragraph. + moveToBeginningOfPreviousParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfPreviousParagraph()) + } + + // Extended: Returns the most recently added {Cursor} + getLastCursor () { + this.createLastSelectionIfNeeded() + return _.last(this.cursors) + } + + // Extended: Returns the word surrounding the most recently added cursor. + // + // * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. + getWordUnderCursor (options) { + return this.getTextInBufferRange(this.getLastCursor().getCurrentWordBufferRange(options)) + } + + // Extended: Get an Array of all {Cursor}s. + getCursors () { + this.createLastSelectionIfNeeded() + return this.cursors.slice() + } + + // Extended: Get all {Cursors}s, ordered by their position in the buffer + // instead of the order in which they were added. + // + // Returns an {Array} of {Selection}s. + getCursorsOrderedByBufferPosition () { + return this.getCursors().sort((a, b) => a.compare(b)) + } + + cursorsForScreenRowRange (startScreenRow, endScreenRow) { + const cursors = [] + for (let marker of this.selectionsMarkerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + const cursor = this.cursorsByMarkerId.get(marker.id) + if (cursor) cursors.push(cursor) + } + return cursors + } + + // Add a cursor based on the given {DisplayMarker}. + addCursor (marker) { + const cursor = new Cursor({editor: this, marker, showCursorOnSelection: this.showCursorOnSelection}) + this.cursors.push(cursor) + this.cursorsByMarkerId.set(marker.id, cursor) + return cursor + } + + moveCursors (fn) { + return this.transact(() => { + this.getCursors().forEach(fn) + return this.mergeCursors() + }) + } + + cursorMoved (event) { + return this.emitter.emit('did-change-cursor-position', event) + } + + // Merge cursors that have the same screen position + mergeCursors () { + const positions = {} + for (let cursor of this.getCursors()) { + const position = cursor.getBufferPosition().toString() + if (positions.hasOwnProperty(position)) { + cursor.destroy() + } else { + positions[position] = true + } + } + } + + /* + Section: Selections + */ + + // Essential: Get the selected text of the most recently added selection. + // + // Returns a {String}. + getSelectedText () { + return this.getLastSelection().getText() + } + + // Essential: Get the {Range} of the most recently added selection in buffer + // coordinates. + // + // Returns a {Range}. + getSelectedBufferRange () { + return this.getLastSelection().getBufferRange() + } + + // Essential: Get the {Range}s of all selections in buffer coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedBufferRanges () { + return this.getSelections().map((selection) => selection.getBufferRange()) + } + + // Essential: 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. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRange (bufferRange, options) { + return this.setSelectedBufferRanges([bufferRange], options) + } + + // Essential: 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. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRanges (bufferRanges, options = {}) { + if (!bufferRanges.length) throw new Error('Passed an empty array to setSelectedBufferRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(bufferRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < bufferRanges.length; i++) { + let bufferRange = bufferRanges[i] + bufferRange = Range.fromObject(bufferRange) + if (selections[i]) { + selections[i].setBufferRange(bufferRange, options) + } else { + this.addSelectionForBufferRange(bufferRange, options) + } + } + }) + } + + // Essential: Get the {Range} of the most recently added selection in screen + // coordinates. + // + // Returns a {Range}. + getSelectedScreenRange () { + return this.getLastSelection().getScreenRange() + } + + // Essential: Get the {Range}s of all selections in screen coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedScreenRanges () { + return this.getSelections().map((selection) => selection.getScreenRange()) + } + + // Essential: 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) { + return this.setSelectedBufferRange(this.bufferRangeForScreenRange(screenRange, options), options) + } + + // Essential: Set the selected ranges in screen coordinates. If there are multiple + // selections, they are replaced by new selections with the given ranges. + // + // * `screenRanges` 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. + setSelectedScreenRanges (screenRanges, options = {}) { + if (!screenRanges.length) throw new Error('Passed an empty array to setSelectedScreenRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(screenRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < screenRanges.length; i++) { + let screenRange = screenRanges[i] + screenRange = Range.fromObject(screenRange) + if (selections[i]) { + selections[i].setScreenRange(screenRange, options) + } else { + this.addSelectionForScreenRange(screenRange, options) + } + } + }) + } + + // Essential: 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. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // + // Returns the added {Selection}. + addSelectionForBufferRange (bufferRange, options = {}) { + bufferRange = Range.fromObject(bufferRange) + if (!options.preserveFolds) { + this.displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) + } + this.selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed != null ? options.reversed : false}) + if (options.autoscroll !== false) this.getLastSelection().autoscroll() + return this.getLastSelection() + } + + // Essential: Add a selection for the given range in screen coordinates. + // + // * `screenRange` A {Range} + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // Returns the added {Selection}. + addSelectionForScreenRange (screenRange, options = {}) { + return this.addSelectionForBufferRange(this.bufferRangeForScreenRange(screenRange), options) + } + + // Essential: Select from the current cursor position to the given position in + // buffer coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToBufferPosition (position) { + const lastSelection = this.getLastSelection() + lastSelection.selectToBufferPosition(position) + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + + // Essential: Select from the current cursor position to the given position in + // screen coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToScreenPosition (position, options) { + const lastSelection = this.getLastSelection() + lastSelection.selectToScreenPosition(position, options) + if (!options || !options.suppressSelectionMerge) { + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + } + + // Essential: Move the cursor of each selection one character upward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectUp (rowCount) { + return this.expandSelectionsBackward(selection => selection.selectUp(rowCount)) + } + + // Essential: Move the cursor of each selection one character downward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectDown (rowCount) { + return this.expandSelectionsForward(selection => selection.selectDown(rowCount)) + } + + // Essential: Move the cursor of each selection one character leftward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectLeft (columnCount) { + return this.expandSelectionsBackward(selection => selection.selectLeft(columnCount)) + } + + // Essential: Move the cursor of each selection one character rightward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectRight (columnCount) { + return this.expandSelectionsForward(selection => selection.selectRight(columnCount)) + } + + // Essential: Select from the top of the buffer to the end of the last selection + // in the buffer. + // + // This method merges multiple selections into a single selection. + selectToTop () { + return this.expandSelectionsBackward(selection => selection.selectToTop()) + } + + // Essential: Selects from the top of the first selection in the buffer to the end + // of the buffer. + // + // This method merges multiple selections into a single selection. + selectToBottom () { + return this.expandSelectionsForward(selection => selection.selectToBottom()) + } + + // Essential: Select all text in the buffer. + // + // This method merges multiple selections into a single selection. + selectAll () { + return this.expandSelectionsForward(selection => selection.selectAll()) + } + + // Essential: Move the cursor of each selection to the beginning of its line + // while preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToBeginningOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfLine()) + } + + // Essential: Move the cursor of each selection to the first non-whitespace + // character of its line while preserving the selection's tail position. If the + // cursor is already on the first character of the line, move it to the + // beginning of the line. + // + // This method may merge selections that end up intersecting. + selectToFirstCharacterOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToFirstCharacterOfLine()) + } + + // Essential: Move the cursor of each selection to the end of its line while + // preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToEndOfLine () { + return this.expandSelectionsForward(selection => selection.selectToEndOfLine()) + } + + // Essential: Expand selections to the beginning of their containing word. + // + // Operates on all selections. Moves the cursor to the beginning of the + // containing word while preserving the selection's tail position. + selectToBeginningOfWord () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfWord()) + } + + // Essential: Expand selections to the end of their containing word. + // + // Operates on all selections. Moves the cursor to the end of the containing + // word while preserving the selection's tail position. + selectToEndOfWord () { + return this.expandSelectionsForward(selection => selection.selectToEndOfWord()) + } + + // Extended: For each selection, move its cursor to the preceding subword + // boundary while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousSubwordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousSubwordBoundary()) + } + + // Extended: For each selection, move its cursor to the next subword boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextSubwordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextSubwordBoundary()) + } + + // Essential: For each cursor, select the containing line. + // + // This method merges selections on successive lines. + selectLinesContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectLine()) + } + + // Essential: Select the word surrounding each cursor. + selectWordsContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectWord()) + } + + // Selection Extended + + // Extended: For each selection, move its cursor to the preceding word boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousWordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousWordBoundary()) + } + + // Extended: For each selection, move its cursor to the next word boundary while + // maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextWordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextWordBoundary()) + } + + // Extended: Expand selections to the beginning of the next word. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // word while preserving the selection's tail position. + selectToBeginningOfNextWord () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextWord()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfNextParagraph () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextParagraph()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfPreviousParagraph () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph()) + } + + // Extended: Select the range of the given marker if it is valid. + // + // * `marker` A {DisplayMarker} + // + // Returns the selected {Range} or `undefined` if the marker is invalid. + selectMarker (marker) { + if (marker.isValid()) { + const range = marker.getBufferRange() + this.setSelectedBufferRange(range) + return range + } + } + + // Extended: Get the most recently added {Selection}. + // + // Returns a {Selection}. + getLastSelection () { + this.createLastSelectionIfNeeded() + return _.last(this.selections) + } + + getSelectionAtScreenPosition (position) { + const markers = this.selectionsMarkerLayer.findMarkers({containsScreenPosition: position}) + if (markers.length > 0) return this.cursorsByMarkerId.get(markers[0].id).selection + } + + // Extended: Get current {Selection}s. + // + // Returns: An {Array} of {Selection}s. + getSelections () { + this.createLastSelectionIfNeeded() + return this.selections.slice() + } + + // Extended: 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 () { + return this.getSelections().sort((a, b) => a.compare(b)) + } + + // Extended: Determine if a given range in buffer coordinates intersects a + // selection. + // + // * `bufferRange` A {Range} or range-compatible {Array}. + // + // Returns a {Boolean}. + selectionIntersectsBufferRange (bufferRange) { + return this.getSelections().some(selection => selection.intersectsBufferRange(bufferRange)) + } + + // Selections Private + + // Add a similarly-shaped selection to the next eligible line below + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next following non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionBelow () { + return this.expandSelectionsForward(selection => selection.addSelectionBelow()) + } + + // Add a similarly-shaped selection to the next eligible line above + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next preceding non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionAbove () { + return this.expandSelectionsBackward(selection => selection.addSelectionAbove()) + } + + // Calls the given function with each selection, then merges selections + expandSelectionsForward (fn) { + this.mergeIntersectingSelections(() => this.getSelections().forEach(fn)) + } + + // Calls the given function with each selection, then merges selections in the + // reversed orientation + expandSelectionsBackward (fn) { + this.mergeIntersectingSelections({reversed: true}, () => this.getSelections().forEach(fn)) + } + + finalizeSelections () { + for (let selection of this.getSelections()) { selection.finalize() } + } + + selectionsForScreenRows (startRow, endRow) { + return this.getSelections().filter(selection => selection.intersectsScreenRowRange(startRow, endRow)) + } + + // Merges intersecting selections. If passed a function, it executes + // the function with merging suppressed, then merges intersecting selections + // afterward. + mergeIntersectingSelections (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const exclusive = !currentSelection.isEmpty() && !previousSelection.isEmpty() + return previousSelection.intersectsWith(currentSelection, exclusive) + }) + } + + mergeSelectionsOnSameRows (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const screenRange = currentSelection.getScreenRange() + return previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) + }) + } + + avoidMergingSelections (...args) { + return this.mergeSelections(...args, () => false) + } + + mergeSelections (...args) { + const mergePredicate = args.pop() + let fn = args.pop() + let options = args.pop() + if (typeof fn !== 'function') { + options = fn + fn = () => {} + } + + if (this.suppressSelectionMerging) return fn() + + this.suppressSelectionMerging = true + const result = fn() + this.suppressSelectionMerging = false + + const selections = this.getSelectionsOrderedByBufferPosition() + let lastSelection = selections.shift() + for (const selection of selections) { + if (mergePredicate(lastSelection, selection)) { + lastSelection.merge(selection, options) + } else { + lastSelection = selection + } + } + + return result + } + + // Add a {Selection} based on the given {DisplayMarker}. + // + // * `marker` The {DisplayMarker} to highlight + // * `options` (optional) An {Object} that pertains to the {Selection} constructor. + // + // Returns the new {Selection}. + addSelection (marker, options = {}) { + const cursor = this.addCursor(marker) + let selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) + this.selections.push(selection) + const selectionBufferRange = selection.getBufferRange() + this.mergeIntersectingSelections({preserveFolds: options.preserveFolds}) + + if (selection.destroyed) { + for (selection of this.getSelections()) { + if (selection.intersectsBufferRange(selectionBufferRange)) return selection + } + } else { + this.emitter.emit('did-add-cursor', cursor) + this.emitter.emit('did-add-selection', selection) + return selection + } + } + + // Remove the given selection. + removeSelection (selection) { + _.remove(this.cursors, selection.cursor) + _.remove(this.selections, selection) + this.cursorsByMarkerId.delete(selection.cursor.marker.id) + this.emitter.emit('did-remove-cursor', selection.cursor) + return this.emitter.emit('did-remove-selection', selection) + } + + // Reduce one or more selections to a single empty selection based on the most + // recently added cursor. + clearSelections (options) { + this.consolidateSelections() + this.getLastSelection().clear(options) + } + + // Reduce multiple selections to the least recently added selection. + consolidateSelections () { + const selections = this.getSelections() + if (selections.length > 1) { + for (let selection of selections.slice(1, (selections.length))) { selection.destroy() } + selections[0].autoscroll({center: true}) + return true + } else { + return false + } + } + + // Called by the selection + selectionRangeChanged (event) { + if (this.component) this.component.didChangeSelectionRange() + this.emitter.emit('did-change-selection-range', event) + } + + createLastSelectionIfNeeded () { + if (this.selections.length === 0) { + this.addSelectionForBufferRange([[0, 0], [0, 0]], {autoscroll: false, preserveFolds: true}) + } + } + + /* + Section: Searching and Replacing + */ + + // Essential: Scan regular expression matches in the entire buffer, calling the + // given iterator function on each match. + // + // `::scan` functions as the replace method as well via the `replace` + // + // If you're programmatically modifying the results, you may want to try + // {::backwardsScanInBufferRange} to avoid tripping over your own changes. + // + // * `regex` A {RegExp} to search for. + // * `options` (optional) {Object} + // * `leadingContextLineCount` {Number} default `0`; The number of lines + // before the matched line to include in the results object. + // * `trailingContextLineCount` {Number} default `0`; The number of lines + // after the matched line to include in the results object. + // * `iterator` A {Function} that's called on each match + // * `object` {Object} + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scan (regex, options = {}, iterator) { + if (_.isFunction(options)) { + iterator = options + options = {} + } + + return this.buffer.scan(regex, options, iterator) + } + + // Essential: Scan regular expression matches in a given range, calling the given + // iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scanInBufferRange (regex, range, iterator) { return this.buffer.scanInRange(regex, range, iterator) } + + // Essential: Scan regular expression matches in a given range in reverse order, + // calling the given iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + backwardsScanInBufferRange (regex, range, iterator) { return this.buffer.backwardsScanInRange(regex, range, iterator) } + + /* + Section: Tab Behavior + */ + + // Essential: Returns a {Boolean} indicating whether softTabs are enabled for this + // editor. + getSoftTabs () { return this.softTabs } + + // Essential: Enable or disable soft tabs for this editor. + // + // * `softTabs` A {Boolean} + setSoftTabs (softTabs) { + this.softTabs = softTabs + this.update({softTabs: this.softTabs}) + } + + // Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. + hasAtomicSoftTabs () { return this.displayLayer.atomicSoftTabs } + + // Essential: Toggle soft tabs for this editor + toggleSoftTabs () { this.setSoftTabs(!this.getSoftTabs()) } + + // Essential: Get the on-screen length of tab characters. + // + // Returns a {Number}. + getTabLength () { return this.tokenizedBuffer.getTabLength() } + + // Essential: Set the on-screen length of tab characters. Setting this to a + // {Number} This will override the `editor.tabLength` setting. + // + // * `tabLength` {Number} length of a single tab. Setting to `null` will + // fallback to using the `editor.tabLength` config setting + setTabLength (tabLength) { this.update({tabLength}) } + + // Returns an {Object} representing the current invisible character + // substitutions for this editor. See {::setInvisibles}. + getInvisibles () { + if (!this.mini && this.showInvisibles && (this.invisibles != null)) { + return this.invisibles + } else { + return {} + } + } + + doesShowIndentGuide () { return this.showIndentGuide && !this.mini } + + getSoftWrapHangingIndentLength () { return this.displayLayer.softWrapHangingIndent } + + // Extended: 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} or undefined if no non-comment lines had leading + // whitespace. + usesSoftTabs () { + for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) { + const tokenizedLine = this.tokenizedBuffer.tokenizedLines[bufferRow] + if (tokenizedLine && tokenizedLine.isComment()) continue + const line = this.buffer.lineForRow(bufferRow) + if (line[0] === ' ') return true + if (line[0] === '\t') return false + } + } + + // Extended: 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 () { return this.buildIndentString(1) } + + // If soft tabs are enabled, convert all hard tabs to soft tabs in the given + // {Range}. + normalizeTabsInBufferRange (bufferRange) { + if (!this.getSoftTabs()) { return } + return this.scanInBufferRange(/\t/g, bufferRange, ({replace}) => replace(this.getTabText())) + } + + /* + Section: Soft Wrap Behavior + */ + + // Essential: Determine whether lines in this editor are soft-wrapped. + // + // Returns a {Boolean}. + isSoftWrapped () { return this.softWrapped } + + // Essential: Enable or disable soft wrapping for this editor. + // + // * `softWrapped` A {Boolean} + // + // Returns a {Boolean}. + setSoftWrapped (softWrapped) { + this.update({softWrapped}) + return this.isSoftWrapped() + } + + getPreferredLineLength () { return this.preferredLineLength } + + // Essential: Toggle soft wrapping for this editor + // + // Returns a {Boolean}. + toggleSoftWrapped () { return this.setSoftWrapped(!this.isSoftWrapped()) } + + // Essential: Gets the column at which column will soft wrap + getSoftWrapColumn () { + if (this.isSoftWrapped() && !this.mini) { + if (this.softWrapAtPreferredLineLength) { + return Math.min(this.getEditorWidthInChars(), this.preferredLineLength) + } else { + return this.getEditorWidthInChars() + } + } else { + return this.maxScreenLineLength + } + } + + /* + Section: Indentation + */ + + // Essential: Get the indentation level of the given buffer row. + // + // Determines 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) { + return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow)) + } + + // Essential: 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} = {}) { + let endColumn + if (preserveLeadingWhitespace) { + endColumn = 0 + } else { + endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length + } + const newIndentString = this.buildIndentString(newLevel) + return this.buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + } + + // Extended: Indent rows intersecting selections by one level. + indentSelectedRows () { + return this.mutateSelectedText(selection => selection.indentSelectedRows()) + } + + // Extended: Outdent rows intersecting selections by one level. + outdentSelectedRows () { + return this.mutateSelectedText(selection => selection.outdentSelectedRows()) + } + + // Extended: Get the indentation level of the given line of text. + // + // Determines 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) { + return this.tokenizedBuffer.indentLevelForLine(line) + } + + // Extended: Indent rows intersecting selections based on the grammar's suggested + // indent level. + autoIndentSelectedRows () { + return this.mutateSelectedText(selection => selection.autoIndentSelectedRows()) + } + + // Indent all lines intersecting selections. See {Selection::indent} for more + // information. + indent (options = {}) { + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent() + this.mutateSelectedText(selection => selection.indent(options)) + } + + // Constructs the string used for indents. + buildIndentString (level, column = 0) { + if (this.getSoftTabs()) { + const tabStopViolation = column % this.getTabLength() + return _.multiplyString(' ', Math.floor(level * this.getTabLength()) - tabStopViolation) + } else { + const excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * this.getTabLength())) + return _.multiplyString('\t', Math.floor(level)) + excessWhitespace + } + } + + /* + Section: Grammars + */ + + // Essential: Get the current {Grammar} of this editor. + getGrammar () { + return this.tokenizedBuffer.grammar + } + + // Essential: Set the current {Grammar} of this editor. + // + // Assigning a grammar will cause the editor to re-tokenize based on the new + // grammar. + // + // * `grammar` {Grammar} + setGrammar (grammar) { + return this.tokenizedBuffer.setGrammar(grammar) + } + + // Reload the grammar based on the file name. + reloadGrammar () { + return this.tokenizedBuffer.reloadGrammar() + } + + // Experimental: Get a notification when async tokenization is completed. + onDidTokenize (callback) { + return this.tokenizedBuffer.onDidTokenize(callback) + } + + /* + Section: Managing Syntax Scopes + */ + + // Essential: Returns a {ScopeDescriptor} that includes this editor's language. + // e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with + // {Config::get} to get language specific config values. + getRootScopeDescriptor () { + return this.tokenizedBuffer.rootScopeDescriptor + } + + // Essential: Get the syntactic scopeDescriptor for the given position in buffer + // coordinates. Useful with {Config::get}. + // + // 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 a {ScopeDescriptor}. + scopeDescriptorForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) + } + + // Extended: 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")`. + // + // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` + // + // Returns a {Range}. + bufferRangeForScopeAtCursor (scopeSelector) { + return this.bufferRangeForScopeAtPosition(scopeSelector, this.getCursorBufferPosition()) + } + + bufferRangeForScopeAtPosition (scopeSelector, position) { + return this.tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) + } + + // Extended: Determine if the given row is entirely a comment + isBufferRowCommented (bufferRow) { + const match = this.lineTextForBufferRow(bufferRow).match(/\S/) + if (match) { + if (!this.commentScopeSelector) this.commentScopeSelector = new TextMateScopeSelector('comment.*') + return this.commentScopeSelector.matches(this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) + } + } + + // Get the scope descriptor at the cursor. + getCursorScope () { + return this.getLastCursor().getScopeDescriptor() + } + + tokenForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.tokenForPosition(bufferPosition) + } + + /* + Section: Clipboard Operations + */ + + // Essential: For each selection, copy the selected text. + copySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (selection.isEmpty()) { + const previousRange = selection.getBufferRange() + selection.selectLine() + selection.copy(maintainClipboard, true) + selection.setBufferRange(previousRange) + } else { + selection.copy(maintainClipboard, false) + } + maintainClipboard = true + } + } + + // Private: For each selection, only copy highlighted text. + copyOnlySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (!selection.isEmpty()) { + selection.copy(maintainClipboard, false) + maintainClipboard = true + } + } + } + + // Essential: For each selection, cut the selected text. + cutSelectedText () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectLine() + selection.cut(maintainClipboard, true) + } else { + selection.cut(maintainClipboard, false) + } + maintainClipboard = true + }) + } + + // Essential: 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) { + options = Object.assign({}, options) + let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata() + if (!this.emitWillInsertTextEvent(clipboardText)) return false + + if (!metadata) metadata = {} + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndentOnPaste() + + this.mutateSelectedText((selection, index) => { + let fullLine, indentBasis, text + if (metadata.selections && metadata.selections.length === this.getSelections().length) { + ({text, indentBasis, fullLine} = metadata.selections[index]) + } else { + ({indentBasis, fullLine} = metadata) + text = clipboardText + } + + if (indentBasis != null && (text.includes('\n') || !selection.cursor.hasPrecedingCharactersOnLine())) { + options.indentBasis = indentBasis + } else { + options.indentBasis = null + } + + let range + if (fullLine && selection.isEmpty()) { + const oldPosition = selection.getBufferRange().start + selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) + range = selection.insertText(text, options) + const newPosition = oldPosition.translate([1, 0]) + selection.setBufferRange([newPosition, newPosition]) + } else { + range = selection.insertText(text, options) + } + + this.emitter.emit('did-insert-text', {text, range}) + }) + } + + // Essential: For each selection, if the selection is empty, cut all characters + // of the containing screen line following the cursor. Otherwise cut the selected + // text. + cutToEndOfLine () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true + }) + } + + // Essential: For each selection, if the selection is empty, cut all characters + // of the containing buffer line following the cursor. Otherwise cut the + // selected text. + cutToEndOfBufferLine () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfBufferLine(maintainClipboard) + maintainClipboard = true + }) + } + + /* + Section: Folds + */ + + // Essential: 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 () { + const {row} = this.getCursorBufferPosition() + const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + if (range) return this.displayLayer.foldBufferRange(range) + } + + // Essential: Unfold the most recent cursor's row by one level. + unfoldCurrentRow () { + const {row} = this.getCursorBufferPosition() + return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) + } + + // Essential: 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) { + let position = Point(bufferRow, Infinity) + while (true) { + const foldableRange = this.tokenizedBuffer.getFoldableRangeContainingPoint(position, this.getTabLength()) + if (foldableRange) { + const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) + if (existingFolds.length === 0) { + this.displayLayer.foldBufferRange(foldableRange) + } else { + const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0]) + if (firstExistingFoldRange.start.isLessThan(position)) { + position = Point(firstExistingFoldRange.start.row, 0) + continue + } + } + } + break + } + } + + // Essential: Unfold all folds containing the given row in buffer coordinates. + // + // * `bufferRow` A {Number} + unfoldBufferRow (bufferRow) { + const position = Point(bufferRow, Infinity) + return this.displayLayer.destroyFoldsContainingBufferPositions([position]) + } + + // Extended: For each selection, fold the rows it intersects. + foldSelectedLines () { + for (let selection of this.selections) { + selection.fold() + } + } + + // Extended: Fold all foldable lines. + foldAll () { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: Unfold all existing folds. + unfoldAll () { + const result = this.displayLayer.destroyAllFolds() + this.scrollToCursorPosition() + return result + } + + // Extended: Fold all foldable lines at the given indent level. + // + // * `level` A {Number}. + foldAllAtIndentLevel (level) { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: 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) { + return this.tokenizedBuffer.isFoldableAtRow(bufferRow) + } + + // Extended: Determine whether the given row in screen coordinates is foldable. + // + // A *foldable* row is a row that *starts* a row range that can be folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldableAtScreenRow (screenRow) { + return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Extended: Fold the given buffer row if it isn't currently folded, and unfold + // it otherwise. + toggleFoldAtBufferRow (bufferRow) { + if (this.isFoldedAtBufferRow(bufferRow)) { + return this.unfoldBufferRow(bufferRow) + } else { + return this.foldBufferRow(bufferRow) + } + } + + // Extended: Determine whether the most recently added cursor's row is folded. + // + // Returns a {Boolean}. + isFoldedAtCursorRow () { + return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row) + } + + // Extended: Determine whether the given row in buffer coordinates is folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtBufferRow (bufferRow) { + const range = Range( + Point(bufferRow, 0), + Point(bufferRow, this.buffer.lineLengthForRow(bufferRow)) + ) + return this.displayLayer.foldsIntersectingBufferRange(range).length > 0 + } + + // Extended: Determine whether the given row in screen coordinates is folded. + // + // * `screenRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtScreenRow (screenRow) { + return this.isFoldedAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Creates a new fold between two row numbers. + // + // startRow - The row {Number} to start folding at + // endRow - The row {Number} to end the fold + // + // Returns the new {Fold}. + foldBufferRowRange (startRow, endRow) { + return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) + } + + foldBufferRange (range) { + return this.displayLayer.foldBufferRange(range) + } + + // Remove any {Fold}s found that intersect the given buffer range. + destroyFoldsIntersectingBufferRange (bufferRange) { + return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) + } + + // Remove any {Fold}s found that contain the given array of buffer positions. + destroyFoldsContainingBufferPositions (bufferPositions, excludeEndpoints) { + return this.displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) + } + + /* + Section: Gutters + */ + + // Essential: Add a custom {Gutter}. + // + // * `options` An {Object} with the following fields: + // * `name` (required) A unique {String} to identify this gutter. + // * `priority` (optional) A {Number} that determines stacking order between + // gutters. Lower priority items are forced closer to the edges of the + // window. (default: -100) + // * `visible` (optional) {Boolean} specifying whether the gutter is visible + // initially after being created. (default: true) + // + // Returns the newly-created {Gutter}. + addGutter (options) { + return this.gutterContainer.addGutter(options) + } + + // Essential: Get this editor's gutters. + // + // Returns an {Array} of {Gutter}s. + getGutters () { + return this.gutterContainer.getGutters() + } + + getLineNumberGutter () { + return this.lineNumberGutter + } + + // Essential: Get the gutter with the given name. + // + // Returns a {Gutter}, or `null` if no gutter exists for the given name. + gutterWithName (name) { + return this.gutterContainer.gutterWithName(name) + } + + /* + Section: Scrolling the TextEditor + */ + + // Essential: Scroll the editor to reveal the most recently added cursor if it is + // off-screen. + // + // * `options` (optional) {Object} + // * `center` Center the editor around the cursor if possible. (default: true) + scrollToCursorPosition (options) { + this.getLastCursor().autoscroll({center: options && options.center !== false}) + } + + // Essential: 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` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToBufferPosition (bufferPosition, options) { + return this.scrollToScreenPosition(this.screenPositionForBufferPosition(bufferPosition), options) + } + + // Essential: Scrolls the editor to the given screen position. + // + // * `screenPosition` An object that represents a screen position. It can be either + // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + // * `options` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToScreenPosition (screenPosition, options) { + this.scrollToScreenRange(new Range(screenPosition, screenPosition), options) + } + + scrollToTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToTop() + } + + scrollToBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToBottom() + } + + scrollToScreenRange (screenRange, options = {}) { + if (options.clip !== false) screenRange = this.clipScreenRange(screenRange) + const scrollEvent = {screenRange, options} + if (this.component) this.component.didRequestAutoscroll(scrollEvent) + this.emitter.emit('did-request-autoscroll', scrollEvent) + } + + getHorizontalScrollbarHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.') + return this.getElement().getHorizontalScrollbarHeight() + } + + getVerticalScrollbarWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.') + return this.getElement().getVerticalScrollbarWidth() + } + + pageUp () { + this.moveUp(this.getRowsPerPage()) + } + + pageDown () { + this.moveDown(this.getRowsPerPage()) + } + + selectPageUp () { + this.selectUp(this.getRowsPerPage()) + } + + selectPageDown () { + this.selectDown(this.getRowsPerPage()) + } + + // Returns the number of rows per page + getRowsPerPage () { + if (this.component) { + const clientHeight = this.component.getScrollContainerClientHeight() + const lineHeight = this.component.getLineHeight() + return Math.max(1, Math.ceil(clientHeight / lineHeight)) + } else { + return 1 + } + } + + /* + Section: Config + */ + + // Experimental: Supply an object that will provide the editor with settings + // for specific syntactic scopes. See the `ScopedSettingsDelegate` in + // `text-editor-registry.js` for an example implementation. + setScopedSettingsDelegate (scopedSettingsDelegate) { + this.scopedSettingsDelegate = scopedSettingsDelegate + this.tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate + } + + // Experimental: Retrieve the {Object} that provides the editor with settings + // for specific syntactic scopes. + getScopedSettingsDelegate () { return this.scopedSettingsDelegate } + + // Experimental: Is auto-indentation enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndent () { return this.autoIndent } + + // Experimental: Is auto-indentation on paste enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndentOnPaste () { return this.autoIndentOnPaste } + + // Experimental: Does this editor allow scrolling past the last line? + // + // Returns a {Boolean}. + getScrollPastEnd () { + if (this.getAutoHeight()) { + return false + } else { + return this.scrollPastEnd + } + } + + // Experimental: How fast does the editor scroll in response to mouse wheel + // movements? + // + // Returns a positive {Number}. + getScrollSensitivity () { return this.scrollSensitivity } + + // Experimental: Does this editor show cursors while there is a selection? + // + // Returns a positive {Boolean}. + getShowCursorOnSelection () { return this.showCursorOnSelection } + + // Experimental: Are line numbers enabled for this editor? + // + // Returns a {Boolean} + doesShowLineNumbers () { return this.showLineNumbers } + + // Experimental: Get the time interval within which text editing operations + // are grouped together in the editor's undo history. + // + // Returns the time interval {Number} in milliseconds. + getUndoGroupingInterval () { return this.undoGroupingInterval } + + // Experimental: Get the characters that are *not* considered part of words, + // for the purpose of word-based cursor movements. + // + // Returns a {String} containing the non-word characters. + getNonWordCharacters (scopes) { + if (this.scopedSettingsDelegate && this.scopedSettingsDelegate.getNonWordCharacters) { + return this.scopedSettingsDelegate.getNonWordCharacters(scopes) || this.nonWordCharacters + } else { + return this.nonWordCharacters + } + } + + /* + Section: Event Handlers + */ + + handleGrammarChange () { + this.unfoldAll() + return this.emitter.emit('did-change-grammar', this.getGrammar()) + } + + /* + Section: TextEditor Rendering + */ + + // Get the Element for the editor. + getElement () { + if (!this.component) { + if (!TextEditorComponent) TextEditorComponent = require('./text-editor-component') + if (!TextEditorElement) TextEditorElement = require('./text-editor-element') + this.component = new TextEditorComponent({ + model: this, + updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, + initialScrollTopRow: this.initialScrollTopRow, + initialScrollLeftColumn: this.initialScrollLeftColumn + }) + } + return this.component.element + } + + getAllowedLocations () { + return ['center'] + } + + // Essential: Retrieves the greyed out placeholder of a mini editor. + // + // Returns a {String}. + getPlaceholderText () { return this.placeholderText } + + // Essential: Set the greyed out placeholder of a mini editor. Placeholder text + // will be displayed when the editor has no content. + // + // * `placeholderText` {String} text that is displayed when the editor has no content. + setPlaceholderText (placeholderText) { this.update({placeholderText}) } + + pixelPositionForBufferPosition (bufferPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead') + return this.getElement().pixelPositionForBufferPosition(bufferPosition) + } + + pixelPositionForScreenPosition (screenPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead') + return this.getElement().pixelPositionForScreenPosition(screenPosition) + } + + getVerticalScrollMargin () { + const maxScrollMargin = Math.floor(((this.height / this.getLineHeightInPixels()) - 1) / 2) + return Math.min(this.verticalScrollMargin, maxScrollMargin) + } + + setVerticalScrollMargin (verticalScrollMargin) { + this.verticalScrollMargin = verticalScrollMargin + return this.verticalScrollMargin + } + + getHorizontalScrollMargin () { + return Math.min(this.horizontalScrollMargin, Math.floor(((this.width / this.getDefaultCharWidth()) - 1) / 2)) + } + setHorizontalScrollMargin (horizontalScrollMargin) { + this.horizontalScrollMargin = horizontalScrollMargin + return this.horizontalScrollMargin + } + + getLineHeightInPixels () { return this.lineHeightInPixels } + setLineHeightInPixels (lineHeightInPixels) { + this.lineHeightInPixels = lineHeightInPixels + return this.lineHeightInPixels + } + + getKoreanCharWidth () { return this.koreanCharWidth } + getHalfWidthCharWidth () { return this.halfWidthCharWidth } + getDoubleWidthCharWidth () { return this.doubleWidthCharWidth } + getDefaultCharWidth () { return this.defaultCharWidth } + + ratioForCharacter (character) { + if (isKoreanCharacter(character)) { + return this.getKoreanCharWidth() / this.getDefaultCharWidth() + } else if (isHalfWidthCharacter(character)) { + return this.getHalfWidthCharWidth() / this.getDefaultCharWidth() + } else if (isDoubleWidthCharacter(character)) { + return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth() + } else { + return 1 + } + } + + setDefaultCharWidth (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) { + if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth } + if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth } + if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth } + if (defaultCharWidth !== this.defaultCharWidth || + (doubleWidthCharWidth !== this.doubleWidthCharWidth && + halfWidthCharWidth !== this.halfWidthCharWidth && + koreanCharWidth !== this.koreanCharWidth)) { + this.defaultCharWidth = defaultCharWidth + this.doubleWidthCharWidth = doubleWidthCharWidth + this.halfWidthCharWidth = halfWidthCharWidth + this.koreanCharWidth = koreanCharWidth + if (this.isSoftWrapped()) { + this.displayLayer.reset({ + softWrapColumn: this.getSoftWrapColumn() + }) + } + } + return defaultCharWidth + } + + setHeight (height) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setHeight instead.') + this.getElement().setHeight(height) + } + + getHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHeight instead.') + return this.getElement().getHeight() + } + + getAutoHeight () { return this.autoHeight != null ? this.autoHeight : true } + + getAutoWidth () { return this.autoWidth != null ? this.autoWidth : false } + + setWidth (width) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setWidth instead.') + this.getElement().setWidth(width) + } + + getWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getWidth instead.') + return this.getElement().getWidth() + } + + // Use setScrollTopRow instead of this method + setFirstVisibleScreenRow (screenRow) { + this.setScrollTopRow(screenRow) + } + + getFirstVisibleScreenRow () { + return this.getElement().component.getFirstVisibleRow() + } + + getLastVisibleScreenRow () { + return this.getElement().component.getLastVisibleRow() + } + + getVisibleRowRange () { + return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()] + } + + // Use setScrollLeftColumn instead of this method + setFirstVisibleScreenColumn (column) { + return this.setScrollLeftColumn(column) + } + + getFirstVisibleScreenColumn () { + return this.getElement().component.getFirstVisibleColumn() + } + + getScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollTop instead.') + return this.getElement().getScrollTop() + } + + setScrollTop (scrollTop) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollTop instead.') + this.getElement().setScrollTop(scrollTop) + } + + getScrollBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollBottom instead.') + return this.getElement().getScrollBottom() + } + + setScrollBottom (scrollBottom) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollBottom instead.') + this.getElement().setScrollBottom(scrollBottom) + } + + getScrollLeft () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollLeft instead.') + return this.getElement().getScrollLeft() + } + + setScrollLeft (scrollLeft) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollLeft instead.') + this.getElement().setScrollLeft(scrollLeft) + } + + getScrollRight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollRight instead.') + return this.getElement().getScrollRight() + } + + setScrollRight (scrollRight) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollRight instead.') + this.getElement().setScrollRight(scrollRight) + } + + getScrollHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollHeight instead.') + return this.getElement().getScrollHeight() + } + + getScrollWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollWidth instead.') + return this.getElement().getScrollWidth() + } + + getMaxScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getMaxScrollTop instead.') + return this.getElement().getMaxScrollTop() + } + + getScrollTopRow () { + return this.getElement().component.getScrollTopRow() + } + + setScrollTopRow (scrollTopRow) { + this.getElement().component.setScrollTopRow(scrollTopRow) + } + + getScrollLeftColumn () { + return this.getElement().component.getScrollLeftColumn() + } + + setScrollLeftColumn (scrollLeftColumn) { + this.getElement().component.setScrollLeftColumn(scrollLeftColumn) + } + + intersectsVisibleRowRange (startRow, endRow) { + Grim.deprecate('This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.') + return this.getElement().intersectsVisibleRowRange(startRow, endRow) + } + + selectionIntersectsVisibleRowRange (selection) { + Grim.deprecate('This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.') + return this.getElement().selectionIntersectsVisibleRowRange(selection) + } + + screenPositionForPixelPosition (pixelPosition) { + Grim.deprecate('This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.') + return this.getElement().screenPositionForPixelPosition(pixelPosition) + } + + pixelRectForScreenRange (screenRange) { + Grim.deprecate('This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.') + return this.getElement().pixelRectForScreenRange(screenRange) + } + + /* + Section: Utility + */ + + inspect () { + return `` + } + + emitWillInsertTextEvent (text) { + let result = true + const cancel = () => { result = false } + this.emitter.emit('will-insert-text', {cancel, text}) + return result + } + + /* + Section: Language Mode Delegated Methods + */ + + suggestedIndentForBufferRow (bufferRow, options) { + return this.tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) + } + + // Given a buffer row, indent it. + // + // * bufferRow - The row {Number}. + // * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. + autoIndentBufferRow (bufferRow, options) { + const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options) + return this.setIndentationForBufferRow(bufferRow, indentLevel, options) + } + + // Indents all the rows between two buffer row numbers. + // + // * startRow - The row {Number} to start at + // * endRow - The row {Number} to end at + autoIndentBufferRows (startRow, endRow) { + let row = startRow + while (row <= endRow) { + this.autoIndentBufferRow(row) + row++ + } + } + + autoDecreaseIndentForBufferRow (bufferRow) { + const indentLevel = this.tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) + if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) } + + toggleLineCommentsForBufferRows (start, end) { + let { + commentStartString, + commentEndString + } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0)) + if (!commentStartString) return + commentStartString = commentStartString.trim() + + if (commentEndString) { + commentEndString = commentEndString.trim() + const startDelimiterColumnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(start), + commentStartString + ) + if (startDelimiterColumnRange) { + const endDelimiterColumnRange = columnRangeForEndDelimiter( + this.buffer.lineForRow(end), + commentEndString + ) + if (endDelimiterColumnRange) { + this.buffer.transact(() => { + this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) + this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) + }) + } + } else { + this.buffer.transact(() => { + const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length + this.buffer.insert([start, indentLength], commentStartString + ' ') + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) + }) + } + } else { + let hasCommentedLines = false + let hasUncommentedLines = false + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEXP.test(line)) { + if (columnRangeForStartDelimiter(line, commentStartString)) { + hasCommentedLines = true + } else { + hasUncommentedLines = true + } + } + } + + const shouldUncomment = hasCommentedLines && !hasUncommentedLines + + if (shouldUncomment) { + for (let row = start; row <= end; row++) { + const columnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(row), + commentStartString + ) + if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) + } + } else { + let minIndentLevel = Infinity + let minBlankIndentLevel = Infinity + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const indentLevel = this.indentLevelForLine(line) + if (NON_WHITESPACE_REGEXP.test(line)) { + if (indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else { + if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel + } + } + minIndentLevel = Number.isFinite(minIndentLevel) + ? minIndentLevel + : Number.isFinite(minBlankIndentLevel) + ? minBlankIndentLevel + : 0 + + const tabLength = this.getTabLength() + const indentString = ' '.repeat(tabLength * minIndentLevel) + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEXP.test(line)) { + const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength()) + this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') + } else { + this.buffer.setTextInRange( + new Range(new Point(row, 0), new Point(row, Infinity)), + indentString + commentStartString + ' ' + ) + } + } + } + } + } + + rowRangeForParagraphAtBufferRow (bufferRow) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return + + const isCommented = this.tokenizedBuffer.isRowCommented(bufferRow) + + let startRow = bufferRow + while (startRow > 0) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break + if (this.tokenizedBuffer.isRowCommented(startRow - 1) !== isCommented) break + startRow-- + } + + let endRow = bufferRow + const rowCount = this.getLineCount() + while (endRow < rowCount) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break + if (this.tokenizedBuffer.isRowCommented(endRow + 1) !== isCommented) break + endRow++ + } + + return new Range(new Point(startRow, 0), new Point(endRow, this.buffer.lineLengthForRow(endRow))) + } +} + +function columnForIndentLevel (line, indentLevel, tabLength) { + let column = 0 + let indentLength = 0 + const goalIndentLength = indentLevel * tabLength + while (indentLength < goalIndentLength) { + const char = line[column] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + column++ + } + return column +} + +function columnRangeForStartDelimiter (line, delimiter) { + const startColumn = line.search(NON_WHITESPACE_REGEXP) + if (startColumn === -1) return null + if (!line.startsWith(delimiter, startColumn)) return null + + let endColumn = startColumn + delimiter.length + if (line[endColumn] === ' ') endColumn++ + return [startColumn, endColumn] +} + +function columnRangeForEndDelimiter (line, delimiter) { + let startColumn = line.lastIndexOf(delimiter) + if (startColumn === -1) return null + + const endColumn = startColumn + delimiter.length + if (NON_WHITESPACE_REGEXP.test(line.slice(endColumn))) return null + if (line[startColumn - 1] === ' ') startColumn-- + return [startColumn, endColumn] +} + +class ChangeEvent { + constructor ({oldRange, newRange}) { + this.oldRange = oldRange + this.newRange = newRange + } + + get start () { + return this.newRange.start + } + + get oldExtent () { + return this.oldRange.getExtent() + } + + get newExtent () { + return this.newRange.getExtent() + } +} From 616ebe71d940e28ecdee1522347e21444155f9ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:51:14 -0700 Subject: [PATCH 146/161] Convert text-editor-spec.coffee to JavaScript --- spec/text-editor-spec.coffee | 5873 ------------------------------ spec/text-editor-spec.js | 6656 +++++++++++++++++++++++++++++++++- 2 files changed, 6653 insertions(+), 5876 deletions(-) delete mode 100644 spec/text-editor-spec.coffee diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee deleted file mode 100644 index b8d4bdcf9..000000000 --- a/spec/text-editor-spec.coffee +++ /dev/null @@ -1,5873 +0,0 @@ -path = require 'path' -clipboard = require '../src/safe-clipboard' -TextEditor = require '../src/text-editor' -TextBuffer = require 'text-buffer' - -describe "TextEditor", -> - [buffer, editor, lineLengths] = [] - - convertToHardTabs = (buffer) -> - buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', {autoIndent: false}).then (o) -> editor = o - - runs -> - buffer = editor.buffer - editor.update({autoIndent: false}) - lineLengths = buffer.getLines().map (line) -> line.length - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - describe "when the editor is deserialized", -> - it "restores selections and folds based on markers in the buffer", -> - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.id).toBe editor.id - expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() - expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - editor2.destroy() - - it "restores the editor's layout configuration", -> - editor.update({ - softTabs: true - atomicSoftTabs: false - tabLength: 12 - softWrapped: true - softWrapAtPreferredLineLength: true - softWrapHangingIndentLength: 8 - invisibles: {space: 'S'} - showInvisibles: true - editorWidthInChars: 120 - }) - - # Force buffer and display layer to be deserialized as well, rather than - # reusing the same buffer instance - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) - expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) - expect(editor2.getTabLength()).toBe(editor.getTabLength()) - expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) - expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) - expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) - expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) - expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) - expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) - - it "ignores buffers with retired IDs", -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> null} - }) - - expect(editor2).toBeNull() - - describe "when the editor is constructed with the largeFileMode option set to true", -> - it "loads the editor but doesn't tokenize", -> - editor = null - - waitsForPromise -> - atom.workspace.openTextFile('sample.js', largeFileMode: true).then (o) -> editor = o - - runs -> - buffer = editor.getBuffer() - expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0) - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.insertText('hey"') - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - - describe ".copy()", -> - it "returns a different editor with the same initial state", -> - expect(editor.getAutoHeight()).toBeFalsy() - expect(editor.getAutoWidth()).toBeFalsy() - expect(editor.getShowCursorOnSelection()).toBeTruthy() - - element = editor.getElement() - element.setHeight(100) - element.setWidth(100) - jasmine.attachToDOM(element) - - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true) - editor.setScrollTopRow(3) - expect(editor.getScrollTopRow()).toBe(3) - editor.setScrollLeftColumn(4) - expect(editor.getScrollLeftColumn()).toBe(4) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - editor2 = editor.copy() - element2 = editor2.getElement() - element2.setHeight(100) - element2.setWidth(100) - jasmine.attachToDOM(element2) - expect(editor2.id).not.toBe editor.id - expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges() - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.getScrollTopRow()).toBe(3) - expect(editor2.getScrollLeftColumn()).toBe(4) - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor2.getAutoWidth()).toBe(false) - expect(editor2.getAutoHeight()).toBe(false) - expect(editor2.getShowCursorOnSelection()).toBeFalsy() - - # editor2 can now diverge from its origin edit session - editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - editor2.unfoldBufferRow(4) - expect(editor2.isFoldedAtBufferRow(4)).not.toBe editor.isFoldedAtBufferRow(4) - - describe ".update()", -> - it "updates the editor with the supplied config parameters", -> - element = editor.element # force element initialization - element.setUpdatedSynchronously(false) - editor.update({showInvisibles: true}) - editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) - - returnedPromise = editor.update({ - tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40, - showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true, - autoHeight: false, maxScreenLineLength: 1000 - }) - - expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) - expect(changeSpy.callCount).toBe(1) - expect(editor.getTabLength()).toBe(6) - expect(editor.getSoftTabs()).toBe(false) - expect(editor.isSoftWrapped()).toBe(true) - expect(editor.getEditorWidthInChars()).toBe(40) - expect(editor.getInvisibles()).toEqual({}) - expect(editor.isMini()).toBe(false) - expect(editor.isLineNumberGutterVisible()).toBe(false) - expect(editor.getScrollPastEnd()).toBe(true) - expect(editor.getAutoHeight()).toBe(false) - - describe "title", -> - describe ".getTitle()", -> - it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> - expect(editor.getTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getTitle()).toBe 'untitled' - - describe ".getLongTitle()", -> - it "returns file name when there is no opened file with identical name", -> - expect(editor.getLongTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getLongTitle()).toBe 'untitled' - - it "returns ' — ' when opened files have identical file names", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-1', 'readme')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" - expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - - it "returns ' — ' when opened files have identical file names in subdirectories", -> - editor1 = null - editor2 = null - path1 = path.join('sample-theme-1', 'src', 'js') - path2 = path.join('sample-theme-2', 'src', 'js') - waitsForPromise -> - atom.workspace.open(path.join(path1, 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join(path2, 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}" - expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}" - - it "returns ' — ' when opened files have identical file and same parent dir name", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin') - - it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangeTitle (title) -> observed.push(title) - - buffer.setPath('/foo/bar/baz.txt') - buffer.setPath(undefined) - - expect(observed).toEqual ['baz.txt', 'untitled'] - - describe "path", -> - it "notifies ::onDidChangePath observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangePath (filePath) -> observed.push(filePath) - - buffer.setPath(__filename) - buffer.setPath(undefined) - - expect(observed).toEqual [__filename, undefined] - - describe "encoding", -> - it "notifies ::onDidChangeEncoding observers when the editor encoding changes", -> - observed = [] - editor.onDidChangeEncoding (encoding) -> observed.push(encoding) - - editor.setEncoding('utf16le') - editor.setEncoding('utf16le') - editor.setEncoding('utf16be') - editor.setEncoding() - editor.setEncoding() - - expect(observed).toEqual ['utf16le', 'utf16be', 'utf8'] - - describe "cursor", -> - describe ".getLastCursor()", -> - it "returns the most recently created cursor", -> - editor.addCursorAtScreenPosition([1, 0]) - lastCursor = editor.addCursorAtScreenPosition([2, 0]) - expect(editor.getLastCursor()).toBe lastCursor - - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) - - describe ".getCursors()", -> - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) - - describe "when the cursor moves", -> - it "clears a goal column established by vertical movement", -> - editor.setText('b') - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - editor.moveUp() - editor.insertText('a') - editor.moveDown() - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - it "emits an event with the old position, new position, and the cursor that moved", -> - cursorCallback = jasmine.createSpy('cursor-changed-position') - editorCallback = jasmine.createSpy('editor-changed-cursor-position') - - editor.getLastCursor().onDidChangePosition(cursorCallback) - editor.onDidChangeCursorPosition(editorCallback) - - editor.setCursorBufferPosition([2, 4]) - - expect(editorCallback).toHaveBeenCalled() - expect(cursorCallback).toHaveBeenCalled() - eventObject = editorCallback.mostRecentCall.args[0] - expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) - - expect(eventObject.oldBufferPosition).toEqual [0, 0] - expect(eventObject.oldScreenPosition).toEqual [0, 0] - expect(eventObject.newBufferPosition).toEqual [2, 4] - expect(eventObject.newScreenPosition).toEqual [2, 4] - expect(eventObject.cursor).toBe editor.getLastCursor() - - describe ".setCursorScreenPosition(screenPosition)", -> - it "clears a goal column established by vertical movement", -> - # set a goal column by moving down - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - editor.moveDown() - expect(editor.getCursorScreenPosition().column).not.toBe 6 - - # clear the goal column by explicitly setting the cursor position - editor.setCursorScreenPosition([4, 6]) - expect(editor.getCursorScreenPosition().column).toBe 6 - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe 6 - - it "merges multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - [cursor1, cursor2] = editor.getCursors() - editor.setCursorScreenPosition([4, 7]) - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()).toEqual [cursor1] - expect(editor.getCursorScreenPosition()).toEqual [4, 7] - - describe "when soft-wrap is enabled and code is folded", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - editor.foldBufferRowRange(2, 3) - - it "positions the cursor at the buffer position that corresponds to the given screen position", -> - editor.setCursorScreenPosition([9, 0]) - expect(editor.getCursorBufferPosition()).toEqual [8, 11] - - describe ".moveUp()", -> - it "moves the cursor up", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - it "retains the goal column across lines of differing length", -> - expect(lineLengths[6]).toBeGreaterThan(32) - editor.setCursorScreenPosition(row: 6, column: 32) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 32 - - describe "when the cursor is on the first line", -> - it "moves the cursor to the beginning of the line, but retains the goal column", -> - editor.setCursorScreenPosition([0, 4]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual([1, 4]) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves above the selection", -> - cursor = editor.getLastCursor() - editor.moveUp() - expect(cursor.getBufferPosition()).toEqual [3, 9] - - it "merges cursors when they overlap", -> - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveUp() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe "when the cursor was moved down from the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the previous line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - describe ".moveDown()", -> - it "moves the cursor down", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [3, 2] - - it "retains the goal column across lines of differing length", -> - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[3] - - describe "when the cursor is on the last line", -> - it "moves the cursor to the end of line, but retains the goal column when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: editor.getTabLength()) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe editor.getTabLength() - - it "retains a goal column of 0 when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: 0) - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 0 - - describe "when the cursor is at the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the line's continuation on the next screen row", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves below the selection", -> - cursor = editor.getLastCursor() - editor.moveDown() - expect(cursor.getBufferPosition()).toEqual [6, 10] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([11, 2]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveDown() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveLeft()", -> - it "moves the cursor by one column to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [1, 7] - - it "moves the cursor by n columns to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 4] - - it "moves the cursor by two rows up when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveLeft(34) - expect(editor.getCursorScreenPosition()).toEqual [0, 29] - - it "moves the cursor to the beginning columnCount is longer than the position in the buffer", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(100) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when the cursor is in the first column", -> - describe "when there is a previous line", -> - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition(row: 1, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) - - it "moves the cursor by one row up and n columns to the left", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 26] - - describe "when the next line is empty", -> - it "wraps to the beginning of the previous line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when line is wrapped and follow previous line indentation", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition([4, 4]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [3, 46] - - describe "when the cursor is on the first line", -> - it "remains in the same position (0,0)", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - it "remains in the same position (0,0) when columnCount is specified", -> - editor.setCursorScreenPosition([0, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> - it "skips tabLength worth of whitespace at a time", -> - editor.setCursorBufferPosition([5, 6]) - - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [5, 4] - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 22] - - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 21] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - - [cursor1, cursor2] = editor.getCursors() - editor.moveLeft() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe ".moveRight()", -> - it "moves the cursor by one column to the right", -> - editor.setCursorScreenPosition([3, 3]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - it "moves the cursor by n columns to the right", -> - editor.setCursorScreenPosition([3, 7]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [3, 11] - - it "moves the cursor by two rows down when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([0, 29]) - editor.moveRight(34) - expect(editor.getCursorScreenPosition()).toEqual [2, 2] - - it "moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position", -> - editor.setCursorScreenPosition([11, 5]) - editor.moveRight(100) - expect(editor.getCursorScreenPosition()).toEqual [12, 2] - - describe "when the cursor is on the last column of a line", -> - describe "when there is a subsequent line", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [1, 0] - - it "moves the cursor by one row down and n columns to the right", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 3] - - describe "when the next line is empty", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([9, 4]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when the cursor is on the last line", -> - it "remains in the same position", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - lastPosition = {row: lastLineIndex, column: lastLine.length} - editor.setCursorScreenPosition(lastPosition) - editor.moveRight() - - expect(editor.getCursorScreenPosition()).toEqual(lastPosition) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 27] - - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 28] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([12, 1]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveRight() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveToTop()", -> - it "moves the cursor to the top of the buffer", -> - editor.setCursorScreenPosition [11, 1] - editor.addCursorAtScreenPosition [12, 0] - editor.moveToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBottom()", -> - it "moves the cursor to the bottom of the buffer", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - - describe ".moveToBeginningOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 0] - - describe "when soft wrap is off", -> - it "moves cursor to the beginning of the line", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - editor.moveToBeginningOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - describe ".moveToEndOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToEndOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 9] - - describe "when soft wrap is off", -> - it "moves cursor to the end of line", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToEndOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - - describe ".moveToBeginningOfLine()", -> - it "moves cursor to the beginning of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [0, 0] - - describe ".moveToEndOfLine()", -> - it "moves cursor to the end of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([0, 2]) - editor.moveToEndOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [4, 4] - - describe ".moveToFirstCharacterOfLine()", -> - describe "when soft wrap is on", -> - it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition [2, 5] - editor.addCursorAtScreenPosition [8, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - describe "when soft wrap is off", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - it "moves to the beginning of the line if it only contains whitespace ", -> - editor.setText("first\n \nthird") - editor.setCursorScreenPosition [1, 2] - editor.moveToFirstCharacterOfLine() - cursor = editor.getLastCursor() - expect(cursor.getBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with soft tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with hard tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', normalizeLineEndings: false) - - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 3] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe ".moveToBeginningOfWord()", -> - it "moves the cursor to the beginning of the word", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [1, 12] - editor.addCursorAtBufferPosition [3, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - expect(cursor3.getBufferPosition()).toEqual [2, 39] - - it "does not fail at position [0, 0]", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveToBeginningOfWord() - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - editor.buffer.setText(buffer.getText().replace(/\r\n/g, "\n")) - - describe ".moveToPreviousWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [2, 4] - editor.addCursorAtBufferPosition [3, 14] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToPreviousWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - expect(cursor3.getBufferPosition()).toEqual [2, 0] - expect(cursor4.getBufferPosition()).toEqual [3, 13] - - describe ".moveToNextWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [3, 0] - editor.addCursorAtBufferPosition [3, 30] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToNextWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 0] - expect(cursor3.getBufferPosition()).toEqual [3, 4] - expect(cursor4.getBufferPosition()).toEqual [3, 31] - - describe ".moveToEndOfWord()", -> - it "moves the cursor to the end of the word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 10] - editor.addCursorAtBufferPosition [2, 40] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToEndOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - expect(cursor3.getBufferPosition()).toEqual [3, 7] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - describe ".moveToBeginningOfNextWord()", -> - it "moves the cursor before the first character of the next word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 11] - editor.addCursorAtBufferPosition [2, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfNextWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - expect(cursor3.getBufferPosition()).toEqual [2, 4] - - # When the cursor is on whitespace - editor.setText("ab cde- ") - editor.setCursorBufferPosition [0, 2] - cursor = editor.getLastCursor() - editor.moveToBeginningOfNextWord() - - expect(cursor.getBufferPosition()).toEqual [0, 3] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 9] - - describe ".moveToPreviousSubwordBoundary", -> - it "does not move the cursor when there is no previous subword boundary", -> - editor.setText('') - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText("sub_word \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 8]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - editor.setText(" word\n") - editor.setCursorBufferPosition([0, 3]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "stops at camelCase boundaries", -> - editor.setText(" getPreviousWord\n") - editor.setCursorBufferPosition([0, 16]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 12]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive non-word characters", -> - editor.setText("e, => \n") - editor.setCursorBufferPosition([0, 6]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 7]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 8]) - editor.addCursorAtBufferPosition([1, 13]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToPreviousSubwordBoundary() - - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 8]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToNextSubwordBoundary", -> - it "does not move the cursor when there is no next subword boundary", -> - editor.setText('') - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText(" sub_word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 9]) - - editor.setText("word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "stops at camelCase boundaries", -> - editor.setText("getPreviousWord \n") - editor.setCursorBufferPosition([0, 0]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 11]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - - it "skips consecutive non-word characters", -> - editor.setText(", => \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToNextSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToBeginningOfNextParagraph()", -> - it "moves the cursor before the first line of the next paragraph", -> - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the next paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBeginningOfPreviousParagraph()", -> - it "moves the cursor before the first line of the previous paragraph", -> - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the previous paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".getCurrentParagraphBufferRange()", -> - it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> - buffer.setText """ - I am the first paragraph, - bordered by the beginning of - the file - #{' '} - - I am the second paragraph - with blank lines above and below - me. - - I am the last paragraph, - bordered by the end of the file. - """ - - # in a paragraph - editor.setCursorBufferPosition([1, 7]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] - - editor.setCursorBufferPosition([7, 1]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] - - editor.setCursorBufferPosition([9, 10]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] - - # between paragraphs - editor.setCursorBufferPosition([3, 1]) - expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() - - it 'will limit paragraph range to comments', -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setText(""" - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; - - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - item; - } - - }; - """) - - paragraphBufferRangeForRow = (row) -> - editor.setCursorBufferPosition([row, 0]) - editor.getLastCursor().getCurrentParagraphBufferRange() - - expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) - expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) - expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) - expect(paragraphBufferRangeForRow(3)).toBeFalsy() - expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) - expect(paragraphBufferRangeForRow(9)).toBeFalsy() - expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) - expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) - expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) - - describe "getCursorAtScreenPosition(screenPosition)", -> - it "returns the cursor at the given screenPosition", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) - expect(cursor2).toBe cursor1 - - describe "::getCursorScreenPositions()", -> - it "returns the cursor positions in the order they were added", -> - editor.foldBufferRow(4) - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([3, 5]) - expect(editor.getCursorScreenPositions()).toEqual [[0, 0], [5, 5], [3, 5]] - - describe "::getCursorsOrderedByBufferPosition()", -> - it "returns all cursors ordered by buffer positions", -> - originalCursor = editor.getLastCursor() - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([4, 5]) - expect(editor.getCursorsOrderedByBufferPosition()).toEqual [originalCursor, cursor2, cursor1] - - describe "addCursorAtScreenPosition(screenPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.addCursorAtScreenPosition([0, 2]) - expect(cursor2).toBe cursor1 - - describe "addCursorAtBufferPosition(bufferPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtBufferPosition([1, 4]) - cursor2 = editor.addCursorAtBufferPosition([1, 4]) - expect(cursor2.marker).toBe cursor1.marker - - describe '.getCursorScope()', -> - it 'returns the current scope', -> - descriptor = editor.getCursorScope() - expect(descriptor.scopes).toContain('source.js') - - describe "selection", -> - selection = null - - beforeEach -> - selection = editor.getLastSelection() - - describe ".getLastSelection()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - - it "doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", -> - callCount = 0 - editor.getLastSelection().destroy() - editor.onDidAddCursor (cursor) -> - callCount++ - editor.getLastSelection() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - expect(callCount).toBe(1) - - describe ".getSelections()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) - - describe "when the selection range changes", -> - it "emits an event with the old range, new range, and the selection that moved", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - - editor.onDidChangeSelectionRange rangeChangedHandler = jasmine.createSpy() - editor.selectToBufferPosition([6, 2]) - - expect(rangeChangedHandler).toHaveBeenCalled() - eventObject = rangeChangedHandler.mostRecentCall.args[0] - - expect(eventObject.oldBufferRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.oldScreenRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.newBufferRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.newScreenRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.selection).toBe selection - - describe ".selectUp/Down/Left/Right()", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 14]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 22]] - - editor.selectLeft() - editor.selectLeft() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown() - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - editor.selectUp() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - it "merges selections when they intersect when moving down", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) - [selection1, selection2, selection3] = editor.getSelections() - - editor.selectDown() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) - expect(selection1.isReversed()).toBeFalsy() - - it "merges selections when they intersect when moving up", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectUp() - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving left", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectLeft() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving right", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) - expect(selection1.isReversed()).toBeFalsy() - - describe "when counts are passed into the selection functions", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 15]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 23]] - - editor.selectLeft(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [3, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [6, 20]] - - editor.selectUp(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - describe ".selectToBufferPosition(bufferPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtBufferPosition([5, 6]) - editor.selectToBufferPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getBufferRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getBufferRange()).toEqual [[5, 6], [6, 2]] - - describe ".selectToScreenPosition(screenPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]] - - describe "when selecting with an initial screen range", -> - it "switches the direction of the selection when selecting to positions before/after the start of the initial range", -> - editor.setCursorScreenPosition([5, 10]) - editor.selectWordsContainingCursors() - editor.selectToScreenPosition([3, 0]) - expect(editor.getLastSelection().isReversed()).toBe true - editor.selectToScreenPosition([9, 0]) - expect(editor.getLastSelection().isReversed()).toBe false - - describe ".selectToBeginningOfNextParagraph()", -> - it "selects from the cursor to first line of the next paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfNextParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]] - - describe ".selectToBeginningOfPreviousParagraph()", -> - it "selects from the cursor to the first line of the previous paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfPreviousParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[0, 0], [5, 6]] - - it "merges selections if they intersect, maintaining the directionality of the last selection", -> - editor.setCursorScreenPosition([4, 10]) - editor.selectToScreenPosition([5, 27]) - editor.addCursorAtScreenPosition([3, 10]) - editor.selectToScreenPosition([6, 27]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [6, 27]] - expect(selection1.isReversed()).toBeFalsy() - - editor.addCursorAtScreenPosition([7, 4]) - editor.selectToScreenPosition([4, 11]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [7, 4]] - expect(selection1.isReversed()).toBeTruthy() - - describe ".selectToTop()", -> - it "selects text from cursor position to the top of the buffer", -> - editor.setCursorScreenPosition [11, 2] - editor.addCursorAtScreenPosition [10, 0] - editor.selectToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.getLastSelection().getBufferRange()).toEqual [[0, 0], [11, 2]] - expect(editor.getLastSelection().isReversed()).toBeTruthy() - - describe ".selectToBottom()", -> - it "selects text from cursor position to the bottom of the buffer", -> - editor.setCursorScreenPosition [10, 0] - editor.addCursorAtScreenPosition [9, 3] - editor.selectToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - expect(editor.getLastSelection().getBufferRange()).toEqual [[9, 3], [12, 2]] - expect(editor.getLastSelection().isReversed()).toBeFalsy() - - describe ".selectAll()", -> - it "selects the entire buffer", -> - editor.selectAll() - expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange() - - describe ".selectToBeginningOfLine()", -> - it "selects text from cursor position to beginning of line", -> - editor.setCursorScreenPosition [12, 2] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToBeginningOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 0] - expect(cursor2.getBufferPosition()).toEqual [11, 0] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[11, 0], [11, 3]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfLine()", -> - it "selects text from cursor position to end of line", -> - editor.setCursorScreenPosition [12, 0] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToEndOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 2] - expect(cursor2.getBufferPosition()).toEqual [11, 44] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[11, 3], [11, 44]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectLinesContainingCursors()", -> - it "selects to the entire line (including newlines) at given row", -> - editor.setCursorScreenPosition([1, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 0]] - expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" - - editor.setCursorScreenPosition([12, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 0], [12, 2]] - - editor.setCursorBufferPosition([0, 2]) - editor.selectLinesContainingCursors() - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [2, 0]] - - describe "when the selection spans multiple row", -> - it "selects from the beginning of the first line to the last line", -> - selection = editor.getLastSelection() - selection.setBufferRange [[1, 10], [3, 20]] - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]] - - describe ".selectToBeginningOfWord()", -> - it "selects text from cursor position to beginning of word", -> - editor.setCursorScreenPosition [0, 13] - editor.addCursorAtScreenPosition [3, 49] - - editor.selectToBeginningOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [3, 47] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[3, 47], [3, 49]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfWord()", -> - it "selects text from cursor position to end of word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToEndOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 50] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 50]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToBeginningOfNextWord()", -> - it "selects text from cursor position to beginning of next word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToBeginningOfNextWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [3, 51] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 14]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 51]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToPreviousWordBoundary()", -> - it "select to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [3, 4] - editor.addCursorAtBufferPosition [3, 14] - - editor.selectToPreviousWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 4]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[2, 0], [1, 30]] - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual [[3, 4], [3, 0]] - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual [[3, 14], [3, 13]] - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextWordBoundary()", -> - it "select to the next word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [4, 0] - editor.addCursorAtBufferPosition [3, 30] - - editor.selectToNextWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[2, 40], [3, 0]] - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual [[4, 0], [4, 4]] - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual [[3, 30], [3, 31]] - expect(selection4.isReversed()).toBeFalsy() - - describe ".selectToPreviousSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToPreviousSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 1]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToNextSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeFalsy() - - describe ".deleteToBeginningOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe(' getviousWord') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 1]) - expect(cursor2.getBufferPosition()).toEqual([1, 4]) - expect(cursor3.getBufferPosition()).toEqual([2, 3]) - expect(cursor4.getBufferPosition()).toEqual([3, 1]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe(' viousWord') - expect(buffer.lineForRow(2)).toBe('e ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 1]) - expect(cursor3.getBufferPosition()).toEqual([2, 1]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('viousWord') - expect(buffer.lineForRow(2)).toBe(' ') - expect(buffer.lineForRow(3)).toBe('') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 0]) - expect(cursor4.getBufferPosition()).toEqual([2, 1]) - - describe ".deleteToEndOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord \n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 0]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe('PreviousWord ') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe('88 ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('Word ') - expect(buffer.lineForRow(2)).toBe('e,') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - describe ".selectWordsContainingCursors()", -> - describe "when the cursor is inside a word", -> - it "selects the entire word", -> - editor.setCursorScreenPosition([0, 8]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - describe "when the cursor is between two words", -> - it "selects the word the cursor is on", -> - editor.setCursorScreenPosition([0, 4]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.setCursorScreenPosition([0, 3]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'var' - - describe "when the cursor is inside a region of whitespace", -> - it "selects the whitespace region", -> - editor.setCursorScreenPosition([5, 2]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - editor.setCursorScreenPosition([5, 0]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - describe "when the cursor is at the end of the text", -> - it "select the previous word", -> - editor.buffer.append 'word' - editor.moveToBottom() - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] - - it "selects words based on the non-word characters configured at the cursor's current scope", -> - editor.setText("one-one; 'two-two'; three-three") - - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([0, 12]) - - scopeDescriptors = editor.getCursors().map (c) -> c.getScopeDescriptor() - expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) - expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) - - editor.setScopedSettingsDelegate({ - getNonWordCharacters: (scopes) -> - result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' - if (scopes.some (scope) -> scope.startsWith('string')) - result - else - result + '-' - }) - - editor.selectWordsContainingCursors() - - expect(editor.getSelections()[0].getText()).toBe('one') - expect(editor.getSelections()[1].getText()).toBe('two-two') - - describe ".selectToFirstCharacterOfLine()", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.selectToFirstCharacterOfLine() - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 2], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - editor.selectToFirstCharacterOfLine() - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 0], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".setSelectedBufferRanges(ranges)", -> - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] - - editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] - - it "merges intersecting selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "does not merge non-empty adjacent selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] - - describe "when the 'preserveFolds' option is false (the default)", -> - it "removes folds that contain one or both of the selection's end points", -> - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.foldBufferRowRange(1, 4) - editor.foldBufferRowRange(2, 3) - editor.foldBufferRowRange(6, 8) - editor.foldBufferRowRange(10, 11) - - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() - - editor.setSelectedBufferRange([[10, 0], [12, 0]]) - expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() - - describe "when the 'preserveFolds' option is true", -> - it "does not remove folds that contain the selections", -> - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.foldBufferRowRange(1, 4) - editor.foldBufferRowRange(6, 8) - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - - describe ".setSelectedScreenRanges(ranges)", -> - beforeEach -> - editor.foldBufferRow(4) - - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 4], [3, 7]], [[8, 4], [8, 7]]] - - editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) - expect(editor.getSelectedScreenRanges()).toEqual [[[6, 2], [6, 4]]] - - it "merges intersecting selections and unfolds the fold which contain them", -> - editor.foldBufferRow(0) - - # Use buffer ranges because only the first line is on screen - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getScreenRange()).toEqual [[2, 2], [3, 4]] - - describe ".selectMarker(marker)", -> - describe "if the marker is valid", -> - it "selects the marker's range and returns the selected range", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - expect(editor.selectMarker(marker)).toEqual [[0, 1], [3, 3]] - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] - - describe "if the marker is invalid", -> - it "does not change the selection and returns a falsy value", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - marker.destroy() - expect(editor.selectMarker(marker)).toBeFalsy() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]] - - describe ".addSelectionForBufferRange(bufferRange)", -> - it "adds a selection for the specified buffer range", -> - editor.addSelectionForBufferRange([[3, 4], [5, 6]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 0]], [[3, 4], [5, 6]]] - - describe ".addSelectionBelow()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line below current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 25], [3, 34]] - [[4, 16], [4, 21]] - [[4, 25], [4, 29]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[3, 31], [3, 38]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 31], [3, 38]] - [[6, 31], [6, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 38]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] - [[6, 22], [6, 38]] - ] - - it "clears selection goal ranges when the selection changes", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.selectLeft() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 28]] - ] - - # goal range from previous add selection is honored next time - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously - [[6, 22], [6, 28]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(40) - editor.setDefaultCharWidth(1) - - editor.setSelectedScreenRange([[3, 10], [3, 15]]) - editor.addSelectionBelow() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 10], [3, 15]] - [[4, 10], [4, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[2, 1], [2, 3]]) - editor.addSelectionBelow() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 1], [2, 3]] - [[3, 1], [3, 2]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([3, 0]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 0], [3, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([3, 37]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 37], [3, 37]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([3, 36]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 36], [3, 36]] - [[4, 29], [4, 29]] - [[5, 30], [5, 30]] - [[6, 36], [6, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([9, 4]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 4], [9, 4]] - [[11, 4], [11, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([9, 0]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 0], [9, 0]] - [[10, 0], [10, 0]] - ] - - describe ".addSelectionAbove()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line above current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 37], [3, 44]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 37], [3, 44]] - [[2, 16], [2, 21]] - [[2, 37], [2, 40]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[6, 31], [6, 38]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 31], [6, 38]] - [[3, 31], [3, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[6, 22], [6, 38]]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 22], [6, 38]] - [[5, 22], [5, 30]] - [[4, 22], [4, 29]] - [[3, 22], [3, 38]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - editor.setSelectedScreenRange([[4, 10], [4, 15]]) - editor.addSelectionAbove() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[4, 10], [4, 15]] - [[3, 10], [3, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[3, 1], [3, 2]]) - editor.addSelectionAbove() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 1], [3, 2]] - [[2, 1], [2, 3]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([5, 0]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 0], [5, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([5, 29]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 29], [5, 29]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([6, 36]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 36], [6, 36]] - [[5, 30], [5, 30]] - [[4, 29], [4, 29]] - [[3, 36], [3, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([11, 4]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[11, 4], [11, 4]] - [[9, 4], [9, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([10, 0]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[10, 0], [10, 0]] - [[9, 0], [9, 0]] - ] - - describe ".splitSelectionsIntoLines()", -> - it "splits all multi-line selections into one selection per line", -> - editor.setSelectedBufferRange([[0, 3], [2, 4]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 30]] - [[2, 0], [2, 4]] - ] - - editor.setSelectedBufferRange([[0, 3], [1, 10]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 10]] - ] - - editor.setSelectedBufferRange([[0, 0], [0, 3]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]] - - describe "::consolidateSelections()", -> - makeMultipleSelections = -> - selection.setBufferRange [[3, 16], [3, 21]] - selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) - selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) - expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4] - [selection, selection2, selection3, selection4] - - it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", -> - [selection1] = makeMultipleSelections() - - autoscrollEvents = [] - editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event) - - expect(editor.consolidateSelections()).toBeTruthy() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.isEmpty()).toBeFalsy() - expect(editor.consolidateSelections()).toBeFalsy() - expect(editor.getSelections()).toEqual [selection1] - - expect(autoscrollEvents).toEqual([ - {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} - ]) - - describe "when the cursor is moved while there is a selection", -> - makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] - - it "clears the selection", -> - makeSelection() - editor.moveDown() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveUp() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveLeft() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveRight() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.setCursorScreenPosition([3, 3]) - expect(selection.isEmpty()).toBeTruthy() - - it "does not share selections between different edit sessions for the same buffer", -> - editor2 = null - waitsForPromise -> - atom.workspace.getActivePane().splitRight() - atom.workspace.open(editor.getPath()).then (o) -> editor2 = o - - runs -> - expect(editor2.getText()).toBe(editor.getText()) - editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - - describe "buffer manipulation", -> - describe ".moveLineUp", -> - it "moves the line under the cursor up", -> - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the the autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.indentationForBufferRow(0)).toBe 0 - expect(editor.indentationForBufferRow(1)).toBe 0 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the preceeding row", -> - it "moves the line to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[3, 2], [3, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]] - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - describe "when the preceding row consists of folded code", -> - it "moves the line above the folded row and perseveres the correct folds", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [8, 4]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [4, 4]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " if (items.length <= 1) return items;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [7, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(8)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 0]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the preceeding row is a folded row", -> - it "moves the lines spanned by the selection to the preceeding row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [9, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 2]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " };" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the preceding row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(0)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(1)).toBe "var quicksort = function () {" - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 2], [1, 9]], - [[3, 2], [3, 9]] - ]) - - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - describe "when there is a fold", -> - it "moves all lines that spanned by a selection to preceding row, preserving all folds", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 0], [4, 3]], [[10, 0], [10, 5]]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[1, 0], [5, 4]], - [[7, 0], [7, 4]] - ], preserveFolds: true) - - editor.moveLineUp() - - expect(editor.lineTextForBufferRow(1)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(4)).toEqual "6;" - expect(editor.lineTextForBufferRow(5)).toEqual "1;" - expect(editor.lineTextForBufferRow(6)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(9)).toEqual "7;" - - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [2, 9]], [[2, 12], [2, 13]]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one of the selections spans line 0", -> - it "doesn't move any lines, since line 0 can't move", -> - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(buffer.isModified()).toBe false - - describe "when one of the selections spans the last line, and it is empty", -> - it "doesn't move any lines, since the last line can't move", -> - buffer.append('\n') - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]] - - describe ".moveLineDown", -> - it "moves the line under the cursor down", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the editor.autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the following row", -> - it "moves the line to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[2, 2], [2, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [5, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the line below the folded row and preserves the fold", -> - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[3, 0], [3, 4]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[7, 0], [7, 4]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 0]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [5, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [9, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " };" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the lines spanned by the selection to the following row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[2, 0], [3, 2]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[6, 0], [7, 2]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the last line of selection does not end with a valid line ending", -> - it "appends line ending to last line and moves the lines spanned by the selection to the preceeding row", -> - expect(editor.lineTextForBufferRow(9)).toBe " };" - expect(editor.lineTextForBufferRow(10)).toBe "" - expect(editor.lineTextForBufferRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(12)).toBe "};" - - editor.setSelectedBufferRange([[10, 0], [12, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[9, 0], [11, 2]] - expect(editor.lineTextForBufferRow(9)).toBe "" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(11)).toBe "};" - expect(editor.lineTextForBufferRow(12)).toBe " };" - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the following row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]] - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[2, 0], [2, 4]], - [[6, 0], [10, 4]] - ], preserveFolds: true) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(2)).toEqual "6;" - expect(editor.lineTextForBufferRow(3)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(6)).toEqual "12;" - expect(editor.lineTextForBufferRow(7)).toEqual "7;" - expect(editor.lineTextForBufferRow(8)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(11)).toEqual "11;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() - - describe "when there is a fold below one of the selected row", -> - it "moves all lines spanned by a selection to the following row, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", -> - it "moves all the lines below the fold, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[5, 2], [5, 9]] - [[3, 2], [3, 9]], - ]) - - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 12], [4, 13]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - - describe "when the selections are above a wrapped line", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(80) - editor.setText(""" - 1 - 2 - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. - 3 - 4 - """) - - it 'moves the lines past the soft wrapped line', -> - editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(0)).not.toBe "2" - expect(editor.lineTextForBufferRow(1)).toBe "1" - expect(editor.lineTextForBufferRow(2)).toBe "2" - - describe "when the line is the last buffer row", -> - it "doesn't move it", -> - editor.setText("abc\ndef") - editor.setCursorBufferPosition([1, 0]) - editor.moveLineDown() - expect(editor.getText()).toBe("abc\ndef") - - describe ".insertText(text)", -> - describe "when there is a single selection", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "replaces the selection with the given text", -> - range = editor.insertText('xxx') - expect(range).toEqual [ [[1, 0], [1, 3]] ] - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - describe "when there are multiple empty selections", -> - describe "when the cursors are on the same line", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([1, 5]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvarxxx sort = function(items) {' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - - describe "when the cursors are on different lines", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([2, 4]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe ' xxxif (items.length <= 1) return items;' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [2, 7] - - describe "when there are multiple non-empty selections", -> - describe "when the selections are on the same line", -> - it "replaces each selection range with the inserted characters", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) - editor.insertText("x") - - [cursor1, cursor2] = editor.getCursors() - [selection1, selection2] = editor.getSelections() - - expect(cursor1.getScreenPosition()).toEqual [0, 5] - expect(cursor2.getScreenPosition()).toEqual [0, 15] - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - expect(editor.lineTextForBufferRow(0)).toBe "var x = functix () {" - - describe "when the selections are on different lines", -> - it "replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", -> - editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe 'xxxif (items.length <= 1) return items;' - [selection1, selection2] = editor.getSelections() - - expect(selection1.isEmpty()).toBeTruthy() - expect(selection1.cursor.getBufferPosition()).toEqual [1, 3] - expect(selection2.isEmpty()).toBeTruthy() - expect(selection2.cursor.getBufferPosition()).toEqual [2, 3] - - describe "when there is a selection that ends on a folded line", -> - it "destroys the selection", -> - editor.foldBufferRowRange(2, 4) - editor.setSelectedBufferRange([[1, 0], [2, 0]]) - editor.insertText('holy cow') - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - - describe "when there are ::onWillInsertText and ::onDidInsertText observers", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "notifies the observers when inserting text", -> - willInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - didInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBeTruthy() - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).toHaveBeenCalled() - - options = willInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - expect(options.cancel).toBeDefined() - - options = didInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - - it "cancels text insertion when an ::onWillInsertText observer calls cancel on an event", -> - willInsertSpy = jasmine.createSpy().andCallFake ({cancel}) -> - cancel() - - didInsertSpy = jasmine.createSpy() - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBe false - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).not.toHaveBeenCalled() - - describe "when the undo option is set to 'skip'", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 2], [1, 2]]) - - it "does not undo the skipped operation", -> - range = editor.insertText('x') - range = editor.insertText('y', undo: 'skip') - editor.undo() - expect(buffer.lineForRow(1)).toBe ' yvar sort = function(items) {' - - describe ".insertNewline()", -> - describe "when there is a single cursor", -> - describe "when the cursor is at the beginning of a line", -> - it "inserts an empty line before it", -> - editor.setCursorScreenPosition(row: 1, column: 0) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is in the middle of a line", -> - it "splits the current line to form a new line", -> - editor.setCursorScreenPosition(row: 1, column: 6) - originalLine = buffer.lineForRow(1) - lineBelowOriginalLine = buffer.lineForRow(2) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe originalLine[0...6] - expect(buffer.lineForRow(2)).toBe originalLine[6..] - expect(buffer.lineForRow(3)).toBe lineBelowOriginalLine - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is on the end of a line", -> - it "inserts an empty line after it", -> - editor.setCursorScreenPosition(row: 1, column: buffer.lineForRow(1).length) - - editor.insertNewline() - - expect(buffer.lineForRow(2)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when there are multiple cursors", -> - describe "when the cursors are on the same line", -> - it "breaks the line at the cursor locations", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.insertNewline() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot" - expect(editor.lineTextForBufferRow(4)).toBe " = items.shift(), current" - expect(editor.lineTextForBufferRow(5)).toBe ", left = [], right = [];" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [5, 0] - - describe "when the cursors are on different lines", -> - it "inserts newlines at each cursor location", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.insertText("\n") - expect(editor.lineTextForBufferRow(3)).toBe "" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(7)).toBe "" - expect(editor.lineTextForBufferRow(8)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(9)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [8, 0] - - describe ".insertNewlineBelow()", -> - describe "when the operation is undone", -> - it "places the cursor back at the previous location", -> - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineBelow() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - - it "inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", -> - editor.update({autoIndent: true}) - editor.insertNewlineBelow() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " " - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - - describe ".insertNewlineAbove()", -> - describe "when the cursor is on first line", -> - it "inserts a newline on the first line and moves the cursor to the first line", -> - editor.setCursorBufferPosition([0]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe 'var quicksort = function () {' - expect(editor.buffer.getLineCount()).toBe 14 - - describe "when the cursor is not on the first line", -> - it "inserts a newline above the current line and moves the cursor to the inserted line", -> - editor.setCursorBufferPosition([3, 4]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - expect(editor.lineTextForBufferRow(3)).toBe '' - expect(editor.lineTextForBufferRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editor.buffer.getLineCount()).toBe 14 - - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [3, 4] - - it "indents the new line to the correct level when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - - editor.setText(' var test') - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.lineTextForBufferRow(0)).toBe ' ' - expect(editor.lineTextForBufferRow(1)).toBe ' var test' - - editor.setText('\n var test') - editor.setCursorBufferPosition([1, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe ' var test' - - editor.setText('function() {\n}') - editor.setCursorBufferPosition([1, 1]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe 'function() {' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe '}' - - describe ".insertNewLine()", -> - describe "when a new line is appended before a closing tag (e.g. by pressing enter before a selection)", -> - it "moves the line down and keeps the indentation level the same when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([9, 2]) - editor.insertNewline() - expect(editor.lineTextForBufferRow(10)).toBe ' };' - - describe "when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)", -> - it "indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.js")) - editor.setText('var test = function () {\n return true;};') - editor.setCursorBufferPosition([1, 14]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - it "indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified", -> - runs -> - editor.setGrammar(atom.grammars.selectGrammar("file")) - editor.update({autoIndent: true}) - editor.setText(' if true') - editor.setCursorBufferPosition([0, 8]) - editor.insertNewline() - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 1 - - it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.coffee")) - editor.setText('if true\n return trueelse\n return false') - editor.setCursorBufferPosition([1, 13]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - expect(editor.indentationForBufferRow(3)).toBe 1 - - describe "when a newline is appended on a line that matches the decreaseNextIndentPattern", -> - it "indents the new line to the correct level when editor.autoIndent is true", -> - waitsForPromise -> - atom.packages.activatePackage('language-go') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.go")) - editor.setText('fmt.Printf("some%s",\n "thing")') - editor.setCursorBufferPosition([1, 10]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - describe ".backspace()", -> - describe "when there is a single cursor", -> - changeScreenRangeHandler = null - - beforeEach -> - selection = editor.getLastSelection() - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.onDidChangeRange changeScreenRangeHandler - - describe "when the cursor is on the middle of the line", -> - it "removes the character before the cursor", -> - editor.setCursorScreenPosition(row: 1, column: 7) - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.backspace() - - line = buffer.lineForRow(1) - expect(line).toBe " var ort = function(items) {" - expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6} - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the beginning of a line", -> - it "joins it with the line above", -> - originalLine0 = buffer.lineForRow(0) - expect(originalLine0).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.setCursorScreenPosition(row: 1, column: 0) - editor.backspace() - - line0 = buffer.lineForRow(0) - line1 = buffer.lineForRow(1) - expect(line0).toBe "var quicksort = function () { var sort = function(items) {" - expect(line1).toBe " if (items.length <= 1) return items;" - expect(editor.getCursorScreenPosition()).toEqual [0, originalLine0.length] - - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the first column of the first line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.backspace() - - describe "when the cursor is after a fold", -> - it "deletes the folded range", -> - editor.foldBufferRange([[4, 7], [5, 8]]) - editor.setCursorBufferPosition([5, 8]) - editor.backspace() - - expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();" - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - - describe "when the cursor is in the middle of a line below a fold", -> - it "backspaces as normal", -> - editor.setCursorScreenPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorScreenPosition([5, 5]) - editor.backspace() - - expect(buffer.lineForRow(7)).toBe " }" - expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" - - describe "when the cursor is on a folded screen line", -> - it "deletes the contents of the fold before the cursor", -> - editor.setCursorBufferPosition([3, 0]) - editor.foldCurrentRow() - editor.backspace() - - expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getCursorScreenPosition()).toEqual [1, 29] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), curren, left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [3, 36] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of their lines", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " whileitems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [4, 9] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are on the first column of their lines", -> - it "removes the newlines preceding each cursor", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.backspace() - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift(); current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(5)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [2, 40] - expect(cursor2.getBufferPosition()).toEqual [4, 30] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character before it", -> - editor.setSelectedBufferRange([[0, 5], [0, 9]]) - editor.backspace() - expect(editor.buffer.lineForRow(0)).toBe 'var qsort = function () {' - - describe "when the selection ends on a folded line", -> - it "preserves the fold", -> - editor.setSelectedBufferRange([[3, 0], [4, 0]]) - editor.foldBufferRow(4) - editor.backspace() - - expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtScreenRow(3)).toBe(true) - - describe "when there are multiple selections", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.backspace() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToPreviousWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the previous word boundary", -> - editor.setCursorBufferPosition([0, 16]) - editor.addCursorAtBufferPosition([1, 21]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = (items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort function () {' - expect(buffer.lineForRow(1)).toBe ' var sort =(items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToNextWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the next word boundary", -> - editor.setCursorBufferPosition([0, 15]) - editor.addCursorAtBufferPosition([1, 24]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort = () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =() {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it{' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToBeginningOfWord()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([3, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' - expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 22] - expect(cursor2.getBufferPosition()).toEqual [3, 4] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 21] - expect(cursor2.getBufferPosition()).toEqual [2, 39] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 13] - expect(cursor2.getBufferPosition()).toEqual [2, 34] - - editor.setText(' var sort') - editor.setCursorBufferPosition([0, 2]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(0)).toBe 'var sort' - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe '.deleteToEndOfLine()', -> - describe 'when no text is selected', -> - it 'deletes all text between the cursor and the end of the line', -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it' - expect(buffer.lineForRow(2)).toBe ' i' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe 'when at the end of the line', -> - it 'deletes the next newline', -> - editor.setCursorBufferPosition([1, 30]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe 'when text is selected', -> - it 'deletes only the text in the selection', -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe ".deleteToBeginningOfLine()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the line", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 0] - expect(cursor2.getBufferPosition()).toEqual [2, 0] - - describe "when at the beginning of the line", -> - it "deletes the newline", -> - editor.setCursorBufferPosition([2]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when text is selected", -> - it "still deletes all text to beginning of the line", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - - describe ".delete()", -> - describe "when there is a single cursor", -> - describe "when the cursor is on the middle of a line", -> - it "deletes the character following the cursor", -> - editor.setCursorScreenPosition([1, 6]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var ort = function(items) {' - - describe "when the cursor is on the end of a line", -> - it "joins the line with the following line", -> - editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when the cursor is on the last column of the last line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) - editor.delete() - expect(buffer.lineForRow(12)).toBe '};' - - describe "when the cursor is before a fold", -> - it "only deletes the lines inside the fold", -> - editor.foldBufferRange([[3, 6], [4, 8]]) - editor.setCursorScreenPosition([3, 6]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {" - expect(buffer.lineForRow(4)).toBe " current = items.shift();" - expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore - - describe "when the cursor is in the middle a line above a fold", -> - it "deletes as normal", -> - editor.foldBufferRow(4) - editor.setCursorScreenPosition([3, 4]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" - expect(editor.isFoldedAtScreenRow(4)).toBe(true) - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - describe "when the cursor is inside a fold", -> - it "removes the folded content after the cursor", -> - editor.foldBufferRange([[2, 6], [6, 21]]) - editor.setCursorBufferPosition([4, 9]) - - editor.delete() - - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);' - expect(buffer.lineForRow(5)).toBe ' }' - expect(editor.getCursorBufferPosition()).toEqual [4, 9] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 37] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of the lines", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(tems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [4, 10] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are at the end of their lines", -> - it "removes the newlines following each cursor", -> - editor.setCursorScreenPosition([0, 29]) - editor.addCursorAtScreenPosition([1, 30]) - - editor.delete() - - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [0, 59] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character following it", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - expect(editor.getLastSelection().isEmpty()).toBeTruthy() - - describe "when there are multiple selections", -> - describe "when selections are on the same line", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.delete() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToEndOfWord()", -> - describe "when no text is selected", -> - it "deletes to the end of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe ' i (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".indent()", -> - describe "when the selection is empty", -> - describe "when autoIndent is disabled", -> - describe "if 'softTabs' is true (the default)", -> - it "inserts 'tabLength' spaces into the buffer", -> - tabRegex = new RegExp("^[ ]{#{editor.getTabLength()}}") - expect(buffer.lineForRow(0)).not.toMatch(tabRegex) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(tabRegex) - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent() - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent() - expect(buffer.lineForRow(13).length).toBe 8 - - describe "if 'softTabs' is false", -> - it "insert a \t into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - - describe "when autoIndent is enabled", -> - describe "when the cursor's column is less than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation", -> - buffer.insert([5, 0], " \n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\s+$/ - expect(buffer.lineForRow(5).length).toBe 6 - expect(editor.getCursorBufferPosition()).toEqual [5, 6] - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13).length).toBe 8 - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([5, 0], "\t\n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [5, 3] - - describe "when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1", -> - it "inserts one tab", -> - editor.setSoftTabs(false) - buffer.setText(" \ntest") - editor.setCursorBufferPosition [1, 0] - - editor.indent(autoIndent: true) - expect(buffer.lineForRow(1)).toBe '\ttest' - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - describe "when the line's indent level is greater than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> - buffer.insert([7, 0], " \n") - editor.setCursorBufferPosition [7, 2] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\s+$/ - expect(buffer.lineForRow(7).length).toBe 8 - expect(editor.getCursorBufferPosition()).toEqual [7, 8] - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts \t into the buffer", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([7, 0], "\t\t\t\n") - editor.setCursorBufferPosition [7, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [7, 4] - - describe "when the selection is not empty", -> - it "indents the selected lines", -> - editor.setSelectedBufferRange([[0, 0], [10, 0]]) - selection = editor.getLastSelection() - spyOn(selection, "indentSelectedRows") - editor.indent() - expect(selection.indentSelectedRows).toHaveBeenCalled() - - describe "if editor.softTabs is false", -> - it "inserts a tab character into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 1] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength()] - - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength() * 2] - - describe "clipboard operations", -> - describe ".cutSelectedText()", -> - it "removes the selected text from the buffer and places it on the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.cutSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(buffer.lineForRow(1)).toBe " var = function(items) {" - expect(clipboard.readText()).toBe 'quicksort\nsort' - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[5, 0], [5, 0]], - ]) - - it "cuts the lines on which there are cursors", -> - editor.cutSelectedText() - expect(buffer.getLineCount()).toBe(11) - expect(buffer.lineForRow(1)).toBe(" if (items.length <= 1) return items;") - expect(buffer.lineForRow(4)).toBe(" current < pivot ? left.push(current) : right.push(current);") - expect(atom.clipboard.read()).toEqual """ - var quicksort = function () { - - current = items.shift(); - - """ - - describe "when many selections get added in shuffle order", -> - it "cuts them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.cutSelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".cutToEndOfLine()", -> - describe "when soft wrap is on", -> - it "cuts up to the end of the line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(25) - editor.setCursorScreenPosition([2, 6]) - editor.cutToEndOfLine() - expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {' - - describe "when soft wrap is off", -> - describe "when nothing is selected", -> - it "cuts up to the end of the line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - editor.cutToEndOfLine() - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".cutToEndOfBufferLine()", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - - describe "when nothing is selected", -> - it "cuts up to the end of the buffer line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the buffer line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".copySelectedText()", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copySelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - it "copies the lines on which there are cursors", -> - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual([ - " var sort = function(items) {\n" - " current = items.shift();\n" - ].join("\n")) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - describe "when many selections get added in shuffle order", -> - it "copies them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".copyOnlySelectedText()", -> - describe "when thee are multiple selections", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copyOnlySelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - it "does not copy anything", -> - editor.setCursorBufferPosition([1, 5]) - editor.copyOnlySelectedText() - expect(atom.clipboard.read()).toEqual "initial clipboard content" - - describe ".pasteText()", -> - copyText = (text, {startColumn, textEditor}={}) -> - startColumn ?= 0 - textEditor ?= editor - textEditor.setCursorBufferPosition([0, 0]) - textEditor.insertText(text) - numberOfNewlines = text.match(/\n/g)?.length - endColumn = text.match(/[^\n]*$/)[0]?.length - textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) - textEditor.cutSelectedText() - - it "pastes text into the buffer", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - atom.clipboard.write('first') - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var first = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var first = function(items) {" - - it "notifies ::onWillInsertText observers", -> - insertedStrings = [] - editor.onWillInsertText ({text, cancel}) -> - insertedStrings.push(text) - cancel() - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - it "notifies ::onDidInsertText observers", -> - insertedStrings = [] - editor.onDidInsertText ({text, range}) -> - insertedStrings.push(text) - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - describe "when `autoIndentOnPaste` is true", -> - beforeEach -> - editor.update({autoIndentOnPaste: true}) - - describe "when pasting multiple lines before any non-whitespace characters", -> - it "auto-indents the lines spanned by the pasted text, based on the first pasted line", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Adjust the indentation of the pasted lines while preserving - # their indentation relative to each other. Also preserve the - # indentation of the following line. - expect(editor.lineTextForBufferRow(5)).toBe " a(x);" - expect(editor.lineTextForBufferRow(6)).toBe " b(x);" - expect(editor.lineTextForBufferRow(7)).toBe " c(x);" - expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" - - it "auto-indents lines with a mix of hard tabs and spaces without removing spaces", -> - editor.setSoftTabs(false) - expect(editor.indentationForBufferRow(5)).toBe(3) - - atom.clipboard.write("/**\n\t * testing\n\t * indent\n\t **/\n", indentBasis: 1) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Do not lose the alignment spaces - expect(editor.lineTextForBufferRow(5)).toBe("\t\t\t/**") - expect(editor.lineTextForBufferRow(6)).toBe("\t\t\t * testing") - expect(editor.lineTextForBufferRow(7)).toBe("\t\t\t * indent") - expect(editor.lineTextForBufferRow(8)).toBe("\t\t\t **/") - - describe "when pasting line(s) above a line that matches the decreaseIndentPattern", -> - it "auto-indents based on the pasted line(s) only", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([7, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(7)).toBe " a(x);" - expect(editor.lineTextForBufferRow(8)).toBe " b(x);" - expect(editor.lineTextForBufferRow(9)).toBe " c(x);" - expect(editor.lineTextForBufferRow(10)).toBe " }" - - describe "when pasting a line of text without line ending", -> - it "does not auto-indent the text", -> - atom.clipboard.write("a(x);", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe "a(x); current = items.shift();" - expect(editor.lineTextForBufferRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" - - describe "when pasting on a line after non-whitespace characters", -> - it "does not auto-indent the affected line", -> - # Before the paste, the indentation is non-standard. - editor.setText """ - if (x) { - y(); - } - """ - - atom.clipboard.write(" z();\n h();") - editor.setCursorBufferPosition([1, Infinity]) - - # The indentation of the non-standard line is unchanged. - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" y(); z();") - expect(editor.lineTextForBufferRow(2)).toBe(" h();") - - describe "when `autoIndentOnPaste` is false", -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - - describe "when the cursor is indented further than the original copied text", -> - it "increases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[1, 2], [3, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([5, 6]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is indented less far than the original copied text", -> - it "decreases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[6, 6], [8, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([1, 2]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(1)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(2)).toBe "}" - - describe "when the first copied line has leading whitespace", -> - it "preserves the line's leading whitespace", -> - editor.setSelectedBufferRange([[4, 0], [6, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([0, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(0)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(1)).toBe " current = items.shift();" - - describe 'when the clipboard has many selections', -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.copySelectedText() - - it "pastes each selection in order separately into the buffer", -> - editor.setSelectedBufferRanges([ - [[1, 6], [1, 10]] - [[0, 4], [0, 13]], - ]) - - editor.moveRight() - editor.insertText("_") - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort_quicksort = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var sort_sort = function(items) {" - - describe 'and the selections count does not match', -> - beforeEach -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]]]) - - it "pastes the whole text into the buffer", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort" - expect(editor.lineTextForBufferRow(1)).toBe "sort = function () {" - - describe "when a full line was cut", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.cutSelectedText() - editor.setCursorBufferPosition([2, 13]) - - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];") - expect(editor.getCursorBufferPosition()).toEqual([3, 13]) - - describe "when a full line was copied", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.copySelectedText() - - describe "when there is a selection", -> - it "overwrites the selection as with any copied text", -> - editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(2)).toBe("") - expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") - expect(editor.getCursorBufferPosition()).toEqual([2, 0]) - - describe "when there is no selection", -> - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") - expect(editor.getCursorBufferPosition()).toEqual([3, 13]) - - it "respects options that preserve the formatting of the pasted text", -> - editor.update({autoIndentOnPaste: true}) - atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.insertText(' ') - editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false}) - - expect(editor.lineTextForBufferRow(5)).toBe " a(x);" - expect(editor.lineTextForBufferRow(6)).toBe " b(x);" - expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n" - expect(editor.lineTextForBufferRow(7)).toBe "c(x);" - expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" - - describe ".indentSelectedRows()", -> - describe "when nothing is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + 1], [0, 3 + 1]] - - describe "when one line is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editor.getTabText()}var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + 1], [0, 14 + 1]] - - describe "when multiple lines are selected", -> - describe "when softTabs is enabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]] - - it "does not indent the last row if the selection ends at column 0", -> - editor.setSelectedBufferRange([[9, 1], [11, 0]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 0]] - - describe "when softTabs is disabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe "\t\t};" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe "\t\treturn sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + 1], [11, 15 + 1]] - - describe ".outdentSelectedRows()", -> - describe "when nothing is selected", -> - it "outdents line and retains selection", -> - editor.setSelectedBufferRange([[1, 3], [1, 3]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]] - - it "outdents when indent is less than a tab length", -> - editor.insertText(' ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs", -> - editor.insertText('\t\t') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents when a mix of hard tabs and soft tabs are used", -> - editor.insertText('\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents only up to the first non-space non-tab character", -> - editor.insertText(' \tfoo\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tfoo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - - describe "when one line is selected", -> - it "outdents line and retains editor", -> - editor.setSelectedBufferRange([[1, 4], [1, 14]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]] - - describe "when multiple lines are selected", -> - it "outdents selected lines and retains editor", -> - editor.setSelectedBufferRange([[0, 1], [3, 15]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editor.getTabLength()]] - - it "does not outdent the last line of the selection if it ends at column 0", -> - editor.setSelectedBufferRange([[0, 1], [3, 0]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 0]] - - describe ".autoIndentSelectedRows", -> - it "auto-indents the selection", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText("function() {\ninside=true\n}\n i=1\n") - editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) - editor.autoIndentSelectedRows() - - expect(editor.lineTextForBufferRow(2)).toBe " function() {" - expect(editor.lineTextForBufferRow(3)).toBe " inside=true" - expect(editor.lineTextForBufferRow(4)).toBe " }" - expect(editor.lineTextForBufferRow(5)).toBe " i=1" - - describe ".undo() and .redo()", -> - it "undoes/redoes the last change", -> - editor.insertText("foo") - editor.undo() - expect(buffer.lineForRow(0)).not.toContain "foo" - - editor.redo() - expect(buffer.lineForRow(0)).toContain "foo" - - it "batches the undo / redo of changes caused by multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([1, 0]) - - editor.insertText("foo") - editor.backspace() - - expect(buffer.lineForRow(0)).toContain "fovar" - expect(buffer.lineForRow(1)).toContain "fo " - - editor.undo() - - expect(buffer.lineForRow(0)).toContain "foo" - expect(buffer.lineForRow(1)).toContain "foo" - - editor.redo() - - expect(buffer.lineForRow(0)).not.toContain "foo" - expect(buffer.lineForRow(0)).toContain "fovar" - - it "restores cursors and selections to their states before and after undone and redone changes", -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]], - ]) - editor.insertText("abc") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.setSelectedBufferRanges([ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ]) - editor.insertText("def") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ] - - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - it "restores the selected ranges after undo and redo", -> - editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) - editor.delete() - editor.delete() - - selections = editor.getSelections() - expect(buffer.lineForRow(1)).toBe ' var = function( {' - - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] - - editor.redo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - xit "restores folds after undo and redo", -> - editor.foldBufferRow(1) - editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - - editor.insertText """ - \ // testing - function foo() { - return 1 + 2; - } - """ - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - editor.foldBufferRow(2) - - editor.undo() - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - editor.redo() - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - - describe "::transact", -> - it "restores the selection when the transaction is undone/redone", -> - buffer.setText('1234') - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - - editor.transact -> - editor.delete() - editor.moveToEndOfLine() - editor.insertText('5') - expect(buffer.getText()).toBe '145' - - editor.undo() - expect(buffer.getText()).toBe '1234' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - editor.redo() - expect(buffer.getText()).toBe '145' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 3]] - - describe "when the buffer is changed (via its direct api, rather than via than edit session)", -> - it "moves the cursor so it is in the same relative position of the buffer", -> - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.addCursorAtScreenPosition([0, 5]) - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2, cursor3] = editor.getCursors() - - buffer.insert([0, 1], 'abc') - - expect(cursor1.getScreenPosition()).toEqual [0, 0] - expect(cursor2.getScreenPosition()).toEqual [0, 8] - expect(cursor3.getScreenPosition()).toEqual [1, 0] - - it "does not destroy cursors or selections when a change encompasses them", -> - cursor = editor.getLastCursor() - cursor.setBufferPosition [3, 3] - editor.buffer.delete([[3, 1], [3, 5]]) - expect(cursor.getBufferPosition()).toEqual [3, 1] - expect(editor.getCursors().indexOf(cursor)).not.toBe -1 - - selection = editor.getLastSelection() - selection.setBufferRange [[3, 5], [3, 10]] - editor.buffer.delete [[3, 3], [3, 8]] - expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] - expect(editor.getSelections().indexOf(selection)).not.toBe -1 - - it "merges cursors when the change causes them to overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 2]) - editor.addCursorAtScreenPosition([1, 2]) - - [cursor1, cursor2, cursor3] = editor.getCursors() - expect(editor.getCursors().length).toBe 3 - - buffer.delete([[0, 0], [0, 2]]) - - expect(editor.getCursors().length).toBe 2 - expect(editor.getCursors()).toEqual [cursor1, cursor3] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor3.getBufferPosition()).toEqual [1, 2] - - describe ".moveSelectionLeft()", -> - it "moves one active selection on one line one column to the left", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]] - - it "moves multiple active selections on one line one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]] - - it "moves multiple active selections on multiple lines one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]] - - describe "when a selection is at the first column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - - editor.moveSelectionLeft() - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]] - - describe ".moveSelectionRight()", -> - it "moves one active selection on one line one column to the right", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionRight() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]] - - it "moves multiple active selections on one line one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]] - - it "moves multiple active selections on multiple lines one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]] - - describe "when a selection is at the last column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - - editor.moveSelectionRight() - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]] - - describe 'reading text', -> - it '.lineTextForScreenRow(row)', -> - editor.foldBufferRow(4) - expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));' - expect(editor.lineTextForScreenRow(9)).toEqual '};' - expect(editor.lineTextForScreenRow(10)).toBeUndefined() - - describe ".deleteLine()", -> - it "deletes the first line when the cursor is there", -> - editor.getLastCursor().moveToTop() - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the last line when the cursor is there", -> - count = buffer.getLineCount() - secondToLastLine = buffer.lineForRow(count - 2) - expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) - editor.getLastCursor().moveToBottom() - editor.deleteLine() - newCount = buffer.getLineCount() - expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) - expect(newCount).toBe(count - 1) - - it "deletes whole lines when partial lines are selected", -> - editor.setSelectedBufferRange([[0, 2], [1, 2]]) - line2 = buffer.lineForRow(2) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line2) - expect(buffer.lineForRow(1)).not.toBe(line2) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line2) - expect(buffer.getLineCount()).toBe(count - 2) - - it "deletes a line only once when multiple selections are on the same line", -> - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 4], [0, 5]] - ]) - expect(buffer.lineForRow(0)).not.toBe(line1) - - editor.deleteLine() - - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "only deletes first line if only newline is selected on second line", -> - editor.setSelectedBufferRange([[0, 2], [1, 0]]) - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the entire region when invoke on a folded region", -> - editor.foldBufferRow(1) - editor.getLastCursor().moveToTop() - editor.getLastCursor().moveDown() - expect(buffer.getLineCount()).toBe(13) - editor.deleteLine() - expect(buffer.getLineCount()).toBe(4) - - it "deletes the entire file from the bottom up", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToBottom() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - it "deletes the entire file from the top down", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToTop() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - describe "when soft wrap is enabled", -> - it "deletes the entire line that the cursor is on", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorBufferPosition([6]) - - line7 = buffer.lineForRow(7) - count = buffer.getLineCount() - expect(buffer.lineForRow(6)).not.toBe(line7) - editor.deleteLine() - expect(buffer.lineForRow(6)).toBe(line7) - expect(buffer.getLineCount()).toBe(count - 1) - - describe "when the line being deleted precedes a fold, and the command is undone", -> - it "restores the line and preserves the fold", -> - editor.setCursorBufferPosition([4]) - editor.foldCurrentRow() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editor.setCursorBufferPosition([3]) - editor.deleteLine() - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - editor.undo() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - describe ".replaceSelectedText(options, fn)", -> - describe "when no text is selected", -> - it "inserts the text returned from the function at the cursor position", -> - editor.replaceSelectedText {}, -> '123' - expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - - editor.setCursorBufferPosition([0]) - editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - - editor.setCursorBufferPosition([10]) - editor.replaceSelectedText null, -> '' - expect(buffer.lineForRow(10)).toBe '' - - describe "when text is selected", -> - it "replaces the selected text with the text returned from the function", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.replaceSelectedText {}, -> 'ia' - expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' - - it "replaces the selected text and selects the replacement text", -> - editor.setSelectedBufferRange([[0, 4], [0, 9]]) - editor.replaceSelectedText {}, -> 'whatnot' - expect(buffer.lineForRow(0)).toBe 'var whatnotsort = function () {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [0, 11]] - - describe ".transpose()", -> - it "swaps two characters", -> - editor.buffer.setText("abc") - editor.setCursorScreenPosition([0, 1]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'bac' - - it "reverses a selection", -> - editor.buffer.setText("xabcz") - editor.setSelectedBufferRange([[0, 1], [0, 4]]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'xcbaz' - - describe ".upperCase()", -> - describe "when there is no selection", -> - it "upper cases the current word", -> - editor.buffer.setText("aBc") - editor.setCursorScreenPosition([0, 1]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "upper cases the current selection", -> - editor.buffer.setText("abc") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe ".lowerCase()", -> - describe "when there is no selection", -> - it "lower cases the current word", -> - editor.buffer.setText("aBC") - editor.setCursorScreenPosition([0, 1]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "lower cases the current selection", -> - editor.buffer.setText("ABC") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe '.setTabLength(tabLength)', -> - it 'clips atomic soft tabs to the given tab length', -> - expect(editor.getTabLength()).toBe 2 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 2]) - - editor.setTabLength(6) - expect(editor.getTabLength()).toBe 6 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 6]) - - changeHandler = jasmine.createSpy('changeHandler') - editor.onDidChange(changeHandler) - editor.setTabLength(6) - expect(changeHandler).not.toHaveBeenCalled() - - it 'does not change its tab length when the given tab length is null', -> - editor.setTabLength(4) - editor.setTabLength(null) - expect(editor.getTabLength()).toBe(4) - - describe ".indentLevelForLine(line)", -> - it "returns the indent level when the line has only leading whitespace", -> - expect(editor.indentLevelForLine(" hello")).toBe(2) - expect(editor.indentLevelForLine(" hello")).toBe(1.5) - - it "returns the indent level when the line has only leading tabs", -> - expect(editor.indentLevelForLine("\t\thello")).toBe(2) - - it "returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs", -> - expect(editor.indentLevelForLine("\t hello")).toBe(2) - expect(editor.indentLevelForLine(" \thello")).toBe(2) - expect(editor.indentLevelForLine(" \t hello")).toBe(2.5) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \t hello")).toBe(4.5) - - describe "when a better-matched grammar is added to syntax", -> - it "switches to the better-matched grammar and re-tokenizes the buffer", -> - editor.destroy() - - jsGrammar = atom.grammars.selectGrammar('a.js') - atom.grammars.removeGrammar(jsGrammar) - - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o - - runs -> - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.tokensForScreenRow(0).length).toBe(1) - - atom.grammars.addGrammar(jsGrammar) - expect(editor.getGrammar()).toBe jsGrammar - expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1 - - describe "editor.autoIndent", -> - describe "when editor.autoIndent is false (default)", -> - describe "when `indent` is triggered", -> - it "does not auto-indent the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: false}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when editor.autoIndent is true", -> - beforeEach -> - editor.update({autoIndent: true}) - - describe "when `indent` is triggered", -> - it "auto-indents the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: true}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when a newline is added", -> - describe "when the line preceding the newline adds a new level of indentation", -> - it "indents the newline to one additional level of indentation beyond the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "when the line preceding the newline doesn't add a level of indentation", -> - it "indents the new line to the same level as the preceding line", -> - editor.setCursorBufferPosition([5, 14]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) - - describe "when the line preceding the newline is a comment", -> - it "maintains the indent of the commented line", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText(' //') - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when the line preceding the newline contains only whitespace", -> - it "bases the new line's indentation on only the preceding line", -> - editor.setCursorBufferPosition([6, Infinity]) - editor.insertText("\n ") - expect(editor.getCursorBufferPosition()).toEqual([7, 2]) - - editor.insertNewline() - expect(editor.lineTextForBufferRow(8)).toBe(" ") - - it "does not indent the line preceding the newline", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText(' var this-line-should-be-indented-more\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([2, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 1 - - describe "when the cursor is before whitespace", -> - it "retains the whitespace following the cursor on the new line", -> - editor.setText(" var sort = function() {}") - editor.setCursorScreenPosition([0, 12]) - editor.insertNewline() - - expect(buffer.lineForRow(0)).toBe ' var sort =' - expect(buffer.lineForRow(1)).toBe ' function() {}' - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - describe "when inserted text matches a decrease indent pattern", -> - describe "when the preceding line matches an increase indent pattern", -> - it "decreases the indentation to match that of the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('}') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) - - describe "when the preceding line doesn't match an increase indent pattern", -> - it "decreases the indentation to be one level below that of the preceding line", -> - editor.setCursorBufferPosition([3, Infinity]) - editor.insertText('\n ') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - editor.insertText('}') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - 1 - - it "doesn't break when decreasing the indentation on a row that has no indentation", -> - editor.setCursorBufferPosition([12, Infinity]) - editor.insertText("\n}; # too many closing brackets!") - expect(editor.lineTextForBufferRow(13)).toBe "}; # too many closing brackets!" - - describe "when inserted text does not match a decrease indent pattern", -> - it "does not decrease the indentation", -> - editor.setCursorBufferPosition([12, 0]) - editor.insertText(' ') - expect(editor.lineTextForBufferRow(12)).toBe ' };' - editor.insertText('\t\t') - expect(editor.lineTextForBufferRow(12)).toBe ' \t\t};' - - describe "when the current line does not match a decrease indent pattern", -> - it "leaves the line unchanged", -> - editor.setCursorBufferPosition([2, 4]) - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('foo') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "atomic soft tabs", -> - it "skips tab-length runs of leading whitespace when moving the cursor", -> - editor.update({tabLength: 4, atomicSoftTabs: true}) - - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - editor.update({atomicSoftTabs: false}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 3] - - editor.update({atomicSoftTabs: true}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - describe ".destroy()", -> - it "destroys marker layers associated with the text editor", -> - buffer.retain() - selectionsMarkerLayerId = editor.selectionsMarkerLayer.id - foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id - editor.destroy() - expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() - expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() - buffer.release() - - it "notifies ::onDidDestroy observers when the editor is destroyed", -> - destroyObserverCalled = false - editor.onDidDestroy -> destroyObserverCalled = true - - editor.destroy() - expect(destroyObserverCalled).toBe true - - it "does not blow up when query methods are called afterward", -> - editor.destroy() - editor.getGrammar() - editor.getLastCursor() - editor.lineTextForBufferRow(0) - - it "emits the destroy event after destroying the editor's buffer", -> - events = [] - editor.getBuffer().onDidDestroy -> - expect(editor.isDestroyed()).toBe(true) - events.push('buffer-destroyed') - editor.onDidDestroy -> - expect(buffer.isDestroyed()).toBe(true) - events.push('editor-destroyed') - editor.destroy() - expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) - - describe ".joinLines()", -> - describe "when no text is selected", -> - describe "when the line below isn't empty", -> - it "joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up", -> - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText(' ') - editor.setCursorBufferPosition([0]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getCursorBufferPosition()).toEqual [0, 29] - - describe "when the line below is empty", -> - it "deletes the line below and moves the cursor to the end of the line", -> - editor.setCursorBufferPosition([9]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' };' - expect(editor.lineTextForBufferRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [9, 4] - - describe "when the cursor is on the last row", -> - it "does nothing", -> - editor.setCursorBufferPosition([Infinity, Infinity]) - editor.joinLines() - expect(editor.lineTextForBufferRow(12)).toBe '};' - - describe "when the line is empty", -> - it "joins the line below with the current line with no added space", -> - editor.setCursorBufferPosition([10]) - editor.joinLines() - expect(editor.lineTextForBufferRow(10)).toBe 'return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - describe "when text is selected", -> - describe "when the selection does not span multiple lines", -> - it "joins the line below with the current line separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - describe "when the selection spans multiple lines", -> - it "joins all selected lines separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[9, 3], [12, 1]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' }; return sort(Array.apply(this, arguments)); };' - expect(editor.getSelectedBufferRange()).toEqual [[9, 3], [9, 49]] - - describe ".duplicateLines()", -> - it "for each selection, duplicates all buffer lines intersected by the selection", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([2, 5]) - editor.addSelectionForBufferRange([[3, 0], [8, 0]], preserveFolds: true) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe """ - \ if (items.length <= 1) return items; - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]] - - # folds are also duplicated - expect(editor.isFoldedAtScreenRow(5)).toBe(true) - expect(editor.isFoldedAtScreenRow(7)).toBe(true) - expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter - expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - it "duplicates all folded lines for empty selections on lines containing folds", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([4, 0]) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe """ - \ if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRange()).toEqual [[8, 0], [8, 0]] - - it "can duplicate the last line of the buffer", -> - editor.setSelectedBufferRange([[11, 0], [12, 2]]) - editor.duplicateLines() - expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe """ - \ return sort(Array.apply(this, arguments)); - }; - return sort(Array.apply(this, arguments)); - }; - """ - expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]] - - it "only duplicates lines containing multiple selections once", -> - editor.setText(""" - aaaaaa - bbbbbb - cccccc - dddddd - """) - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 3], [0, 4]], - [[2, 1], [2, 2]], - [[2, 3], [3, 1]], - [[3, 3], [3, 4]], - ]) - editor.duplicateLines() - expect(editor.getText()).toBe(""" - aaaaaa - aaaaaa - bbbbbb - cccccc - dddddd - cccccc - dddddd - """) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 1], [1, 2]], - [[1, 3], [1, 4]], - [[5, 1], [5, 2]], - [[5, 3], [6, 1]], - [[6, 3], [6, 4]], - ]) - - describe "when the editor contains surrogate pair characters", -> - it "correctly backspaces over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe "when the editor contains variation sequence character pairs", -> - it "correctly backspaces over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".setIndentationForBufferRow", -> - describe "when the editor uses soft tabs but the row has hard tabs", -> - it "only replaces whitespace characters", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the indentation level is a non-integer", -> - it "does not throw an exception", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2.1) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the editor's grammar has an injection selector", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-text') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - it "includes the grammar's patterns when the selector matches the current scope in other grammars", -> - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - grammar = atom.grammars.selectGrammar("text.js") - {line, tags} = grammar.tokenizeLine("var i; // http://github.com") - - tokens = atom.grammars.decodeTokens(line, tags) - expect(tokens[0].value).toBe "var" - expect(tokens[0].scopes).toEqual ["source.js", "storage.type.var.js"] - - expect(tokens[6].value).toBe "http://github.com" - expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] - - describe "when the grammar is added", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// http://github.com") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} - ] - - describe "when the grammar is updated", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// SELECT * FROM OCTOCATS") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('package-with-injection-selector') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-sql') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - describe ".normalizeTabsInBufferRange()", -> - it "normalizes tabs depending on the editor's soft tab/tab length settings", -> - editor.setTabLength(1) - editor.setSoftTabs(true) - editor.setText('\t\t\t') - editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) - expect(editor.getText()).toBe ' \t\t' - - editor.setTabLength(2) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - editor.setSoftTabs(false) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - describe ".pageUp/Down()", -> - it "moves the cursor down one page length", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 10 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 0 - - describe ".selectPageUp/Down()", -> - it "selects one screen height of text up or down", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [5, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [10, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - editor.moveToBottom() - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - describe "::scrollToScreenPosition(position, [options])", -> - it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> - scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") - editor.onDidRequestAutoscroll(scrollSpy) - - editor.scrollToScreenPosition([8, 20]) - editor.scrollToScreenPosition([8, 20], center: true) - editor.scrollToScreenPosition([8, 20], center: false, reversed: true) - - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) - - describe "scroll past end", -> - it "returns false by default but can be customized", -> - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(true) - editor.update({scrollPastEnd: false}) - expect(editor.getScrollPastEnd()).toBe(false) - - it "always returns false when autoHeight is on", -> - editor.update({autoHeight: true, scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({autoHeight: false}) - expect(editor.getScrollPastEnd()).toBe(true) - - describe "auto height", -> - it "returns true by default but can be customized", -> - editor = new TextEditor - expect(editor.getAutoHeight()).toBe(true) - editor.update({autoHeight: false}) - expect(editor.getAutoHeight()).toBe(false) - editor.update({autoHeight: true}) - expect(editor.getAutoHeight()).toBe(true) - editor.destroy() - - describe "auto width", -> - it "returns false by default but can be customized", -> - expect(editor.getAutoWidth()).toBe(false) - editor.update({autoWidth: true}) - expect(editor.getAutoWidth()).toBe(true) - editor.update({autoWidth: false}) - expect(editor.getAutoWidth()).toBe(false) - - describe '.get/setPlaceholderText()', -> - it 'can be created with placeholderText', -> - newEditor = new TextEditor({ - mini: true - placeholderText: 'yep' - }) - expect(newEditor.getPlaceholderText()).toBe 'yep' - - it 'models placeholderText and emits an event when changed', -> - editor.onDidChangePlaceholderText handler = jasmine.createSpy() - - expect(editor.getPlaceholderText()).toBeUndefined() - - editor.setPlaceholderText('OK') - expect(handler).toHaveBeenCalledWith 'OK' - expect(editor.getPlaceholderText()).toBe 'OK' - - describe 'gutters', -> - describe 'the TextEditor constructor', -> - it 'creates a line-number gutter', -> - expect(editor.getGutters().length).toBe 1 - lineNumberGutter = editor.gutterWithName('line-number') - expect(lineNumberGutter.name).toBe 'line-number' - expect(lineNumberGutter.priority).toBe 0 - - describe '::addGutter', -> - it 'can add a gutter', -> - expect(editor.getGutters().length).toBe 1 # line-number gutter - options = - name: 'test-gutter' - priority: 1 - gutter = editor.addGutter options - expect(editor.getGutters().length).toBe 2 - expect(editor.getGutters()[1]).toBe gutter - - it "does not allow a custom gutter with the 'line-number' name.", -> - expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow() - - describe '::decorateMarker', -> - [marker] = [] - - beforeEach -> - marker = editor.markBufferRange([[1, 0], [1, 0]]) - - it 'reflects an added decoration when one of its custom gutters is decorated.', -> - gutter = editor.addGutter {'name': 'custom-gutter'} - decoration = gutter.decorateMarker marker, {class: 'custom-class'} - gutterDecorations = editor.getDecorations - type: 'gutter' - gutterName: 'custom-gutter' - class: 'custom-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - it 'reflects an added decoration when its line-number gutter is decorated.', -> - decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'} - gutterDecorations = editor.getDecorations - type: 'line-number' - gutterName: 'line-number' - class: 'test-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - describe '::observeGutters', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', -> - lineNumberGutter = editor.gutterWithName('line-number') - editor.observeGutters(callback) - expect(payloads).toEqual [lineNumberGutter] - gutter1 = editor.addGutter({name: 'test-gutter-1'}) - expect(payloads).toEqual [lineNumberGutter, gutter1] - gutter2 = editor.addGutter({name: 'test-gutter-2'}) - expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2] - - it 'does not call the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.observeGutters(callback) - payloads = [] - gutter.destroy() - expect(payloads).toEqual [] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.observeGutters(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidAddGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback with each newly-added gutter, but not with existing gutters.', -> - editor.onDidAddGutter(callback) - expect(payloads).toEqual [] - gutter = editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [gutter] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.onDidAddGutter(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidRemoveGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.onDidRemoveGutter(callback) - expect(payloads).toEqual [] - gutter.destroy() - expect(payloads).toEqual ['test-gutter'] - - it 'does not call the callback after the subscription has been disposed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - subscription = editor.onDidRemoveGutter(callback) - subscription.dispose() - gutter.destroy() - expect(payloads).toEqual [] - - describe "decorations", -> - describe "::decorateMarker", -> - it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", -> - marker = editor.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual { - properties: {type: 'highlight', class: 'foo'} - screenRange: marker.getScreenRange(), - bufferRange: marker.getBufferRange(), - rangeIsReversed: false - } - - it "does not throw errors after the marker's containing layer is destroyed", -> - layer = editor.addMarkerLayer() - marker = layer.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - layer.destroy() - editor.decorationsStateForScreenRowRange(0, 5) - - describe "::decorateMarkerLayer", -> - it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", -> - layer1 = editor.getBuffer().addMarkerLayer() - marker1 = layer1.markRange([[2, 4], [6, 8]]) - marker2 = layer1.markRange([[11, 0], [11, 12]]) - layer2 = editor.getBuffer().addMarkerLayer() - marker3 = layer2.markRange([[8, 0], [9, 0]]) - - layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo') - layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar') - layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz') - - decorationState = editor.decorationsStateForScreenRowRange(0, 13) - - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration1.destroy() - - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'quux'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, null) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - describe "invisibles", -> - beforeEach -> - editor.update({showInvisibles: true}) - - it "substitutes invisible characters according to the given rules", -> - previousLineText = editor.lineTextForScreenRow(0) - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - expect(editor.getInvisibles()).toEqual(eol: '?') - - it "does not use invisibles if showInvisibles is set to false", -> - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - - editor.update({showInvisibles: false}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) - - describe "indent guides", -> - it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", -> - editor.setText(" foo") - editor.setTabLength(2) - - editor.update({showIndentGuide: false}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.update({showIndentGuide: true}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.setMini(true) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - describe "when the editor is constructed with the grammar option set", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - it "sets the grammar", -> - editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) - expect(editor.getGrammar().name).toBe 'CoffeeScript' - - describe "softWrapAtPreferredLineLength", -> - it "soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini", -> - editor.update({ - editorWidthInChars: 30 - softWrapped: true - softWrapAtPreferredLineLength: true - preferredLineLength: 20 - }) - - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = ' - - editor.update({editorWidthInChars: 10}) - expect(editor.lineTextForScreenRow(0)).toBe 'var ' - - editor.update({mini: true}) - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = function () {' - - describe "softWrapHangingIndentLength", -> - it "controls how much extra indentation is applied to soft-wrapped lines", -> - editor.setText('123456789') - editor.update({ - editorWidthInChars: 8 - softWrapped: true - softWrapHangingIndentLength: 2 - }) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - editor.update({softWrapHangingIndentLength: 4}) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - describe "::getElement", -> - it "returns an element", -> - expect(editor.getElement() instanceof HTMLElement).toBe(true) - - describe 'setMaxScreenLineLength', -> - it "sets the maximum line length in the editor before soft wrapping is forced", -> - expect(editor.getSoftWrapColumn()).toBe(500) - editor.update({ - maxScreenLineLength: 1500 - }) - expect(editor.getSoftWrapColumn()).toBe(1500) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index d10efa695..b2cc41ab7 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,9 +1,6655 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') + const fs = require('fs') +const path = require('path') const temp = require('temp').track() -const {Point, Range} = require('text-buffer') -const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') -const TextBuffer = require('text-buffer') +const dedent = require('dedent') +const clipboard = require('../src/safe-clipboard') const TextEditor = require('../src/text-editor') +const TextBuffer = require('text-buffer') + +describe('TextEditor', () => { + let buffer, editor, lineLengths + + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + buffer = editor.buffer + editor.update({autoIndent: false}) + lineLengths = buffer.getLines().map(line => line.length) + await atom.packages.activatePackage('language-javascript') + }) + + describe('when the editor is deserialized', () => { + it('restores selections and folds based on markers in the buffer', async () => { + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 5]], {reversed: true}) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.id).toBe(editor.id) + expect(editor2.getBuffer().getPath()).toBe(editor.getBuffer().getPath()) + expect(editor2.getSelectedBufferRanges()).toEqual([[[1, 2], [3, 4]], [[5, 6], [7, 5]]]) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + editor2.destroy() + }) + + it("restores the editor's layout configuration", async () => { + editor.update({ + softTabs: true, + atomicSoftTabs: false, + tabLength: 12, + softWrapped: true, + softWrapAtPreferredLineLength: true, + softWrapHangingIndentLength: 8, + invisibles: {space: 'S'}, + showInvisibles: true, + editorWidthInChars: 120 + }) + + // Force buffer and display layer to be deserialized as well, rather than + // reusing the same buffer instance + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) + expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) + expect(editor2.getTabLength()).toBe(editor.getTabLength()) + expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) + expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) + expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) + expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) + expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) + expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) + }) + + it('ignores buffers with retired IDs', () => { + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return null }} + }) + + expect(editor2).toBeNull() + }) + }) + + describe('when the editor is constructed with the largeFileMode option set to true', () => { + it("loads the editor but doesn't tokenize", async () => { + editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true}) + buffer = editor.getBuffer() + expect(editor.lineTextForScreenRow(0)).toBe(buffer.lineForRow(0)) + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) // soft tab + expect(editor.lineTextForScreenRow(12)).toBe(buffer.lineForRow(12)) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.insertText('hey"') + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) + }) + }) + + describe('.copy()', () => { + it('returns a different editor with the same initial state', () => { + expect(editor.getAutoHeight()).toBeFalsy() + expect(editor.getAutoWidth()).toBeFalsy() + expect(editor.getShowCursorOnSelection()).toBeTruthy() + + const element = editor.getElement() + element.setHeight(100) + element.setWidth(100) + jasmine.attachToDOM(element) + + editor.update({showCursorOnSelection: false}) + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 8]], {reversed: true}) + editor.setScrollTopRow(3) + expect(editor.getScrollTopRow()).toBe(3) + editor.setScrollLeftColumn(4) + expect(editor.getScrollLeftColumn()).toBe(4) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const editor2 = editor.copy() + const element2 = editor2.getElement() + element2.setHeight(100) + element2.setWidth(100) + jasmine.attachToDOM(element2) + expect(editor2.id).not.toBe(editor.id) + expect(editor2.getSelectedBufferRanges()).toEqual(editor.getSelectedBufferRanges()) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.getScrollTopRow()).toBe(3) + expect(editor2.getScrollLeftColumn()).toBe(4) + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor2.getAutoWidth()).toBe(false) + expect(editor2.getAutoHeight()).toBe(false) + expect(editor2.getShowCursorOnSelection()).toBeFalsy() + + // editor2 can now diverge from its origin edit session + editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + editor2.unfoldBufferRow(4) + expect(editor2.isFoldedAtBufferRow(4)).not.toBe(editor.isFoldedAtBufferRow(4)) + }) + }) + + describe('.update()', () => { + it('updates the editor with the supplied config parameters', () => { + let changeSpy + const { element } = editor // force element initialization + element.setUpdatedSynchronously(false) + editor.update({showInvisibles: true}) + editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) + + const returnedPromise = editor.update({ + tabLength: 6, + softTabs: false, + softWrapped: true, + editorWidthInChars: 40, + showInvisibles: false, + mini: false, + lineNumberGutterVisible: false, + scrollPastEnd: true, + autoHeight: false, + maxScreenLineLength: 1000 + }) + + expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) + expect(changeSpy.callCount).toBe(1) + expect(editor.getTabLength()).toBe(6) + expect(editor.getSoftTabs()).toBe(false) + expect(editor.isSoftWrapped()).toBe(true) + expect(editor.getEditorWidthInChars()).toBe(40) + expect(editor.getInvisibles()).toEqual({}) + expect(editor.isMini()).toBe(false) + expect(editor.isLineNumberGutterVisible()).toBe(false) + expect(editor.getScrollPastEnd()).toBe(true) + expect(editor.getAutoHeight()).toBe(false) + }) + }) + + describe('title', () => { + describe('.getTitle()', () => { + it("uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", () => { + expect(editor.getTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getTitle()).toBe('untitled') + }) + }) + + describe('.getLongTitle()', () => { + it('returns file name when there is no opened file with identical name', () => { + expect(editor.getLongTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getLongTitle()).toBe('untitled') + }) + + it("returns ' — ' when opened files have identical file names", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-1', 'readme')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'readme')) + expect(editor1.getLongTitle()).toBe('readme \u2014 sample-theme-1') + expect(editor2.getLongTitle()).toBe('readme \u2014 sample-theme-2') + }) + + it("returns ' — ' when opened files have identical file names in subdirectories", async () => { + const path1 = path.join('sample-theme-1', 'src', 'js') + const path2 = path.join('sample-theme-2', 'src', 'js') + const editor1 = await atom.workspace.open(path.join(path1, 'main.js')) + const editor2 = await atom.workspace.open(path.join(path2, 'main.js')) + expect(editor1.getLongTitle()).toBe(`main.js \u2014 ${path1}`) + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path2}`) + }) + + it("returns ' — ' when opened files have identical file and same parent dir name", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')) + expect(editor1.getLongTitle()).toBe('main.js \u2014 js') + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) + }) + }) + + it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangeTitle(title => observed.push(title)) + + buffer.setPath('/foo/bar/baz.txt') + buffer.setPath(undefined) + + expect(observed).toEqual(['baz.txt', 'untitled']) + }) + }) + + describe('path', () => { + it('notifies ::onDidChangePath observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangePath(filePath => observed.push(filePath)) + + buffer.setPath(__filename) + buffer.setPath(undefined) + + expect(observed).toEqual([__filename, undefined]) + }) + }) + + describe('encoding', () => { + it('notifies ::onDidChangeEncoding observers when the editor encoding changes', () => { + const observed = [] + editor.onDidChangeEncoding(encoding => observed.push(encoding)) + + editor.setEncoding('utf16le') + editor.setEncoding('utf16le') + editor.setEncoding('utf16be') + editor.setEncoding() + editor.setEncoding() + + expect(observed).toEqual(['utf16le', 'utf16be', 'utf8']) + }) + }) + + describe('cursor', () => { + describe('.getLastCursor()', () => { + it('returns the most recently created cursor', () => { + editor.addCursorAtScreenPosition([1, 0]) + const lastCursor = editor.addCursorAtScreenPosition([2, 0]) + expect(editor.getLastCursor()).toBe(lastCursor) + }) + + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCursors()', () => { + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the cursor moves', () => { + it('clears a goal column established by vertical movement', () => { + editor.setText('b') + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + editor.moveUp() + editor.insertText('a') + editor.moveDown() + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + + it('emits an event with the old position, new position, and the cursor that moved', () => { + const cursorCallback = jasmine.createSpy('cursor-changed-position') + const editorCallback = jasmine.createSpy('editor-changed-cursor-position') + + editor.getLastCursor().onDidChangePosition(cursorCallback) + editor.onDidChangeCursorPosition(editorCallback) + + editor.setCursorBufferPosition([2, 4]) + + expect(editorCallback).toHaveBeenCalled() + expect(cursorCallback).toHaveBeenCalled() + const eventObject = editorCallback.mostRecentCall.args[0] + expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) + + expect(eventObject.oldBufferPosition).toEqual([0, 0]) + expect(eventObject.oldScreenPosition).toEqual([0, 0]) + expect(eventObject.newBufferPosition).toEqual([2, 4]) + expect(eventObject.newScreenPosition).toEqual([2, 4]) + expect(eventObject.cursor).toBe(editor.getLastCursor()) + }) + }) + + describe('.setCursorScreenPosition(screenPosition)', () => { + it('clears a goal column established by vertical movement', () => { + // set a goal column by moving down + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + editor.moveDown() + expect(editor.getCursorScreenPosition().column).not.toBe(6) + + // clear the goal column by explicitly setting the cursor position + editor.setCursorScreenPosition([4, 6]) + expect(editor.getCursorScreenPosition().column).toBe(6) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(6) + }) + + it('merges multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + const [cursor1, cursor2] = editor.getCursors() + editor.setCursorScreenPosition([4, 7]) + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursors()).toEqual([cursor1]) + expect(editor.getCursorScreenPosition()).toEqual([4, 7]) + }) + + describe('when soft-wrap is enabled and code is folded', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + editor.foldBufferRowRange(2, 3) + }) + + it('positions the cursor at the buffer position that corresponds to the given screen position', () => { + editor.setCursorScreenPosition([9, 0]) + expect(editor.getCursorBufferPosition()).toEqual([8, 11]) + }) + }) + }) + + describe('.moveUp()', () => { + it('moves the cursor up', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + expect(lineLengths[6]).toBeGreaterThan(32) + editor.setCursorScreenPosition({row: 6, column: 32}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(32) + }) + + describe('when the cursor is on the first line', () => { + it('moves the cursor to the beginning of the line, but retains the goal column', () => { + editor.setCursorScreenPosition([0, 4]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves above the selection', () => { + const cursor = editor.getLastCursor() + editor.moveUp() + expect(cursor.getBufferPosition()).toEqual([3, 9]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveUp() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + + describe('when the cursor was moved down from the beginning of an indented soft-wrapped line', () => { + it('moves to the beginning of the previous line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + }) + + describe('.moveDown()', () => { + it('moves the cursor down', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([3, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[3]) + }) + + describe('when the cursor is on the last line', () => { + it('moves the cursor to the end of line, but retains the goal column when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: editor.getTabLength()}) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual({row: lastLineIndex, column: lastLine.length}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(editor.getTabLength()) + }) + + it('retains a goal column of 0 when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: 0}) + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(0) + }) + }) + + describe('when the cursor is at the beginning of an indented soft-wrapped line', () => { + it("moves to the beginning of the line's continuation on the next screen row", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves below the selection', () => { + const cursor = editor.getLastCursor() + editor.moveDown() + expect(cursor.getBufferPosition()).toEqual([6, 10]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 2]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveDown() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveLeft()', () => { + it('moves the cursor by one column to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([1, 7]) + }) + + it('moves the cursor by n columns to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + + it('moves the cursor by two rows up when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveLeft(34) + expect(editor.getCursorScreenPosition()).toEqual([0, 29]) + }) + + it('moves the cursor to the beginning columnCount is longer than the position in the buffer', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(100) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + + describe('when the cursor is in the first column', () => { + describe('when there is a previous line', () => { + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: buffer.lineForRow(0).length}) + }) + + it('moves the cursor by one row up and n columns to the left', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 26]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the previous line', () => { + editor.setCursorScreenPosition([11, 0]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when line is wrapped and follow previous line indentation', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + }) + + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition([4, 4]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([3, 46]) + }) + }) + + describe('when the cursor is on the first line', () => { + it('remains in the same position (0,0)', () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: 0}) + }) + + it('remains in the same position (0,0) when columnCount is specified', () => { + editor.setCursorScreenPosition([0, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + }) + }) + + describe('when softTabs is enabled and the cursor is preceded by leading whitespace', () => { + it('skips tabLength worth of whitespace at a time', () => { + editor.setCursorBufferPosition([5, 6]) + + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([5, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 22]) + + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 21]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + + const [cursor1, cursor2] = editor.getCursors() + editor.moveLeft() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveRight()', () => { + it('moves the cursor by one column to the right', () => { + editor.setCursorScreenPosition([3, 3]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + + it('moves the cursor by n columns to the right', () => { + editor.setCursorScreenPosition([3, 7]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([3, 11]) + }) + + it('moves the cursor by two rows down when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([0, 29]) + editor.moveRight(34) + expect(editor.getCursorScreenPosition()).toEqual([2, 2]) + }) + + it('moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position', () => { + editor.setCursorScreenPosition([11, 5]) + editor.moveRight(100) + expect(editor.getCursorScreenPosition()).toEqual([12, 2]) + }) + + describe('when the cursor is on the last column of a line', () => { + describe('when there is a subsequent line', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([1, 0]) + }) + + it('moves the cursor by one row down and n columns to the right', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 3]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([9, 4]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when the cursor is on the last line', () => { + it('remains in the same position', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + const lastPosition = {row: lastLineIndex, column: lastLine.length} + editor.setCursorScreenPosition(lastPosition) + editor.moveRight() + + expect(editor.getCursorScreenPosition()).toEqual(lastPosition) + }) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 27]) + + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 28]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([12, 1]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveRight() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToTop()', () => { + it('moves the cursor to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 1]) + editor.addCursorAtScreenPosition([12, 0]) + editor.moveToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBottom()', () => { + it('moves the cursor to the bottom of the buffer', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToBeginningOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 0]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the beginning of the line', () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + editor.moveToBeginningOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + }) + }) + + describe('.moveToEndOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToEndOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 9]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the end of line', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToEndOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + }) + }) + }) + + describe('.moveToBeginningOfLine()', () => { + it('moves cursor to the beginning of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToEndOfLine()', () => { + it('moves cursor to the end of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([0, 2]) + editor.moveToEndOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('.moveToFirstCharacterOfLine()', () => { + describe('when soft wrap is on', () => { + it("moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([2, 5]) + editor.addCursorAtScreenPosition([8, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + }) + }) + + describe('when soft wrap is off', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + + it('moves to the beginning of the line if it only contains whitespace ', () => { + editor.setText('first\n \nthird') + editor.setCursorScreenPosition([1, 2]) + editor.moveToFirstCharacterOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getBufferPosition()).toEqual([1, 0]) + }) + + describe('when invisible characters are enabled with soft tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + + describe('when invisible characters are enabled with hard tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', {normalizeLineEndings: false}) + + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 3]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + }) + }) + + describe('.moveToBeginningOfWord()', () => { + it('moves the cursor to the beginning of the word', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 12]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + expect(cursor3.getBufferPosition()).toEqual([2, 39]) + }) + + it('does not fail at position [0, 0]', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfWord() + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + editor.buffer.setText(buffer.getText().replace(/\r\n/g, '\n')) + }) + }) + + describe('.moveToPreviousWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([2, 4]) + editor.addCursorAtBufferPosition([3, 14]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToPreviousWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('.moveToNextWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([3, 0]) + editor.addCursorAtBufferPosition([3, 30]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToNextWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 0]) + expect(cursor3.getBufferPosition()).toEqual([3, 4]) + expect(cursor4.getBufferPosition()).toEqual([3, 31]) + }) + }) + + describe('.moveToEndOfWord()', () => { + it('moves the cursor to the end of the word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 10]) + editor.addCursorAtBufferPosition([2, 40]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToEndOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + expect(cursor3.getBufferPosition()).toEqual([3, 7]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + }) + + describe('.moveToBeginningOfNextWord()', () => { + it('moves the cursor before the first character of the next word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 11]) + editor.addCursorAtBufferPosition([2, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfNextWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + expect(cursor3.getBufferPosition()).toEqual([2, 4]) + + // When the cursor is on whitespace + editor.setText('ab cde- ') + editor.setCursorBufferPosition([0, 2]) + const cursor = editor.getLastCursor() + editor.moveToBeginningOfNextWord() + + expect(cursor.getBufferPosition()).toEqual([0, 3]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 9]) + }) + }) + + describe('.moveToPreviousSubwordBoundary', () => { + it('does not move the cursor when there is no previous subword boundary', () => { + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText('sub_word \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 8]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText(' word\n') + editor.setCursorBufferPosition([0, 3]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText(' getPreviousWord\n') + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText('e, => \n') + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToPreviousSubwordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 8]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToNextSubwordBoundary', () => { + it('does not move the cursor when there is no next subword boundary', () => { + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText(' sub_word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 9]) + + editor.setText('word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText('getPreviousWord \n') + editor.setCursorBufferPosition([0, 0]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText(', => \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToBeginningOfNextParagraph()', () => { + it('moves the cursor before the first line of the next paragraph', () => { + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the next paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBeginningOfPreviousParagraph()', () => { + it('moves the cursor before the first line of the previous paragraph', () => { + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the previous paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCurrentParagraphBufferRange()', () => { + it('returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file', () => { + buffer.setText(' ' + dedent` + I am the first paragraph, + bordered by the beginning of + the file + ${' '} + + I am the second paragraph + with blank lines above and below + me. + + I am the last paragraph, + bordered by the end of the file.\ + `) + + // in a paragraph + editor.setCursorBufferPosition([1, 7]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[0, 0], [2, 8]]) + + editor.setCursorBufferPosition([7, 1]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[5, 0], [7, 3]]) + + editor.setCursorBufferPosition([9, 10]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[9, 0], [10, 32]]) + + // between paragraphs + editor.setCursorBufferPosition([3, 1]) + expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() + }) + + it('will limit paragraph range to comments', () => { + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + editor.setText(dedent` + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + };\ + `) + + function paragraphBufferRangeForRow (row) { + editor.setCursorBufferPosition([row, 0]) + return editor.getLastCursor().getCurrentParagraphBufferRange() + } + + expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) + expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) + expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) + expect(paragraphBufferRangeForRow(3)).toBeFalsy() + expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) + expect(paragraphBufferRangeForRow(9)).toBeFalsy() + expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) + expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) + expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + + describe('getCursorAtScreenPosition(screenPosition)', () => { + it('returns the cursor at the given screenPosition', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) + expect(cursor2).toBe(cursor1) + }) + }) + + describe('::getCursorScreenPositions()', () => { + it('returns the cursor positions in the order they were added', () => { + editor.foldBufferRow(4) + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([3, 5]) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [5, 5], [3, 5]]) + }) + }) + + describe('::getCursorsOrderedByBufferPosition()', () => { + it('returns all cursors ordered by buffer positions', () => { + const originalCursor = editor.getLastCursor() + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([4, 5]) + expect(editor.getCursorsOrderedByBufferPosition()).toEqual([originalCursor, cursor2, cursor1]) + }) + }) + + describe('addCursorAtScreenPosition(screenPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.addCursorAtScreenPosition([0, 2]) + expect(cursor2).toBe(cursor1) + }) + }) + }) + + describe('addCursorAtBufferPosition(bufferPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtBufferPosition([1, 4]) + const cursor2 = editor.addCursorAtBufferPosition([1, 4]) + expect(cursor2.marker).toBe(cursor1.marker) + }) + }) + }) + + describe('.getCursorScope()', () => { + it('returns the current scope', () => { + const descriptor = editor.getCursorScope() + expect(descriptor.scopes).toContain('source.js') + }) + }) + }) + + describe('selection', () => { + let selection + + beforeEach(() => { + selection = editor.getLastSelection() + }) + + describe('.getLastSelection()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + + it("doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", () => { + let callCount = 0 + editor.getLastSelection().destroy() + editor.onDidAddCursor(function (cursor) { + callCount++ + editor.getLastSelection() + }) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + expect(callCount).toBe(1) + }) + }) + + describe('.getSelections()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + + describe('when the selection range changes', () => { + it('emits an event with the old range, new range, and the selection that moved', () => { + let rangeChangedHandler + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + + editor.onDidChangeSelectionRange(rangeChangedHandler = jasmine.createSpy()) + editor.selectToBufferPosition([6, 2]) + + expect(rangeChangedHandler).toHaveBeenCalled() + const eventObject = rangeChangedHandler.mostRecentCall.args[0] + + expect(eventObject.oldBufferRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.oldScreenRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.newBufferRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.newScreenRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.selection).toBe(selection) + }) + }) + + describe('.selectUp/Down/Left/Right()', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 14]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 22]]) + + editor.selectLeft() + editor.selectLeft() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown() + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + + editor.selectUp() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + }) + + it('merges selections when they intersect when moving down', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) + const [selection1, selection2, selection3] = editor.getSelections() + + editor.selectDown() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + it('merges selections when they intersect when moving up', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectUp() + expect(editor.getSelections().length).toBe(1) + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving left', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectLeft() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving right', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + describe('when counts are passed into the selection functions', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 15]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 23]]) + + editor.selectLeft(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [3, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [6, 20]]) + + editor.selectUp(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + }) + }) + }) + + describe('.selectToBufferPosition(bufferPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtBufferPosition([5, 6]) + editor.selectToBufferPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getBufferRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getBufferRange()).toEqual([[5, 6], [6, 2]]) + }) + }) + + describe('.selectToScreenPosition(screenPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getScreenRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getScreenRange()).toEqual([[5, 6], [6, 2]]) + }) + + describe('when selecting with an initial screen range', () => { + it('switches the direction of the selection when selecting to positions before/after the start of the initial range', () => { + editor.setCursorScreenPosition([5, 10]) + editor.selectWordsContainingCursors() + editor.selectToScreenPosition([3, 0]) + expect(editor.getLastSelection().isReversed()).toBe(true) + editor.selectToScreenPosition([9, 0]) + expect(editor.getLastSelection().isReversed()).toBe(false) + }) + }) + }) + + describe('.selectToBeginningOfNextParagraph()', () => { + it('selects from the cursor to first line of the next paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfNextParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[3, 0], [10, 0]]) + }) + }) + + describe('.selectToBeginningOfPreviousParagraph()', () => { + it('selects from the cursor to the first line of the previous paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfPreviousParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[0, 0], [5, 6]]) + }) + + it('merges selections if they intersect, maintaining the directionality of the last selection', () => { + editor.setCursorScreenPosition([4, 10]) + editor.selectToScreenPosition([5, 27]) + editor.addCursorAtScreenPosition([3, 10]) + editor.selectToScreenPosition([6, 27]) + + let selections = editor.getSelections() + expect(selections.length).toBe(1) + let [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [6, 27]]) + expect(selection1.isReversed()).toBeFalsy() + + editor.addCursorAtScreenPosition([7, 4]) + editor.selectToScreenPosition([4, 11]) + + selections = editor.getSelections() + expect(selections.length).toBe(1); + [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [7, 4]]) + expect(selection1.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToTop()', () => { + it('selects text from cursor position to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 2]) + editor.addCursorAtScreenPosition([10, 0]) + editor.selectToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [11, 2]]) + expect(editor.getLastSelection().isReversed()).toBeTruthy() + }) + }) + + describe('.selectToBottom()', () => { + it('selects text from cursor position to the bottom of the buffer', () => { + editor.setCursorScreenPosition([10, 0]) + editor.addCursorAtScreenPosition([9, 3]) + editor.selectToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[9, 3], [12, 2]]) + expect(editor.getLastSelection().isReversed()).toBeFalsy() + }) + }) + + describe('.selectAll()', () => { + it('selects the entire buffer', () => { + editor.selectAll() + expect(editor.getLastSelection().getBufferRange()).toEqual(buffer.getRange()) + }) + }) + + describe('.selectToBeginningOfLine()', () => { + it('selects text from cursor position to beginning of line', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToBeginningOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 0]) + expect(cursor2.getBufferPosition()).toEqual([11, 0]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[11, 0], [11, 3]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfLine()', () => { + it('selects text from cursor position to end of line', () => { + editor.setCursorScreenPosition([12, 0]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToEndOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + expect(cursor2.getBufferPosition()).toEqual([11, 44]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[11, 3], [11, 44]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectLinesContainingCursors()', () => { + it('selects to the entire line (including newlines) at given row', () => { + editor.setCursorScreenPosition([1, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [2, 0]]) + expect(editor.getSelectedText()).toBe(' var sort = function(items) {\n') + + editor.setCursorScreenPosition([12, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 0], [12, 2]]) + + editor.setCursorBufferPosition([0, 2]) + editor.selectLinesContainingCursors() + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [2, 0]]) + }) + + describe('when the selection spans multiple row', () => { + it('selects from the beginning of the first line to the last line', () => { + selection = editor.getLastSelection() + selection.setBufferRange([[1, 10], [3, 20]]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [4, 0]]) + }) + }) + }) + + describe('.selectToBeginningOfWord()', () => { + it('selects text from cursor position to beginning of word', () => { + editor.setCursorScreenPosition([0, 13]) + editor.addCursorAtScreenPosition([3, 49]) + + editor.selectToBeginningOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([3, 47]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[3, 47], [3, 49]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfWord()', () => { + it('selects text from cursor position to end of word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToEndOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 50]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 50]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToBeginningOfNextWord()', () => { + it('selects text from cursor position to beginning of next word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToBeginningOfNextWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([3, 51]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 14]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 51]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousWordBoundary()', () => { + it('select to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([3, 4]) + editor.addCursorAtBufferPosition([3, 14]) + + editor.selectToPreviousWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 4]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[2, 0], [1, 30]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[3, 4], [3, 0]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 14], [3, 13]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextWordBoundary()', () => { + it('select to the next word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([4, 0]) + editor.addCursorAtBufferPosition([3, 30]) + + editor.selectToNextWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[2, 40], [3, 0]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 30], [3, 31]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToPreviousSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 1]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToNextSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.deleteToBeginningOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe(' getviousWord') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 1]) + expect(cursor2.getBufferPosition()).toEqual([1, 4]) + expect(cursor3.getBufferPosition()).toEqual([2, 3]) + expect(cursor4.getBufferPosition()).toEqual([3, 1]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe(' viousWord') + expect(buffer.lineForRow(2)).toBe('e ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 1]) + expect(cursor3.getBufferPosition()).toEqual([2, 1]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('viousWord') + expect(buffer.lineForRow(2)).toBe(' ') + expect(buffer.lineForRow(3)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([2, 1]) + }) + }) + + describe('.deleteToEndOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord \n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe('PreviousWord ') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe('88 ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('Word ') + expect(buffer.lineForRow(2)).toBe('e,') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + }) + }) + + describe('.selectWordsContainingCursors()', () => { + describe('when the cursor is inside a word', () => { + it('selects the entire word', () => { + editor.setCursorScreenPosition([0, 8]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + }) + }) + + describe('when the cursor is between two words', () => { + it('selects the word the cursor is on', () => { + editor.setCursorScreenPosition([0, 4]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + + editor.setCursorScreenPosition([0, 3]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('var') + }) + }) + + describe('when the cursor is inside a region of whitespace', () => { + it('selects the whitespace region', () => { + editor.setCursorScreenPosition([5, 2]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + + editor.setCursorScreenPosition([5, 0]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + }) + }) + + describe('when the cursor is at the end of the text', () => { + it('select the previous word', () => { + editor.buffer.append('word') + editor.moveToBottom() + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 2], [12, 6]]) + }) + }) + + it("selects words based on the non-word characters configured at the cursor's current scope", () => { + editor.setText("one-one; 'two-two'; three-three") + + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([0, 12]) + + const scopeDescriptors = editor.getCursors().map(c => c.getScopeDescriptor()) + expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) + expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) + + editor.setScopedSettingsDelegate({ + getNonWordCharacters (scopes) { + const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' + if (scopes.some(scope => scope.startsWith('string'))) { + return result + } else { + return result + '-' + } + } + }) + + editor.selectWordsContainingCursors() + + expect(editor.getSelections()[0].getText()).toBe('one') + expect(editor.getSelections()[1].getText()).toBe('two-two') + }) + }) + + describe('.selectToFirstCharacterOfLine()', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.selectToFirstCharacterOfLine() + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + expect(editor.getSelections().length).toBe(2) + let [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 2], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + + editor.selectToFirstCharacterOfLine(); + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 0], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.setSelectedBufferRanges(ranges)', () => { + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[5, 5], [6, 6]]]) + }) + + it('merges intersecting selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('does not merge non-empty adjacent selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getBufferRange()).toEqual([[2, 2], [3, 3]]) + }) + + describe("when the 'preserveFolds' option is false (the default)", () => { + it("removes folds that contain one or both of the selection's end points", () => { + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(2, 3) + editor.foldBufferRowRange(6, 8) + editor.foldBufferRowRange(10, 11) + + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + + editor.setSelectedBufferRange([[10, 0], [12, 0]]) + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + }) + }) + + describe("when the 'preserveFolds' option is true", () => { + it('does not remove folds that contain the selections', () => { + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(6, 8) + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + }) + }) + }) + + describe('.setSelectedScreenRanges(ranges)', () => { + beforeEach(() => editor.foldBufferRow(4)) + + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 4], [3, 7]], [[8, 4], [8, 7]]]) + + editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) + expect(editor.getSelectedScreenRanges()).toEqual([[[6, 2], [6, 4]]]) + }) + + it('merges intersecting selections and unfolds the fold which contain them', () => { + editor.foldBufferRow(0) + + // Use buffer ranges because only the first line is on screen + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getScreenRange()).toEqual([[2, 2], [3, 4]]) + }) + }) + + describe('.selectMarker(marker)', () => { + describe('if the marker is valid', () => { + it("selects the marker's range and returns the selected range", () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + expect(editor.selectMarker(marker)).toEqual([[0, 1], [3, 3]]) + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 3]]) + }) + }) + + describe('if the marker is invalid', () => { + it('does not change the selection and returns a falsy value', () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + marker.destroy() + expect(editor.selectMarker(marker)).toBeFalsy() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + }) + + describe('.addSelectionForBufferRange(bufferRange)', () => { + it('adds a selection for the specified buffer range', () => { + editor.addSelectionForBufferRange([[3, 4], [5, 6]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 0]], [[3, 4], [5, 6]]]) + }) + }) + + describe('.addSelectionBelow()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line below current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 25], [3, 34]], + [[4, 16], [4, 21]], + [[4, 25], [4, 29]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[3, 31], [3, 38]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 31], [3, 38]], + [[6, 31], [6, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 38]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], + [[6, 22], [6, 38]] + ]) + }) + + it('clears selection goal ranges when the selection changes', () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.selectLeft() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 28]] + ]) + + // goal range from previous add selection is honored next time + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], // select to end of line 5 because line 4's goal range was reset by line 3 previously + [[6, 22], [6, 28]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + editor.setDefaultCharWidth(1) + + editor.setSelectedScreenRange([[3, 10], [3, 15]]) + editor.addSelectionBelow() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 10], [3, 15]], + [[4, 10], [4, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[2, 1], [2, 3]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 1], [2, 3]], + [[3, 1], [3, 2]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 0], [3, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([3, 37]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 37], [3, 37]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([3, 36]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 36], [3, 36]], + [[4, 29], [4, 29]], + [[5, 30], [5, 30]], + [[6, 36], [6, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([9, 4]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 4], [9, 4]], + [[11, 4], [11, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([9, 0]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 0], [9, 0]], + [[10, 0], [10, 0]] + ]) + }) + }) + }) + + describe('.addSelectionAbove()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line above current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 37], [3, 44]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 37], [3, 44]], + [[2, 16], [2, 21]], + [[2, 37], [2, 40]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[6, 31], [6, 38]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 31], [6, 38]], + [[3, 31], [3, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[6, 22], [6, 38]]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 22], [6, 38]], + [[5, 22], [5, 30]], + [[4, 22], [4, 29]], + [[3, 22], [3, 38]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + + editor.setSelectedScreenRange([[4, 10], [4, 15]]) + editor.addSelectionAbove() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[4, 10], [4, 15]], + [[3, 10], [3, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[3, 1], [3, 2]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 1], [3, 2]], + [[2, 1], [2, 3]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([5, 0]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 0], [5, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([5, 29]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 29], [5, 29]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([6, 36]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 36], [6, 36]], + [[5, 30], [5, 30]], + [[4, 29], [4, 29]], + [[3, 36], [3, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([11, 4]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[11, 4], [11, 4]], + [[9, 4], [9, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([10, 0]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[10, 0], [10, 0]], + [[9, 0], [9, 0]] + ]) + }) + }) + }) + + describe('.splitSelectionsIntoLines()', () => { + it('splits all multi-line selections into one selection per line', () => { + editor.setSelectedBufferRange([[0, 3], [2, 4]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 30]], + [[2, 0], [2, 4]] + ]) + + editor.setSelectedBufferRange([[0, 3], [1, 10]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 10]] + ]) + + editor.setSelectedBufferRange([[0, 0], [0, 3]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]]]) + }) + }) + + describe('::consolidateSelections()', () => { + const makeMultipleSelections = () => { + selection.setBufferRange([[3, 16], [3, 21]]) + const selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + const selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) + const selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) + expect(editor.getSelections()).toEqual([selection, selection2, selection3, selection4]) + return [selection, selection2, selection3, selection4] + } + + it('destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed', () => { + const [selection1] = makeMultipleSelections() + + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) + + expect(editor.consolidateSelections()).toBeTruthy() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.isEmpty()).toBeFalsy() + expect(editor.consolidateSelections()).toBeFalsy() + expect(editor.getSelections()).toEqual([selection1]) + + expect(autoscrollEvents).toEqual([ + {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} + ]) + }) + }) + + describe('when the cursor is moved while there is a selection', () => { + const makeSelection = () => selection.setBufferRange([[1, 2], [1, 5]]) + + it('clears the selection', () => { + makeSelection() + editor.moveDown() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveUp() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveLeft() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveRight() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.setCursorScreenPosition([3, 3]) + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + it('does not share selections between different edit sessions for the same buffer', async () => { + atom.workspace.getActivePane().splitRight() + const editor2 = await atom.workspace.open(editor.getPath()) + + expect(editor2.getText()).toBe(editor.getText()) + editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + }) + }) + + describe('buffer manipulation', () => { + describe('.moveLineUp', () => { + it('moves the line under the cursor up', () => { + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe(' var sort = function(items) {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the the autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.indentationForBufferRow(0)).toBe(0) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the preceeding row', () => + it('moves the line to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[3, 2], [3, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [2, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [4, 9]]) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe('when the preceding row consists of folded code', () => + it('moves the line above the folded row and perseveres the correct folds', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [8, 4]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' if (items.length <= 1) return items;') + }) + + describe("when the selection's end intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' if (items.length <= 1) return items;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe("when the selection's start intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [7, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(8)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 0]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the preceeding row is a folded row', () => { + it('moves the lines spanned by the selection to the preceeding row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [9, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [5, 2]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' };') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the preceding row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(0)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + }) + ) + + describe('when one selection intersects a fold', () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 2], [1, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + + describe('when there is a fold', () => + it('moves all lines that spanned by a selection to preceding row, preserving all folds', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 0], [4, 3]], [[10, 0], [10, 5]]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[1, 0], [5, 4]], + [[7, 0], [7, 4]] + ], {preserveFolds: true}) + + editor.moveLineUp() + + expect(editor.lineTextForBufferRow(1)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(4)).toEqual('6;') + expect(editor.lineTextForBufferRow(5)).toEqual('1;') + expect(editor.lineTextForBufferRow(6)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(9)).toEqual('7;') + + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [2, 9]], [[2, 12], [2, 13]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when one of the selections spans line 0', () => { + it("doesn't move any lines, since line 0 can't move", () => { + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(buffer.isModified()).toBe(false) + }) + }) + + describe('when one of the selections spans the last line, and it is empty', () => { + it("doesn't move any lines, since the last line can't move", () => { + buffer.append('\n') + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + }) + }) + }) + }) + + describe('.moveLineDown', () => { + it('moves the line under the cursor down', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe('var quicksort = function () {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the editor.autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the following row', () => + it('moves the line to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[2, 2], [2, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + + describe('when the following row is a folded row', () => + it('moves the line below the folded row and preserves the fold', () => { + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[3, 0], [3, 4]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[7, 0], [7, 4]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 0]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe("when the selection's end intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe("when the selection's start intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [9, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' };') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + + describe('when the following row is a folded row', () => { + it('moves the lines spanned by the selection to the following row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[2, 0], [3, 2]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[6, 0], [7, 2]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + }) + + describe('when the last line of selection does not end with a valid line ending', () => { + it('appends line ending to last line and moves the lines spanned by the selection to the preceeding row', () => { + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe('') + expect(editor.lineTextForBufferRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(12)).toBe('};') + + editor.setSelectedBufferRange([[10, 0], [12, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[9, 0], [11, 2]]) + expect(editor.lineTextForBufferRow(9)).toBe('') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(11)).toBe('};') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the following row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]]) + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + }) + ) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[2, 0], [2, 4]], + [[6, 0], [10, 4]] + ], {preserveFolds: true}) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(2)).toEqual('6;') + expect(editor.lineTextForBufferRow(3)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(6)).toEqual('12;') + expect(editor.lineTextForBufferRow(7)).toEqual('7;') + expect(editor.lineTextForBufferRow(8)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(11)).toEqual('11;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() + }) + ) + }) + + describe('when there is a fold below one of the selected row', () => + it('moves all lines spanned by a selection to the following row, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + ) + + describe('when there is a fold below a group of multiple selections without any lines with no selection in-between', () => + it('moves all the lines below the fold, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [7, 4]], [[6, 2], [6, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when one selection intersects a fold', () => { + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[5, 2], [5, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 12], [4, 13]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the selections are above a wrapped line', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(80) + editor.setText(`\ +1 +2 +Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +3 +4\ +`) + }) + + it('moves the lines past the soft wrapped line', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(0)).not.toBe('2') + expect(editor.lineTextForBufferRow(1)).toBe('1') + expect(editor.lineTextForBufferRow(2)).toBe('2') + }) + }) + }) + + describe('when the line is the last buffer row', () => { + it("doesn't move it", () => { + editor.setText('abc\ndef') + editor.setCursorBufferPosition([1, 0]) + editor.moveLineDown() + expect(editor.getText()).toBe('abc\ndef') + }) + }) + }) + + describe('.insertText(text)', () => { + describe('when there is a single selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('replaces the selection with the given text', () => { + const range = editor.insertText('xxx') + expect(range).toEqual([ [[1, 0], [1, 3]] ]) + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + }) + }) + + describe('when there are multiple empty selections', () => { + describe('when the cursors are on the same line', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([1, 5]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvarxxx sort = function(items) {') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + }) + }) + + describe('when the cursors are on different lines', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([2, 4]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' xxxif (items.length <= 1) return items;') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([2, 7]) + }) + }) + }) + + describe('when there are multiple non-empty selections', () => { + describe('when the selections are on the same line', () => { + it('replaces each selection range with the inserted characters', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) + editor.insertText('x') + + const [cursor1, cursor2] = editor.getCursors() + const [selection1, selection2] = editor.getSelections() + + expect(cursor1.getScreenPosition()).toEqual([0, 5]) + expect(cursor2.getScreenPosition()).toEqual([0, 15]) + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + expect(editor.lineTextForBufferRow(0)).toBe('var x = functix () {') + }) + }) + + describe('when the selections are on different lines', () => { + it("replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", () => { + editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe('xxxif (items.length <= 1) return items;') + const [selection1, selection2] = editor.getSelections() + + expect(selection1.isEmpty()).toBeTruthy() + expect(selection1.cursor.getBufferPosition()).toEqual([1, 3]) + expect(selection2.isEmpty()).toBeTruthy() + expect(selection2.cursor.getBufferPosition()).toEqual([2, 3]) + }) + }) + }) + + describe('when there is a selection that ends on a folded line', () => { + it('destroys the selection', () => { + editor.foldBufferRowRange(2, 4) + editor.setSelectedBufferRange([[1, 0], [2, 0]]) + editor.insertText('holy cow') + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + }) + }) + + describe('when there are ::onWillInsertText and ::onDidInsertText observers', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('notifies the observers when inserting text', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {')) + + const didInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {')) + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBeTruthy() + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).toHaveBeenCalled() + + let options = willInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + expect(options.cancel).toBeDefined() + + options = didInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + }) + + it('cancels text insertion when an ::onWillInsertText observer calls cancel on an event', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(({cancel}) => cancel()) + + const didInsertSpy = jasmine.createSpy() + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBe(false) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).not.toHaveBeenCalled() + }) + }) + + describe("when the undo option is set to 'skip'", () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]])) + + it('does not undo the skipped operation', () => { + let range = editor.insertText('x') + range = editor.insertText('y', {undo: 'skip'}) + editor.undo() + expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {') + }) + }) + }) + + describe('.insertNewline()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is at the beginning of a line', () => { + it('inserts an empty line before it', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is in the middle of a line', () => { + it('splits the current line to form a new line', () => { + editor.setCursorScreenPosition({row: 1, column: 6}) + const originalLine = buffer.lineForRow(1) + const lineBelowOriginalLine = buffer.lineForRow(2) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe(originalLine.slice(0, 6)) + expect(buffer.lineForRow(2)).toBe(originalLine.slice(6)) + expect(buffer.lineForRow(3)).toBe(lineBelowOriginalLine) + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('inserts an empty line after it', () => { + editor.setCursorScreenPosition({row: 1, column: buffer.lineForRow(1).length}) + + editor.insertNewline() + + expect(buffer.lineForRow(2)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when the cursors are on the same line', () => { + it('breaks the line at the cursor locations', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.insertNewline() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot') + expect(editor.lineTextForBufferRow(4)).toBe(' = items.shift(), current') + expect(editor.lineTextForBufferRow(5)).toBe(', left = [], right = [];') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([5, 0]) + }) + }) + + describe('when the cursors are on different lines', () => { + it('inserts newlines at each cursor location', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.insertText('\n') + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(7)).toBe('') + expect(editor.lineTextForBufferRow(8)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(9)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([8, 0]) + }) + }) + }) + }) + + describe('.insertNewlineBelow()', () => { + describe('when the operation is undone', () => { + it('places the cursor back at the previous location', () => { + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineBelow() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + }) + + it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", () => { + editor.update({autoIndent: true}) + editor.insertNewlineBelow() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' ') + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.insertNewlineAbove()', () => { + describe('when the cursor is on first line', () => { + it('inserts a newline on the first line and moves the cursor to the first line', () => { + editor.setCursorBufferPosition([0]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.buffer.getLineCount()).toBe(14) + }) + }) + + describe('when the cursor is not on the first line', () => { + it('inserts a newline above the current line and moves the cursor to the inserted line', () => { + editor.setCursorBufferPosition([3, 4]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([3, 0]) + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.buffer.getLineCount()).toBe(14) + + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([3, 4]) + }) + }) + + it('indents the new line to the correct level when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + + editor.setText(' var test') + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var test') + + editor.setText('\n var test') + editor.setCursorBufferPosition([1, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe(' var test') + + editor.setText('function() {\n}') + editor.setCursorBufferPosition([1, 1]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('function() {') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + }) + + describe('.insertNewLine()', () => { + describe('when a new line is appended before a closing tag (e.g. by pressing enter before a selection)', () => { + it('moves the line down and keeps the indentation level the same when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([9, 2]) + editor.insertNewline() + expect(editor.lineTextForBufferRow(10)).toBe(' };') + }) + }) + + describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => { + it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => { + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.js')) + editor.setText('var test = () => {\n return true;};') + editor.setCursorBufferPosition([1, 14]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + + it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => { + editor.setGrammar(atom.grammars.selectGrammar('file')) + editor.update({autoIndent: true}) + editor.setText(' if true') + editor.setCursorBufferPosition([0, 8]) + editor.insertNewline() + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(1) + }) + + it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => { + await atom.packages.activatePackage('language-coffee-script') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.coffee')) + editor.setText('if true\n return trueelse\n return false') + editor.setCursorBufferPosition([1, 13]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + expect(editor.indentationForBufferRow(3)).toBe(1) + }) + }) + + describe('when a newline is appended on a line that matches the decreaseNextIndentPattern', () => { + it('indents the new line to the correct level when editor.autoIndent is true', async () => { + await atom.packages.activatePackage('language-go') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.go')) + editor.setText('fmt.Printf("some%s",\n "thing")') + editor.setCursorBufferPosition([1, 10]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + }) + }) + + describe('.backspace()', () => { + describe('when there is a single cursor', () => { + let changeScreenRangeHandler = null + + beforeEach(() => { + const selection = editor.getLastSelection() + changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.onDidChangeRange(changeScreenRangeHandler) + }) + + describe('when the cursor is on the middle of the line', () => { + it('removes the character before the cursor', () => { + editor.setCursorScreenPosition({row: 1, column: 7}) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.backspace() + + const line = buffer.lineForRow(1) + expect(line).toBe(' var ort = function(items) {') + expect(editor.getCursorScreenPosition()).toEqual({row: 1, column: 6}) + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the beginning of a line', () => { + it('joins it with the line above', () => { + const originalLine0 = buffer.lineForRow(0) + expect(originalLine0).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.backspace() + + const line0 = buffer.lineForRow(0) + const line1 = buffer.lineForRow(1) + expect(line0).toBe('var quicksort = function () { var sort = function(items) {') + expect(line1).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorScreenPosition()).toEqual([0, originalLine0.length]) + + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the first column of the first line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.backspace() + }) + }) + + describe('when the cursor is after a fold', () => { + it('deletes the folded range', () => { + editor.foldBufferRange([[4, 7], [5, 8]]) + editor.setCursorBufferPosition([5, 8]) + editor.backspace() + + expect(buffer.lineForRow(4)).toBe(' whirrent = items.shift();') + expect(editor.isFoldedAtBufferRow(4)).toBe(false) + }) + }) + + describe('when the cursor is in the middle of a line below a fold', () => { + it('backspaces as normal', () => { + editor.setCursorScreenPosition([4, 0]) + editor.foldCurrentRow() + editor.setCursorScreenPosition([5, 5]) + editor.backspace() + + expect(buffer.lineForRow(7)).toBe(' }') + expect(buffer.lineForRow(8)).toBe(' eturn sort(left).concat(pivot).concat(sort(right));') + }) + }) + + describe('when the cursor is on a folded screen line', () => { + it('deletes the contents of the fold before the cursor', () => { + editor.setCursorBufferPosition([3, 0]) + editor.foldCurrentRow() + editor.backspace() + + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorScreenPosition()).toEqual([1, 29]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), curren, left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([3, 36]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of their lines', () => + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' whileitems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([4, 9]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are on the first column of their lines', () => + it('removes the newlines preceding each cursor', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.backspace() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift(); current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(5)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([2, 40]) + expect(cursor2.getBufferPosition()).toEqual([4, 30]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character before it', () => { + editor.setSelectedBufferRange([[0, 5], [0, 9]]) + editor.backspace() + expect(editor.buffer.lineForRow(0)).toBe('var qsort = function () {') + }) + + describe('when the selection ends on a folded line', () => { + it('preserves the fold', () => { + editor.setSelectedBufferRange([[3, 0], [4, 0]]) + editor.foldBufferRow(4) + editor.backspace() + + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtScreenRow(3)).toBe(true) + }) + }) + }) + + describe('when there are multiple selections', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.backspace() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + }) + + describe('.deleteToPreviousWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 16]) + editor.addCursorAtBufferPosition([1, 21]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = (items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort function () {') + expect(buffer.lineForRow(1)).toBe(' var sort =(items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToNextWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the next word boundary', () => { + editor.setCursorBufferPosition([0, 15]) + editor.addCursorAtBufferPosition([1, 24]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort = () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =() {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it{') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToBeginningOfWord()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([3, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(ems) {') + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 22]) + expect(cursor2.getBufferPosition()).toEqual([3, 4]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = functionems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 21]) + expect(cursor2.getBufferPosition()).toEqual([2, 39]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 13]) + expect(cursor2.getBufferPosition()).toEqual([2, 34]) + + editor.setText(' var sort') + editor.setCursorBufferPosition([0, 2]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(0)).toBe('var sort') + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToEndOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the end of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it') + expect(buffer.lineForRow(2)).toBe(' i') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + + describe('when at the end of the line', () => { + it('deletes the next newline', () => { + editor.setCursorBufferPosition([1, 30]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('deletes only the text in the selection', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToBeginningOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe('f (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 0]) + expect(cursor2.getBufferPosition()).toEqual([2, 0]) + }) + + describe('when at the beginning of the line', () => { + it('deletes the newline', () => { + editor.setCursorBufferPosition([2]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('still deletes all text to beginning of the line', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + }) + }) + }) + + describe('.delete()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is on the middle of a line', () => { + it('deletes the character following the cursor', () => { + editor.setCursorScreenPosition([1, 6]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var ort = function(items) {') + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('joins the line with the following line', () => { + editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + + describe('when the cursor is on the last column of the last line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) + editor.delete() + expect(buffer.lineForRow(12)).toBe('};') + }) + }) + + describe('when the cursor is before a fold', () => { + it('only deletes the lines inside the fold', () => { + editor.foldBufferRange([[3, 6], [4, 8]]) + editor.setCursorScreenPosition([3, 6]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' vae(items.length > 0) {') + expect(buffer.lineForRow(4)).toBe(' current = items.shift();') + expect(editor.getCursorScreenPosition()).toEqual(cursorPositionBefore) + }) + }) + + describe('when the cursor is in the middle a line above a fold', () => { + it('deletes as normal', () => { + editor.foldBufferRow(4) + editor.setCursorScreenPosition([3, 4]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(editor.isFoldedAtScreenRow(4)).toBe(true) + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + }) + + describe('when the cursor is inside a fold', () => { + it('removes the folded content after the cursor', () => { + editor.foldBufferRange([[2, 6], [6, 21]]) + editor.setCursorBufferPosition([4, 9]) + + editor.delete() + + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(buffer.lineForRow(4)).toBe(' while ? left.push(current) : right.push(current);') + expect(buffer.lineForRow(5)).toBe(' }') + expect(editor.getCursorBufferPosition()).toEqual([4, 9]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 37]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of the lines', () => + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(tems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([4, 10]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are at the end of their lines', () => + it('removes the newlines following each cursor', () => { + editor.setCursorScreenPosition([0, 29]) + editor.addCursorAtScreenPosition([1, 30]) + + editor.delete() + + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([0, 59]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character following it', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + expect(editor.getLastSelection().isEmpty()).toBeTruthy() + }) + }) + + describe('when there are multiple selections', () => + describe('when selections are on the same line', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.delete() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + ) + }) + + describe('.deleteToEndOfWord()', () => { + describe('when no text is selected', () => { + it('deletes to the end of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe(' i (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(buffer.lineForRow(2)).toBe(' iitems.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.indent()', () => { + describe('when the selection is empty', () => { + describe('when autoIndent is disabled', () => { + describe("if 'softTabs' is true (the default)", () => { + it("inserts 'tabLength' spaces into the buffer", () => { + const tabRegex = new RegExp(`^[ ]{${editor.getTabLength()}}`) + expect(buffer.lineForRow(0)).not.toMatch(tabRegex) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(tabRegex) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent() + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent() + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("if 'softTabs' is false", () => + it('insert a \t into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + }) + ) + }) + + describe('when autoIndent is enabled', () => { + describe("when the cursor's column is less than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation', () => { + buffer.insert([5, 0], ' \n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\s+$/) + expect(buffer.lineForRow(5).length).toBe(6) + expect(editor.getCursorBufferPosition()).toEqual([5, 6]) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("when 'softTabs' is false", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([5, 0], '\t\n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([5, 3]) + }) + + describe('when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1', () => + it('inserts one tab', () => { + editor.setSoftTabs(false) + buffer.setText(' \ntest') + editor.setCursorBufferPosition([1, 0]) + + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(1)).toBe('\ttest') + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + ) + }) + }) + + describe("when the line's indent level is greater than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => + it("moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", () => { + buffer.insert([7, 0], ' \n') + editor.setCursorBufferPosition([7, 2]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\s+$/) + expect(buffer.lineForRow(7).length).toBe(8) + expect(editor.getCursorBufferPosition()).toEqual([7, 8]) + }) + ) + + describe("when 'softTabs' is false", () => + it('moves the cursor to the end of the leading whitespace and inserts \t into the buffer', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([7, 0], '\t\t\t\n') + editor.setCursorBufferPosition([7, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\t\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([7, 4]) + }) + ) + }) + }) + }) + + describe('when the selection is not empty', () => { + it('indents the selected lines', () => { + editor.setSelectedBufferRange([[0, 0], [10, 0]]) + const selection = editor.getLastSelection() + spyOn(selection, 'indentSelectedRows') + editor.indent() + expect(selection.indentSelectedRows).toHaveBeenCalled() + }) + }) + + describe('if editor.softTabs is false', () => { + it('inserts a tab character into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength()]) + + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength() * 2]) + }) + }) + }) + + describe('clipboard operations', () => { + describe('.cutSelectedText()', () => { + it('removes the selected text from the buffer and places it on the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.cutSelectedText() + expect(buffer.lineForRow(0)).toBe('var = function () {') + expect(buffer.lineForRow(1)).toBe(' var = function(items) {') + expect(clipboard.readText()).toBe('quicksort\nsort') + }) + + describe('when no text is selected', () => { + beforeEach(() => + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[5, 0], [5, 0]] + ]) + ) + + it('cuts the lines on which there are cursors', () => { + editor.cutSelectedText() + expect(buffer.getLineCount()).toBe(11) + expect(buffer.lineForRow(1)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(4)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(atom.clipboard.read()).toEqual([ + 'var quicksort = function () {', + '', + ' current = items.shift();', + '' + ].join('\n')) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('cuts them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.cutSelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.cutToEndOfLine()', () => { + describe('when soft wrap is on', () => { + it('cuts up to the end of the line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(25) + editor.setCursorScreenPosition([2, 6]) + editor.cutToEndOfLine() + expect(editor.lineTextForScreenRow(2)).toBe(' var function(items) {') + }) + }) + + describe('when soft wrap is off', () => { + describe('when nothing is selected', () => + it('cuts up to the end of the line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + ) + + describe('when text is selected', () => + it('only cuts the selected text, not to the end of the line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + ) + }) + }) + + describe('.cutToEndOfBufferLine()', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + }) + + describe('when nothing is selected', () => { + it('cuts up to the end of the buffer line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + }) + + describe('when text is selected', () => { + it('only cuts the selected text, not to the end of the buffer line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.copySelectedText()', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + editor.copySelectedText() + + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual('quicksort\nsort\nitems') + }) + + describe('when no text is selected', () => { + beforeEach(() => { + editor.setSelectedBufferRanges([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + + it('copies the lines on which there are cursors', () => { + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual([ + ' var sort = function(items) {\n', + ' current = items.shift();\n' + ].join('\n')) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('copies them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.copyOnlySelectedText()', () => { + describe('when thee are multiple selections', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + + editor.copyOnlySelectedText() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + + describe('when no text is selected', () => { + it('does not copy anything', () => { + editor.setCursorBufferPosition([1, 5]) + editor.copyOnlySelectedText() + expect(atom.clipboard.read()).toEqual('initial clipboard content') + }) + }) + }) + + describe('.pasteText()', () => { + const copyText = function (text, {startColumn, textEditor} = {}) { + if (startColumn == null) startColumn = 0 + if (textEditor == null) textEditor = editor + textEditor.setCursorBufferPosition([0, 0]) + textEditor.insertText(text) + const numberOfNewlines = text.match(/\n/g).length + const endColumn = text.match(/[^\n]*$/)[0].length + textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) + return textEditor.cutSelectedText() + } + + it('pastes text into the buffer', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + atom.clipboard.write('first') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var first = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var first = function(items) {') + }) + + it('notifies ::onWillInsertText observers', () => { + const insertedStrings = [] + editor.onWillInsertText(function ({text, cancel}) { + insertedStrings.push(text) + cancel() + }) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + it('notifies ::onDidInsertText observers', () => { + const insertedStrings = [] + editor.onDidInsertText(({text, range}) => insertedStrings.push(text)) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + describe('when `autoIndentOnPaste` is true', () => { + beforeEach(() => editor.update({autoIndentOnPaste: true})) + + describe('when pasting multiple lines before any non-whitespace characters', () => { + it('auto-indents the lines spanned by the pasted text, based on the first pasted line', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Adjust the indentation of the pasted lines while preserving + // their indentation relative to each other. Also preserve the + // indentation of the following line. + expect(editor.lineTextForBufferRow(5)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(6)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(7)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' current = items.shift();') + }) + + it('auto-indents lines with a mix of hard tabs and spaces without removing spaces', () => { + editor.setSoftTabs(false) + expect(editor.indentationForBufferRow(5)).toBe(3) + + atom.clipboard.write('/**\n\t * testing\n\t * indent\n\t **/\n', {indentBasis: 1}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Do not lose the alignment spaces + expect(editor.lineTextForBufferRow(5)).toBe('\t\t\t/**') + expect(editor.lineTextForBufferRow(6)).toBe('\t\t\t * testing') + expect(editor.lineTextForBufferRow(7)).toBe('\t\t\t * indent') + expect(editor.lineTextForBufferRow(8)).toBe('\t\t\t **/') + }) + }) + + describe('when pasting line(s) above a line that matches the decreaseIndentPattern', () => + it('auto-indents based on the pasted line(s) only', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([7, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(7)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(9)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(10)).toBe(' }') + }) + ) + + describe('when pasting a line of text without line ending', () => + it('does not auto-indent the text', () => { + atom.clipboard.write('a(x);', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe('a(x); current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);') + }) + ) + + describe('when pasting on a line after non-whitespace characters', () => + it('does not auto-indent the affected line', () => { + // Before the paste, the indentation is non-standard. + editor.setText(dedent`\ + if (x) { + y(); + }\ + `) + + atom.clipboard.write(' z();\n h();') + editor.setCursorBufferPosition([1, Infinity]) + + // The indentation of the non-standard line is unchanged. + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' y(); z();') + expect(editor.lineTextForBufferRow(2)).toBe(' h();') + }) + ) + }) + + describe('when `autoIndentOnPaste` is false', () => { + beforeEach(() => editor.update({autoIndentOnPaste: false})) + + describe('when the cursor is indented further than the original copied text', () => + it('increases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[1, 2], [3, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([5, 6]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is indented less far than the original copied text', () => + it('decreases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[6, 6], [8, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([1, 2]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(1)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + ) + + describe('when the first copied line has leading whitespace', () => + it("preserves the line's leading whitespace", () => { + editor.setSelectedBufferRange([[4, 0], [6, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([0, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(0)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(1)).toBe(' current = items.shift();') + }) + ) + }) + + describe('when the clipboard has many selections', () => { + beforeEach(() => { + editor.update({autoIndentOnPaste: false}) + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.copySelectedText() + }) + + it('pastes each selection in order separately into the buffer', () => { + editor.setSelectedBufferRanges([ + [[1, 6], [1, 10]], + [[0, 4], [0, 13]] + ]) + + editor.moveRight() + editor.insertText('_') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort_quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var sort_sort = function(items) {') + }) + + describe('and the selections count does not match', () => { + beforeEach(() => editor.setSelectedBufferRanges([[[0, 4], [0, 13]]])) + + it('pastes the whole text into the buffer', () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort') + expect(editor.lineTextForBufferRow(1)).toBe('sort = function () {') + }) + }) + }) + + describe('when a full line was cut', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.cutSelectedText() + editor.setCursorBufferPosition([2, 13]) + }) + + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('when a full line was copied', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.copySelectedText() + }) + + describe('when there is a selection', () => + it('overwrites the selection as with any copied text', () => { + editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe('') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorBufferPosition()).toEqual([2, 0]) + }) + ) + + describe('when there is no selection', () => + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + }) + ) + }) + + it('respects options that preserve the formatting of the pasted text', () => { + editor.update({autoIndentOnPaste: true}) + atom.clipboard.write('a(x);\n b(x);\r\nc(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.insertText(' ') + editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false}) + + expect(editor.lineTextForBufferRow(5)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(6)).toBe(' b(x);') + expect(editor.buffer.lineEndingForRow(6)).toBe('\r\n') + expect(editor.lineTextForBufferRow(7)).toBe('c(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' current = items.shift();') + }) + }) + }) + + describe('.indentSelectedRows()', () => { + describe('when nothing is selected', () => { + describe('when softTabs is enabled', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + 1], [0, 3 + 1]]) + }) + }) + }) + + describe('when one line is selected', () => { + describe('when softTabs is enabled', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(`${editor.getTabText()}var quicksort = function () {`) + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + 1], [0, 14 + 1]]) + }) + }) + }) + + describe('when multiple lines are selected', () => { + describe('when softTabs is enabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]]) + }) + + it('does not indent the last row if the selection ends at column 0', () => { + editor.setSelectedBufferRange([[9, 1], [11, 0]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 0]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe('\t\t};') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe('\t\treturn sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + 1], [11, 15 + 1]]) + }) + }) + }) + }) + + describe('.outdentSelectedRows()', () => { + describe('when nothing is selected', () => { + it('outdents line and retains selection', () => { + editor.setSelectedBufferRange([[1, 3], [1, 3]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]]) + }) + + it('outdents when indent is less than a tab length', () => { + editor.insertText(' ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs', () => { + editor.insertText('\t\t') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents when a mix of hard tabs and soft tabs are used', () => { + editor.insertText('\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents only up to the first non-space non-tab character', () => { + editor.insertText(' \tfoo\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tfoo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + }) + }) + + describe('when one line is selected', () => { + it('outdents line and retains editor', () => { + editor.setSelectedBufferRange([[1, 4], [1, 14]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]]) + }) + }) + + describe('when multiple lines are selected', () => { + it('outdents selected lines and retains editor', () => { + editor.setSelectedBufferRange([[0, 1], [3, 15]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 15 - editor.getTabLength()]]) + }) + + it('does not outdent the last line of the selection if it ends at column 0', () => { + editor.setSelectedBufferRange([[0, 1], [3, 0]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 0]]) + }) + }) + }) + + describe('.autoIndentSelectedRows', () => { + it('auto-indents the selection', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText('function() {\ninside=true\n}\n i=1\n') + editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) + editor.autoIndentSelectedRows() + + expect(editor.lineTextForBufferRow(2)).toBe(' function() {') + expect(editor.lineTextForBufferRow(3)).toBe(' inside=true') + expect(editor.lineTextForBufferRow(4)).toBe(' }') + expect(editor.lineTextForBufferRow(5)).toBe(' i=1') + }) + }) + + describe('.undo() and .redo()', () => { + it('undoes/redoes the last change', () => { + editor.insertText('foo') + editor.undo() + expect(buffer.lineForRow(0)).not.toContain('foo') + + editor.redo() + expect(buffer.lineForRow(0)).toContain('foo') + }) + + it('batches the undo / redo of changes caused by multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + + editor.insertText('foo') + editor.backspace() + + expect(buffer.lineForRow(0)).toContain('fovar') + expect(buffer.lineForRow(1)).toContain('fo ') + + editor.undo() + + expect(buffer.lineForRow(0)).toContain('foo') + expect(buffer.lineForRow(1)).toContain('foo') + + editor.redo() + + expect(buffer.lineForRow(0)).not.toContain('foo') + expect(buffer.lineForRow(0)).toContain('fovar') + }) + + it('restores cursors and selections to their states before and after undone and redone changes', () => { + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + editor.insertText('abc') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.setSelectedBufferRanges([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + editor.insertText('def') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + }) + + it('restores the selected ranges after undo and redo', () => { + editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + editor.delete() + editor.delete() + + const selections = editor.getSelections() + expect(buffer.lineForRow(1)).toBe(' var = function( {') + + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 17], [1, 17]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + + editor.redo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + }) + + xit('restores folds after undo and redo', () => { + editor.foldBufferRow(1) + editor.setSelectedBufferRange([[1, 0], [10, Infinity]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + + editor.insertText(dedent`\ + // testing + function foo() { + return 1 + 2; + }\ + `) + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + editor.foldBufferRow(2) + + editor.undo() + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + + editor.redo() + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + }) + }) + + describe('::transact', () => { + it('restores the selection when the transaction is undone/redone', () => { + buffer.setText('1234') + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + + editor.transact(() => { + editor.delete() + editor.moveToEndOfLine() + editor.insertText('5') + expect(buffer.getText()).toBe('145') + }) + + editor.undo() + expect(buffer.getText()).toBe('1234') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + + editor.redo() + expect(buffer.getText()).toBe('145') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 3]]) + }) + }) + + describe('when the buffer is changed (via its direct api, rather than via than edit session)', () => { + it('moves the cursor so it is in the same relative position of the buffer', () => { + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + editor.addCursorAtScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + buffer.insert([0, 1], 'abc') + + expect(cursor1.getScreenPosition()).toEqual([0, 0]) + expect(cursor2.getScreenPosition()).toEqual([0, 8]) + expect(cursor3.getScreenPosition()).toEqual([1, 0]) + }) + + it('does not destroy cursors or selections when a change encompasses them', () => { + const cursor = editor.getLastCursor() + cursor.setBufferPosition([3, 3]) + editor.buffer.delete([[3, 1], [3, 5]]) + expect(cursor.getBufferPosition()).toEqual([3, 1]) + expect(editor.getCursors().indexOf(cursor)).not.toBe(-1) + + const selection = editor.getLastSelection() + selection.setBufferRange([[3, 5], [3, 10]]) + editor.buffer.delete([[3, 3], [3, 8]]) + expect(selection.getBufferRange()).toEqual([[3, 3], [3, 5]]) + expect(editor.getSelections().indexOf(selection)).not.toBe(-1) + }) + + it('merges cursors when the change causes them to overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 2]) + editor.addCursorAtScreenPosition([1, 2]) + + const [cursor1, cursor2, cursor3] = editor.getCursors() + expect(editor.getCursors().length).toBe(3) + + buffer.delete([[0, 0], [0, 2]]) + + expect(editor.getCursors().length).toBe(2) + expect(editor.getCursors()).toEqual([cursor1, cursor3]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor3.getBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.moveSelectionLeft()', () => { + it('moves one active selection on one line one column to the left', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 12]]) + }) + + it('moves multiple active selections on one line one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[0, 15], [0, 23]]]) + }) + + it('moves multiple active selections on multiple lines one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[1, 5], [1, 9]]]) + }) + + describe('when a selection is at the first column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + + editor.moveSelectionLeft() + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + }) + }) + }) + }) + + describe('.moveSelectionRight()', () => { + it('moves one active selection on one line one column to the right', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionRight() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 14]]) + }) + + it('moves multiple active selections on one line one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[0, 17], [0, 25]]]) + }) + + it('moves multiple active selections on multiple lines one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[1, 7], [1, 11]]]) + }) + + describe('when a selection is at the last column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + + editor.moveSelectionRight() + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + }) + }) + }) + }) + }) + + describe('reading text', () => { + it('.lineTextForScreenRow(row)', () => { + editor.foldBufferRow(4) + expect(editor.lineTextForScreenRow(5)).toEqual(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForScreenRow(9)).toEqual('};') + expect(editor.lineTextForScreenRow(10)).toBeUndefined() + }) + }) + + describe('.deleteLine()', () => { + it('deletes the first line when the cursor is there', () => { + editor.getLastCursor().moveToTop() + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the last line when the cursor is there', () => { + const count = buffer.getLineCount() + const secondToLastLine = buffer.lineForRow(count - 2) + expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) + editor.getLastCursor().moveToBottom() + editor.deleteLine() + const newCount = buffer.getLineCount() + expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) + expect(newCount).toBe(count - 1) + }) + + it('deletes whole lines when partial lines are selected', () => { + editor.setSelectedBufferRange([[0, 2], [1, 2]]) + const line2 = buffer.lineForRow(2) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line2) + expect(buffer.lineForRow(1)).not.toBe(line2) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line2) + expect(buffer.getLineCount()).toBe(count - 2) + }) + + it('deletes a line only once when multiple selections are on the same line', () => { + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 4], [0, 5]] + ]) + expect(buffer.lineForRow(0)).not.toBe(line1) + + editor.deleteLine() + + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('only deletes first line if only newline is selected on second line', () => { + editor.setSelectedBufferRange([[0, 2], [1, 0]]) + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the entire region when invoke on a folded region', () => { + editor.foldBufferRow(1) + editor.getLastCursor().moveToTop() + editor.getLastCursor().moveDown() + expect(buffer.getLineCount()).toBe(13) + editor.deleteLine() + expect(buffer.getLineCount()).toBe(4) + }) + + it('deletes the entire file from the bottom up', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToBottom() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + it('deletes the entire file from the top down', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToTop() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + describe('when soft wrap is enabled', () => { + it('deletes the entire line that the cursor is on', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorBufferPosition([6]) + + const line7 = buffer.lineForRow(7) + const count = buffer.getLineCount() + expect(buffer.lineForRow(6)).not.toBe(line7) + editor.deleteLine() + expect(buffer.lineForRow(6)).toBe(line7) + expect(buffer.getLineCount()).toBe(count - 1) + }) + }) + + describe('when the line being deleted precedes a fold, and the command is undone', () => { + it('restores the line and preserves the fold', () => { + editor.setCursorBufferPosition([4]) + editor.foldCurrentRow() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + editor.setCursorBufferPosition([3]) + editor.deleteLine() + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + editor.undo() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.replaceSelectedText(options, fn)', () => { + describe('when no text is selected', () => { + it('inserts the text returned from the function at the cursor position', () => { + editor.replaceSelectedText({}, () => '123') + expect(buffer.lineForRow(0)).toBe('123var quicksort = function () {') + + editor.setCursorBufferPosition([0]) + editor.replaceSelectedText({selectWordIfEmpty: true}, () => 'var') + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + + editor.setCursorBufferPosition([10]) + editor.replaceSelectedText(null, () => '') + expect(buffer.lineForRow(10)).toBe('') + }) + }) + + describe('when text is selected', () => { + it('replaces the selected text with the text returned from the function', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.replaceSelectedText({}, () => 'ia') + expect(buffer.lineForRow(0)).toBe('via quicksort = function () {') + }) + + it('replaces the selected text and selects the replacement text', () => { + editor.setSelectedBufferRange([[0, 4], [0, 9]]) + editor.replaceSelectedText({}, () => 'whatnot') + expect(buffer.lineForRow(0)).toBe('var whatnotsort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4], [0, 11]]) + }) + }) + }) + + describe('.transpose()', () => { + it('swaps two characters', () => { + editor.buffer.setText('abc') + editor.setCursorScreenPosition([0, 1]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('bac') + }) + + it('reverses a selection', () => { + editor.buffer.setText('xabcz') + editor.setSelectedBufferRange([[0, 1], [0, 4]]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('xcbaz') + }) + }) + + describe('.upperCase()', () => { + describe('when there is no selection', () => { + it('upper cases the current word', () => { + editor.buffer.setText('aBc') + editor.setCursorScreenPosition([0, 1]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('upper cases the current selection', () => { + editor.buffer.setText('abc') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.lowerCase()', () => { + describe('when there is no selection', () => { + it('lower cases the current word', () => { + editor.buffer.setText('aBC') + editor.setCursorScreenPosition([0, 1]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('lower cases the current selection', () => { + editor.buffer.setText('ABC') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.setTabLength(tabLength)', () => { + it('clips atomic soft tabs to the given tab length', () => { + expect(editor.getTabLength()).toBe(2) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 2]) + + editor.setTabLength(6) + expect(editor.getTabLength()).toBe(6) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 6]) + + const changeHandler = jasmine.createSpy('changeHandler') + editor.onDidChange(changeHandler) + editor.setTabLength(6) + expect(changeHandler).not.toHaveBeenCalled() + }) + + it('does not change its tab length when the given tab length is null', () => { + editor.setTabLength(4) + editor.setTabLength(null) + expect(editor.getTabLength()).toBe(4) + }) + }) + + describe('.indentLevelForLine(line)', () => { + it('returns the indent level when the line has only leading whitespace', () => { + expect(editor.indentLevelForLine(' hello')).toBe(2) + expect(editor.indentLevelForLine(' hello')).toBe(1.5) + }) + + it('returns the indent level when the line has only leading tabs', () => expect(editor.indentLevelForLine('\t\thello')).toBe(2)) + + it('returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs', () => { + expect(editor.indentLevelForLine('\t hello')).toBe(2) + expect(editor.indentLevelForLine(' \thello')).toBe(2) + expect(editor.indentLevelForLine(' \t hello')).toBe(2.5) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \t hello')).toBe(4.5) + }) + }) + + describe('when a better-matched grammar is added to syntax', () => { + it('switches to the better-matched grammar and re-tokenizes the buffer', async () => { + editor.destroy() + + const jsGrammar = atom.grammars.selectGrammar('a.js') + atom.grammars.removeGrammar(jsGrammar) + + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.tokensForScreenRow(0).length).toBe(1) + + atom.grammars.addGrammar(jsGrammar) + expect(editor.getGrammar()).toBe(jsGrammar) + expect(editor.tokensForScreenRow(0).length).toBeGreaterThan(1) + }) + }) + + describe('editor.autoIndent', () => { + describe('when editor.autoIndent is false (default)', () => { + describe('when `indent` is triggered', () => { + it('does not auto-indent the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: false}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + }) + + describe('when editor.autoIndent is true', () => { + beforeEach(() => editor.update({autoIndent: true})) + + describe('when `indent` is triggered', () => { + it('auto-indents the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: true}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + + describe('when a newline is added', () => { + describe('when the line preceding the newline adds a new level of indentation', () => { + it('indents the newline to one additional level of indentation beyond the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + + describe("when the line preceding the newline doesn't add a level of indentation", () => { + it('indents the new line to the same level as the preceding line', () => { + editor.setCursorBufferPosition([5, 14]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(6)).toBe(editor.indentationForBufferRow(5)) + }) + }) + + describe('when the line preceding the newline is a comment', () => { + it('maintains the indent of the commented line', () => { + editor.setCursorBufferPosition([0, 0]) + editor.insertText(' //') + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + }) + + describe('when the line preceding the newline contains only whitespace', () => { + it("bases the new line's indentation on only the preceding line", () => { + editor.setCursorBufferPosition([6, Infinity]) + editor.insertText('\n ') + expect(editor.getCursorBufferPosition()).toEqual([7, 2]) + + editor.insertNewline() + expect(editor.lineTextForBufferRow(8)).toBe(' ') + }) + }) + + it('does not indent the line preceding the newline', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText(' var this-line-should-be-indented-more\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([2, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(1) + }) + + describe('when the cursor is before whitespace', () => { + it('retains the whitespace following the cursor on the new line', () => { + editor.setText(' var sort = function() {}') + editor.setCursorScreenPosition([0, 12]) + editor.insertNewline() + + expect(buffer.lineForRow(0)).toBe(' var sort =') + expect(buffer.lineForRow(1)).toBe(' function() {}') + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + }) + }) + + describe('when inserted text matches a decrease indent pattern', () => { + describe('when the preceding line matches an increase indent pattern', () => { + it('decreases the indentation to match that of the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('}') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1)) + }) + }) + + describe("when the preceding line doesn't match an increase indent pattern", () => { + it('decreases the indentation to be one level below that of the preceding line', () => { + editor.setCursorBufferPosition([3, Infinity]) + editor.insertText('\n ') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3)) + editor.insertText('}') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3) - 1) + }) + + it("doesn't break when decreasing the indentation on a row that has no indentation", () => { + editor.setCursorBufferPosition([12, Infinity]) + editor.insertText('\n}; # too many closing brackets!') + expect(editor.lineTextForBufferRow(13)).toBe('}; # too many closing brackets!') + }) + }) + }) + + describe('when inserted text does not match a decrease indent pattern', () => { + it('does not decrease the indentation', () => { + editor.setCursorBufferPosition([12, 0]) + editor.insertText(' ') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + editor.insertText('\t\t') + expect(editor.lineTextForBufferRow(12)).toBe(' \t\t};') + }) + }) + + describe('when the current line does not match a decrease indent pattern', () => { + it('leaves the line unchanged', () => { + editor.setCursorBufferPosition([2, 4]) + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('foo') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + }) + }) + + describe('atomic soft tabs', () => { + it('skips tab-length runs of leading whitespace when moving the cursor', () => { + editor.update({tabLength: 4, atomicSoftTabs: true}) + + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + + editor.update({atomicSoftTabs: false}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 3]) + + editor.update({atomicSoftTabs: true}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + }) + }) + + describe('.destroy()', () => { + it('destroys marker layers associated with the text editor', () => { + buffer.retain() + const selectionsMarkerLayerId = editor.selectionsMarkerLayer.id + const foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id + editor.destroy() + expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() + expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() + buffer.release() + }) + + it('notifies ::onDidDestroy observers when the editor is destroyed', () => { + let destroyObserverCalled = false + editor.onDidDestroy(() => destroyObserverCalled = true) + + editor.destroy() + expect(destroyObserverCalled).toBe(true) + }) + + it('does not blow up when query methods are called afterward', () => { + editor.destroy() + editor.getGrammar() + editor.getLastCursor() + editor.lineTextForBufferRow(0) + }) + + it("emits the destroy event after destroying the editor's buffer", () => { + const events = [] + editor.getBuffer().onDidDestroy(() => { + expect(editor.isDestroyed()).toBe(true) + events.push('buffer-destroyed') + }) + editor.onDidDestroy(() => { + expect(buffer.isDestroyed()).toBe(true) + events.push('editor-destroyed') + }) + editor.destroy() + expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) + }) + }) + + describe('.joinLines()', () => { + describe('when no text is selected', () => { + describe("when the line below isn't empty", () => { + it('joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up', () => { + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText(' ') + editor.setCursorBufferPosition([0]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getCursorBufferPosition()).toEqual([0, 29]) + }) + }) + + describe('when the line below is empty', () => { + it('deletes the line below and moves the cursor to the end of the line', () => { + editor.setCursorBufferPosition([9]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([9, 4]) + }) + }) + + describe('when the cursor is on the last row', () => { + it('does nothing', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + editor.joinLines() + expect(editor.lineTextForBufferRow(12)).toBe('};') + }) + }) + + describe('when the line is empty', () => { + it('joins the line below with the current line with no added space', () => { + editor.setCursorBufferPosition([10]) + editor.joinLines() + expect(editor.lineTextForBufferRow(10)).toBe('return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + }) + }) + + describe('when text is selected', () => { + describe('when the selection does not span multiple lines', () => { + it('joins the line below with the current line separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + }) + }) + + describe('when the selection spans multiple lines', () => { + it('joins all selected lines separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[9, 3], [12, 1]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' }; return sort(Array.apply(this, arguments)); };') + expect(editor.getSelectedBufferRange()).toEqual([[9, 3], [9, 49]]) + }) + }) + }) + }) + + describe('.duplicateLines()', () => { + it('for each selection, duplicates all buffer lines intersected by the selection', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([2, 5]) + editor.addSelectionForBufferRange([[3, 0], [8, 0]], {preserveFolds: true}) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 5], [3, 5]], [[9, 0], [14, 0]]]) + + // folds are also duplicated + expect(editor.isFoldedAtScreenRow(5)).toBe(true) + expect(editor.isFoldedAtScreenRow(7)).toBe(true) + expect(editor.lineTextForScreenRow(7)).toBe(` while(items.length > 0) {${editor.displayLayer.foldCharacter}`) + expect(editor.lineTextForScreenRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + + it('duplicates all folded lines for empty selections on lines containing folds', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([4, 0]) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[8, 0], [8, 0]]) + }) + + it('can duplicate the last line of the buffer', () => { + editor.setSelectedBufferRange([[11, 0], [12, 2]]) + editor.duplicateLines() + expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe(`\ +\ return sort(Array.apply(this, arguments)); +}; + return sort(Array.apply(this, arguments)); +};\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[13, 0], [14, 2]]) + }) + + it('only duplicates lines containing multiple selections once', () => { + editor.setText(`\ +aaaaaa +bbbbbb +cccccc +dddddd\ +`) + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 3], [0, 4]], + [[2, 1], [2, 2]], + [[2, 3], [3, 1]], + [[3, 3], [3, 4]] + ]) + editor.duplicateLines() + expect(editor.getText()).toBe(`\ +aaaaaa +aaaaaa +bbbbbb +cccccc +dddddd +cccccc +dddddd\ +`) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 1], [1, 2]], + [[1, 3], [1, 4]], + [[5, 1], [5, 2]], + [[5, 3], [6, 1]], + [[6, 3], [6, 4]] + ]) + }) + }) + + describe('when the editor contains surrogate pair characters', () => { + it('correctly backspaces over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the editor contains variation sequence character pairs', () => { + it('correctly backspaces over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.setIndentationForBufferRow', () => { + describe('when the editor uses soft tabs but the row has hard tabs', () => { + it('only replaces whitespace characters', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + + describe('when the indentation level is a non-integer', () => { + it('does not throw an exception', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2.1) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + }) + + describe("when the editor's grammar has an injection selector", () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-text') + await atom.packages.activatePackage('language-javascript') + }) + + it("includes the grammar's patterns when the selector matches the current scope in other grammars", async () => { + await atom.packages.activatePackage('language-hyperlink') + + const grammar = atom.grammars.selectGrammar('text.js') + const {line, tags} = grammar.tokenizeLine('var i; // http://github.com') + + const tokens = atom.grammars.decodeTokens(line, tags) + expect(tokens[0].value).toBe('var') + expect(tokens[0].scopes).toEqual(['source.js', 'storage.type.var.js']) + expect(tokens[6].value).toBe('http://github.com') + expect(tokens[6].scopes).toEqual(['source.js', 'comment.line.double-slash.js', 'markup.underline.link.http.hyperlink']) + }) + + describe('when the grammar is added', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// http://github.com') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-hyperlink') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} + ]) + }) + + describe('when the grammar is updated', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// SELECT * FROM OCTOCATS') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('package-with-injection-selector') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-sql') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + }) + }) + }) + }) + + describe('.normalizeTabsInBufferRange()', () => { + it("normalizes tabs depending on the editor's soft tab/tab length settings", () => { + editor.setTabLength(1) + editor.setSoftTabs(true) + editor.setText('\t\t\t') + editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) + expect(editor.getText()).toBe(' \t\t') + + editor.setTabLength(2) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + + editor.setSoftTabs(false) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + }) + }) + + describe('.pageUp/Down()', () => { + it('moves the cursor down one page length', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(10) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(0) + }) + }) + + describe('.selectPageUp/Down()', () => { + it('selects one screen height of text up or down', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [5, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [10, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + + editor.moveToBottom() + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + }) + }) + + describe('::scrollToScreenPosition(position, [options])', () => { + it('triggers ::onDidRequestAutoscroll with the logical coordinates along with the options', () => { + const scrollSpy = jasmine.createSpy('::onDidRequestAutoscroll') + editor.onDidRequestAutoscroll(scrollSpy) + + editor.scrollToScreenPosition([8, 20]) + editor.scrollToScreenPosition([8, 20], {center: true}) + editor.scrollToScreenPosition([8, 20], {center: false, reversed: true}) + + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: true}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}}) + }) + }) + + describe('scroll past end', () => { + it('returns false by default but can be customized', () => { + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(true) + editor.update({scrollPastEnd: false}) + expect(editor.getScrollPastEnd()).toBe(false) + }) + + it('always returns false when autoHeight is on', () => { + editor.update({autoHeight: true, scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({autoHeight: false}) + expect(editor.getScrollPastEnd()).toBe(true) + }) + }) + + describe('auto height', () => { + it('returns true by default but can be customized', () => { + editor = new TextEditor() + expect(editor.getAutoHeight()).toBe(true) + editor.update({autoHeight: false}) + expect(editor.getAutoHeight()).toBe(false) + editor.update({autoHeight: true}) + expect(editor.getAutoHeight()).toBe(true) + editor.destroy() + }) + }) + + describe('auto width', () => { + it('returns false by default but can be customized', () => { + expect(editor.getAutoWidth()).toBe(false) + editor.update({autoWidth: true}) + expect(editor.getAutoWidth()).toBe(true) + editor.update({autoWidth: false}) + expect(editor.getAutoWidth()).toBe(false) + }) + }) + + describe('.get/setPlaceholderText()', () => { + it('can be created with placeholderText', () => { + const newEditor = new TextEditor({ + mini: true, + placeholderText: 'yep' + }) + expect(newEditor.getPlaceholderText()).toBe('yep') + }) + + it('models placeholderText and emits an event when changed', () => { + let handler + editor.onDidChangePlaceholderText(handler = jasmine.createSpy()) + + expect(editor.getPlaceholderText()).toBeUndefined() + + editor.setPlaceholderText('OK') + expect(handler).toHaveBeenCalledWith('OK') + expect(editor.getPlaceholderText()).toBe('OK') + }) + }) + + describe('gutters', () => { + describe('the TextEditor constructor', () => { + it('creates a line-number gutter', () => { + expect(editor.getGutters().length).toBe(1) + const lineNumberGutter = editor.gutterWithName('line-number') + expect(lineNumberGutter.name).toBe('line-number') + expect(lineNumberGutter.priority).toBe(0) + }) + }) + + describe('::addGutter', () => { + it('can add a gutter', () => { + expect(editor.getGutters().length).toBe(1) // line-number gutter + const options = { + name: 'test-gutter', + priority: 1 + } + const gutter = editor.addGutter(options) + expect(editor.getGutters().length).toBe(2) + expect(editor.getGutters()[1]).toBe(gutter) + }) + + it("does not allow a custom gutter with the 'line-number' name.", () => expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow()) + }) + + describe('::decorateMarker', () => { + let marker + + beforeEach(() => marker = editor.markBufferRange([[1, 0], [1, 0]])) + + it('reflects an added decoration when one of its custom gutters is decorated.', () => { + const gutter = editor.addGutter({'name': 'custom-gutter'}) + const decoration = gutter.decorateMarker(marker, {class: 'custom-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'gutter', + gutterName: 'custom-gutter', + class: 'custom-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + + it('reflects an added decoration when its line-number gutter is decorated.', () => { + const decoration = editor.gutterWithName('line-number').decorateMarker(marker, {class: 'test-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'line-number', + gutterName: 'line-number', + class: 'test-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + }) + + describe('::observeGutters', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback immediately with each existing gutter, and with each added gutter after that.', () => { + const lineNumberGutter = editor.gutterWithName('line-number') + editor.observeGutters(callback) + expect(payloads).toEqual([lineNumberGutter]) + const gutter1 = editor.addGutter({name: 'test-gutter-1'}) + expect(payloads).toEqual([lineNumberGutter, gutter1]) + const gutter2 = editor.addGutter({name: 'test-gutter-2'}) + expect(payloads).toEqual([lineNumberGutter, gutter1, gutter2]) + }) + + it('does not call the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.observeGutters(callback) + payloads = [] + gutter.destroy() + expect(payloads).toEqual([]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.observeGutters(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidAddGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback with each newly-added gutter, but not with existing gutters.', () => { + editor.onDidAddGutter(callback) + expect(payloads).toEqual([]) + const gutter = editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([gutter]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.onDidAddGutter(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidRemoveGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.onDidRemoveGutter(callback) + expect(payloads).toEqual([]) + gutter.destroy() + expect(payloads).toEqual(['test-gutter']) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + const subscription = editor.onDidRemoveGutter(callback) + subscription.dispose() + gutter.destroy() + expect(payloads).toEqual([]) + }) + }) + }) + + describe('decorations', () => { + describe('::decorateMarker', () => { + it('includes the decoration in the object returned from ::decorationsStateForScreenRowRange', () => { + const marker = editor.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker.getScreenRange(), + bufferRange: marker.getBufferRange(), + rangeIsReversed: false + }) + }) + + it("does not throw errors after the marker's containing layer is destroyed", () => { + const layer = editor.addMarkerLayer() + const marker = layer.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + layer.destroy() + editor.decorationsStateForScreenRowRange(0, 5) + }) + }) + + describe('::decorateMarkerLayer', () => { + it('based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange', () => { + const layer1 = editor.getBuffer().addMarkerLayer() + const marker1 = layer1.markRange([[2, 4], [6, 8]]) + const marker2 = layer1.markRange([[11, 0], [11, 12]]) + const layer2 = editor.getBuffer().addMarkerLayer() + const marker3 = layer2.markRange([[8, 0], [9, 0]]) + + const layer1Decoration1 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'foo'}) + const layer1Decoration2 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'bar'}) + const layer2Decoration = editor.decorateMarkerLayer(layer2, {type: 'highlight', class: 'baz'}) + + let decorationState = editor.decorationsStateForScreenRowRange(0, 13) + + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration1.destroy() + + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'quux'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, null) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + }) + }) + }) + + describe('invisibles', () => { + beforeEach(() => { + editor.update({showInvisibles: true}) + }) + + it('substitutes invisible characters according to the given rules', () => { + const previousLineText = editor.lineTextForScreenRow(0) + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + expect(editor.getInvisibles()).toEqual({eol: '?'}) + }) + + it('does not use invisibles if showInvisibles is set to false', () => { + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + + editor.update({showInvisibles: false}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) + }) + }) + + describe('indent guides', () => { + it('shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini', () => { + editor.setText(' foo') + editor.setTabLength(2) + + editor.update({showIndentGuide: false}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.update({showIndentGuide: true}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.setMini(true) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + }) + }) + + describe('when the editor is constructed with the grammar option set', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-coffee-script') + }) + + it('sets the grammar', () => { + editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) + expect(editor.getGrammar().name).toBe('CoffeeScript') + }) + }) + + describe('softWrapAtPreferredLineLength', () => { + it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => { + editor.update({ + editorWidthInChars: 30, + softWrapped: true, + softWrapAtPreferredLineLength: true, + preferredLineLength: 20 + }) + + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = ') + + editor.update({editorWidthInChars: 10}) + expect(editor.lineTextForScreenRow(0)).toBe('var ') + + editor.update({mini: true}) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + }) + }) + + describe('softWrapHangingIndentLength', () => { + it('controls how much extra indentation is applied to soft-wrapped lines', () => { + editor.setText('123456789') + editor.update({ + editorWidthInChars: 8, + softWrapped: true, + softWrapHangingIndentLength: 2 + }) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + + editor.update({softWrapHangingIndentLength: 4}) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + }) + }) + + describe('::getElement', () => { + it('returns an element', () => expect(editor.getElement() instanceof HTMLElement).toBe(true)) + }) + + describe('setMaxScreenLineLength', () => { + it('sets the maximum line length in the editor before soft wrapping is forced', () => { + expect(editor.getSoftWrapColumn()).toBe(500) + editor.update({ + maxScreenLineLength: 1500 + }) + expect(editor.getSoftWrapColumn()).toBe(1500) + }) + }) +}) describe('TextEditor', () => { let editor @@ -539,3 +7185,7 @@ describe('TextEditor', () => { }) }) }) + +function convertToHardTabs (buffer) { + buffer.setText(buffer.getText().replace(/[ ]{2}/g, '\t')) +} From af82dff75bfebe79b056ddae744f99d4fa499f38 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:15:32 -0700 Subject: [PATCH 147/161] Fix error in .getLongTitle when editors have no path --- src/text-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index 4d7d94de0..033cea5d6 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1059,11 +1059,11 @@ class TextEditor { let myPathSegments const openEditorPathSegmentsWithSameFilename = [] for (const textEditor of atom.workspace.getTextEditors()) { - const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) if (textEditor.getFileName() === fileName) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) openEditorPathSegmentsWithSameFilename.push(pathSegments) + if (textEditor === this) myPathSegments = pathSegments } - if (textEditor === this) myPathSegments = pathSegments } if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName From 887975c4034f6ed3ef5dfa5ad7473b167cf0a317 Mon Sep 17 00:00:00 2001 From: Roy Giladi Date: Thu, 2 Nov 2017 01:33:39 +0200 Subject: [PATCH 148/161] Remove duplicate variable declaration Hey, just noticed that "Project" has already been declared on line 36 --- src/atom-environment.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a32c4424b..af61ffb36 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -42,7 +42,6 @@ PaneContainer = require './pane-container' PaneAxis = require './pane-axis' Pane = require './pane' Dock = require './dock' -Project = require './project' TextEditor = require './text-editor' TextBuffer = require 'text-buffer' Gutter = require './gutter' From 96e6b3a2ce467193b6671bff3b532ff501a1ce0c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:51:01 -0700 Subject: [PATCH 149/161] Fix error in .getLongTitle when editor isn't in the workspace --- spec/text-editor-spec.js | 9 +++++++++ src/text-editor.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index b2cc41ab7..cece5d753 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -218,6 +218,15 @@ describe('TextEditor', () => { expect(editor1.getLongTitle()).toBe('main.js \u2014 js') expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) }) + + it('returns the filename when the editor is not in the workspace', async () => { + editor.onDidDestroy(() => { + expect(editor.getLongTitle()).toBe('sample.js') + }) + + await atom.workspace.getActivePane().close() + expect(editor.isDestroyed()).toBe(true) + }) }) it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { diff --git a/src/text-editor.js b/src/text-editor.js index 033cea5d6..a0b9d19a0 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1066,7 +1066,7 @@ class TextEditor { } } - if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + if (!myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1) return fileName let commonPathSegmentCount for (let i = 0, {length} = myPathSegments; i < length; i++) { From d5445db78465645a6e8d4121262feeffea8d11ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:59:52 -0700 Subject: [PATCH 150/161] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2716fac9f..6b5d407eb 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0", + "text-buffer": "13.8.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 7f48c140ba61c9cf72d1848514ab610b16643413 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 17:37:29 -0700 Subject: [PATCH 151/161] :arrow_up: tabs for spec fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72250311d..584ea0af6 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", - "tabs": "0.109.0", + "tabs": "0.109.1", "timecop": "0.36.0", "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", From 9540d3f33ebb248c61e6ed92edb402024e35f510 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 21:42:04 -0700 Subject: [PATCH 152/161] :arrow_up: whitespace, snippets --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 584ea0af6..1dc4406aa 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.8", + "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", @@ -134,7 +134,7 @@ "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.4", + "whitespace": "0.37.5", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", From 1e9753d8a54a027c5902be0c9f8b10c665f271ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 10:22:22 -0600 Subject: [PATCH 153/161] Fix select-word command between word and non-word chararacters In #15776, we accidentally stopped passing an option to the wordRegExp method that caused us to prefer word characters when selecting words at a boundary between word and non-word characters. --- spec/text-editor-spec.js | 8 ++++++-- src/cursor.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index cece5d753..382d020d4 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -2007,13 +2007,17 @@ describe('TextEditor', () => { describe('when the cursor is between two words', () => { it('selects the word the cursor is on', () => { - editor.setCursorScreenPosition([0, 4]) + editor.setCursorBufferPosition([0, 4]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('quicksort') - editor.setCursorScreenPosition([0, 3]) + editor.setCursorBufferPosition([0, 3]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('var') + + editor.setCursorBufferPosition([1, 22]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('items') }) }) diff --git a/src/cursor.js b/src/cursor.js index 6cd0cc623..10bdef804 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -594,7 +594,7 @@ class Cursor extends Model { getCurrentWordBufferRange (options = {}) { const position = this.getBufferPosition() const ranges = this.editor.buffer.findAllInRangeSync( - options.wordRegex || this.wordRegExp(), + options.wordRegex || this.wordRegExp(options), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) const range = ranges.find(range => From 4ce351d0f34fc36cfd4cfc3b304e6af026257ed6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:22:58 -0700 Subject: [PATCH 154/161] Convert Selection to JS --- src/selection.coffee | 840 ------------------------------------- src/selection.js | 975 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 975 insertions(+), 840 deletions(-) delete mode 100644 src/selection.coffee create mode 100644 src/selection.js diff --git a/src/selection.coffee b/src/selection.coffee deleted file mode 100644 index e55f17e88..000000000 --- a/src/selection.coffee +++ /dev/null @@ -1,840 +0,0 @@ -{Point, Range} = require 'text-buffer' -{pick} = require 'underscore-plus' -{Emitter} = require 'event-kit' -Model = require './model' - -NonWhitespaceRegExp = /\S/ - -# Extended: Represents a selection in the {TextEditor}. -module.exports = -class Selection extends Model - cursor: null - marker: null - editor: null - initialScreenRange: null - wordwise: false - - constructor: ({@cursor, @marker, @editor, id}) -> - @emitter = new Emitter - - @assignId(id) - @cursor.selection = this - @decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection') - - @marker.onDidChange (e) => @markerDidChange(e) - @marker.onDidDestroy => @markerDidDestroy() - - destroy: -> - @marker.destroy() - - isLastSelection: -> - this is @editor.getLastSelection() - - ### - Section: Event Subscription - ### - - # Extended: Calls your `callback` when the selection was moved. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferRange` {Range} - # * `oldScreenRange` {Range} - # * `newBufferRange` {Range} - # * `newScreenRange` {Range} - # * `selection` {Selection} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeRange: (callback) -> - @emitter.on 'did-change-range', callback - - # Extended: Calls your `callback` when the selection was destroyed - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Managing the selection range - ### - - # Public: Returns the screen {Range} for the selection. - getScreenRange: -> - @marker.getScreenRange() - - # Public: Modifies the screen range for the selection. - # - # * `screenRange` The new {Range} to use. - # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - setScreenRange: (screenRange, options) -> - @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options) - - # Public: Returns the buffer {Range} for the selection. - getBufferRange: -> - @marker.getBufferRange() - - # Public: Modifies the buffer {Range} for the selection. - # - # * `bufferRange` The new {Range} to select. - # * `options` (optional) {Object} with the keys: - # * `preserveFolds` if `true`, the fold settings are preserved after the - # selection moves. - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # range. Defaults to `true` if this is the most recently added selection, - # `false` otherwise. - setBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - options.reversed ?= @isReversed() - @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds - @modifySelection => - needsFlash = options.flash - delete options.flash if options.flash? - @marker.setBufferRange(bufferRange, options) - @autoscroll() if options?.autoscroll ? @isLastSelection() - @decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash - - # Public: Returns the starting and ending buffer rows the selection is - # highlighting. - # - # Returns an {Array} of two {Number}s: the starting row, and the ending row. - getBufferRowRange: -> - range = @getBufferRange() - start = range.start.row - end = range.end.row - end = Math.max(start, end - 1) if range.end.column is 0 - [start, end] - - getTailScreenPosition: -> - @marker.getTailScreenPosition() - - getTailBufferPosition: -> - @marker.getTailBufferPosition() - - getHeadScreenPosition: -> - @marker.getHeadScreenPosition() - - getHeadBufferPosition: -> - @marker.getHeadBufferPosition() - - ### - Section: Info about the selection - ### - - # Public: Determines if the selection contains anything. - isEmpty: -> - @getBufferRange().isEmpty() - - # Public: Determines if the ending position of a marker is greater than the - # starting position. - # - # This can happen when, for example, you highlight text "up" in a {TextBuffer}. - isReversed: -> - @marker.isReversed() - - # Public: Returns whether the selection is a single line or not. - isSingleScreenLine: -> - @getScreenRange().isSingleLine() - - # Public: Returns the text in the selection. - getText: -> - @editor.buffer.getTextInRange(@getBufferRange()) - - # Public: Identifies if a selection intersects with a given buffer range. - # - # * `bufferRange` A {Range} to check against. - # - # Returns a {Boolean} - intersectsBufferRange: (bufferRange) -> - @getBufferRange().intersectsWith(bufferRange) - - intersectsScreenRowRange: (startRow, endRow) -> - @getScreenRange().intersectsRowRange(startRow, endRow) - - intersectsScreenRow: (screenRow) -> - @getScreenRange().intersectsRow(screenRow) - - # Public: Identifies if a selection intersects with another selection. - # - # * `otherSelection` A {Selection} to check against. - # - # Returns a {Boolean} - intersectsWith: (otherSelection, exclusive) -> - @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) - - ### - Section: Modifying the selected range - ### - - # Public: Clears the selection, moving the marker to the head. - # - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # range. Defaults to `true` if this is the most recently added selection, - # `false` otherwise. - clear: (options) -> - @goalScreenRange = null - @marker.clearTail() unless @retainSelection - @autoscroll() if options?.autoscroll ? @isLastSelection() - @finalize() - - # Public: Selects the text from the current cursor position to a given screen - # position. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, options) -> - position = Point.fromObject(position) - - @modifySelection => - if @initialScreenRange - if position.isLessThan(@initialScreenRange.start) - @marker.setScreenRange([position, @initialScreenRange.end], reversed: true) - else - @marker.setScreenRange([@initialScreenRange.start, position], reversed: false) - else - @cursor.setScreenPosition(position, options) - - if @linewise - @expandOverLine(options) - else if @wordwise - @expandOverWord(options) - - # 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`. - selectToBufferPosition: (position) -> - @modifySelection => @cursor.setBufferPosition(position) - - # Public: Selects the text one position right of the cursor. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - selectRight: (columnCount) -> - @modifySelection => @cursor.moveRight(columnCount) - - # Public: Selects the text one position left of the cursor. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - selectLeft: (columnCount) -> - @modifySelection => @cursor.moveLeft(columnCount) - - # Public: Selects all the text one position above the cursor. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - selectUp: (rowCount) -> - @modifySelection => @cursor.moveUp(rowCount) - - # Public: Selects all the text one position below the cursor. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - selectDown: (rowCount) -> - @modifySelection => @cursor.moveDown(rowCount) - - # Public: Selects all the text from the current cursor position to the top of - # the buffer. - selectToTop: -> - @modifySelection => @cursor.moveToTop() - - # Public: Selects all the text from the current cursor position to the bottom - # of the buffer. - selectToBottom: -> - @modifySelection => @cursor.moveToBottom() - - # Public: Selects all the text in the buffer. - selectAll: -> - @setBufferRange(@editor.buffer.getRange(), autoscroll: false) - - # Public: Selects all the text from the current cursor position to the - # beginning of the line. - selectToBeginningOfLine: -> - @modifySelection => @cursor.moveToBeginningOfLine() - - # Public: Selects all the text from the current cursor position to the first - # character of the line. - selectToFirstCharacterOfLine: -> - @modifySelection => @cursor.moveToFirstCharacterOfLine() - - # Public: Selects all the text from the current cursor position to the end of - # the screen line. - selectToEndOfLine: -> - @modifySelection => @cursor.moveToEndOfScreenLine() - - # Public: Selects all the text from the current cursor position to the end of - # the buffer line. - selectToEndOfBufferLine: -> - @modifySelection => @cursor.moveToEndOfLine() - - # Public: Selects all the text from the current cursor position to the - # beginning of the word. - selectToBeginningOfWord: -> - @modifySelection => @cursor.moveToBeginningOfWord() - - # Public: Selects all the text from the current cursor position to the end of - # the word. - selectToEndOfWord: -> - @modifySelection => @cursor.moveToEndOfWord() - - # Public: Selects all the text from the current cursor position to the - # beginning of the next word. - selectToBeginningOfNextWord: -> - @modifySelection => @cursor.moveToBeginningOfNextWord() - - # Public: Selects text to the previous word boundary. - selectToPreviousWordBoundary: -> - @modifySelection => @cursor.moveToPreviousWordBoundary() - - # Public: Selects text to the next word boundary. - selectToNextWordBoundary: -> - @modifySelection => @cursor.moveToNextWordBoundary() - - # Public: Selects text to the previous subword boundary. - selectToPreviousSubwordBoundary: -> - @modifySelection => @cursor.moveToPreviousSubwordBoundary() - - # Public: Selects text to the next subword boundary. - selectToNextSubwordBoundary: -> - @modifySelection => @cursor.moveToNextSubwordBoundary() - - # Public: Selects all the text from the current cursor position to the - # beginning of the next paragraph. - selectToBeginningOfNextParagraph: -> - @modifySelection => @cursor.moveToBeginningOfNextParagraph() - - # Public: Selects all the text from the current cursor position to the - # beginning of the previous paragraph. - selectToBeginningOfPreviousParagraph: -> - @modifySelection => @cursor.moveToBeginningOfPreviousParagraph() - - # Public: Modifies the selection to encompass the current word. - # - # Returns a {Range}. - selectWord: (options={}) -> - options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() - if @cursor.isBetweenWordAndNonWord() - options.includeNonWordCharacters = false - - @setBufferRange(@cursor.getCurrentWordBufferRange(options), options) - @wordwise = true - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire word on which - # the cursors rests. - expandOverWord: (options) -> - @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false) - @cursor.autoscroll() if options?.autoscroll ? true - - # Public: Selects an entire line in the buffer. - # - # * `row` The line {Number} to select (default: the row of the cursor). - selectLine: (row, options) -> - if row? - @setBufferRange(@editor.bufferRangeForBufferRow(row, includeNewline: true), options) - else - startRange = @editor.bufferRangeForBufferRow(@marker.getStartBufferPosition().row) - endRange = @editor.bufferRangeForBufferRow(@marker.getEndBufferPosition().row, includeNewline: true) - @setBufferRange(startRange.union(endRange), options) - - @linewise = true - @wordwise = false - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire line on which - # the cursor currently rests. - # - # It also includes the newline character. - expandOverLine: (options) -> - range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) - @setBufferRange(range, autoscroll: false) - @cursor.autoscroll() if options?.autoscroll ? true - - ### - Section: Modifying the selected text - ### - - # Public: Replaces text at the current selection. - # - # * `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). - # * `preserveTrailingLineIndentation` By default, when pasting multiple - # lines, Atom attempts to preserve the relative indent level between the - # first line and trailing lines, even if the indent level of the first - # line has changed from the copied text. If this option is `true`, this - # behavior is suppressed. - # level between the first lines and the trailing lines. - # * `normalizeLineEndings` (optional) {Boolean} (default: true) - # * `undo` If `skip`, skips the undo stack for this operation. - insertText: (text, options={}) -> - oldBufferRange = @getBufferRange() - wasReversed = @isReversed() - @clear(options) - - autoIndentFirstLine = false - precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) - remainingLines = text.split('\n') - firstInsertedLine = remainingLines.shift() - - if options.indentBasis? and not options.preserveTrailingLineIndentation - indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis - @adjustIndent(remainingLines, indentAdjustment) - - textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text) - if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 - autoIndentFirstLine = true - firstLine = precedingText + firstInsertedLine - desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) - indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine) - @adjustIndent(remainingLines, indentAdjustment) - - text = firstInsertedLine - text += '\n' + remainingLines.join('\n') if remainingLines.length > 0 - - newBufferRange = @editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings')) - - if options.select - @setBufferRange(newBufferRange, reversed: wasReversed) - else - @cursor.setBufferPosition(newBufferRange.end) if wasReversed - - if autoIndentFirstLine - @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) - - if options.autoIndentNewline and text is '\n' - @editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false) - else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text) - @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) - - @autoscroll() if options.autoscroll ? @isLastSelection() - - newBufferRange - - # Public: Removes the first character before the selection if the selection - # is empty otherwise it deletes the selection. - backspace: -> - @selectLeft() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or, if nothing is selected, then all - # characters from the start of the selection back to the previous word - # boundary. - deleteToPreviousWordBoundary: -> - @selectToPreviousWordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or, if nothing is selected, then all - # characters from the start of the selection up to the next word - # boundary. - deleteToNextWordBoundary: -> - @selectToNextWordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes from the start of the selection to the beginning of the - # current word if the selection is empty otherwise it deletes the selection. - deleteToBeginningOfWord: -> - @selectToBeginningOfWord() if @isEmpty() - @deleteSelectedText() - - # Public: Removes from the beginning of the line which the selection begins on - # all the way through to the end of the selection. - deleteToBeginningOfLine: -> - if @isEmpty() and @cursor.isAtBeginningOfLine() - @selectLeft() - else - @selectToBeginningOfLine() - @deleteSelectedText() - - # Public: Removes the selection or the next character after the start of the - # selection if the selection is empty. - delete: -> - @selectRight() if @isEmpty() - @deleteSelectedText() - - # Public: If the selection is empty, removes all text from the cursor to the - # end of the line. If the cursor is already at the end of the line, it - # removes the following newline. If the selection isn't empty, only deletes - # the contents of the selection. - deleteToEndOfLine: -> - return @delete() if @isEmpty() and @cursor.isAtEndOfLine() - @selectToEndOfLine() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToEndOfWord: -> - @selectToEndOfWord() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToBeginningOfSubword: -> - @selectToPreviousSubwordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToEndOfSubword: -> - @selectToNextSubwordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes only the selected text. - deleteSelectedText: -> - bufferRange = @getBufferRange() - @editor.buffer.delete(bufferRange) unless bufferRange.isEmpty() - @cursor?.setBufferPosition(bufferRange.start) - - # Public: Removes the line at the beginning of the selection if the selection - # is empty unless the selection spans multiple lines in which case all lines - # are removed. - deleteLine: -> - if @isEmpty() - start = @cursor.getScreenRow() - range = @editor.bufferRowsForScreenRows(start, start + 1) - if range[1] > range[0] - @editor.buffer.deleteRows(range[0], range[1] - 1) - else - @editor.buffer.deleteRow(range[0]) - else - range = @getBufferRange() - start = range.start.row - end = range.end.row - if end isnt @editor.buffer.getLastRow() and range.end.column is 0 - end-- - @editor.buffer.deleteRows(start, end) - - # Public: Joins the current line with the one below it. Lines will - # be separated by a single space. - # - # If there selection spans more than one line, all the lines are joined together. - joinLines: -> - selectedRange = @getBufferRange() - if selectedRange.isEmpty() - return if selectedRange.start.row is @editor.buffer.getLastRow() - else - joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never') - - rowCount = Math.max(1, selectedRange.getRowCount() - 1) - for [0...rowCount] - @cursor.setBufferPosition([selectedRange.start.row]) - @cursor.moveToEndOfLine() - - # Remove trailing whitespace from the current line - scanRange = @cursor.getCurrentLineBufferRange() - trailingWhitespaceRange = null - @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) -> - trailingWhitespaceRange = range - if trailingWhitespaceRange? - @setBufferRange(trailingWhitespaceRange) - @deleteSelectedText() - - currentRow = selectedRange.start.row - nextRow = currentRow + 1 - insertSpace = nextRow <= @editor.buffer.getLastRow() and - @editor.buffer.lineLengthForRow(nextRow) > 0 and - @editor.buffer.lineLengthForRow(currentRow) > 0 - @insertText(' ') if insertSpace - - @cursor.moveToEndOfLine() - - # Remove leading whitespace from the line below - @modifySelection => - @cursor.moveRight() - @cursor.moveToFirstCharacterOfLine() - @deleteSelectedText() - - @cursor.moveLeft() if insertSpace - - if joinMarker? - newSelectedRange = joinMarker.getBufferRange() - @setBufferRange(newSelectedRange) - joinMarker.destroy() - - # Public: Removes one level of indent from the currently selected rows. - outdentSelectedRows: -> - [start, end] = @getBufferRowRange() - buffer = @editor.buffer - leadingTabRegex = new RegExp("^( {1,#{@editor.getTabLength()}}|\t)") - for row in [start..end] - if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length - buffer.delete [[row, 0], [row, matchLength]] - return - - # Public: Sets the indentation level of all selected rows to values suggested - # by the relevant grammars. - autoIndentSelectedRows: -> - [start, end] = @getBufferRowRange() - @editor.autoIndentBufferRows(start, end) - - # Public: Wraps the selected lines in comments if they aren't currently part - # of a comment. - # - # Removes the comment if they are currently wrapped in a comment. - toggleLineComments: -> - @editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...) - - # Public: Cuts the selection until the end of the screen line. - cutToEndOfLine: (maintainClipboard) -> - @selectToEndOfLine() if @isEmpty() - @cut(maintainClipboard) - - # Public: Cuts the selection until the end of the buffer line. - cutToEndOfBufferLine: (maintainClipboard) -> - @selectToEndOfBufferLine() if @isEmpty() - @cut(maintainClipboard) - - # Public: Copies the selection to the clipboard and then deletes it. - # - # * `maintainClipboard` {Boolean} (default: false) See {::copy} - # * `fullLine` {Boolean} (default: false) See {::copy} - cut: (maintainClipboard=false, fullLine=false) -> - @copy(maintainClipboard, fullLine) - @delete() - - # Public: Copies the current selection to the clipboard. - # - # * `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) - # * `fullLine` {Boolean} if `true`, the copied text will always be pasted - # at the beginning of the line containing the cursor, regardless of the - # cursor's horizontal position. (default: false) - copy: (maintainClipboard=false, fullLine=false) -> - return if @isEmpty() - {start, end} = @getBufferRange() - selectionText = @editor.getTextInRange([start, end]) - precedingText = @editor.getTextInRange([[start.row, 0], start]) - startLevel = @editor.indentLevelForLine(precedingText) - - if maintainClipboard - {text: clipboardText, metadata} = @editor.constructor.clipboard.readWithMetadata() - metadata ?= {} - unless metadata.selections? - metadata.selections = [{ - text: clipboardText, - indentBasis: metadata.indentBasis, - fullLine: metadata.fullLine, - }] - metadata.selections.push({ - text: selectionText, - indentBasis: startLevel, - fullLine: fullLine - }) - @editor.constructor.clipboard.write([clipboardText, selectionText].join("\n"), metadata) - else - @editor.constructor.clipboard.write(selectionText, { - indentBasis: startLevel, - fullLine: fullLine - }) - - # Public: Creates a fold containing the current selection. - fold: -> - range = @getBufferRange() - unless range.isEmpty() - @editor.foldBufferRange(range) - @cursor.setBufferPosition(range.end) - - # Private: Increase the indentation level of the given text by given number - # of levels. Leaves the first line unchanged. - adjustIndent: (lines, indentAdjustment) -> - for line, i in lines - if indentAdjustment is 0 or line is '' - continue - else if indentAdjustment > 0 - lines[i] = @editor.buildIndentString(indentAdjustment) + line - else - currentIndentLevel = @editor.indentLevelForLine(lines[i]) - indentLevel = Math.max(0, currentIndentLevel + indentAdjustment) - lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel)) - return - - # Indent the current line(s). - # - # If the selection is empty, indents the current line if the cursor precedes - # non-whitespace characters, and otherwise inserts a tab. If the selection is - # non empty, calls {::indentSelectedRows}. - # - # * `options` (optional) {Object} with the keys: - # * `autoIndent` If `true`, the line is indented to an automatically-inferred - # level. Otherwise, {TextEditor::getTabText} is inserted. - indent: ({autoIndent}={}) -> - {row} = @cursor.getBufferPosition() - - if @isEmpty() - @cursor.skipLeadingWhitespace() - desiredIndent = @editor.suggestedIndentForBufferRow(row) - delta = desiredIndent - @cursor.getIndentLevel() - - if autoIndent and delta > 0 - delta = Math.max(delta, 1) unless @editor.getSoftTabs() - @insertText(@editor.buildIndentString(delta)) - else - @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) - else - @indentSelectedRows() - - # Public: If the selection spans multiple rows, indent all of them. - indentSelectedRows: -> - [start, end] = @getBufferRowRange() - for row in [start..end] - @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0 - return - - ### - Section: Managing multiple selections - ### - - # Public: Moves the selection down one row. - addSelectionBelow: -> - range = @getGoalScreenRange().copy() - nextRow = range.end.row + 1 - - for row in [nextRow..@editor.getLastScreenRow()] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - selection = @editor.addSelectionForScreenRange(clippedRange) - selection.setGoalScreenRange(range) - break - - return - - # Public: Moves the selection up one row. - addSelectionAbove: -> - range = @getGoalScreenRange().copy() - previousRow = range.end.row - 1 - - for row in [previousRow..0] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - selection = @editor.addSelectionForScreenRange(clippedRange) - selection.setGoalScreenRange(range) - break - - return - - # Public: Combines the given selection into this selection and then destroys - # the given selection. - # - # * `otherSelection` A {Selection} to merge with. - # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - merge: (otherSelection, options = {}) -> - myGoalScreenRange = @getGoalScreenRange() - otherGoalScreenRange = otherSelection.getGoalScreenRange() - - if myGoalScreenRange? and otherGoalScreenRange? - options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange) - else - options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange - - @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), Object.assign(autoscroll: false, options)) - otherSelection.destroy() - - ### - Section: Comparing to other selections - ### - - # Public: Compare this selection's buffer range to another selection's buffer - # range. - # - # See {Range::compare} for more details. - # - # * `otherSelection` A {Selection} to compare against - compare: (otherSelection) -> - @marker.compare(otherSelection.marker) - - ### - Section: Private Utilities - ### - - setGoalScreenRange: (range) -> - @goalScreenRange = Range.fromObject(range) - - getGoalScreenRange: -> - @goalScreenRange ? @getScreenRange() - - markerDidChange: (e) -> - {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e - {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e - {textChanged} = e - - unless oldHeadScreenPosition.isEqual(newHeadScreenPosition) - @cursor.goalColumn = null - cursorMovedEvent = { - oldBufferPosition: oldHeadBufferPosition - oldScreenPosition: oldHeadScreenPosition - newBufferPosition: newHeadBufferPosition - newScreenPosition: newHeadScreenPosition - textChanged: textChanged - cursor: @cursor - } - @cursor.emitter.emit('did-change-position', cursorMovedEvent) - @editor.cursorMoved(cursorMovedEvent) - - @emitter.emit 'did-change-range' - @editor.selectionRangeChanged( - oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition) - oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition) - newBufferRange: @getBufferRange() - newScreenRange: @getScreenRange() - selection: this - ) - - markerDidDestroy: -> - return if @editor.isDestroyed() - - @destroyed = true - @cursor.destroyed = true - - @editor.removeSelection(this) - - @cursor.emitter.emit 'did-destroy' - @emitter.emit 'did-destroy' - - @cursor.emitter.dispose() - @emitter.dispose() - - finalize: -> - @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) - if @isEmpty() - @wordwise = false - @linewise = false - - autoscroll: (options) -> - if @marker.hasTail() - @editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options)) - else - @cursor.autoscroll(options) - - clearAutoscroll: -> - - modifySelection: (fn) -> - @retainSelection = true - @plantTail() - fn() - @retainSelection = false - - # Sets the marker's tail to the same position as the marker's head. - # - # This only works if there isn't already a tail position. - # - # Returns a {Point} representing the new tail position. - plantTail: -> - @marker.plantTail() diff --git a/src/selection.js b/src/selection.js new file mode 100644 index 000000000..20561fd64 --- /dev/null +++ b/src/selection.js @@ -0,0 +1,975 @@ +const {Point, Range} = require('text-buffer') +const {pick} = require('underscore-plus') +const {Emitter} = require('event-kit') + +const NonWhitespaceRegExp = /\S/ +let nextId = 0 + +// Extended: Represents a selection in the {TextEditor}. +module.exports = +class Selection { + constructor ({cursor, marker, editor, id}) { + this.id = (id != null) ? id : nextId++ + this.cursor = cursor + this.marker = marker + this.editor = editor + this.emitter = new Emitter() + this.initialScreenRange = null + this.wordwise = false + this.cursor.selection = this + this.decoration = this.editor.decorateMarker(this.marker, {type: 'highlight', class: 'selection'}) + this.marker.onDidChange(e => this.markerDidChange(e)) + this.marker.onDidDestroy(() => this.markerDidDestroy()) + } + + destroy () { + this.marker.destroy() + } + + isLastSelection () { + return this === this.editor.getLastSelection() + } + + /* + Section: Event Subscription + */ + + // Extended: Calls your `callback` when the selection was moved. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferRange` {Range} + // * `oldScreenRange` {Range} + // * `newBufferRange` {Range} + // * `newScreenRange` {Range} + // * `selection` {Selection} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeRange (callback) { + return this.emitter.on('did-change-range', callback) + } + + // Extended: Calls your `callback` when the selection was destroyed + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Managing the selection range + */ + + // Public: Returns the screen {Range} for the selection. + getScreenRange () { + return this.marker.getScreenRange() + } + + // Public: Modifies the screen range for the selection. + // + // * `screenRange` The new {Range} to use. + // * `options` (optional) {Object} options matching those found in {::setBufferRange}. + setScreenRange (screenRange, options) { + return this.setBufferRange(this.editor.bufferRangeForScreenRange(screenRange), options) + } + + // Public: Returns the buffer {Range} for the selection. + getBufferRange () { + return this.marker.getBufferRange() + } + + // Public: Modifies the buffer {Range} for the selection. + // + // * `bufferRange` The new {Range} to select. + // * `options` (optional) {Object} with the keys: + // * `preserveFolds` if `true`, the fold settings are preserved after the + // selection moves. + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // range. Defaults to `true` if this is the most recently added selection, + // `false` otherwise. + setBufferRange (bufferRange, options = {}) { + bufferRange = Range.fromObject(bufferRange) + if (options.reversed == null) options.reversed = this.isReversed() + if (!options.preserveFolds) this.editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) + this.modifySelection(() => { + const needsFlash = options.flash + options.flash = null + this.marker.setBufferRange(bufferRange, options) + const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.autoscroll() + if (needsFlash) this.decoration.flash('flash', this.editor.selectionFlashDuration) + }) + } + + // Public: Returns the starting and ending buffer rows the selection is + // highlighting. + // + // Returns an {Array} of two {Number}s: the starting row, and the ending row. + getBufferRowRange () { + const range = this.getBufferRange() + const start = range.start.row + let end = range.end.row + if (range.end.column === 0) end = Math.max(start, end - 1) + return [start, end] + } + + getTailScreenPosition () { + return this.marker.getTailScreenPosition() + } + + getTailBufferPosition () { + return this.marker.getTailBufferPosition() + } + + getHeadScreenPosition () { + return this.marker.getHeadScreenPosition() + } + + getHeadBufferPosition () { + return this.marker.getHeadBufferPosition() + } + + /* + Section: Info about the selection + */ + + // Public: Determines if the selection contains anything. + isEmpty () { + return this.getBufferRange().isEmpty() + } + + // Public: Determines if the ending position of a marker is greater than the + // starting position. + // + // This can happen when, for example, you highlight text "up" in a {TextBuffer}. + isReversed () { + return this.marker.isReversed() + } + + // Public: Returns whether the selection is a single line or not. + isSingleScreenLine () { + return this.getScreenRange().isSingleLine() + } + + // Public: Returns the text in the selection. + getText () { + return this.editor.buffer.getTextInRange(this.getBufferRange()) + } + + // Public: Identifies if a selection intersects with a given buffer range. + // + // * `bufferRange` A {Range} to check against. + // + // Returns a {Boolean} + intersectsBufferRange (bufferRange) { + return this.getBufferRange().intersectsWith(bufferRange) + } + + intersectsScreenRowRange (startRow, endRow) { + return this.getScreenRange().intersectsRowRange(startRow, endRow) + } + + intersectsScreenRow (screenRow) { + return this.getScreenRange().intersectsRow(screenRow) + } + + // Public: Identifies if a selection intersects with another selection. + // + // * `otherSelection` A {Selection} to check against. + // + // Returns a {Boolean} + intersectsWith (otherSelection, exclusive) { + return this.getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + } + + /* + Section: Modifying the selected range + */ + + // Public: Clears the selection, moving the marker to the head. + // + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // range. Defaults to `true` if this is the most recently added selection, + // `false` otherwise. + clear (options) { + this.goalScreenRange = null + if (!this.retainSelection) this.marker.clearTail() + const autoscroll = options && options.autoscroll != null + ? options.autoscroll + : this.isLastSelection() + if (autoscroll) this.autoscroll() + this.finalize() + } + + // 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`. + selectToScreenPosition (position, options) { + position = Point.fromObject(position) + + this.modifySelection(() => { + if (this.initialScreenRange) { + if (position.isLessThan(this.initialScreenRange.start)) { + this.marker.setScreenRange([position, this.initialScreenRange.end], {reversed: true}) + } else { + this.marker.setScreenRange([this.initialScreenRange.start, position], {reversed: false}) + } + } else { + this.cursor.setScreenPosition(position, options) + } + + if (this.linewise) { + this.expandOverLine(options) + } else if (this.wordwise) { + this.expandOverWord(options) + } + }) + } + + // 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`. + selectToBufferPosition (position) { + this.modifySelection(() => this.cursor.setBufferPosition(position)) + } + + // Public: Selects the text one position right of the cursor. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectRight (columnCount) { + this.modifySelection(() => this.cursor.moveRight(columnCount)) + } + + // Public: Selects the text one position left of the cursor. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectLeft (columnCount) { + this.modifySelection(() => this.cursor.moveLeft(columnCount)) + } + + // Public: Selects all the text one position above the cursor. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + selectUp (rowCount) { + this.modifySelection(() => this.cursor.moveUp(rowCount)) + } + + // Public: Selects all the text one position below the cursor. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + selectDown (rowCount) { + this.modifySelection(() => this.cursor.moveDown(rowCount)) + } + + // Public: Selects all the text from the current cursor position to the top of + // the buffer. + selectToTop () { + this.modifySelection(() => this.cursor.moveToTop()) + } + + // Public: Selects all the text from the current cursor position to the bottom + // of the buffer. + selectToBottom () { + this.modifySelection(() => this.cursor.moveToBottom()) + } + + // Public: Selects all the text in the buffer. + selectAll () { + this.setBufferRange(this.editor.buffer.getRange(), {autoscroll: false}) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the line. + selectToBeginningOfLine () { + this.modifySelection(() => this.cursor.moveToBeginningOfLine()) + } + + // Public: Selects all the text from the current cursor position to the first + // character of the line. + selectToFirstCharacterOfLine () { + this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the screen line. + selectToEndOfLine () { + this.modifySelection(() => this.cursor.moveToEndOfScreenLine()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the buffer line. + selectToEndOfBufferLine () { + this.modifySelection(() => this.cursor.moveToEndOfLine()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the word. + selectToBeginningOfWord () { + this.modifySelection(() => this.cursor.moveToBeginningOfWord()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the word. + selectToEndOfWord () { + this.modifySelection(() => this.cursor.moveToEndOfWord()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the next word. + selectToBeginningOfNextWord () { + this.modifySelection(() => this.cursor.moveToBeginningOfNextWord()) + } + + // Public: Selects text to the previous word boundary. + selectToPreviousWordBoundary () { + this.modifySelection(() => this.cursor.moveToPreviousWordBoundary()) + } + + // Public: Selects text to the next word boundary. + selectToNextWordBoundary () { + this.modifySelection(() => this.cursor.moveToNextWordBoundary()) + } + + // Public: Selects text to the previous subword boundary. + selectToPreviousSubwordBoundary () { + this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary()) + } + + // Public: Selects text to the next subword boundary. + selectToNextSubwordBoundary () { + this.modifySelection(() => this.cursor.moveToNextSubwordBoundary()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the next paragraph. + selectToBeginningOfNextParagraph () { + this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the previous paragraph. + selectToBeginningOfPreviousParagraph () { + this.modifySelection(() => this.cursor.moveToBeginningOfPreviousParagraph()) + } + + // Public: Modifies the selection to encompass the current word. + // + // Returns a {Range}. + selectWord (options = {}) { + if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/ + if (this.cursor.isBetweenWordAndNonWord()) { + options.includeNonWordCharacters = false + } + + this.setBufferRange(this.cursor.getCurrentWordBufferRange(options), options) + this.wordwise = true + this.initialScreenRange = this.getScreenRange() + } + + // Public: Expands the newest selection to include the entire word on which + // the cursors rests. + expandOverWord (options) { + this.setBufferRange(this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()), {autoscroll: false}) + const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.cursor.autoscroll() + } + + // Public: Selects an entire line in the buffer. + // + // * `row` The line {Number} to select (default: the row of the cursor). + selectLine (row, options) { + if (row != null) { + this.setBufferRange(this.editor.bufferRangeForBufferRow(row, {includeNewline: true}), options) + } else { + const startRange = this.editor.bufferRangeForBufferRow(this.marker.getStartBufferPosition().row) + const endRange = this.editor.bufferRangeForBufferRow(this.marker.getEndBufferPosition().row, {includeNewline: true}) + this.setBufferRange(startRange.union(endRange), options) + } + + this.linewise = true + this.wordwise = false + this.initialScreenRange = this.getScreenRange() + } + + // Public: Expands the newest selection to include the entire line on which + // the cursor currently rests. + // + // It also includes the newline character. + expandOverLine (options) { + const range = this.getBufferRange().union(this.cursor.getCurrentLineBufferRange({includeNewline: true})) + this.setBufferRange(range, {autoscroll: false}) + const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.cursor.autoscroll() + } + + /* + Section: Modifying the selected text + */ + + // Public: Replaces text at the current selection. + // + // * `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). + // * `preserveTrailingLineIndentation` By default, when pasting multiple + // lines, Atom attempts to preserve the relative indent level between the + // first line and trailing lines, even if the indent level of the first + // line has changed from the copied text. If this option is `true`, this + // behavior is suppressed. + // level between the first lines and the trailing lines. + // * `normalizeLineEndings` (optional) {Boolean} (default: true) + // * `undo` If `skip`, skips the undo stack for this operation. + insertText (text, options = {}) { + let desiredIndentLevel, indentAdjustment + const oldBufferRange = this.getBufferRange() + const wasReversed = this.isReversed() + this.clear(options) + + let autoIndentFirstLine = false + const precedingText = this.editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) + const remainingLines = text.split('\n') + const firstInsertedLine = remainingLines.shift() + + if (options.indentBasis != null && !options.preserveTrailingLineIndentation) { + indentAdjustment = this.editor.indentLevelForLine(precedingText) - options.indentBasis + this.adjustIndent(remainingLines, indentAdjustment) + } + + const textIsAutoIndentable = (text === '\n') || (text === '\r\n') || NonWhitespaceRegExp.test(text) + if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) { + autoIndentFirstLine = true + const firstLine = precedingText + firstInsertedLine + desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) + indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine) + this.adjustIndent(remainingLines, indentAdjustment) + } + + text = firstInsertedLine + if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}` + + const newBufferRange = this.editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings')) + + if (options.select) { + this.setBufferRange(newBufferRange, {reversed: wasReversed}) + } else { + if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end) + } + + if (autoIndentFirstLine) { + this.editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) + } + + if (options.autoIndentNewline && (text === '\n')) { + this.editor.autoIndentBufferRow(newBufferRange.end.row, {preserveLeadingWhitespace: true, skipBlankLines: false}) + } else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) { + this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) + } + + const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.autoscroll() + + return newBufferRange + } + + // Public: Removes the first character before the selection if the selection + // is empty otherwise it deletes the selection. + backspace () { + if (this.isEmpty()) this.selectLeft() + this.deleteSelectedText() + } + + // Public: Removes the selection or, if nothing is selected, then all + // characters from the start of the selection back to the previous word + // boundary. + deleteToPreviousWordBoundary () { + if (this.isEmpty()) this.selectToPreviousWordBoundary() + this.deleteSelectedText() + } + + // Public: Removes the selection or, if nothing is selected, then all + // characters from the start of the selection up to the next word + // boundary. + deleteToNextWordBoundary () { + if (this.isEmpty()) this.selectToNextWordBoundary() + this.deleteSelectedText() + } + + // Public: Removes from the start of the selection to the beginning of the + // current word if the selection is empty otherwise it deletes the selection. + deleteToBeginningOfWord () { + if (this.isEmpty()) this.selectToBeginningOfWord() + this.deleteSelectedText() + } + + // Public: Removes from the beginning of the line which the selection begins on + // all the way through to the end of the selection. + deleteToBeginningOfLine () { + if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) { + this.selectLeft() + } else { + this.selectToBeginningOfLine() + } + this.deleteSelectedText() + } + + // Public: Removes the selection or the next character after the start of the + // selection if the selection is empty. + delete () { + if (this.isEmpty()) this.selectRight() + this.deleteSelectedText() + } + + // Public: If the selection is empty, removes all text from the cursor to the + // end of the line. If the cursor is already at the end of the line, it + // removes the following newline. If the selection isn't empty, only deletes + // the contents of the selection. + deleteToEndOfLine () { + if (this.isEmpty()) { + if (this.cursor.isAtEndOfLine()) { + this.delete() + return + } + this.selectToEndOfLine() + } + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToEndOfWord () { + if (this.isEmpty()) this.selectToEndOfWord() + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToBeginningOfSubword () { + if (this.isEmpty()) this.selectToPreviousSubwordBoundary() + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToEndOfSubword () { + if (this.isEmpty()) this.selectToNextSubwordBoundary() + this.deleteSelectedText() + } + + // Public: Removes only the selected text. + deleteSelectedText () { + const bufferRange = this.getBufferRange() + if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange) + if (this.cursor) this.cursor.setBufferPosition(bufferRange.start) + } + + // Public: Removes the line at the beginning of the selection if the selection + // is empty unless the selection spans multiple lines in which case all lines + // are removed. + deleteLine () { + if (this.isEmpty()) { + const start = this.cursor.getScreenRow() + const range = this.editor.bufferRowsForScreenRows(start, start + 1) + if (range[1] > range[0]) { + this.editor.buffer.deleteRows(range[0], range[1] - 1) + } else { + this.editor.buffer.deleteRow(range[0]) + } + } else { + const range = this.getBufferRange() + const start = range.start.row + let end = range.end.row + if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end-- + this.editor.buffer.deleteRows(start, end) + } + } + + // Public: Joins the current line with the one below it. Lines will + // be separated by a single space. + // + // If there selection spans more than one line, all the lines are joined together. + joinLines () { + let joinMarker + const selectedRange = this.getBufferRange() + if (selectedRange.isEmpty()) { + if (selectedRange.start.row === this.editor.buffer.getLastRow()) return + } else { + joinMarker = this.editor.markBufferRange(selectedRange, {invalidate: 'never'}) + } + + const rowCount = Math.max(1, selectedRange.getRowCount() - 1) + for (let i = 0; i < rowCount; i++) { + this.cursor.setBufferPosition([selectedRange.start.row]) + this.cursor.moveToEndOfLine() + + // Remove trailing whitespace from the current line + const scanRange = this.cursor.getCurrentLineBufferRange() + let trailingWhitespaceRange = null + this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => trailingWhitespaceRange = range) + if (trailingWhitespaceRange) { + this.setBufferRange(trailingWhitespaceRange) + this.deleteSelectedText() + } + + const currentRow = selectedRange.start.row + const nextRow = currentRow + 1 + const insertSpace = + (nextRow <= this.editor.buffer.getLastRow()) && + (this.editor.buffer.lineLengthForRow(nextRow) > 0) && + (this.editor.buffer.lineLengthForRow(currentRow) > 0) + if (insertSpace) this.insertText(' ') + + this.cursor.moveToEndOfLine() + + // Remove leading whitespace from the line below + this.modifySelection(() => { + this.cursor.moveRight() + this.cursor.moveToFirstCharacterOfLine() + }) + this.deleteSelectedText() + + if (insertSpace) this.cursor.moveLeft() + } + + if (joinMarker) { + const newSelectedRange = joinMarker.getBufferRange() + this.setBufferRange(newSelectedRange) + joinMarker.destroy() + } + } + + // Public: Removes one level of indent from the currently selected rows. + outdentSelectedRows () { + const [start, end] = this.getBufferRowRange() + const {buffer} = this.editor + const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`) + for (let row = start; row <= end; row++) { + const match = buffer.lineForRow(row).match(leadingTabRegex) + if (match && match[0].length > 0) { + buffer.delete([[row, 0], [row, match[0].length]]) + } + } + } + + // Public: Sets the indentation level of all selected rows to values suggested + // by the relevant grammars. + autoIndentSelectedRows () { + const [start, end] = this.getBufferRowRange() + return this.editor.autoIndentBufferRows(start, end) + } + + // Public: Wraps the selected lines in comments if they aren't currently part + // of a comment. + // + // Removes the comment if they are currently wrapped in a comment. + toggleLineComments () { + this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || [])) + } + + // Public: Cuts the selection until the end of the screen line. + cutToEndOfLine (maintainClipboard) { + if (this.isEmpty()) this.selectToEndOfLine() + return this.cut(maintainClipboard) + } + + // Public: Cuts the selection until the end of the buffer line. + cutToEndOfBufferLine (maintainClipboard) { + if (this.isEmpty()) this.selectToEndOfBufferLine() + this.cut(maintainClipboard) + } + + // Public: Copies the selection to the clipboard and then deletes it. + // + // * `maintainClipboard` {Boolean} (default: false) See {::copy} + // * `fullLine` {Boolean} (default: false) See {::copy} + cut (maintainClipboard = false, fullLine = false) { + this.copy(maintainClipboard, fullLine) + this.delete() + } + + // Public: Copies the current selection to the clipboard. + // + // * `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) + // * `fullLine` {Boolean} if `true`, the copied text will always be pasted + // at the beginning of the line containing the cursor, regardless of the + // cursor's horizontal position. (default: false) + copy (maintainClipboard = false, fullLine = false) { + if (this.isEmpty()) return + const {start, end} = this.getBufferRange() + const selectionText = this.editor.getTextInRange([start, end]) + const precedingText = this.editor.getTextInRange([[start.row, 0], start]) + const startLevel = this.editor.indentLevelForLine(precedingText) + + if (maintainClipboard) { + let {text: clipboardText, metadata} = this.editor.constructor.clipboard.readWithMetadata() + if (!metadata) metadata = {} + if (!metadata.selections) { + metadata.selections = [{ + text: clipboardText, + indentBasis: metadata.indentBasis, + fullLine: metadata.fullLine + }] + } + metadata.selections.push({ + text: selectionText, + indentBasis: startLevel, + fullLine + }) + this.editor.constructor.clipboard.write([clipboardText, selectionText].join('\n'), metadata) + } else { + this.editor.constructor.clipboard.write(selectionText, { + indentBasis: startLevel, + fullLine + }) + } + } + + // Public: Creates a fold containing the current selection. + fold () { + const range = this.getBufferRange() + if (!range.isEmpty()) { + this.editor.foldBufferRange(range) + this.cursor.setBufferPosition(range.end) + } + } + + // Private: Increase the indentation level of the given text by given number + // of levels. Leaves the first line unchanged. + adjustIndent (lines, indentAdjustment) { + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (indentAdjustment === 0 || line === '') { + continue + } else if (indentAdjustment > 0) { + lines[i] = this.editor.buildIndentString(indentAdjustment) + line + } else { + const currentIndentLevel = this.editor.indentLevelForLine(lines[i]) + const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment) + lines[i] = line.replace(/^[\t ]+/, this.editor.buildIndentString(indentLevel)) + } + } + } + + // Indent the current line(s). + // + // If the selection is empty, indents the current line if the cursor precedes + // non-whitespace characters, and otherwise inserts a tab. If the selection is + // non empty, calls {::indentSelectedRows}. + // + // * `options` (optional) {Object} with the keys: + // * `autoIndent` If `true`, the line is indented to an automatically-inferred + // level. Otherwise, {TextEditor::getTabText} is inserted. + indent ({autoIndent} = {}) { + const {row} = this.cursor.getBufferPosition() + + if (this.isEmpty()) { + this.cursor.skipLeadingWhitespace() + const desiredIndent = this.editor.suggestedIndentForBufferRow(row) + let delta = desiredIndent - this.cursor.getIndentLevel() + + if (autoIndent && delta > 0) { + if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1) + this.insertText(this.editor.buildIndentString(delta)) + } else { + this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn())) + } + } else { + this.indentSelectedRows() + } + } + + // Public: If the selection spans multiple rows, indent all of them. + indentSelectedRows () { + const [start, end] = this.getBufferRowRange() + for (let row = start; row <= end; row++) { + if (this.editor.buffer.lineLengthForRow(row) !== 0) { + this.editor.buffer.insert([row, 0], this.editor.getTabText()) + } + } + } + + /* + Section: Managing multiple selections + */ + + // Public: Moves the selection down one row. + addSelectionBelow () { + const range = this.getGoalScreenRange().copy() + const nextRow = range.end.row + 1 + + for (let row = nextRow, end = this.editor.getLastScreenRow(); row <= end; row++) { + range.start.row = row + range.end.row = row + const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true}) + + if (range.isEmpty()) { + if (range.end.column > 0 && clippedRange.end.column === 0) continue + } else { + if (clippedRange.isEmpty()) continue + } + + const selection = this.editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) + break + } + } + + // Public: Moves the selection up one row. + addSelectionAbove () { + const range = this.getGoalScreenRange().copy() + const previousRow = range.end.row - 1 + + for (let row = previousRow; row >= 0; row--) { + range.start.row = row + range.end.row = row + const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true}) + + if (range.isEmpty()) { + if (range.end.column > 0 && clippedRange.end.column === 0) continue + } else { + if (clippedRange.isEmpty()) continue + } + + const selection = this.editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) + break + } + } + + // Public: Combines the given selection into this selection and then destroys + // the given selection. + // + // * `otherSelection` A {Selection} to merge with. + // * `options` (optional) {Object} options matching those found in {::setBufferRange}. + merge (otherSelection, options = {}) { + const myGoalScreenRange = this.getGoalScreenRange() + const otherGoalScreenRange = otherSelection.getGoalScreenRange() + + if (myGoalScreenRange && otherGoalScreenRange) { + options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange) + } else { + options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange + } + + const bufferRange = this.getBufferRange().union(otherSelection.getBufferRange()) + this.setBufferRange(bufferRange, Object.assign({autoscroll: false}, options)) + otherSelection.destroy() + } + + /* + Section: Comparing to other selections + */ + + // Public: Compare this selection's buffer range to another selection's buffer + // range. + // + // See {Range::compare} for more details. + // + // * `otherSelection` A {Selection} to compare against + compare (otherSelection) { + return this.marker.compare(otherSelection.marker) + } + + /* + Section: Private Utilities + */ + + setGoalScreenRange (range) { + return this.goalScreenRange = Range.fromObject(range) + } + + getGoalScreenRange () { + return this.goalScreenRange || this.getScreenRange() + } + + markerDidChange (e) { + const {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e + const {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e + const {textChanged} = e + + if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) { + this.cursor.goalColumn = null + const cursorMovedEvent = { + oldBufferPosition: oldHeadBufferPosition, + oldScreenPosition: oldHeadScreenPosition, + newBufferPosition: newHeadBufferPosition, + newScreenPosition: newHeadScreenPosition, + textChanged, + cursor: this.cursor + } + this.cursor.emitter.emit('did-change-position', cursorMovedEvent) + this.editor.cursorMoved(cursorMovedEvent) + } + + this.emitter.emit('did-change-range') + this.editor.selectionRangeChanged({ + oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition), + oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition), + newBufferRange: this.getBufferRange(), + newScreenRange: this.getScreenRange(), + selection: this + }) + } + + markerDidDestroy () { + if (this.editor.isDestroyed()) return + + this.destroyed = true + this.cursor.destroyed = true + + this.editor.removeSelection(this) + + this.cursor.emitter.emit('did-destroy') + this.emitter.emit('did-destroy') + + this.cursor.emitter.dispose() + this.emitter.dispose() + } + + finalize () { + if (!this.initialScreenRange || !this.initialScreenRange.isEqual(this.getScreenRange())) { + this.initialScreenRange = null + } + if (this.isEmpty()) { + this.wordwise = false + this.linewise = false + } + } + + autoscroll (options) { + if (this.marker.hasTail()) { + this.editor.scrollToScreenRange(this.getScreenRange(), Object.assign({reversed: this.isReversed()}, options)) + } else { + this.cursor.autoscroll(options) + } + } + + clearAutoscroll () {} + + modifySelection (fn) { + this.retainSelection = true + this.plantTail() + fn() + this.retainSelection = false + } + + // Sets the marker's tail to the same position as the marker's head. + // + // This only works if there isn't already a tail position. + // + // Returns a {Point} representing the new tail position. + plantTail () { + this.marker.plantTail() + } +} From 99f90af42729593e42337203b8c7c5f22c68801b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:26:58 -0700 Subject: [PATCH 155/161] Convert Selection spec to JS --- spec/selection-spec.coffee | 128 ------------------------------ spec/selection-spec.js | 157 +++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 128 deletions(-) delete mode 100644 spec/selection-spec.coffee create mode 100644 spec/selection-spec.js diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee deleted file mode 100644 index b0e65be30..000000000 --- a/spec/selection-spec.coffee +++ /dev/null @@ -1,128 +0,0 @@ -TextEditor = require '../src/text-editor' - -describe "Selection", -> - [buffer, editor, selection] = [] - - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - editor = new TextEditor({buffer: buffer, tabLength: 2}) - selection = editor.getLastSelection() - - afterEach -> - buffer.destroy() - - describe ".deleteSelectedText()", -> - describe "when nothing is selected", -> - it "deletes nothing", -> - selection.setBufferRange [[0, 3], [0, 3]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - describe "when one line is selected", -> - it "deletes selected text and clears the selection", -> - selection.setBufferRange [[0, 4], [0, 14]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - - endOfLine = buffer.lineForRow(0).length - selection.setBufferRange [[0, 0], [0, endOfLine]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "" - - expect(selection.isEmpty()).toBeTruthy() - - describe "when multiple lines are selected", -> - it "deletes selected text and clears the selection", -> - selection.setBufferRange [[0, 1], [2, 39]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "v;" - expect(selection.isEmpty()).toBeTruthy() - - describe "when the cursor precedes the tail", -> - it "deletes selected text and clears the selection", -> - selection.cursor.setScreenPosition [0, 13] - selection.selectToScreenPosition [0, 4] - - selection.delete() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(selection.isEmpty()).toBeTruthy() - - describe ".isReversed()", -> - it "returns true if the cursor precedes the tail", -> - selection.cursor.setScreenPosition([0, 20]) - selection.selectToScreenPosition([0, 10]) - expect(selection.isReversed()).toBeTruthy() - - selection.selectToScreenPosition([0, 25]) - expect(selection.isReversed()).toBeFalsy() - - describe ".selectLine(row)", -> - describe "when passed a row", -> - it "selects the specified row", -> - selection.setBufferRange([[2, 4], [3, 4]]) - selection.selectLine(5) - expect(selection.getBufferRange()).toEqual [[5, 0], [6, 0]] - - describe "when not passed a row", -> - it "selects all rows spanned by the selection", -> - selection.setBufferRange([[2, 4], [3, 4]]) - selection.selectLine() - expect(selection.getBufferRange()).toEqual [[2, 0], [4, 0]] - - describe "when only the selection's tail is moved (regression)", -> - it "notifies ::onDidChangeRange observers", -> - selection.setBufferRange([[2, 0], [2, 10]], reversed: true) - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.onDidChangeRange changeScreenRangeHandler - - buffer.insert([2, 5], 'abc') - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the selection is destroyed", -> - it "destroys its marker", -> - selection.setBufferRange([[2, 0], [2, 10]]) - marker = selection.marker - selection.destroy() - expect(marker.isDestroyed()).toBeTruthy() - - describe ".insertText(text, options)", -> - it "allows pasting white space only lines when autoIndent is enabled", -> - selection.setBufferRange [[0, 0], [0, 0]] - selection.insertText(" \n \n\n", autoIndent: true) - expect(buffer.lineForRow(0)).toBe " " - expect(buffer.lineForRow(1)).toBe " " - expect(buffer.lineForRow(2)).toBe "" - - it "auto-indents if only a newline is inserted", -> - selection.setBufferRange [[2, 0], [3, 0]] - selection.insertText("\n", autoIndent: true) - expect(buffer.lineForRow(2)).toBe " " - - it "auto-indents if only a carriage return + newline is inserted", -> - selection.setBufferRange [[2, 0], [3, 0]] - selection.insertText("\r\n", autoIndent: true) - expect(buffer.lineForRow(2)).toBe " " - - it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", -> - selection.setBufferRange [[5, 0], [5, 0]] - selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1) - expect(buffer.lineForRow(6)).toBe(' bar') - - describe ".fold()", -> - it "folds the buffer range spanned by the selection", -> - selection.setBufferRange([[0, 3], [1, 6]]) - selection.fold() - - expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]) - expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]) - expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {" - expect(editor.isFoldedAtBufferRow(0)).toBe(true) - - it "doesn't create a fold when the selection is empty", -> - selection.setBufferRange([[0, 3], [0, 3]]) - selection.fold() - - expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]) - expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.isFoldedAtBufferRow(0)).toBe(false) diff --git a/spec/selection-spec.js b/spec/selection-spec.js new file mode 100644 index 000000000..cb586da26 --- /dev/null +++ b/spec/selection-spec.js @@ -0,0 +1,157 @@ +const TextEditor = require('../src/text-editor') + +describe('Selection', () => { + let buffer, editor, selection + + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + editor = new TextEditor({buffer, tabLength: 2}) + selection = editor.getLastSelection() + }) + + afterEach(() => buffer.destroy()) + + describe('.deleteSelectedText()', () => { + describe('when nothing is selected', () => { + it('deletes nothing', () => { + selection.setBufferRange([[0, 3], [0, 3]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + }) + + describe('when one line is selected', () => { + it('deletes selected text and clears the selection', () => { + selection.setBufferRange([[0, 4], [0, 14]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('var = function () {') + + const endOfLine = buffer.lineForRow(0).length + selection.setBufferRange([[0, 0], [0, endOfLine]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('') + + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + describe('when multiple lines are selected', () => { + it('deletes selected text and clears the selection', () => { + selection.setBufferRange([[0, 1], [2, 39]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('v;') + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + describe('when the cursor precedes the tail', () => { + it('deletes selected text and clears the selection', () => { + selection.cursor.setScreenPosition([0, 13]) + selection.selectToScreenPosition([0, 4]) + + selection.delete() + expect(buffer.lineForRow(0)).toBe('var = function () {') + expect(selection.isEmpty()).toBeTruthy() + }) + }) + }) + + describe('.isReversed()', () => { + it('returns true if the cursor precedes the tail', () => { + selection.cursor.setScreenPosition([0, 20]) + selection.selectToScreenPosition([0, 10]) + expect(selection.isReversed()).toBeTruthy() + + selection.selectToScreenPosition([0, 25]) + expect(selection.isReversed()).toBeFalsy() + }) + }) + + describe('.selectLine(row)', () => { + describe('when passed a row', () => { + it('selects the specified row', () => { + selection.setBufferRange([[2, 4], [3, 4]]) + selection.selectLine(5) + expect(selection.getBufferRange()).toEqual([[5, 0], [6, 0]]) + }) + }) + + describe('when not passed a row', () => { + it('selects all rows spanned by the selection', () => { + selection.setBufferRange([[2, 4], [3, 4]]) + selection.selectLine() + expect(selection.getBufferRange()).toEqual([[2, 0], [4, 0]]) + }) + }) + }) + + describe("when only the selection's tail is moved (regression)", () => { + it('notifies ::onDidChangeRange observers', () => { + selection.setBufferRange([[2, 0], [2, 10]], {reversed: true}) + const changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.onDidChangeRange(changeScreenRangeHandler) + + buffer.insert([2, 5], 'abc') + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the selection is destroyed', () => { + it('destroys its marker', () => { + selection.setBufferRange([[2, 0], [2, 10]]) + const { marker } = selection + selection.destroy() + expect(marker.isDestroyed()).toBeTruthy() + }) + }) + + describe('.insertText(text, options)', () => { + it('allows pasting white space only lines when autoIndent is enabled', () => { + selection.setBufferRange([[0, 0], [0, 0]]) + selection.insertText(' \n \n\n', {autoIndent: true}) + expect(buffer.lineForRow(0)).toBe(' ') + expect(buffer.lineForRow(1)).toBe(' ') + expect(buffer.lineForRow(2)).toBe('') + }) + + it('auto-indents if only a newline is inserted', () => { + selection.setBufferRange([[2, 0], [3, 0]]) + selection.insertText('\n', {autoIndent: true}) + expect(buffer.lineForRow(2)).toBe(' ') + }) + + it('auto-indents if only a carriage return + newline is inserted', () => { + selection.setBufferRange([[2, 0], [3, 0]]) + selection.insertText('\r\n', {autoIndent: true}) + expect(buffer.lineForRow(2)).toBe(' ') + }) + + it('does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true', () => { + selection.setBufferRange([[5, 0], [5, 0]]) + selection.insertText(' foo\n bar\n', {preserveTrailingLineIndentation: true, indentBasis: 1}) + expect(buffer.lineForRow(6)).toBe(' bar') + }) + }) + + describe('.fold()', () => { + it('folds the buffer range spanned by the selection', () => { + selection.setBufferRange([[0, 3], [1, 6]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]) + expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]) + expect(editor.lineTextForScreenRow(0)).toBe(`var${editor.displayLayer.foldCharacter}sort = function(items) {`) + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + }) + + it("doesn't create a fold when the selection is empty", () => { + selection.setBufferRange([[0, 3], [0, 3]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]) + expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + }) + }) +}) From 3b6f98b446b7ac581f555542677afd986523d1f9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:29:33 -0700 Subject: [PATCH 156/161] Fix lint errors --- src/selection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/selection.js b/src/selection.js index 20561fd64..a54ba68b8 100644 --- a/src/selection.js +++ b/src/selection.js @@ -613,7 +613,9 @@ class Selection { // Remove trailing whitespace from the current line const scanRange = this.cursor.getCurrentLineBufferRange() let trailingWhitespaceRange = null - this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => trailingWhitespaceRange = range) + this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => { + trailingWhitespaceRange = range + }) if (trailingWhitespaceRange) { this.setBufferRange(trailingWhitespaceRange) this.deleteSelectedText() @@ -886,7 +888,7 @@ class Selection { */ setGoalScreenRange (range) { - return this.goalScreenRange = Range.fromObject(range) + this.goalScreenRange = Range.fromObject(range) } getGoalScreenRange () { From 3d3042baf2086e306845e383b863a6c8b5eb36d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 12:18:46 -0600 Subject: [PATCH 157/161] :arrow_up: command-palette --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8681e18e..ebf9448c0 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", - "command-palette": "0.41.1", + "command-palette": "0.42.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", From 76ac08c49cd829ecb3b1383f7d9725acc9490628 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 2 Nov 2017 20:40:23 +0100 Subject: [PATCH 158/161] :arrow_up: open-on-github@1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebf9448c0..b19548235 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "markdown-preview": "0.159.18", "metrics": "1.2.6", "notifications": "0.69.2", - "open-on-github": "1.2.1", + "open-on-github": "1.3.0", "package-generator": "1.1.1", "settings-view": "0.252.2", "snippets": "1.1.9", From 7639afe684c490db7aa612e6c3095551e5e4bf4d Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 2 Nov 2017 12:50:42 -0600 Subject: [PATCH 159/161] Judge resize of overlay by contentRect changing --- src/text-editor-component.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 2a77e30f8..91ea18361 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -126,7 +126,6 @@ class TextEditorComponent { this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this)) this.lineComponentsByScreenLineId = new Map() this.overlayComponents = new Set() - this.overlayDimensionsByElement = new WeakMap() this.shouldRenderDummyScrollbars = true this.remeasureScrollbars = false this.pendingAutoscroll = null @@ -803,15 +802,9 @@ class TextEditorComponent { { key: overlayProps.element, overlayComponents: this.overlayComponents, - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), didResize: (overlayComponent) => { this.updateOverlayToRender(overlayProps) - overlayComponent.update(Object.assign( - { - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) - }, - overlayProps - )) + overlayComponent.update(overlayProps) } }, overlayProps @@ -1357,7 +1350,6 @@ class TextEditorComponent { let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) const clientRect = element.getBoundingClientRect() - this.overlayDimensionsByElement.set(element, clientRect) if (avoidOverflow !== false) { const computedStyle = window.getComputedStyle(element) @@ -4226,17 +4218,26 @@ class OverlayComponent { this.element.style.zIndex = 4 this.element.style.top = (this.props.pixelTop || 0) + 'px' this.element.style.left = (this.props.pixelLeft || 0) + 'px' + this.currentContentRect = null // Synchronous DOM updates in response to resize events might trigger a // "loop limit exceeded" error. We disconnect the observer before // potentially mutating the DOM, and then reconnect it on the next tick. + // Note: ResizeObserver calls its callback when .observe is called this.resizeObserver = new ResizeObserver((entries) => { const {contentRect} = entries[0] - if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) { + + if ( + this.currentContentRect && + (this.currentContentRect.width !== contentRect.width || + this.currentContentRect.height !== contentRect.height) + ) { this.resizeObserver.disconnect() this.props.didResize(this) process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } + + this.currentContentRect = contentRect }) this.didAttach() this.props.overlayComponents.add(this) From 667634191eeaa4629cad63f01fc6ceff2fbf8af3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 16:15:36 -0600 Subject: [PATCH 160/161] Document hiddenInCommandPalette option in atom.commands.add --- src/command-registry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/command-registry.js b/src/command-registry.js index ba75918ab..9e6d8c2e1 100644 --- a/src/command-registry.js +++ b/src/command-registry.js @@ -107,6 +107,13 @@ module.exports = class CommandRegistry { // otherwise be generated from the event name. // * `description`: Used by consumers to display detailed information about // the command. + // * `hiddenInCommandPalette`: If `true`, this command will not appear in + // the bundled command palette by default, but can still be shown with. + // the `Command Palette: Show Hidden Commands` command. This is a good + // option when you need to register large numbers of commands that don't + // make sense to be executed from the command palette. Please use this + // option conservatively, as it could reduce the discoverability of your + // package's commands. // // ## Arguments: Registering Multiple Commands // From 07ac7041d9385ea8aab7e7a89b75299ad1f66d2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:19:19 -0700 Subject: [PATCH 161/161] :arrow_up: settings-view@0.253.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b19548235..87b37cfb1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.3.0", "package-generator": "1.1.1", - "settings-view": "0.252.2", + "settings-view": "0.253.0", "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14",