diff --git a/apm/package.json b/apm/package.json
index e8d4321b1..5391c9972 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.7"
+ "atom-package-manager": "1.18.8"
}
}
diff --git a/package.json b/package.json
index ffc655280..418fa0b75 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
- "version": "1.22.0-dev",
+ "version": "1.23.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@@ -12,11 +12,11 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
- "electronVersion": "1.6.9",
+ "electronVersion": "1.6.14",
"dependencies": {
"@atom/source-map-support": "^0.3.4",
"async": "0.2.6",
- "atom-keymap": "8.2.5",
+ "atom-keymap": "8.2.6",
"atom-select-list": "^0.1.0",
"atom-ui": "0.4.1",
"babel-core": "5.8.38",
@@ -31,13 +31,13 @@
"etch": "^0.12.6",
"event-kit": "^2.4.0",
"find-parent-dir": "^0.3.0",
- "first-mate": "7.0.7",
+ "first-mate": "7.0.9",
"focus-trap": "^2.3.0",
"fs-admin": "^0.1.6",
"fs-plus": "^3.0.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
- "git-utils": "5.0.0",
+ "git-utils": "5.1.0",
"glob": "^7.1.1",
"grim": "1.5.0",
"jasmine-json": "~0.0",
@@ -70,7 +70,7 @@
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
- "text-buffer": "13.3.4",
+ "text-buffer": "13.5.2",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
@@ -90,14 +90,14 @@
"solarized-dark-syntax": "1.1.2",
"solarized-light-syntax": "1.1.2",
"about": "1.7.8",
- "archive-view": "0.63.3",
+ "archive-view": "0.63.4",
"autocomplete-atom-api": "0.10.3",
"autocomplete-css": "0.17.3",
"autocomplete-html": "0.8.2",
- "autocomplete-plus": "2.35.10",
+ "autocomplete-plus": "2.36.2",
"autocomplete-snippets": "1.11.1",
"autoflow": "0.29.0",
- "autosave": "0.24.4",
+ "autosave": "0.24.6",
"background-tips": "0.27.1",
"bookmarks": "0.44.4",
"bracket-matcher": "0.88.0",
@@ -105,20 +105,20 @@
"dalek": "0.2.1",
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.47.1",
- "encoding-selector": "0.23.6",
+ "encoding-selector": "0.23.7",
"exception-reporting": "0.41.4",
"find-and-replace": "0.212.3",
- "fuzzy-finder": "1.6.0",
- "github": "0.6.2",
+ "fuzzy-finder": "1.6.1",
+ "github": "0.6.3",
"git-diff": "1.3.6",
"go-to-line": "0.32.1",
"grammar-selector": "0.49.6",
- "image-view": "0.62.3",
+ "image-view": "0.62.4",
"incompatible-packages": "0.27.3",
"keybinding-resolver": "0.38.0",
"line-ending-selector": "0.7.4",
"link": "0.31.3",
- "markdown-preview": "0.159.13",
+ "markdown-preview": "0.159.14",
"metrics": "1.2.6",
"notifications": "0.69.2",
"open-on-github": "1.2.1",
@@ -128,8 +128,8 @@
"spell-check": "0.72.2",
"status-bar": "1.8.13",
"styleguide": "0.49.7",
- "symbols-view": "0.118.0",
- "tabs": "0.107.3",
+ "symbols-view": "0.118.1",
+ "tabs": "0.107.4",
"timecop": "0.36.0",
"tree-view": "0.218.0",
"update-package-dependencies": "0.12.0",
@@ -139,22 +139,22 @@
"language-c": "0.58.1",
"language-clojure": "0.22.4",
"language-coffee-script": "0.49.1",
- "language-csharp": "0.14.2",
+ "language-csharp": "0.14.3",
"language-css": "0.42.6",
"language-gfm": "0.90.1",
"language-git": "0.19.1",
"language-go": "0.44.2",
- "language-html": "0.48.0",
+ "language-html": "0.48.1",
"language-hyperlink": "0.16.2",
"language-java": "0.27.4",
"language-javascript": "0.127.5",
"language-json": "0.19.1",
"language-less": "0.33.0",
"language-make": "0.22.3",
- "language-mustache": "0.14.2",
+ "language-mustache": "0.14.3",
"language-objective-c": "0.15.1",
"language-perl": "0.37.0",
- "language-php": "0.42.0",
+ "language-php": "0.42.1",
"language-property-list": "0.9.1",
"language-python": "0.45.4",
"language-ruby": "0.71.3",
diff --git a/resources/app-icons/beta/atom.icns b/resources/app-icons/beta/atom.icns
index 69abe031e..6737fa27f 100644
Binary files a/resources/app-icons/beta/atom.icns and b/resources/app-icons/beta/atom.icns differ
diff --git a/resources/app-icons/dev/atom.icns b/resources/app-icons/dev/atom.icns
index 84319be3d..b006a1a83 100644
Binary files a/resources/app-icons/dev/atom.icns and b/resources/app-icons/dev/atom.icns differ
diff --git a/resources/app-icons/stable/atom.icns b/resources/app-icons/stable/atom.icns
index 2f3246bb8..73ef96330 100644
Binary files a/resources/app-icons/stable/atom.icns and b/resources/app-icons/stable/atom.icns differ
diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js
index 7701b6a34..2905bca1b 100644
--- a/script/lib/generate-startup-snapshot.js
+++ b/script/lib/generate-startup-snapshot.js
@@ -39,7 +39,7 @@ module.exports = function (packagedAppPath) {
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', 'lib', 'git.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') ||
diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee
deleted file mode 100644
index 16ccf8938..000000000
--- a/spec/git-repository-provider-spec.coffee
+++ /dev/null
@@ -1,98 +0,0 @@
-path = require 'path'
-fs = require 'fs-plus'
-temp = require('temp').track()
-{Directory} = require 'pathwatcher'
-GitRepository = require '../src/git-repository'
-GitRepositoryProvider = require '../src/git-repository-provider'
-
-describe "GitRepositoryProvider", ->
- provider = null
-
- beforeEach ->
- provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
-
- afterEach ->
- try
- temp.cleanupSync()
-
- describe ".repositoryForDirectory(directory)", ->
- describe "when specified a Directory with a Git repository", ->
- it "returns a Promise that resolves to a GitRepository", ->
- waitsForPromise ->
- directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
- provider.repositoryForDirectory(directory).then (result) ->
- expect(result).toBeInstanceOf GitRepository
- expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
- expect(result.statusTask).toBeTruthy()
- expect(result.getType()).toBe 'git'
-
- it "returns the same GitRepository for different Directory objects in the same repo", ->
- firstRepo = null
- secondRepo = null
-
- waitsForPromise ->
- directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
- provider.repositoryForDirectory(directory).then (result) -> firstRepo = result
-
- waitsForPromise ->
- directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')
- provider.repositoryForDirectory(directory).then (result) -> secondRepo = result
-
- runs ->
- expect(firstRepo).toBeInstanceOf GitRepository
- expect(firstRepo).toBe secondRepo
-
- describe "when specified a Directory without a Git repository", ->
- it "returns a Promise that resolves to null", ->
- waitsForPromise ->
- directory = new Directory temp.mkdirSync('dir')
- provider.repositoryForDirectory(directory).then (result) ->
- expect(result).toBe null
-
- describe "when specified a Directory with an invalid Git repository", ->
- it "returns a Promise that resolves to null", ->
- waitsForPromise ->
- dirPath = temp.mkdirSync('dir')
- fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
- fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
- fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
-
- directory = new Directory dirPath
- provider.repositoryForDirectory(directory).then (result) ->
- expect(result).toBe null
-
- describe "when specified a Directory with a valid gitfile-linked repository", ->
- it "returns a Promise that resolves to a GitRepository", ->
- waitsForPromise ->
- gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
- workDirPath = temp.mkdirSync('git-workdir')
- fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n')
-
- directory = new Directory workDirPath
- provider.repositoryForDirectory(directory).then (result) ->
- expect(result).toBeInstanceOf GitRepository
- expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
- expect(result.statusTask).toBeTruthy()
- expect(result.getType()).toBe 'git'
-
- describe "when specified a Directory without existsSync()", ->
- directory = null
- provider = null
- beforeEach ->
- # An implementation of Directory that does not implement existsSync().
- subdirectory = {}
- directory =
- getSubdirectory: ->
- isRoot: -> true
- spyOn(directory, "getSubdirectory").andReturn(subdirectory)
-
- it "returns null", ->
- repo = provider.repositoryForDirectorySync(directory)
- expect(repo).toBe null
- expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
-
- it "returns a Promise that resolves to null for the async implementation", ->
- waitsForPromise ->
- provider.repositoryForDirectory(directory).then (repo) ->
- expect(repo).toBe null
- expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
diff --git a/spec/git-repository-provider-spec.js b/spec/git-repository-provider-spec.js
new file mode 100644
index 000000000..e1d0168a9
--- /dev/null
+++ b/spec/git-repository-provider-spec.js
@@ -0,0 +1,103 @@
+const path = require('path')
+const fs = require('fs-plus')
+const temp = require('temp').track()
+const {Directory} = require('pathwatcher')
+const GitRepository = require('../src/git-repository')
+const GitRepositoryProvider = require('../src/git-repository-provider')
+const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
+
+describe('GitRepositoryProvider', () => {
+ let provider
+
+ beforeEach(() => {
+ provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
+ })
+
+ describe('.repositoryForDirectory(directory)', () => {
+ describe('when specified a Directory with a Git repository', () => {
+ it('resolves with a GitRepository', async () => {
+ const directory = new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ const result = await provider.repositoryForDirectory(directory)
+ expect(result).toBeInstanceOf(GitRepository)
+ expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
+ expect(result.getType()).toBe('git')
+
+ // Refresh should be started
+ await new Promise(resolve => result.onDidChangeStatuses(resolve))
+ })
+
+ it('resolves with the same GitRepository for different Directory objects in the same repo', async () => {
+ const firstRepo = await provider.repositoryForDirectory(
+ new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ )
+ const secondRepo = await provider.repositoryForDirectory(
+ new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
+ )
+
+ expect(firstRepo).toBeInstanceOf(GitRepository)
+ expect(firstRepo).toBe(secondRepo)
+ })
+ })
+
+ describe('when specified a Directory without a Git repository', () => {
+ it('resolves with null', async () => {
+ const directory = new Directory(temp.mkdirSync('dir'))
+ const repo = await provider.repositoryForDirectory(directory)
+ expect(repo).toBe(null)
+ })
+ })
+
+ describe('when specified a Directory with an invalid Git repository', () => {
+ it('resolves with null', async () => {
+ const dirPath = temp.mkdirSync('dir')
+ fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
+ fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
+ fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
+
+ const directory = new Directory(dirPath)
+ const repo = await provider.repositoryForDirectory(directory)
+ expect(repo).toBe(null)
+ })
+ })
+
+ describe('when specified a Directory with a valid gitfile-linked repository', () => {
+ it('returns a Promise that resolves to a GitRepository', async () => {
+ const gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
+ const workDirPath = temp.mkdirSync('git-workdir')
+ fs.writeFileSync(path.join(workDirPath, '.git'), `gitdir: ${gitDirPath}\n`)
+
+ const directory = new Directory(workDirPath)
+ const result = await provider.repositoryForDirectory(directory)
+ expect(result).toBeInstanceOf(GitRepository)
+ expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
+ expect(result.getType()).toBe('git')
+ })
+ })
+
+ describe('when specified a Directory without existsSync()', () => {
+ let directory
+
+ beforeEach(() => {
+ // An implementation of Directory that does not implement existsSync().
+ const subdirectory = {}
+ directory = {
+ getSubdirectory () {},
+ isRoot () { return true }
+ }
+ spyOn(directory, 'getSubdirectory').andReturn(subdirectory)
+ })
+
+ it('returns null', () => {
+ const repo = provider.repositoryForDirectorySync(directory)
+ expect(repo).toBe(null)
+ expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
+ })
+
+ it('returns a Promise that resolves to null for the async implementation', async () => {
+ const repo = await provider.repositoryForDirectory(directory)
+ expect(repo).toBe(null)
+ expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
+ })
+ })
+ })
+})
diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee
index 47ca84580..e4d1e0c7f 100644
--- a/spec/git-repository-spec.coffee
+++ b/spec/git-repository-spec.coffee
@@ -283,11 +283,15 @@ describe "GitRepository", ->
[editor] = []
beforeEach ->
+ statusRefreshed = false
atom.project.setPaths([copyRepository()])
+ atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true
waitsForPromise ->
atom.workspace.open('other.txt').then (o) -> editor = o
+ waitsFor 'repo to refresh', -> statusRefreshed
+
it "emits a status-changed event when a buffer is saved", ->
editor.insertNewline()
diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee
deleted file mode 100644
index 68d0f7b09..000000000
--- a/spec/language-mode-spec.coffee
+++ /dev/null
@@ -1,506 +0,0 @@
-describe "LanguageMode", ->
- [editor, buffer, languageMode] = []
-
- afterEach ->
- editor.destroy()
-
- describe "javascript", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('sample.js', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe ".minIndentLevelForRowRange(startRow, endRow)", ->
- it "returns the minimum indent level for the given row range", ->
- expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2
- expect(languageMode.minIndentLevelForRowRange(5, 7)).toBe 2
- expect(languageMode.minIndentLevelForRowRange(5, 6)).toBe 3
- expect(languageMode.minIndentLevelForRowRange(9, 11)).toBe 1
- expect(languageMode.minIndentLevelForRowRange(10, 10)).toBe 0
-
- describe ".toggleLineCommentsForBufferRows(start, end)", ->
- it "comments/uncomments lines in the given range", ->
- languageMode.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 " // }"
-
- languageMode.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;')
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe "\t// var i;"
-
- buffer.setText('var i;')
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe "// var i;"
-
- buffer.setText(' var i;')
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe " // var i;"
-
- buffer.setText(' ')
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe " // "
-
- buffer.setText(' a\n \n b')
- languageMode.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;')
- languageMode.toggleLineCommentsForBufferRows(0, 1)
- expect(buffer.lineForRow(0)).toBe ' '
- expect(buffer.lineForRow(1)).toBe ' var i;'
-
- describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", ->
- it "returns the start/end rows of the foldable region starting at the given row", ->
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 12]
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 9]
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull()
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7]
-
- describe ".rowRangeForCommentAtBufferRow(bufferRow)", ->
- it "returns the start/end rows of the foldable comment starting at the given row", ->
- buffer.setText("//this is a multi line comment\n//another line")
- expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1]
- expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1]
-
- buffer.setText("//this is a multi line comment\n//another line\n//and one more")
- expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2]
- expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2]
-
- buffer.setText("//this is a multi line comment\n\n//with an empty line")
- expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
- expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined()
- expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined()
-
- buffer.setText("//this is a single line comment\n")
- expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
- expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined()
-
- buffer.setText("//this is a single line comment")
- expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
-
- describe ".suggestedIndentForBufferRow", ->
- it "bases indentation off of the previous non-blank line", ->
- expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0
- expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1
- expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2
- expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3
- expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2
- expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1
- expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1
-
- it "does not take invisibles into account", ->
- editor.update({showInvisibles: true})
- expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0
- expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1
- expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2
- expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3
- expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2
- expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1
- expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1
-
- describe "rowRangeForParagraphAtBufferRow", ->
- describe "with code and comments", ->
- beforeEach ->
- buffer.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) {
- return item;
- }
-
- };
- '''
-
- it "will limit paragraph range to comments", ->
- range = languageMode.rowRangeForParagraphAtBufferRow(0)
- expect(range).toEqual [[0, 0], [0, 29]]
-
- range = languageMode.rowRangeForParagraphAtBufferRow(10)
- expect(range).toEqual [[10, 0], [10, 14]]
- range = languageMode.rowRangeForParagraphAtBufferRow(11)
- expect(range).toBeFalsy()
- range = languageMode.rowRangeForParagraphAtBufferRow(12)
- expect(range).toEqual [[12, 0], [13, 10]]
-
- range = languageMode.rowRangeForParagraphAtBufferRow(14)
- expect(range).toEqual [[14, 0], [14, 32]]
-
- range = languageMode.rowRangeForParagraphAtBufferRow(15)
- expect(range).toEqual [[15, 0], [15, 26]]
-
- range = languageMode.rowRangeForParagraphAtBufferRow(18)
- expect(range).toEqual [[17, 0], [19, 3]]
-
- describe "coffeescript", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('coffee.coffee', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe ".toggleLineCommentsForBufferRows(start, end)", ->
- it "comments/uncomments lines in the given range", ->
- languageMode.toggleLineCommentsForBufferRows(4, 6)
- expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()"
- expect(buffer.lineForRow(5)).toBe " # left = []"
- expect(buffer.lineForRow(6)).toBe " # right = []"
-
- languageMode.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 lines when empty line", ->
- languageMode.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 " # "
-
- languageMode.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 "fold suggestion", ->
- describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", ->
- it "returns the start/end rows of the foldable region starting at the given row", ->
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 20]
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 17]
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull()
- expect(languageMode.rowRangeForCodeFoldAtBufferRow(19)).toEqual [19, 20]
-
- describe "css", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('css.css', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-css')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe ".toggleLineCommentsForBufferRows(start, end)", ->
- it "comments/uncomments lines in the given range", ->
- languageMode.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;"
-
- languageMode.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;"
-
- languageMode.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%;*/")
- languageMode.toggleLineCommentsForBufferRows(2, 2)
- expect(buffer.lineForRow(2)).toBe " width: 110%;"
-
- it "uncomments lines with trailing whitespace", ->
- buffer.setTextInRange([[2, 0], [2, Infinity]], "/*width: 110%;*/ ")
- languageMode.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%;*/ ")
- languageMode.toggleLineCommentsForBufferRows(2, 2)
- expect(buffer.lineForRow(2)).toBe " width: 110%; "
-
- describe "less", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('sample.less', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-less')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-css')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe "when commenting lines", ->
- it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", ->
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe "// @color: #4D926F;"
-
- describe "xml", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('sample.xml', autoIndent: false).then (o) ->
- editor = o
- editor.setText("")
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-xml')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe "when uncommenting lines", ->
- it "removes the leading whitespace from the comment end pattern match", ->
- languageMode.toggleLineCommentsForBufferRows(0, 0)
- expect(buffer.lineForRow(0)).toBe "test"
-
- describe "folding", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('sample.js', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- it "maintains cursor buffer position when a folding/unfolding", ->
- editor.setCursorBufferPosition([5, 5])
- languageMode.foldAll()
- expect(editor.getCursorBufferPosition()).toEqual([5, 5])
-
- describe ".unfoldAll()", ->
- it "unfolds every folded line", ->
- initialScreenLineCount = editor.getScreenLineCount()
- languageMode.foldBufferRow(0)
- languageMode.foldBufferRow(1)
- expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount
- languageMode.unfoldAll()
- expect(editor.getScreenLineCount()).toBe initialScreenLineCount
-
- describe ".foldAll()", ->
- it "folds every foldable line", ->
- languageMode.foldAll()
-
- [fold1, fold2, fold3] = languageMode.unfoldAll()
- expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
- expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
- expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
-
- describe ".foldBufferRow(bufferRow)", ->
- describe "when bufferRow can be folded", ->
- it "creates a fold based on the syntactic region starting at the given row", ->
- languageMode.foldBufferRow(1)
- [fold] = languageMode.unfoldAll()
- expect([fold.start.row, fold.end.row]).toEqual [1, 9]
-
- describe "when bufferRow can't be folded", ->
- it "searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)", ->
- languageMode.foldBufferRow(8)
- [fold] = languageMode.unfoldAll()
- expect([fold.start.row, fold.end.row]).toEqual [1, 9]
-
- describe "when the bufferRow is already folded", ->
- it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", ->
- languageMode.foldBufferRow(2)
- expect(editor.isFoldedAtBufferRow(0)).toBe(false)
- expect(editor.isFoldedAtBufferRow(1)).toBe(true)
-
- languageMode.foldBufferRow(1)
- expect(editor.isFoldedAtBufferRow(0)).toBe(true)
-
- describe "when the bufferRow is in a multi-line comment", ->
- it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
- buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
- languageMode.foldBufferRow(1)
- [fold] = languageMode.unfoldAll()
- expect([fold.start.row, fold.end.row]).toEqual [1, 3]
-
- describe "when the bufferRow is a single-line comment", ->
- it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", ->
- buffer.insert([1, 0], " //this is a single line comment\n")
- languageMode.foldBufferRow(1)
- [fold] = languageMode.unfoldAll()
- expect([fold.start.row, fold.end.row]).toEqual [0, 13]
-
- describe ".foldAllAtIndentLevel(indentLevel)", ->
- it "folds blocks of text at the given indentation level", ->
- languageMode.foldAllAtIndentLevel(0)
- expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
- expect(editor.getLastScreenRow()).toBe 0
-
- languageMode.foldAllAtIndentLevel(1)
- expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
- expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
- expect(editor.getLastScreenRow()).toBe 4
-
- languageMode.foldAllAtIndentLevel(2)
- expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
- expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
- expect(editor.lineTextForScreenRow(2)).toBe " if (items.length <= 1) return items;"
- expect(editor.getLastScreenRow()).toBe 9
-
- describe "folding with comments", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('sample-with-comments.js', autoIndent: false).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe ".unfoldAll()", ->
- it "unfolds every folded line", ->
- initialScreenLineCount = editor.getScreenLineCount()
- languageMode.foldBufferRow(0)
- languageMode.foldBufferRow(5)
- expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount
- languageMode.unfoldAll()
- expect(editor.getScreenLineCount()).toBe initialScreenLineCount
-
- describe ".foldAll()", ->
- it "folds every foldable line", ->
- languageMode.foldAll()
-
- folds = languageMode.unfoldAll()
- expect(folds.length).toBe 8
- expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
- expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
- expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
- expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
- expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
- expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
- expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
- expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
-
- describe ".foldAllAtIndentLevel()", ->
- it "folds every foldable range at a given indentLevel", ->
- languageMode.foldAllAtIndentLevel(2)
-
- folds = languageMode.unfoldAll()
- expect(folds.length).toBe 5
- expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
- expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
- expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
- expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
- expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
-
- it "does not fold anything but the indentLevel", ->
- languageMode.foldAllAtIndentLevel(0)
-
- folds = languageMode.unfoldAll()
- expect(folds.length).toBe 1
- expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
-
- describe ".isFoldableAtBufferRow(bufferRow)", ->
- it "returns true if the line starts a multi-line comment", ->
- expect(languageMode.isFoldableAtBufferRow(1)).toBe true
- expect(languageMode.isFoldableAtBufferRow(6)).toBe true
- expect(languageMode.isFoldableAtBufferRow(8)).toBe false
- expect(languageMode.isFoldableAtBufferRow(11)).toBe true
- expect(languageMode.isFoldableAtBufferRow(15)).toBe false
- expect(languageMode.isFoldableAtBufferRow(17)).toBe true
- expect(languageMode.isFoldableAtBufferRow(21)).toBe true
- expect(languageMode.isFoldableAtBufferRow(24)).toBe true
- expect(languageMode.isFoldableAtBufferRow(28)).toBe false
-
- it "returns true for lines that end with a comment and are followed by an indented line", ->
- expect(languageMode.isFoldableAtBufferRow(5)).toBe true
-
- it "does not return true for a line in the middle of a comment that's followed by an indented line", ->
- expect(languageMode.isFoldableAtBufferRow(7)).toBe false
- editor.buffer.insert([8, 0], ' ')
- expect(languageMode.isFoldableAtBufferRow(7)).toBe false
-
- describe "css", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('css.css', autoIndent: true).then (o) ->
- editor = o
- {buffer, languageMode} = editor
-
- waitsForPromise ->
- atom.packages.activatePackage('language-source')
- atom.packages.activatePackage('language-css')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- describe "suggestedIndentForBufferRow", ->
- it "does not return negative values (regression)", ->
- editor.setText('.test {\npadding: 0;\n}')
- expect(editor.suggestedIndentForBufferRow(2)).toBe 0
diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee
deleted file mode 100644
index 9b7d46340..000000000
--- a/spec/package-manager-spec.coffee
+++ /dev/null
@@ -1,1410 +0,0 @@
-path = require 'path'
-Package = require '../src/package'
-PackageManager = require '../src/package-manager'
-temp = require('temp').track()
-fs = require 'fs-plus'
-{Disposable} = require 'atom'
-{buildKeydownEvent} = require '../src/keymap-extensions'
-{mockLocalStorage} = require './spec-helper'
-ModuleCache = require '../src/module-cache'
-
-describe "PackageManager", ->
- createTestElement = (className) ->
- element = document.createElement('div')
- element.className = className
- element
-
- beforeEach ->
- spyOn(ModuleCache, 'add')
-
- afterEach ->
- try
- temp.cleanupSync()
-
- describe "initialize", ->
- it "adds regular package path", ->
- packageManger = new PackageManager({})
- configDirPath = path.join('~', 'someConfig')
- packageManger.initialize({configDirPath})
- expect(packageManger.packageDirPaths.length).toBe 1
- expect(packageManger.packageDirPaths[0]).toBe path.join(configDirPath, 'packages')
-
- it "adds regular package path and dev package path in dev mode", ->
- packageManger = new PackageManager({})
- configDirPath = path.join('~', 'someConfig')
- packageManger.initialize({configDirPath, devMode: true})
- expect(packageManger.packageDirPaths.length).toBe 2
- expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'packages')
- expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'dev', 'packages')
-
- describe "::getApmPath()", ->
- it "returns the path to the apm command", ->
- apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm")
- if process.platform is 'win32'
- apmPath += ".cmd"
- expect(atom.packages.getApmPath()).toBe apmPath
-
- describe "when the core.apmPath setting is set", ->
- beforeEach ->
- atom.config.set("core.apmPath", "/path/to/apm")
-
- it "returns the value of the core.apmPath config setting", ->
- expect(atom.packages.getApmPath()).toBe "/path/to/apm"
-
- describe "::loadPackages()", ->
- beforeEach ->
- spyOn(atom.packages, 'loadAvailablePackage')
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
-
- it "sets hasLoadedInitialPackages", ->
- expect(atom.packages.hasLoadedInitialPackages()).toBe false
- atom.packages.loadPackages()
- expect(atom.packages.hasLoadedInitialPackages()).toBe true
-
- describe "::loadPackage(name)", ->
- beforeEach ->
- atom.config.set("core.disabledPackages", [])
-
- it "returns the package", ->
- pack = atom.packages.loadPackage("package-with-index")
- expect(pack instanceof Package).toBe true
- expect(pack.metadata.name).toBe "package-with-index"
-
- it "returns the package if it has an invalid keymap", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- pack = atom.packages.loadPackage("package-with-broken-keymap")
- expect(pack instanceof Package).toBe true
- expect(pack.metadata.name).toBe "package-with-broken-keymap"
-
- it "returns the package if it has an invalid stylesheet", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- pack = atom.packages.loadPackage("package-with-invalid-styles")
- expect(pack instanceof Package).toBe true
- expect(pack.metadata.name).toBe "package-with-invalid-styles"
- expect(pack.stylesheets.length).toBe 0
-
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
- expect(-> pack.reloadStylesheets()).not.toThrow()
- expect(addErrorHandler.callCount).toBe 2
- expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to reload the package-with-invalid-styles package stylesheets")
- expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual "package-with-invalid-styles"
-
- it "returns null if the package has an invalid package.json", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
- expect(atom.packages.loadPackage("package-with-broken-package-json")).toBeNull()
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json"
-
- it "returns null if the package name or path starts with a dot", ->
- expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull()
-
- it "normalizes short repository urls in package.json", ->
- {metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
- expect(metadata.repository.type).toBe "git"
- expect(metadata.repository.url).toBe "https://github.com/example/repo"
-
- {metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json")
- expect(metadata.repository.type).toBe "git"
- expect(metadata.repository.url).toBe "foo"
-
- it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", ->
- {metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url")
- expect(metadata.repository.type).toBe "git"
- expect(metadata.repository.url).toBe "https://github.com/example/repo"
-
- it "returns null if the package is not found in any package directory", ->
- spyOn(console, 'warn')
- expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull()
- expect(console.warn.callCount).toBe(1)
- expect(console.warn.argsForCall[0][0]).toContain("Could not resolve")
-
- describe "when the package is deprecated", ->
- it "returns null", ->
- spyOn(console, 'warn')
- expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull()
- expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe true
- expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe true
- expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe false
- expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe '<=2.2.0'
-
- it "invokes ::onDidLoadPackage listeners with the loaded package", ->
- loadedPackage = null
- atom.packages.onDidLoadPackage (pack) -> loadedPackage = pack
-
- atom.packages.loadPackage("package-with-main")
-
- expect(loadedPackage.name).toBe "package-with-main"
-
- it "registers any deserializers specified in the package's package.json", ->
- pack = atom.packages.loadPackage("package-with-deserializers")
-
- state1 = {deserializer: 'Deserializer1', a: 'b'}
- expect(atom.deserializers.deserialize(state1)).toEqual {
- wasDeserializedBy: 'deserializeMethod1'
- state: state1
- }
-
- state2 = {deserializer: 'Deserializer2', c: 'd'}
- expect(atom.deserializers.deserialize(state2)).toEqual {
- wasDeserializedBy: 'deserializeMethod2'
- state: state2
- }
-
- it "early-activates any atom.directory-provider or atom.repository-provider services that the package provide", ->
- jasmine.useRealClock()
-
- providers = []
- atom.packages.serviceHub.consume 'atom.directory-provider', '^0.1.0', (provider) ->
- providers.push(provider)
-
- atom.packages.loadPackage('package-with-directory-provider')
- expect(providers.map((p) -> p.name)).toEqual(['directory provider from package-with-directory-provider'])
-
- describe "when there are view providers specified in the package's package.json", ->
- model1 = {worksWithViewProvider1: true}
- model2 = {worksWithViewProvider2: true}
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackage('package-with-view-providers')
- runs ->
- atom.packages.unloadPackage('package-with-view-providers')
-
- it "does not load the view providers immediately", ->
- pack = atom.packages.loadPackage("package-with-view-providers")
- expect(pack.mainModule).toBeNull()
-
- expect(-> atom.views.getView(model1)).toThrow()
- expect(-> atom.views.getView(model2)).toThrow()
-
- it "registers the view providers when the package is activated", ->
- pack = atom.packages.loadPackage("package-with-view-providers")
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-view-providers").then ->
- element1 = atom.views.getView(model1)
- expect(element1 instanceof HTMLDivElement).toBe true
- expect(element1.dataset.createdBy).toBe 'view-provider-1'
-
- element2 = atom.views.getView(model2)
- expect(element2 instanceof HTMLDivElement).toBe true
- expect(element2.dataset.createdBy).toBe 'view-provider-2'
-
- it "registers the view providers when any of the package's deserializers are used", ->
- pack = atom.packages.loadPackage("package-with-view-providers")
-
- spyOn(atom.views, 'addViewProvider').andCallThrough()
- atom.deserializers.deserialize({
- deserializer: 'DeserializerFromPackageWithViewProviders',
- a: 'b'
- })
- expect(atom.views.addViewProvider.callCount).toBe 2
-
- atom.deserializers.deserialize({
- deserializer: 'DeserializerFromPackageWithViewProviders',
- a: 'b'
- })
- expect(atom.views.addViewProvider.callCount).toBe 2
-
- element1 = atom.views.getView(model1)
- expect(element1 instanceof HTMLDivElement).toBe true
- expect(element1.dataset.createdBy).toBe 'view-provider-1'
-
- element2 = atom.views.getView(model2)
- expect(element2 instanceof HTMLDivElement).toBe true
- expect(element2.dataset.createdBy).toBe 'view-provider-2'
-
- it "registers the config schema in the package's metadata, if present", ->
- pack = atom.packages.loadPackage("package-with-json-config-schema")
- expect(atom.config.getSchema('package-with-json-config-schema')).toEqual {
- type: 'object'
- properties: {
- a: {type: 'number', default: 5}
- b: {type: 'string', default: 'five'}
- }
- }
-
- expect(pack.mainModule).toBeNull()
-
- atom.packages.unloadPackage('package-with-json-config-schema')
- atom.config.clear()
-
- pack = atom.packages.loadPackage("package-with-json-config-schema")
- expect(atom.config.getSchema('package-with-json-config-schema')).toEqual {
- type: 'object'
- properties: {
- a: {type: 'number', default: 5}
- b: {type: 'string', default: 'five'}
- }
- }
-
- describe "when a package does not have deserializers, view providers or a config schema in its package.json", ->
- beforeEach ->
- mockLocalStorage()
-
- it "defers loading the package's main module if the package previously used no Atom APIs when its main module was required", ->
- pack1 = atom.packages.loadPackage('package-with-main')
- expect(pack1.mainModule).toBeDefined()
-
- atom.packages.unloadPackage('package-with-main')
-
- pack2 = atom.packages.loadPackage('package-with-main')
- expect(pack2.mainModule).toBeNull()
-
- it "does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", ->
- pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls')
- expect(pack1.mainModule).toBeDefined()
-
- atom.packages.unloadPackage('package-with-eval-time-api-calls')
-
- pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls')
- expect(pack2.mainModule).not.toBeNull()
-
- describe "::loadAvailablePackage(availablePackage)", ->
- describe "if the package was preloaded", ->
- it "adds the package path to the module cache", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
-
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- atom.packages.preloadPackage(
- availablePackage.name,
- {
- rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
- metadata
- }
- )
- atom.packages.loadAvailablePackage(availablePackage)
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true)
- expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata)
-
- it "deactivates it if it had been disabled", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
-
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- preloadedPackage = atom.packages.preloadPackage(
- availablePackage.name,
- {
- rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
- metadata
- }
- )
- expect(preloadedPackage.keymapActivated).toBe(true)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
-
- atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name]))
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
- expect(preloadedPackage.keymapActivated).toBe(false)
- expect(preloadedPackage.settingsActivated).toBe(false)
- expect(preloadedPackage.menusActivated).toBe(false)
-
- it "deactivates it and reloads the new one if trying to load the same package outside of the bundle", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
-
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- preloadedPackage = atom.packages.preloadPackage(
- availablePackage.name,
- {
- rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
- metadata
- }
- )
- expect(preloadedPackage.keymapActivated).toBe(true)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
-
- availablePackage.isBundled = false
- atom.packages.loadAvailablePackage(availablePackage)
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true)
- expect(preloadedPackage.keymapActivated).toBe(false)
- expect(preloadedPackage.settingsActivated).toBe(false)
- expect(preloadedPackage.menusActivated).toBe(false)
-
- describe "if the package was not preloaded", ->
- it "adds the package path to the module cache", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- atom.packages.loadAvailablePackage(availablePackage)
- expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata)
-
- describe "preloading", ->
- it "requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
-
- atom.packages.packagesCache = {}
- atom.packages.packagesCache[availablePackage.name] = {
- main: path.join(availablePackage.path, metadata.main),
- grammarPaths: []
- }
- preloadedPackage = atom.packages.preloadPackage(
- availablePackage.name,
- {
- rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
- metadata
- }
- )
- expect(preloadedPackage.keymapActivated).toBe(true)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
- expect(preloadedPackage.mainModule).toBeTruthy()
- expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy()
-
- spyOn(atom.keymaps, 'add')
- spyOn(atom.menu, 'add')
- spyOn(atom.contextMenu, 'add')
- spyOn(atom.config, 'setSchema')
-
- atom.packages.loadAvailablePackage(availablePackage)
- expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main))
-
- atom.packages.activatePackage(availablePackage.name)
- expect(atom.keymaps.add).not.toHaveBeenCalled()
- expect(atom.menu.add).not.toHaveBeenCalled()
- expect(atom.contextMenu.add).not.toHaveBeenCalled()
- expect(atom.config.setSchema).not.toHaveBeenCalled()
- expect(preloadedPackage.keymapActivated).toBe(true)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
- expect(preloadedPackage.mainModule).toBeTruthy()
- expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy()
-
- it "deactivates disabled keymaps during package activation", ->
- availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
- availablePackage.isBundled = true
- metadata = atom.packages.loadPackageMetadata(availablePackage)
- expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
- expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
-
- atom.packages.packagesCache = {}
- atom.packages.packagesCache[availablePackage.name] = {
- main: path.join(availablePackage.path, metadata.main),
- grammarPaths: []
- }
- preloadedPackage = atom.packages.preloadPackage(
- availablePackage.name,
- {
- rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
- metadata
- }
- )
- expect(preloadedPackage.keymapActivated).toBe(true)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
-
- atom.packages.loadAvailablePackage(availablePackage)
- atom.config.set("core.packagesWithKeymapsDisabled", [availablePackage.name])
- atom.packages.activatePackage(availablePackage.name)
-
- expect(preloadedPackage.keymapActivated).toBe(false)
- expect(preloadedPackage.settingsActivated).toBe(true)
- expect(preloadedPackage.menusActivated).toBe(true)
-
- describe "::unloadPackage(name)", ->
- describe "when the package is active", ->
- it "throws an error", ->
- pack = null
- waitsForPromise ->
- atom.packages.activatePackage('package-with-main').then (p) -> pack = p
-
- runs ->
- expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
- expect(atom.packages.isPackageActive(pack.name)).toBeTruthy()
- expect( -> atom.packages.unloadPackage(pack.name)).toThrow()
- expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
- expect(atom.packages.isPackageActive(pack.name)).toBeTruthy()
-
- describe "when the package is not loaded", ->
- it "throws an error", ->
- expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy()
- expect( -> atom.packages.unloadPackage('unloaded')).toThrow()
- expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy()
-
- describe "when the package is loaded", ->
- it "no longers reports it as being loaded", ->
- pack = atom.packages.loadPackage('package-with-main')
- expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
- atom.packages.unloadPackage(pack.name)
- expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy()
-
- it "invokes ::onDidUnloadPackage listeners with the unloaded package", ->
- atom.packages.loadPackage('package-with-main')
- unloadedPackage = null
- atom.packages.onDidUnloadPackage (pack) -> unloadedPackage = pack
- atom.packages.unloadPackage('package-with-main')
- expect(unloadedPackage.name).toBe 'package-with-main'
-
- describe "::activatePackage(id)", ->
- describe "when called multiple times", ->
- it "it only calls activate on the package once", ->
- spyOn(Package.prototype, 'activateNow').andCallThrough()
- waitsForPromise ->
- atom.packages.activatePackage('package-with-index')
- waitsForPromise ->
- atom.packages.activatePackage('package-with-index')
- waitsForPromise ->
- atom.packages.activatePackage('package-with-index')
-
- runs ->
- expect(Package.prototype.activateNow.callCount).toBe 1
-
- describe "when the package has a main module", ->
- describe "when the metadata specifies a main module path˜", ->
- it "requires the module at the specified path", ->
- mainModule = require('./fixtures/packages/package-with-main/main-module')
- spyOn(mainModule, 'activate')
- pack = null
- waitsForPromise ->
- atom.packages.activatePackage('package-with-main').then (p) -> pack = p
-
- runs ->
- expect(mainModule.activate).toHaveBeenCalled()
- expect(pack.mainModule).toBe mainModule
-
- describe "when the metadata does not specify a main module", ->
- it "requires index.coffee", ->
- indexModule = require('./fixtures/packages/package-with-index/index')
- spyOn(indexModule, 'activate')
- pack = null
- waitsForPromise ->
- atom.packages.activatePackage('package-with-index').then (p) -> pack = p
-
- runs ->
- expect(indexModule.activate).toHaveBeenCalled()
- expect(pack.mainModule).toBe indexModule
-
- it "assigns config schema, including defaults when package contains a schema", ->
- expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined()
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-config-schema')
-
- runs ->
- expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 1
- expect(atom.config.get('package-with-config-schema.numbers.two')).toBe 2
-
- expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false
- expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe true
- expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 10
-
- describe "when the package metadata includes `activationCommands`", ->
- [mainModule, promise, workspaceCommandListener, registration] = []
-
- beforeEach ->
- jasmine.attachToDOM(atom.workspace.getElement())
- mainModule = require './fixtures/packages/package-with-activation-commands/index'
- mainModule.activationCommandCallCount = 0
- spyOn(mainModule, 'activate').andCallThrough()
- spyOn(Package.prototype, 'requireMainModule').andCallThrough()
-
- workspaceCommandListener = jasmine.createSpy('workspaceCommandListener')
- registration = atom.commands.add '.workspace', 'activation-command', workspaceCommandListener
-
- promise = atom.packages.activatePackage('package-with-activation-commands')
-
- afterEach ->
- registration?.dispose()
- mainModule = null
-
- it "defers requiring/activating the main module until an activation event bubbles to the root view", ->
- expect(Package.prototype.requireMainModule.callCount).toBe 0
-
- atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', bubbles: true))
-
- waitsForPromise ->
- promise
-
- runs ->
- expect(Package.prototype.requireMainModule.callCount).toBe 1
-
- it "triggers the activation event on all handlers registered during activation", ->
- waitsForPromise ->
- atom.workspace.open()
-
- runs ->
- editorElement = atom.workspace.getActiveTextEditor().getElement()
- editorCommandListener = jasmine.createSpy("editorCommandListener")
- atom.commands.add 'atom-text-editor', 'activation-command', editorCommandListener
- atom.commands.dispatch(editorElement, 'activation-command')
- expect(mainModule.activate.callCount).toBe 1
- expect(mainModule.activationCommandCallCount).toBe 1
- expect(editorCommandListener.callCount).toBe 1
- expect(workspaceCommandListener.callCount).toBe 1
- atom.commands.dispatch(editorElement, 'activation-command')
- expect(mainModule.activationCommandCallCount).toBe 2
- expect(editorCommandListener.callCount).toBe 2
- expect(workspaceCommandListener.callCount).toBe 2
- expect(mainModule.activate.callCount).toBe 1
-
- it "activates the package immediately when the events are empty", ->
- mainModule = require './fixtures/packages/package-with-empty-activation-commands/index'
- spyOn(mainModule, 'activate').andCallThrough()
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-empty-activation-commands')
-
- runs ->
- expect(mainModule.activate.callCount).toBe 1
-
- it "adds a notification when the activation commands are invalid", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
- expect(-> atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow()
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-activation-commands package")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-activation-commands"
-
- it "adds a notification when the context menu is invalid", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
- expect(-> atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow()
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-context-menu package")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-context-menu"
-
- it "adds a notification when the grammar is invalid", ->
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
-
- expect(-> atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow()
-
- waitsFor ->
- addErrorHandler.callCount > 0
-
- runs ->
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load a package-with-invalid-grammar package grammar")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-grammar"
-
- it "adds a notification when the settings are invalid", ->
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
-
- expect(-> atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow()
-
- waitsFor ->
- addErrorHandler.callCount > 0
-
- runs ->
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-invalid-settings package settings")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-settings"
-
- describe "when the package metadata includes `activationHooks`", ->
- [mainModule, promise] = []
-
- beforeEach ->
- mainModule = require './fixtures/packages/package-with-activation-hooks/index'
- spyOn(mainModule, 'activate').andCallThrough()
- spyOn(Package.prototype, 'requireMainModule').andCallThrough()
-
- it "defers requiring/activating the main module until an triggering of an activation hook occurs", ->
- promise = atom.packages.activatePackage('package-with-activation-hooks')
- expect(Package.prototype.requireMainModule.callCount).toBe 0
- atom.packages.triggerActivationHook('language-fictitious:grammar-used')
- atom.packages.triggerDeferredActivationHooks()
-
- waitsForPromise ->
- promise
-
- runs ->
- expect(Package.prototype.requireMainModule.callCount).toBe 1
-
- it "does not double register activation hooks when deactivating and reactivating", ->
- promise = atom.packages.activatePackage('package-with-activation-hooks')
- expect(mainModule.activate.callCount).toBe 0
- atom.packages.triggerActivationHook('language-fictitious:grammar-used')
- atom.packages.triggerDeferredActivationHooks()
-
- waitsForPromise ->
- promise
-
- runs ->
- expect(mainModule.activate.callCount).toBe 1
-
- waitsForPromise ->
- atom.packages.deactivatePackage('package-with-activation-hooks')
-
- runs ->
- promise = atom.packages.activatePackage('package-with-activation-hooks')
- atom.packages.triggerActivationHook('language-fictitious:grammar-used')
- atom.packages.triggerDeferredActivationHooks()
-
- waitsForPromise ->
- promise
-
- runs ->
- expect(mainModule.activate.callCount).toBe 2
-
- it "activates the package immediately when activationHooks is empty", ->
- mainModule = require './fixtures/packages/package-with-empty-activation-hooks/index'
- spyOn(mainModule, 'activate').andCallThrough()
-
- runs ->
- expect(Package.prototype.requireMainModule.callCount).toBe 0
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-empty-activation-hooks')
-
- runs ->
- expect(mainModule.activate.callCount).toBe 1
- expect(Package.prototype.requireMainModule.callCount).toBe 1
-
- it "activates the package immediately if the activation hook had already been triggered", ->
- atom.packages.triggerActivationHook('language-fictitious:grammar-used')
- atom.packages.triggerDeferredActivationHooks()
- expect(Package.prototype.requireMainModule.callCount).toBe 0
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-activation-hooks')
-
- runs ->
- expect(Package.prototype.requireMainModule.callCount).toBe 1
-
- describe "when the package has no main module", ->
- it "does not throw an exception", ->
- spyOn(console, "error")
- spyOn(console, "warn").andCallThrough()
- expect(-> atom.packages.activatePackage('package-without-module')).not.toThrow()
- expect(console.error).not.toHaveBeenCalled()
- expect(console.warn).not.toHaveBeenCalled()
-
- describe "when the package does not export an activate function", ->
- it "activates the package and does not throw an exception or log a warning", ->
- spyOn(console, "warn")
- expect(-> atom.packages.activatePackage('package-with-no-activate')).not.toThrow()
-
- waitsFor ->
- atom.packages.isPackageActive('package-with-no-activate')
-
- runs ->
- expect(console.warn).not.toHaveBeenCalled()
-
- it "passes the activate method the package's previously serialized state if it exists", ->
- pack = null
- waitsForPromise ->
- atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p
- runs ->
- expect(pack.mainModule.someNumber).not.toBe 77
- pack.mainModule.someNumber = 77
- atom.packages.serializePackage("package-with-serialization")
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-serialization")
- runs ->
- spyOn(pack.mainModule, 'activate').andCallThrough()
- waitsForPromise ->
- atom.packages.activatePackage("package-with-serialization")
- runs ->
- expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
-
- it "invokes ::onDidActivatePackage listeners with the activated package", ->
- activatedPackage = null
- atom.packages.onDidActivatePackage (pack) ->
- activatedPackage = pack
-
- atom.packages.activatePackage('package-with-main')
-
- waitsFor -> activatedPackage?
- runs -> expect(activatedPackage.name).toBe 'package-with-main'
-
- describe "when the package's main module throws an error on load", ->
- it "adds a notification instead of throwing an exception", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- atom.config.set("core.disabledPackages", [])
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
- expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow()
- expect(addErrorHandler.callCount).toBe 1
- expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-that-throws-an-exception package")
- expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-that-throws-an-exception"
-
- it "re-throws the exception in test mode", ->
- atom.config.set("core.disabledPackages", [])
- addErrorHandler = jasmine.createSpy()
- expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).toThrow("This package throws an exception")
-
- describe "when the package is not found", ->
- it "rejects the promise", ->
- atom.config.set("core.disabledPackages", [])
-
- onSuccess = jasmine.createSpy('onSuccess')
- onFailure = jasmine.createSpy('onFailure')
- spyOn(console, 'warn')
-
- atom.packages.activatePackage("this-doesnt-exist").then(onSuccess, onFailure)
-
- waitsFor "promise to be rejected", ->
- onFailure.callCount > 0
-
- runs ->
- expect(console.warn.callCount).toBe 1
- expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe true
- expect(onFailure.mostRecentCall.args[0].message).toContain "Failed to load package 'this-doesnt-exist'"
-
- describe "keymap loading", ->
- describe "when the metadata does not contain a 'keymaps' manifest", ->
- it "loads all the .cson/.json files in the keymaps directory", ->
- element1 = createTestElement('test-1')
- element2 = createTestElement('test-2')
- element3 = createTestElement('test-3')
-
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)).toHaveLength 0
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps")
-
- runs ->
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe "test-1"
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)[0].command).toBe "test-2"
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0
-
- describe "when the metadata contains a 'keymaps' manifest", ->
- it "loads only the keymaps specified by the manifest, in the specified order", ->
- element1 = createTestElement('test-1')
- element3 = createTestElement('test-3')
-
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps-manifest")
-
- runs ->
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1'
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-n', target: element1)[0].command).toBe 'keymap-2'
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-y', target: element3)).toHaveLength 0
-
- describe "when the keymap file is empty", ->
- it "does not throw an error on activation", ->
- waitsForPromise ->
- atom.packages.activatePackage("package-with-empty-keymap")
-
- runs ->
- expect(atom.packages.isPackageActive("package-with-empty-keymap")).toBe true
-
- describe "when the package's keymaps have been disabled", ->
- it "does not add the keymaps", ->
- element1 = createTestElement('test-1')
-
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
-
- atom.config.set("core.packagesWithKeymapsDisabled", ["package-with-keymaps-manifest"])
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps-manifest")
-
- runs ->
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
-
- describe "when setting core.packagesWithKeymapsDisabled", ->
- it "ignores package names in the array that aren't loaded", ->
- atom.packages.observePackagesWithKeymapsDisabled()
-
- expect(-> atom.config.set("core.packagesWithKeymapsDisabled", ["package-does-not-exist"])).not.toThrow()
- expect(-> atom.config.set("core.packagesWithKeymapsDisabled", [])).not.toThrow()
-
- describe "when the package's keymaps are disabled and re-enabled after it is activated", ->
- it "removes and re-adds the keymaps", ->
- element1 = createTestElement('test-1')
- atom.packages.observePackagesWithKeymapsDisabled()
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps-manifest")
-
- runs ->
- atom.config.set("core.packagesWithKeymapsDisabled", ['package-with-keymaps-manifest'])
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0
-
- atom.config.set("core.packagesWithKeymapsDisabled", [])
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1'
-
- describe "when the package is de-activated and re-activated", ->
- [element, events, userKeymapPath] = []
-
- beforeEach ->
- userKeymapPath = path.join(temp.mkdirSync(), "user-keymaps.cson")
- spyOn(atom.keymaps, "getUserKeymapPath").andReturn(userKeymapPath)
-
- element = createTestElement('test-1')
- jasmine.attachToDOM(element)
-
- events = []
- element.addEventListener 'user-command', (e) -> events.push(e)
- element.addEventListener 'test-1', (e) -> events.push(e)
-
- afterEach ->
- element.remove()
-
- # Avoid leaking user keymap subscription
- atom.keymaps.watchSubscriptions[userKeymapPath].dispose()
- delete atom.keymaps.watchSubscriptions[userKeymapPath]
-
- temp.cleanupSync()
-
- it "doesn't override user-defined keymaps", ->
- fs.writeFileSync userKeymapPath, """
- ".test-1":
- "ctrl-z": "user-command"
- """
- atom.keymaps.loadUserKeymap()
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps")
-
- runs ->
- atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element))
-
- expect(events.length).toBe(1)
- expect(events[0].type).toBe("user-command")
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-keymaps")
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-keymaps")
-
- runs ->
- atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element))
-
- expect(events.length).toBe(2)
- expect(events[1].type).toBe("user-command")
-
- describe "menu loading", ->
- beforeEach ->
- atom.contextMenu.definitions = []
- atom.menu.template = []
-
- describe "when the metadata does not contain a 'menus' manifest", ->
- it "loads all the .cson/.json files in the menus directory", ->
- element = createTestElement('test-1')
-
- expect(atom.contextMenu.templateForElement(element)).toEqual []
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-menus")
-
- runs ->
- expect(atom.menu.template.length).toBe 2
- expect(atom.menu.template[0].label).toBe "Second to Last"
- expect(atom.menu.template[1].label).toBe "Last"
- expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1"
- expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2"
- expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3"
-
- describe "when the metadata contains a 'menus' manifest", ->
- it "loads only the menus specified by the manifest, in the specified order", ->
- element = createTestElement('test-1')
-
- expect(atom.contextMenu.templateForElement(element)).toEqual []
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-menus-manifest")
-
- runs ->
- expect(atom.menu.template[0].label).toBe "Second to Last"
- expect(atom.menu.template[1].label).toBe "Last"
- expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2"
- expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1"
- expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined()
-
- describe "when the menu file is empty", ->
- it "does not throw an error on activation", ->
- waitsForPromise ->
- atom.packages.activatePackage("package-with-empty-menu")
-
- runs ->
- expect(atom.packages.isPackageActive("package-with-empty-menu")).toBe true
-
- describe "stylesheet loading", ->
- describe "when the metadata contains a 'styleSheets' manifest", ->
- it "loads style sheets from the styles directory as specified by the manifest", ->
- one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css")
- two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less")
- three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css")
-
- expect(atom.themes.stylesheetElementForId(one)).toBeNull()
- expect(atom.themes.stylesheetElementForId(two)).toBeNull()
- expect(atom.themes.stylesheetElementForId(three)).toBeNull()
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-style-sheets-manifest")
-
- runs ->
- expect(atom.themes.stylesheetElementForId(one)).not.toBeNull()
- expect(atom.themes.stylesheetElementForId(two)).not.toBeNull()
- expect(atom.themes.stylesheetElementForId(three)).toBeNull()
-
- expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '1px'
-
- describe "when the metadata does not contain a 'styleSheets' manifest", ->
- it "loads all style sheets from the styles directory", ->
- one = require.resolve("./fixtures/packages/package-with-styles/styles/1.css")
- two = require.resolve("./fixtures/packages/package-with-styles/styles/2.less")
- three = require.resolve("./fixtures/packages/package-with-styles/styles/3.test-context.css")
- four = require.resolve("./fixtures/packages/package-with-styles/styles/4.css")
-
- expect(atom.themes.stylesheetElementForId(one)).toBeNull()
- expect(atom.themes.stylesheetElementForId(two)).toBeNull()
- expect(atom.themes.stylesheetElementForId(three)).toBeNull()
- expect(atom.themes.stylesheetElementForId(four)).toBeNull()
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-styles")
-
- runs ->
- expect(atom.themes.stylesheetElementForId(one)).not.toBeNull()
- expect(atom.themes.stylesheetElementForId(two)).not.toBeNull()
- expect(atom.themes.stylesheetElementForId(three)).not.toBeNull()
- expect(atom.themes.stylesheetElementForId(four)).not.toBeNull()
- expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '3px'
-
- it "assigns the stylesheet's context based on the filename", ->
- waitsForPromise ->
- atom.packages.activatePackage("package-with-styles")
-
- runs ->
- count = 0
-
- for styleElement in atom.styles.getStyleElements()
- if styleElement.sourcePath.match /1.css/
- expect(styleElement.context).toBe undefined
- count++
-
- if styleElement.sourcePath.match /2.less/
- expect(styleElement.context).toBe undefined
- count++
-
- if styleElement.sourcePath.match /3.test-context.css/
- expect(styleElement.context).toBe 'test-context'
- count++
-
- if styleElement.sourcePath.match /4.css/
- expect(styleElement.context).toBe undefined
- count++
-
- expect(count).toBe 4
-
- describe "grammar loading", ->
- it "loads the package's grammars", ->
- waitsForPromise ->
- atom.packages.activatePackage('package-with-grammars')
-
- runs ->
- expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Alot'
- expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Alittle'
-
- describe "scoped-property loading", ->
- it "loads the scoped properties", ->
- waitsForPromise ->
- atom.packages.activatePackage("package-with-settings")
-
- runs ->
- expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a'
-
- describe "service registration", ->
- it "registers the package's provided and consumed services", ->
- consumerModule = require "./fixtures/packages/package-with-consumed-services"
- firstServiceV3Disposed = false
- firstServiceV4Disposed = false
- secondServiceDisposed = false
- spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable -> firstServiceV3Disposed = true)
- spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable -> firstServiceV4Disposed = true)
- spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable -> secondServiceDisposed = true)
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-consumed-services")
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-provided-services")
-
- runs ->
- expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1)
- expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3')
- expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4')
- expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service')
-
- consumerModule.consumeFirstServiceV3.reset()
- consumerModule.consumeFirstServiceV4.reset()
- consumerModule.consumeSecondService.reset()
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-provided-services")
-
- runs ->
- expect(firstServiceV3Disposed).toBe true
- expect(firstServiceV4Disposed).toBe true
- expect(secondServiceDisposed).toBe true
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-consumed-services")
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-provided-services")
-
- runs ->
- expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled()
- expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled()
- expect(consumerModule.consumeSecondService).not.toHaveBeenCalled()
-
- it "ignores provided and consumed services that do not exist", ->
- addErrorHandler = jasmine.createSpy()
- atom.notifications.onDidAddNotification(addErrorHandler)
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-missing-consumed-services")
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-missing-provided-services")
-
- runs ->
- expect(atom.packages.isPackageActive("package-with-missing-consumed-services")).toBe true
- expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true
- expect(addErrorHandler.callCount).toBe 0
-
- describe "::serialize", ->
- it "does not serialize packages that threw an error during activation", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- spyOn(console, 'warn')
- badPack = null
- waitsForPromise ->
- atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
-
- runs ->
- spyOn(badPack.mainModule, 'serialize').andCallThrough()
-
- atom.packages.serialize()
- expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
-
- it "absorbs exceptions that are thrown by the package module's serialize method", ->
- spyOn(console, 'error')
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-serialize-error')
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-serialization')
-
- runs ->
- atom.packages.serialize()
- expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
- expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
- expect(console.error).toHaveBeenCalled()
-
- describe "::deactivatePackages()", ->
- it "deactivates all packages but does not serialize them", ->
- [pack1, pack2] = []
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-deactivate").then (p) -> pack1 = p
- atom.packages.activatePackage("package-with-serialization").then (p) -> pack2 = p
-
- runs ->
- spyOn(pack1.mainModule, 'deactivate')
- spyOn(pack2.mainModule, 'serialize')
-
- waitsForPromise ->
- atom.packages.deactivatePackages()
-
- runs ->
- expect(pack1.mainModule.deactivate).toHaveBeenCalled()
- expect(pack2.mainModule.serialize).not.toHaveBeenCalled()
-
- describe "::deactivatePackage(id)", ->
- afterEach ->
- atom.packages.unloadPackages()
-
- it "calls `deactivate` on the package's main module if activate was successful", ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- pack = null
- waitsForPromise ->
- atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p
-
- runs ->
- expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy()
- spyOn(pack.mainModule, 'deactivate').andCallThrough()
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-deactivate")
-
- runs ->
- expect(pack.mainModule.deactivate).toHaveBeenCalled()
- expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy()
-
- spyOn(console, 'warn')
-
- badPack = null
- waitsForPromise ->
- atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
-
- runs ->
- expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy()
- spyOn(badPack.mainModule, 'deactivate').andCallThrough()
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-that-throws-on-activate")
-
- runs ->
- expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()
- expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy()
-
- it "absorbs exceptions that are thrown by the package module's deactivate method", ->
- spyOn(console, 'error')
- thrownError = null
-
- waitsForPromise ->
- atom.packages.activatePackage("package-that-throws-on-deactivate")
-
- waitsForPromise ->
- try
- atom.packages.deactivatePackage("package-that-throws-on-deactivate")
- catch error
- thrownError = error
-
- runs ->
- expect(thrownError).toBeNull()
- expect(console.error).toHaveBeenCalled()
-
- it "removes the package's grammars", ->
- waitsForPromise ->
- atom.packages.activatePackage('package-with-grammars')
-
- waitsForPromise ->
- atom.packages.deactivatePackage('package-with-grammars')
-
- runs ->
- expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Null Grammar'
- expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Null Grammar'
-
- it "removes the package's keymaps", ->
- waitsForPromise ->
- atom.packages.activatePackage('package-with-keymaps')
-
- waitsForPromise ->
- atom.packages.deactivatePackage('package-with-keymaps')
-
- runs ->
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-1'))).toHaveLength 0
- expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-2'))).toHaveLength 0
-
- it "removes the package's stylesheets", ->
- waitsForPromise ->
- atom.packages.activatePackage('package-with-styles')
-
- waitsForPromise ->
- atom.packages.deactivatePackage('package-with-styles')
-
- runs ->
- one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css")
- two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less")
- three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css")
- expect(atom.themes.stylesheetElementForId(one)).not.toExist()
- expect(atom.themes.stylesheetElementForId(two)).not.toExist()
- expect(atom.themes.stylesheetElementForId(three)).not.toExist()
-
- it "removes the package's scoped-properties", ->
- waitsForPromise ->
- atom.packages.activatePackage("package-with-settings")
-
- runs ->
- expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a'
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-settings")
-
- runs ->
- expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBeUndefined()
-
- it "invokes ::onDidDeactivatePackage listeners with the deactivated package", ->
- deactivatedPackage = null
-
- waitsForPromise ->
- atom.packages.activatePackage("package-with-main")
-
- runs ->
- atom.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack
-
- waitsForPromise ->
- atom.packages.deactivatePackage("package-with-main")
-
- runs ->
- expect(deactivatedPackage.name).toBe "package-with-main"
-
- describe "::activate()", ->
- beforeEach ->
- spyOn(atom, 'inSpecMode').andReturn(false)
- jasmine.snapshotDeprecations()
- spyOn(console, 'warn')
- atom.packages.loadPackages()
-
- loadedPackages = atom.packages.getLoadedPackages()
- expect(loadedPackages.length).toBeGreaterThan 0
-
- afterEach ->
- waitsForPromise ->
- atom.packages.deactivatePackages()
- runs ->
- atom.packages.unloadPackages()
- jasmine.restoreDeprecationsSnapshot()
-
- it "sets hasActivatedInitialPackages", ->
- spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null)
- spyOn(atom.packages, 'activatePackages')
- expect(atom.packages.hasActivatedInitialPackages()).toBe false
- waitsForPromise -> atom.packages.activate()
- runs -> expect(atom.packages.hasActivatedInitialPackages()).toBe true
-
- it "activates all the packages, and none of the themes", ->
- packageActivator = spyOn(atom.packages, 'activatePackages')
- themeActivator = spyOn(atom.themes, 'activatePackages')
-
- atom.packages.activate()
-
- expect(packageActivator).toHaveBeenCalled()
- expect(themeActivator).toHaveBeenCalled()
-
- packages = packageActivator.mostRecentCall.args[0]
- expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages
-
- themes = themeActivator.mostRecentCall.args[0]
- expect(['theme']).toContain(theme.getType()) for theme in themes
-
- it "calls callbacks registered with ::onDidActivateInitialPackages", ->
- package1 = atom.packages.loadPackage('package-with-main')
- package2 = atom.packages.loadPackage('package-with-index')
- package3 = atom.packages.loadPackage('package-with-activation-commands')
- spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3])
- spyOn(atom.themes, 'activatePackages')
- activateSpy = jasmine.createSpy('activateSpy')
- atom.packages.onDidActivateInitialPackages(activateSpy)
-
- atom.packages.activate()
- waitsFor -> activateSpy.callCount > 0
- runs ->
- jasmine.unspy(atom.packages, 'getLoadedPackages')
- expect(package1 in atom.packages.getActivePackages()).toBe true
- expect(package2 in atom.packages.getActivePackages()).toBe true
- expect(package3 in atom.packages.getActivePackages()).toBe false
-
- describe "::enablePackage(id) and ::disablePackage(id)", ->
- describe "with packages", ->
- it "enables a disabled package", ->
- packageName = 'package-with-main'
- atom.config.pushAtKeyPath('core.disabledPackages', packageName)
- atom.packages.observeDisabledPackages()
- expect(atom.config.get('core.disabledPackages')).toContain packageName
-
- pack = atom.packages.enablePackage(packageName)
- loadedPackages = atom.packages.getLoadedPackages()
- activatedPackages = null
- waitsFor ->
- activatedPackages = atom.packages.getActivePackages()
- activatedPackages.length > 0
-
- runs ->
- expect(loadedPackages).toContain(pack)
- expect(activatedPackages).toContain(pack)
- expect(atom.config.get('core.disabledPackages')).not.toContain packageName
-
- it "disables an enabled package", ->
- packageName = 'package-with-main'
- pack = null
- activatedPackages = null
-
- waitsForPromise ->
- atom.packages.activatePackage(packageName)
-
- runs ->
- atom.packages.observeDisabledPackages()
- expect(atom.config.get('core.disabledPackages')).not.toContain packageName
-
- pack = atom.packages.disablePackage(packageName)
-
- waitsFor ->
- activatedPackages = atom.packages.getActivePackages()
- activatedPackages.length is 0
-
- runs ->
- expect(activatedPackages).not.toContain(pack)
- expect(atom.config.get('core.disabledPackages')).toContain packageName
-
- it "returns null if the package cannot be loaded", ->
- spyOn(console, 'warn')
- expect(atom.packages.enablePackage("this-doesnt-exist")).toBeNull()
- expect(console.warn.callCount).toBe 1
-
- it "does not disable an already disabled package", ->
- packageName = 'package-with-main'
- atom.config.pushAtKeyPath('core.disabledPackages', packageName)
- atom.packages.observeDisabledPackages()
- expect(atom.config.get('core.disabledPackages')).toContain packageName
-
- atom.packages.disablePackage(packageName)
- packagesDisabled = atom.config.get('core.disabledPackages').filter((pack) -> pack is packageName)
- expect(packagesDisabled.length).toEqual 1
-
- describe "with themes", ->
- didChangeActiveThemesHandler = null
-
- beforeEach ->
- waitsForPromise ->
- atom.themes.activateThemes()
-
- afterEach ->
- waitsForPromise ->
- atom.themes.deactivateThemes()
-
- it "enables and disables a theme", ->
- packageName = 'theme-with-package-file'
-
- expect(atom.config.get('core.themes')).not.toContain packageName
- expect(atom.config.get('core.disabledPackages')).not.toContain packageName
-
- # enabling of theme
- pack = atom.packages.enablePackage(packageName)
-
- waitsFor 'theme to enable', 500, ->
- pack in atom.packages.getActivePackages()
-
- runs ->
- expect(atom.config.get('core.themes')).toContain packageName
- expect(atom.config.get('core.disabledPackages')).not.toContain packageName
-
- didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler')
- didChangeActiveThemesHandler.reset()
- atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler
-
- pack = atom.packages.disablePackage(packageName)
-
- waitsFor 'did-change-active-themes event to fire', 500, ->
- didChangeActiveThemesHandler.callCount is 1
-
- runs ->
- expect(atom.packages.getActivePackages()).not.toContain pack
- expect(atom.config.get('core.themes')).not.toContain packageName
- expect(atom.config.get('core.themes')).not.toContain packageName
- expect(atom.config.get('core.disabledPackages')).not.toContain packageName
diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js
new file mode 100644
index 000000000..1d949859d
--- /dev/null
+++ b/spec/package-manager-spec.js
@@ -0,0 +1,1339 @@
+const path = require('path')
+const Package = require('../src/package')
+const PackageManager = require('../src/package-manager')
+const temp = require('temp').track()
+const fs = require('fs-plus')
+const {Disposable} = require('atom')
+const {buildKeydownEvent} = require('../src/keymap-extensions')
+const {mockLocalStorage} = require('./spec-helper')
+const ModuleCache = require('../src/module-cache')
+const {it, fit, ffit, beforeEach, afterEach} = require('./async-spec-helpers')
+
+describe('PackageManager', () => {
+ function createTestElement (className) {
+ const element = document.createElement('div')
+ element.className = className
+ return element
+ }
+
+ beforeEach(() => {
+ spyOn(ModuleCache, 'add')
+ })
+
+ describe('initialize', () => {
+ it('adds regular package path', () => {
+ const packageManger = new PackageManager({})
+ const configDirPath = path.join('~', 'someConfig')
+ packageManger.initialize({configDirPath})
+ expect(packageManger.packageDirPaths.length).toBe(1)
+ expect(packageManger.packageDirPaths[0]).toBe(path.join(configDirPath, 'packages'))
+ })
+
+ it('adds regular package path and dev package path in dev mode', () => {
+ const packageManger = new PackageManager({})
+ const configDirPath = path.join('~', 'someConfig')
+ packageManger.initialize({configDirPath, devMode: true})
+ expect(packageManger.packageDirPaths.length).toBe(2)
+ expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'packages'))
+ expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'dev', 'packages'))
+ })
+ })
+
+ describe('::getApmPath()', () => {
+ it('returns the path to the apm command', () => {
+ let apmPath = path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm')
+ if (process.platform === 'win32') {
+ apmPath += '.cmd'
+ }
+ expect(atom.packages.getApmPath()).toBe(apmPath)
+ })
+
+ describe('when the core.apmPath setting is set', () => {
+ beforeEach(() => atom.config.set('core.apmPath', '/path/to/apm'))
+
+ it('returns the value of the core.apmPath config setting', () => {
+ expect(atom.packages.getApmPath()).toBe('/path/to/apm')
+ })
+ })
+ })
+
+ describe('::loadPackages()', () => {
+ beforeEach(() => spyOn(atom.packages, 'loadAvailablePackage'))
+
+ afterEach(async () => {
+ await atom.packages.deactivatePackages()
+ atom.packages.unloadPackages()
+ })
+
+ it('sets hasLoadedInitialPackages', () => {
+ expect(atom.packages.hasLoadedInitialPackages()).toBe(false)
+ atom.packages.loadPackages()
+ expect(atom.packages.hasLoadedInitialPackages()).toBe(true)
+ })
+ })
+
+ describe('::loadPackage(name)', () => {
+ beforeEach(() => atom.config.set('core.disabledPackages', []))
+
+ it('returns the package', () => {
+ const pack = atom.packages.loadPackage('package-with-index')
+ expect(pack instanceof Package).toBe(true)
+ expect(pack.metadata.name).toBe('package-with-index')
+ })
+
+ it('returns the package if it has an invalid keymap', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ const pack = atom.packages.loadPackage('package-with-broken-keymap')
+ expect(pack instanceof Package).toBe(true)
+ expect(pack.metadata.name).toBe('package-with-broken-keymap')
+ })
+
+ it('returns the package if it has an invalid stylesheet', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ const pack = atom.packages.loadPackage('package-with-invalid-styles')
+ expect(pack instanceof Package).toBe(true)
+ expect(pack.metadata.name).toBe('package-with-invalid-styles')
+ expect(pack.stylesheets.length).toBe(0)
+
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+ expect(() => pack.reloadStylesheets()).not.toThrow()
+ expect(addErrorHandler.callCount).toBe(2)
+ expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to reload the package-with-invalid-styles package stylesheets')
+ expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual('package-with-invalid-styles')
+ })
+
+ it('returns null if the package has an invalid package.json', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+ expect(atom.packages.loadPackage('package-with-broken-package-json')).toBeNull()
+ expect(addErrorHandler.callCount).toBe(1)
+ expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-broken-package-json package')
+ expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-broken-package-json')
+ })
+
+ it('returns null if the package name or path starts with a dot', () => {
+ expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull()
+ })
+
+ it('normalizes short repository urls in package.json', () => {
+ let {metadata} = atom.packages.loadPackage('package-with-short-url-package-json')
+ expect(metadata.repository.type).toBe('git')
+ expect(metadata.repository.url).toBe('https://github.com/example/repo');
+
+ ({metadata} = atom.packages.loadPackage('package-with-invalid-url-package-json'))
+ expect(metadata.repository.type).toBe('git')
+ expect(metadata.repository.url).toBe('foo')
+ })
+
+ it('trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ', () => {
+ const {metadata} = atom.packages.loadPackage('package-with-prefixed-and-suffixed-repo-url')
+ expect(metadata.repository.type).toBe('git')
+ expect(metadata.repository.url).toBe('https://github.com/example/repo')
+ })
+
+ it('returns null if the package is not found in any package directory', () => {
+ spyOn(console, 'warn')
+ expect(atom.packages.loadPackage('this-package-cannot-be-found')).toBeNull()
+ expect(console.warn.callCount).toBe(1)
+ expect(console.warn.argsForCall[0][0]).toContain('Could not resolve')
+ })
+
+ describe('when the package is deprecated', () => {
+ it('returns null', () => {
+ spyOn(console, 'warn')
+ expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull()
+ expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe(true)
+ expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe(true)
+ expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe(false)
+ expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe('<=2.2.0')
+ })
+ })
+
+ it('invokes ::onDidLoadPackage listeners with the loaded package', () => {
+ let loadedPackage = null
+
+ atom.packages.onDidLoadPackage(pack => {
+ loadedPackage = pack
+ })
+
+ atom.packages.loadPackage('package-with-main')
+
+ expect(loadedPackage.name).toBe('package-with-main')
+ })
+
+ it("registers any deserializers specified in the package's package.json", () => {
+ atom.packages.loadPackage('package-with-deserializers')
+
+ const state1 = {deserializer: 'Deserializer1', a: 'b'}
+ expect(atom.deserializers.deserialize(state1)).toEqual({
+ wasDeserializedBy: 'deserializeMethod1',
+ state: state1
+ })
+
+ const state2 = {deserializer: 'Deserializer2', c: 'd'}
+ expect(atom.deserializers.deserialize(state2)).toEqual({
+ wasDeserializedBy: 'deserializeMethod2',
+ state: state2
+ })
+ })
+
+ it('early-activates any atom.directory-provider or atom.repository-provider services that the package provide', () => {
+ jasmine.useRealClock()
+
+ const providers = []
+ atom.packages.serviceHub.consume('atom.directory-provider', '^0.1.0', provider => providers.push(provider))
+
+ atom.packages.loadPackage('package-with-directory-provider')
+ expect(providers.map(p => p.name)).toEqual(['directory provider from package-with-directory-provider'])
+ })
+
+ describe("when there are view providers specified in the package's package.json", () => {
+ const model1 = {worksWithViewProvider1: true}
+ const model2 = {worksWithViewProvider2: true}
+
+ afterEach(async () => {
+ await atom.packages.deactivatePackage('package-with-view-providers')
+ atom.packages.unloadPackage('package-with-view-providers')
+ })
+
+ it('does not load the view providers immediately', () => {
+ const pack = atom.packages.loadPackage('package-with-view-providers')
+ expect(pack.mainModule).toBeNull()
+
+ expect(() => atom.views.getView(model1)).toThrow()
+ expect(() => atom.views.getView(model2)).toThrow()
+ })
+
+ it('registers the view providers when the package is activated', async () => {
+ atom.packages.loadPackage('package-with-view-providers')
+
+ await atom.packages.activatePackage('package-with-view-providers')
+
+ const element1 = atom.views.getView(model1)
+ expect(element1 instanceof HTMLDivElement).toBe(true)
+ expect(element1.dataset.createdBy).toBe('view-provider-1')
+
+ const element2 = atom.views.getView(model2)
+ expect(element2 instanceof HTMLDivElement).toBe(true)
+ expect(element2.dataset.createdBy).toBe('view-provider-2')
+ })
+
+ it("registers the view providers when any of the package's deserializers are used", () => {
+ atom.packages.loadPackage('package-with-view-providers')
+
+ spyOn(atom.views, 'addViewProvider').andCallThrough()
+ atom.deserializers.deserialize({
+ deserializer: 'DeserializerFromPackageWithViewProviders',
+ a: 'b'
+ })
+ expect(atom.views.addViewProvider.callCount).toBe(2)
+
+ atom.deserializers.deserialize({
+ deserializer: 'DeserializerFromPackageWithViewProviders',
+ a: 'b'
+ })
+ expect(atom.views.addViewProvider.callCount).toBe(2)
+
+ const element1 = atom.views.getView(model1)
+ expect(element1 instanceof HTMLDivElement).toBe(true)
+ expect(element1.dataset.createdBy).toBe('view-provider-1')
+
+ const element2 = atom.views.getView(model2)
+ expect(element2 instanceof HTMLDivElement).toBe(true)
+ expect(element2.dataset.createdBy).toBe('view-provider-2')
+ })
+ })
+
+ it("registers the config schema in the package's metadata, if present", () => {
+ let pack = atom.packages.loadPackage('package-with-json-config-schema')
+ expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({
+ type: 'object',
+ properties: {
+ a: {type: 'number', default: 5},
+ b: {type: 'string', default: 'five'}
+ }
+ })
+
+ expect(pack.mainModule).toBeNull()
+
+ atom.packages.unloadPackage('package-with-json-config-schema')
+ atom.config.clear()
+
+ pack = atom.packages.loadPackage('package-with-json-config-schema')
+ expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({
+ type: 'object',
+ properties: {
+ a: {type: 'number', default: 5},
+ b: {type: 'string', default: 'five'}
+ }
+ })
+ })
+
+ describe('when a package does not have deserializers, view providers or a config schema in its package.json', () => {
+ beforeEach(() => mockLocalStorage())
+
+ it("defers loading the package's main module if the package previously used no Atom APIs when its main module was required", () => {
+ const pack1 = atom.packages.loadPackage('package-with-main')
+ expect(pack1.mainModule).toBeDefined()
+
+ atom.packages.unloadPackage('package-with-main')
+
+ const pack2 = atom.packages.loadPackage('package-with-main')
+ expect(pack2.mainModule).toBeNull()
+ })
+
+ it("does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", () => {
+ const pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls')
+ expect(pack1.mainModule).toBeDefined()
+
+ atom.packages.unloadPackage('package-with-eval-time-api-calls')
+
+ const pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls')
+ expect(pack2.mainModule).not.toBeNull()
+ })
+ })
+ })
+
+ describe('::loadAvailablePackage(availablePackage)', () => {
+ describe('if the package was preloaded', () => {
+ it('adds the package path to the module cache', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ atom.packages.preloadPackage(
+ availablePackage.name,
+ {
+ rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
+ metadata
+ }
+ )
+ atom.packages.loadAvailablePackage(availablePackage)
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true)
+ expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata)
+ })
+
+ it('deactivates it if it had been disabled', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ const preloadedPackage = atom.packages.preloadPackage(
+ availablePackage.name,
+ {
+ rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
+ metadata
+ }
+ )
+ expect(preloadedPackage.keymapActivated).toBe(true)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+
+ atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name]))
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+ expect(preloadedPackage.keymapActivated).toBe(false)
+ expect(preloadedPackage.settingsActivated).toBe(false)
+ expect(preloadedPackage.menusActivated).toBe(false)
+ })
+
+ it('deactivates it and reloads the new one if trying to load the same package outside of the bundle', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ const preloadedPackage = atom.packages.preloadPackage(
+ availablePackage.name,
+ {
+ rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
+ metadata
+ }
+ )
+ expect(preloadedPackage.keymapActivated).toBe(true)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+
+ availablePackage.isBundled = false
+ atom.packages.loadAvailablePackage(availablePackage)
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true)
+ expect(preloadedPackage.keymapActivated).toBe(false)
+ expect(preloadedPackage.settingsActivated).toBe(false)
+ expect(preloadedPackage.menusActivated).toBe(false)
+ })
+ })
+
+ describe('if the package was not preloaded', () => {
+ it('adds the package path to the module cache', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ atom.packages.loadAvailablePackage(availablePackage)
+ expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata)
+ })
+ })
+ })
+
+ describe('preloading', () => {
+ it('requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+
+ atom.packages.packagesCache = {}
+ atom.packages.packagesCache[availablePackage.name] = {
+ main: path.join(availablePackage.path, metadata.main),
+ grammarPaths: []
+ }
+ const preloadedPackage = atom.packages.preloadPackage(
+ availablePackage.name,
+ {
+ rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
+ metadata
+ }
+ )
+ expect(preloadedPackage.keymapActivated).toBe(true)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+ expect(preloadedPackage.mainModule).toBeTruthy()
+ expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy()
+
+ spyOn(atom.keymaps, 'add')
+ spyOn(atom.menu, 'add')
+ spyOn(atom.contextMenu, 'add')
+ spyOn(atom.config, 'setSchema')
+
+ atom.packages.loadAvailablePackage(availablePackage)
+ expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main))
+
+ atom.packages.activatePackage(availablePackage.name)
+ expect(atom.keymaps.add).not.toHaveBeenCalled()
+ expect(atom.menu.add).not.toHaveBeenCalled()
+ expect(atom.contextMenu.add).not.toHaveBeenCalled()
+ expect(atom.config.setSchema).not.toHaveBeenCalled()
+ expect(preloadedPackage.keymapActivated).toBe(true)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+ expect(preloadedPackage.mainModule).toBeTruthy()
+ expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy()
+ })
+
+ it('deactivates disabled keymaps during package activation', () => {
+ const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check')
+ availablePackage.isBundled = true
+ const metadata = atom.packages.loadPackageMetadata(availablePackage)
+ expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
+ expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
+
+ atom.packages.packagesCache = {}
+ atom.packages.packagesCache[availablePackage.name] = {
+ main: path.join(availablePackage.path, metadata.main),
+ grammarPaths: []
+ }
+ const preloadedPackage = atom.packages.preloadPackage(
+ availablePackage.name,
+ {
+ rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path),
+ metadata
+ }
+ )
+ expect(preloadedPackage.keymapActivated).toBe(true)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+
+ atom.packages.loadAvailablePackage(availablePackage)
+ atom.config.set('core.packagesWithKeymapsDisabled', [availablePackage.name])
+ atom.packages.activatePackage(availablePackage.name)
+
+ expect(preloadedPackage.keymapActivated).toBe(false)
+ expect(preloadedPackage.settingsActivated).toBe(true)
+ expect(preloadedPackage.menusActivated).toBe(true)
+ })
+ })
+
+ describe('::unloadPackage(name)', () => {
+ describe('when the package is active', () => {
+ it('throws an error', async () => {
+ const pack = await atom.packages.activatePackage('package-with-main')
+ expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
+ expect(atom.packages.isPackageActive(pack.name)).toBeTruthy()
+
+ expect(() => atom.packages.unloadPackage(pack.name)).toThrow()
+ expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
+ expect(atom.packages.isPackageActive(pack.name)).toBeTruthy()
+ })
+ })
+
+ describe('when the package is not loaded', () => {
+ it('throws an error', () => {
+ expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy()
+ expect(() => atom.packages.unloadPackage('unloaded')).toThrow()
+ expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy()
+ })
+ })
+
+ describe('when the package is loaded', () => {
+ it('no longers reports it as being loaded', () => {
+ const pack = atom.packages.loadPackage('package-with-main')
+ expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy()
+ atom.packages.unloadPackage(pack.name)
+ expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy()
+ })
+ })
+
+ it('invokes ::onDidUnloadPackage listeners with the unloaded package', () => {
+ atom.packages.loadPackage('package-with-main')
+ let unloadedPackage
+ atom.packages.onDidUnloadPackage(pack => {
+ unloadedPackage = pack
+ })
+ atom.packages.unloadPackage('package-with-main')
+ expect(unloadedPackage.name).toBe('package-with-main')
+ })
+ })
+
+ describe('::activatePackage(id)', () => {
+ describe('when called multiple times', () => {
+ it('it only calls activate on the package once', async () => {
+ spyOn(Package.prototype, 'activateNow').andCallThrough()
+ await atom.packages.activatePackage('package-with-index')
+ await atom.packages.activatePackage('package-with-index')
+ await atom.packages.activatePackage('package-with-index')
+
+ expect(Package.prototype.activateNow.callCount).toBe(1)
+ })
+ })
+
+ describe('when the package has a main module', () => {
+ describe('when the metadata specifies a main module path˜', () => {
+ it('requires the module at the specified path', async () => {
+ const mainModule = require('./fixtures/packages/package-with-main/main-module')
+ spyOn(mainModule, 'activate')
+
+ const pack = await atom.packages.activatePackage('package-with-main')
+ expect(mainModule.activate).toHaveBeenCalled()
+ expect(pack.mainModule).toBe(mainModule)
+ })
+ })
+
+ describe('when the metadata does not specify a main module', () => {
+ it('requires index.coffee', async () => {
+ const indexModule = require('./fixtures/packages/package-with-index/index')
+ spyOn(indexModule, 'activate')
+
+ const pack = await atom.packages.activatePackage('package-with-index')
+ expect(indexModule.activate).toHaveBeenCalled()
+ expect(pack.mainModule).toBe(indexModule)
+ })
+ })
+
+ it('assigns config schema, including defaults when package contains a schema', async () => {
+ expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined()
+
+ await atom.packages.activatePackage('package-with-config-schema')
+ expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1)
+ expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2)
+ expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false)
+ expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true)
+ expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10)
+ })
+
+ describe('when the package metadata includes `activationCommands`', () => {
+ let mainModule, promise, workspaceCommandListener, registration
+
+ beforeEach(() => {
+ jasmine.attachToDOM(atom.workspace.getElement())
+ mainModule = require('./fixtures/packages/package-with-activation-commands/index')
+ mainModule.activationCommandCallCount = 0
+ spyOn(mainModule, 'activate').andCallThrough()
+ spyOn(Package.prototype, 'requireMainModule').andCallThrough()
+
+ workspaceCommandListener = jasmine.createSpy('workspaceCommandListener')
+ registration = atom.commands.add('.workspace', 'activation-command', workspaceCommandListener)
+
+ promise = atom.packages.activatePackage('package-with-activation-commands')
+ })
+
+ afterEach(() => {
+ if (registration) {
+ registration.dispose()
+ }
+ mainModule = null
+ })
+
+ it('defers requiring/activating the main module until an activation event bubbles to the root view', async () => {
+ expect(Package.prototype.requireMainModule.callCount).toBe(0)
+
+ atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true}))
+
+ await promise
+ expect(Package.prototype.requireMainModule.callCount).toBe(1)
+ })
+
+ it('triggers the activation event on all handlers registered during activation', async () => {
+ await atom.workspace.open()
+
+ const editorElement = atom.workspace.getActiveTextEditor().getElement()
+ const editorCommandListener = jasmine.createSpy('editorCommandListener')
+ atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener)
+
+ atom.commands.dispatch(editorElement, 'activation-command')
+ expect(mainModule.activate.callCount).toBe(1)
+ expect(mainModule.activationCommandCallCount).toBe(1)
+ expect(editorCommandListener.callCount).toBe(1)
+ expect(workspaceCommandListener.callCount).toBe(1)
+
+ atom.commands.dispatch(editorElement, 'activation-command')
+ expect(mainModule.activationCommandCallCount).toBe(2)
+ expect(editorCommandListener.callCount).toBe(2)
+ expect(workspaceCommandListener.callCount).toBe(2)
+ expect(mainModule.activate.callCount).toBe(1)
+ })
+
+ it('activates the package immediately when the events are empty', async () => {
+ mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index')
+ spyOn(mainModule, 'activate').andCallThrough()
+
+ atom.packages.activatePackage('package-with-empty-activation-commands')
+
+ expect(mainModule.activate.callCount).toBe(1)
+ })
+
+ it('adds a notification when the activation commands are invalid', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+ expect(() => atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow()
+ expect(addErrorHandler.callCount).toBe(1)
+ expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-activation-commands package')
+ expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-activation-commands')
+ })
+
+ it('adds a notification when the context menu is invalid', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+ expect(() => atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow()
+ expect(addErrorHandler.callCount).toBe(1)
+ expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-context-menu package')
+ expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-context-menu')
+ })
+
+ it('adds a notification when the grammar is invalid', async () => {
+ let notificationEvent
+
+ await new Promise(resolve => {
+ const subscription = atom.notifications.onDidAddNotification(event => {
+ notificationEvent = event
+ subscription.dispose()
+ resolve()
+ })
+
+ atom.packages.activatePackage('package-with-invalid-grammar')
+ })
+
+ expect(notificationEvent.message).toContain('Failed to load a package-with-invalid-grammar package grammar')
+ expect(notificationEvent.options.packageName).toEqual('package-with-invalid-grammar')
+ })
+
+ it('adds a notification when the settings are invalid', async () => {
+ let notificationEvent
+
+ await new Promise(resolve => {
+ const subscription = atom.notifications.onDidAddNotification(event => {
+ notificationEvent = event
+ subscription.dispose()
+ resolve()
+ })
+
+ atom.packages.activatePackage('package-with-invalid-settings')
+ })
+
+ expect(notificationEvent.message).toContain('Failed to load the package-with-invalid-settings package settings')
+ expect(notificationEvent.options.packageName).toEqual('package-with-invalid-settings')
+ })
+ })
+ })
+
+ describe('when the package metadata includes `activationHooks`', () => {
+ let mainModule, promise
+
+ beforeEach(() => {
+ mainModule = require('./fixtures/packages/package-with-activation-hooks/index')
+ spyOn(mainModule, 'activate').andCallThrough()
+ spyOn(Package.prototype, 'requireMainModule').andCallThrough()
+ })
+
+ it('defers requiring/activating the main module until an triggering of an activation hook occurs', async () => {
+ promise = atom.packages.activatePackage('package-with-activation-hooks')
+ expect(Package.prototype.requireMainModule.callCount).toBe(0)
+ atom.packages.triggerActivationHook('language-fictitious:grammar-used')
+ atom.packages.triggerDeferredActivationHooks()
+
+ await promise
+ expect(Package.prototype.requireMainModule.callCount).toBe(1)
+ })
+
+ it('does not double register activation hooks when deactivating and reactivating', async () => {
+ promise = atom.packages.activatePackage('package-with-activation-hooks')
+ expect(mainModule.activate.callCount).toBe(0)
+ atom.packages.triggerActivationHook('language-fictitious:grammar-used')
+ atom.packages.triggerDeferredActivationHooks()
+
+ await promise
+ expect(mainModule.activate.callCount).toBe(1)
+
+ await atom.packages.deactivatePackage('package-with-activation-hooks')
+
+ promise = atom.packages.activatePackage('package-with-activation-hooks')
+ atom.packages.triggerActivationHook('language-fictitious:grammar-used')
+ atom.packages.triggerDeferredActivationHooks()
+
+ await promise
+ expect(mainModule.activate.callCount).toBe(2)
+ })
+
+ it('activates the package immediately when activationHooks is empty', async () => {
+ mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index')
+ spyOn(mainModule, 'activate').andCallThrough()
+
+ expect(Package.prototype.requireMainModule.callCount).toBe(0)
+
+ await atom.packages.activatePackage('package-with-empty-activation-hooks')
+ expect(mainModule.activate.callCount).toBe(1)
+ expect(Package.prototype.requireMainModule.callCount).toBe(1)
+ })
+
+ it('activates the package immediately if the activation hook had already been triggered', async () => {
+ atom.packages.triggerActivationHook('language-fictitious:grammar-used')
+ atom.packages.triggerDeferredActivationHooks()
+ expect(Package.prototype.requireMainModule.callCount).toBe(0)
+
+ await atom.packages.activatePackage('package-with-activation-hooks')
+ expect(Package.prototype.requireMainModule.callCount).toBe(1)
+ })
+ })
+
+ describe('when the package has no main module', () => {
+ it('does not throw an exception', () => {
+ spyOn(console, 'error')
+ spyOn(console, 'warn').andCallThrough()
+ expect(() => atom.packages.activatePackage('package-without-module')).not.toThrow()
+ expect(console.error).not.toHaveBeenCalled()
+ expect(console.warn).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('when the package does not export an activate function', () => {
+ it('activates the package and does not throw an exception or log a warning', async () => {
+ spyOn(console, 'warn')
+ await atom.packages.activatePackage('package-with-no-activate')
+ expect(console.warn).not.toHaveBeenCalled()
+ })
+ })
+
+ it("passes the activate method the package's previously serialized state if it exists", async () => {
+ const pack = await atom.packages.activatePackage('package-with-serialization')
+ expect(pack.mainModule.someNumber).not.toBe(77)
+ pack.mainModule.someNumber = 77
+ atom.packages.serializePackage('package-with-serialization')
+ await atom.packages.deactivatePackage('package-with-serialization')
+
+ spyOn(pack.mainModule, 'activate').andCallThrough()
+ await atom.packages.activatePackage('package-with-serialization')
+ expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
+ })
+
+ it('invokes ::onDidActivatePackage listeners with the activated package', async () => {
+ let activatedPackage
+ atom.packages.onDidActivatePackage(pack => {
+ activatedPackage = pack
+ })
+
+ await atom.packages.activatePackage('package-with-main')
+ expect(activatedPackage.name).toBe('package-with-main')
+ })
+
+ describe("when the package's main module throws an error on load", () => {
+ it('adds a notification instead of throwing an exception', () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ atom.config.set('core.disabledPackages', [])
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+ expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).not.toThrow()
+ expect(addErrorHandler.callCount).toBe(1)
+ expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-that-throws-an-exception package')
+ expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-that-throws-an-exception')
+ })
+
+ it('re-throws the exception in test mode', () => {
+ atom.config.set('core.disabledPackages', [])
+ expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).toThrow('This package throws an exception')
+ })
+ })
+
+ describe('when the package is not found', () => {
+ it('rejects the promise', async () => {
+ spyOn(console, 'warn')
+ atom.config.set('core.disabledPackages', [])
+
+ try {
+ await atom.packages.activatePackage('this-doesnt-exist')
+ expect('Error to be thrown').toBe('')
+ } catch (error) {
+ expect(console.warn.callCount).toBe(1)
+ expect(error.message).toContain("Failed to load package 'this-doesnt-exist'")
+ }
+ })
+ })
+
+ describe('keymap loading', () => {
+ describe("when the metadata does not contain a 'keymaps' manifest", () => {
+ it('loads all the .cson/.json files in the keymaps directory', async () => {
+ const element1 = createTestElement('test-1')
+ const element2 = createTestElement('test-2')
+ const element3 = createTestElement('test-3')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})).toHaveLength(0)
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0)
+
+ await atom.packages.activatePackage('package-with-keymaps')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0)
+ })
+ })
+
+ describe("when the metadata contains a 'keymaps' manifest", () => {
+ it('loads only the keymaps specified by the manifest, in the specified order', async () => {
+ const element1 = createTestElement('test-1')
+ const element3 = createTestElement('test-3')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)
+
+ await atom.packages.activatePackage('package-with-keymaps-manifest')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0)
+ })
+ })
+
+ describe('when the keymap file is empty', () => {
+ it('does not throw an error on activation', async () => {
+ await atom.packages.activatePackage('package-with-empty-keymap')
+ expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true)
+ })
+ })
+
+ describe("when the package's keymaps have been disabled", () => {
+ it('does not add the keymaps', async () => {
+ const element1 = createTestElement('test-1')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)
+
+ atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest'])
+ await atom.packages.activatePackage('package-with-keymaps-manifest')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)
+ })
+ })
+
+ describe('when setting core.packagesWithKeymapsDisabled', () => {
+ it("ignores package names in the array that aren't loaded", () => {
+ atom.packages.observePackagesWithKeymapsDisabled()
+
+ expect(() => atom.config.set('core.packagesWithKeymapsDisabled', ['package-does-not-exist'])).not.toThrow()
+ expect(() => atom.config.set('core.packagesWithKeymapsDisabled', [])).not.toThrow()
+ })
+ })
+
+ describe("when the package's keymaps are disabled and re-enabled after it is activated", () => {
+ it('removes and re-adds the keymaps', async () => {
+ const element1 = createTestElement('test-1')
+ atom.packages.observePackagesWithKeymapsDisabled()
+
+ await atom.packages.activatePackage('package-with-keymaps-manifest')
+
+ atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest'])
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)
+
+ atom.config.set('core.packagesWithKeymapsDisabled', [])
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1')
+ })
+ })
+
+ describe('when the package is de-activated and re-activated', () => {
+ let element, events, userKeymapPath
+
+ beforeEach(() => {
+ userKeymapPath = path.join(temp.mkdirSync(), 'user-keymaps.cson')
+ spyOn(atom.keymaps, 'getUserKeymapPath').andReturn(userKeymapPath)
+
+ element = createTestElement('test-1')
+ jasmine.attachToDOM(element)
+
+ events = []
+ element.addEventListener('user-command', e => events.push(e))
+ element.addEventListener('test-1', e => events.push(e))
+ })
+
+ afterEach(() => {
+ element.remove()
+
+ // Avoid leaking user keymap subscription
+ atom.keymaps.watchSubscriptions[userKeymapPath].dispose()
+ delete atom.keymaps.watchSubscriptions[userKeymapPath]
+
+ temp.cleanupSync()
+ })
+
+ it("doesn't override user-defined keymaps", async () => {
+ fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`)
+ atom.keymaps.loadUserKeymap()
+
+ await atom.packages.activatePackage('package-with-keymaps')
+ atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element}))
+ expect(events.length).toBe(1)
+ expect(events[0].type).toBe('user-command')
+
+ await atom.packages.deactivatePackage('package-with-keymaps')
+ await atom.packages.activatePackage('package-with-keymaps')
+ atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element}))
+ expect(events.length).toBe(2)
+ expect(events[1].type).toBe('user-command')
+ })
+ })
+ })
+
+ describe('menu loading', () => {
+ beforeEach(() => {
+ atom.contextMenu.definitions = []
+ atom.menu.template = []
+ })
+
+ describe("when the metadata does not contain a 'menus' manifest", () => {
+ it('loads all the .cson/.json files in the menus directory', async () => {
+ const element = createTestElement('test-1')
+ expect(atom.contextMenu.templateForElement(element)).toEqual([])
+
+ await atom.packages.activatePackage('package-with-menus')
+ expect(atom.menu.template.length).toBe(2)
+ expect(atom.menu.template[0].label).toBe('Second to Last')
+ expect(atom.menu.template[1].label).toBe('Last')
+ expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1')
+ expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2')
+ expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3')
+ })
+ })
+
+ describe("when the metadata contains a 'menus' manifest", () => {
+ it('loads only the menus specified by the manifest, in the specified order', async () => {
+ const element = createTestElement('test-1')
+ expect(atom.contextMenu.templateForElement(element)).toEqual([])
+
+ await atom.packages.activatePackage('package-with-menus-manifest')
+ expect(atom.menu.template[0].label).toBe('Second to Last')
+ expect(atom.menu.template[1].label).toBe('Last')
+ expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2')
+ expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1')
+ expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined()
+ })
+ })
+
+ describe('when the menu file is empty', () => {
+ it('does not throw an error on activation', async () => {
+ await atom.packages.activatePackage('package-with-empty-menu')
+ expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true)
+ })
+ })
+ })
+
+ describe('stylesheet loading', () => {
+ describe("when the metadata contains a 'styleSheets' manifest", () => {
+ it('loads style sheets from the styles directory as specified by the manifest', async () => {
+ const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css')
+ const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less')
+ const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css')
+
+ expect(atom.themes.stylesheetElementForId(one)).toBeNull()
+ expect(atom.themes.stylesheetElementForId(two)).toBeNull()
+ expect(atom.themes.stylesheetElementForId(three)).toBeNull()
+
+ await atom.packages.activatePackage('package-with-style-sheets-manifest')
+ expect(atom.themes.stylesheetElementForId(one)).not.toBeNull()
+ expect(atom.themes.stylesheetElementForId(two)).not.toBeNull()
+ expect(atom.themes.stylesheetElementForId(three)).toBeNull()
+ expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px')
+ })
+ })
+
+ describe("when the metadata does not contain a 'styleSheets' manifest", () => {
+ it('loads all style sheets from the styles directory', async () => {
+ const one = require.resolve('./fixtures/packages/package-with-styles/styles/1.css')
+ const two = require.resolve('./fixtures/packages/package-with-styles/styles/2.less')
+ const three = require.resolve('./fixtures/packages/package-with-styles/styles/3.test-context.css')
+ const four = require.resolve('./fixtures/packages/package-with-styles/styles/4.css')
+
+ expect(atom.themes.stylesheetElementForId(one)).toBeNull()
+ expect(atom.themes.stylesheetElementForId(two)).toBeNull()
+ expect(atom.themes.stylesheetElementForId(three)).toBeNull()
+ expect(atom.themes.stylesheetElementForId(four)).toBeNull()
+
+ await atom.packages.activatePackage('package-with-styles')
+ expect(atom.themes.stylesheetElementForId(one)).not.toBeNull()
+ expect(atom.themes.stylesheetElementForId(two)).not.toBeNull()
+ expect(atom.themes.stylesheetElementForId(three)).not.toBeNull()
+ expect(atom.themes.stylesheetElementForId(four)).not.toBeNull()
+ expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px')
+ })
+ })
+
+ it("assigns the stylesheet's context based on the filename", async () => {
+ await atom.packages.activatePackage('package-with-styles')
+
+ let count = 0
+ for (let styleElement of atom.styles.getStyleElements()) {
+ if (styleElement.sourcePath.match(/1.css/)) {
+ expect(styleElement.context).toBe(undefined)
+ count++
+ }
+
+ if (styleElement.sourcePath.match(/2.less/)) {
+ expect(styleElement.context).toBe(undefined)
+ count++
+ }
+
+ if (styleElement.sourcePath.match(/3.test-context.css/)) {
+ expect(styleElement.context).toBe('test-context')
+ count++
+ }
+
+ if (styleElement.sourcePath.match(/4.css/)) {
+ expect(styleElement.context).toBe(undefined)
+ count++
+ }
+ }
+
+ expect(count).toBe(4)
+ })
+ })
+
+ describe('grammar loading', () => {
+ it("loads the package's grammars", async () => {
+ await atom.packages.activatePackage('package-with-grammars')
+ expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot')
+ expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle')
+ })
+ })
+
+ describe('scoped-property loading', () => {
+ it('loads the scoped properties', async () => {
+ await atom.packages.activatePackage('package-with-settings')
+ expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')
+ })
+ })
+
+ describe('service registration', () => {
+ it("registers the package's provided and consumed services", async () => {
+ const consumerModule = require('./fixtures/packages/package-with-consumed-services')
+
+ let firstServiceV3Disposed = false
+ let firstServiceV4Disposed = false
+ let secondServiceDisposed = false
+ spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable(() => { firstServiceV3Disposed = true }))
+ spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable(() => { firstServiceV4Disposed = true }))
+ spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable(() => { secondServiceDisposed = true }))
+
+ await atom.packages.activatePackage('package-with-consumed-services')
+ await atom.packages.activatePackage('package-with-provided-services')
+ expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1)
+ expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3')
+ expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4')
+ expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service')
+
+ consumerModule.consumeFirstServiceV3.reset()
+ consumerModule.consumeFirstServiceV4.reset()
+ consumerModule.consumeSecondService.reset()
+
+ await atom.packages.deactivatePackage('package-with-provided-services')
+ expect(firstServiceV3Disposed).toBe(true)
+ expect(firstServiceV4Disposed).toBe(true)
+ expect(secondServiceDisposed).toBe(true)
+
+ await atom.packages.deactivatePackage('package-with-consumed-services')
+ await atom.packages.activatePackage('package-with-provided-services')
+ expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled()
+ expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled()
+ expect(consumerModule.consumeSecondService).not.toHaveBeenCalled()
+ })
+
+ it('ignores provided and consumed services that do not exist', async () => {
+ const addErrorHandler = jasmine.createSpy()
+ atom.notifications.onDidAddNotification(addErrorHandler)
+
+ await atom.packages.activatePackage('package-with-missing-consumed-services')
+ await atom.packages.activatePackage('package-with-missing-provided-services')
+ expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true)
+ expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true)
+ expect(addErrorHandler.callCount).toBe(0)
+ })
+ })
+ })
+
+ describe('::serialize', () => {
+ it('does not serialize packages that threw an error during activation', async () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ spyOn(console, 'warn')
+
+ const badPack = await atom.packages.activatePackage('package-that-throws-on-activate')
+ spyOn(badPack.mainModule, 'serialize').andCallThrough()
+
+ atom.packages.serialize()
+ expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
+ })
+
+ it("absorbs exceptions that are thrown by the package module's serialize method", async () => {
+ spyOn(console, 'error')
+
+ await atom.packages.activatePackage('package-with-serialize-error')
+ await atom.packages.activatePackage('package-with-serialization')
+ atom.packages.serialize()
+ expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
+ expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1})
+ expect(console.error).toHaveBeenCalled()
+ })
+ })
+
+ describe('::deactivatePackages()', () => {
+ it('deactivates all packages but does not serialize them', async () => {
+ const pack1 = await atom.packages.activatePackage('package-with-deactivate')
+ const pack2 = await atom.packages.activatePackage('package-with-serialization')
+
+ spyOn(pack1.mainModule, 'deactivate')
+ spyOn(pack2.mainModule, 'serialize')
+ await atom.packages.deactivatePackages()
+ expect(pack1.mainModule.deactivate).toHaveBeenCalled()
+ expect(pack2.mainModule.serialize).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('::deactivatePackage(id)', () => {
+ afterEach(() => atom.packages.unloadPackages())
+
+ it("calls `deactivate` on the package's main module if activate was successful", async () => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+
+ const pack = await atom.packages.activatePackage('package-with-deactivate')
+ expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy()
+ spyOn(pack.mainModule, 'deactivate').andCallThrough()
+
+ await atom.packages.deactivatePackage('package-with-deactivate')
+ expect(pack.mainModule.deactivate).toHaveBeenCalled()
+ expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy()
+
+ spyOn(console, 'warn')
+ const badPack = await atom.packages.activatePackage('package-that-throws-on-activate')
+ expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy()
+ spyOn(badPack.mainModule, 'deactivate').andCallThrough()
+
+ await atom.packages.deactivatePackage('package-that-throws-on-activate')
+ expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()
+ expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy()
+ })
+
+ it("absorbs exceptions that are thrown by the package module's deactivate method", async () => {
+ spyOn(console, 'error')
+ await atom.packages.activatePackage('package-that-throws-on-deactivate')
+ await atom.packages.deactivatePackage('package-that-throws-on-deactivate')
+ expect(console.error).toHaveBeenCalled()
+ })
+
+ it("removes the package's grammars", async () => {
+ await atom.packages.activatePackage('package-with-grammars')
+ await atom.packages.deactivatePackage('package-with-grammars')
+ expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar')
+ expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar')
+ })
+
+ it("removes the package's keymaps", async () => {
+ await atom.packages.activatePackage('package-with-keymaps')
+ await atom.packages.deactivatePackage('package-with-keymaps')
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0)
+ expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0)
+ })
+
+ it("removes the package's stylesheets", async () => {
+ await atom.packages.activatePackage('package-with-styles')
+ await atom.packages.deactivatePackage('package-with-styles')
+
+ const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css')
+ const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less')
+ const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css')
+ expect(atom.themes.stylesheetElementForId(one)).not.toExist()
+ expect(atom.themes.stylesheetElementForId(two)).not.toExist()
+ expect(atom.themes.stylesheetElementForId(three)).not.toExist()
+ })
+
+ it("removes the package's scoped-properties", async () => {
+ await atom.packages.activatePackage('package-with-settings')
+ expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')
+
+ await atom.packages.deactivatePackage('package-with-settings')
+ expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined()
+ })
+
+ it('invokes ::onDidDeactivatePackage listeners with the deactivated package', async () => {
+ await atom.packages.activatePackage('package-with-main')
+
+ let deactivatedPackage
+ atom.packages.onDidDeactivatePackage(pack => {
+ deactivatedPackage = pack
+ })
+
+ await atom.packages.deactivatePackage('package-with-main')
+ expect(deactivatedPackage.name).toBe('package-with-main')
+ })
+ })
+
+ describe('::activate()', () => {
+ beforeEach(() => {
+ spyOn(atom, 'inSpecMode').andReturn(false)
+ jasmine.snapshotDeprecations()
+ spyOn(console, 'warn')
+ atom.packages.loadPackages()
+
+ const loadedPackages = atom.packages.getLoadedPackages()
+ expect(loadedPackages.length).toBeGreaterThan(0)
+ })
+
+ afterEach(async () => {
+ await atom.packages.deactivatePackages()
+ atom.packages.unloadPackages()
+ jasmine.restoreDeprecationsSnapshot()
+ })
+
+ it('sets hasActivatedInitialPackages', async () => {
+ spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null)
+ spyOn(atom.packages, 'activatePackages')
+ expect(atom.packages.hasActivatedInitialPackages()).toBe(false)
+
+ await atom.packages.activate()
+ expect(atom.packages.hasActivatedInitialPackages()).toBe(true)
+ })
+
+ it('activates all the packages, and none of the themes', () => {
+ const packageActivator = spyOn(atom.packages, 'activatePackages')
+ const themeActivator = spyOn(atom.themes, 'activatePackages')
+
+ atom.packages.activate()
+
+ expect(packageActivator).toHaveBeenCalled()
+ expect(themeActivator).toHaveBeenCalled()
+
+ const packages = packageActivator.mostRecentCall.args[0]
+ for (let pack of packages) { expect(['atom', 'textmate']).toContain(pack.getType()) }
+
+ const themes = themeActivator.mostRecentCall.args[0]
+ themes.map((theme) => expect(['theme']).toContain(theme.getType()))
+ })
+
+ it('calls callbacks registered with ::onDidActivateInitialPackages', async () => {
+ const package1 = atom.packages.loadPackage('package-with-main')
+ const package2 = atom.packages.loadPackage('package-with-index')
+ const package3 = atom.packages.loadPackage('package-with-activation-commands')
+ spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3])
+ spyOn(atom.themes, 'activatePackages')
+
+ atom.packages.activate()
+ await new Promise(resolve => atom.packages.onDidActivateInitialPackages(resolve))
+
+ jasmine.unspy(atom.packages, 'getLoadedPackages')
+ expect(atom.packages.getActivePackages().includes(package1)).toBe(true)
+ expect(atom.packages.getActivePackages().includes(package2)).toBe(true)
+ expect(atom.packages.getActivePackages().includes(package3)).toBe(false)
+ })
+ })
+
+ describe('::enablePackage(id) and ::disablePackage(id)', () => {
+ describe('with packages', () => {
+ it('enables a disabled package', async () => {
+ const packageName = 'package-with-main'
+ atom.config.pushAtKeyPath('core.disabledPackages', packageName)
+ atom.packages.observeDisabledPackages()
+ expect(atom.config.get('core.disabledPackages')).toContain(packageName)
+
+ const pack = atom.packages.enablePackage(packageName)
+ await new Promise(resolve => atom.packages.onDidActivatePackage(resolve))
+
+ expect(atom.packages.getLoadedPackages()).toContain(pack)
+ expect(atom.packages.getActivePackages()).toContain(pack)
+ expect(atom.config.get('core.disabledPackages')).not.toContain(packageName)
+ })
+
+ it('disables an enabled package', async () => {
+ const packageName = 'package-with-main'
+ const pack = await atom.packages.activatePackage(packageName)
+
+ atom.packages.observeDisabledPackages()
+ expect(atom.config.get('core.disabledPackages')).not.toContain(packageName)
+ await new Promise(resolve => {
+ atom.packages.onDidDeactivatePackage(resolve)
+ atom.packages.disablePackage(packageName)
+ })
+
+ expect(atom.packages.getActivePackages()).not.toContain(pack)
+ expect(atom.config.get('core.disabledPackages')).toContain(packageName)
+ })
+
+ it('returns null if the package cannot be loaded', () => {
+ spyOn(console, 'warn')
+ expect(atom.packages.enablePackage('this-doesnt-exist')).toBeNull()
+ expect(console.warn.callCount).toBe(1)
+ })
+
+ it('does not disable an already disabled package', () => {
+ const packageName = 'package-with-main'
+ atom.config.pushAtKeyPath('core.disabledPackages', packageName)
+ atom.packages.observeDisabledPackages()
+ expect(atom.config.get('core.disabledPackages')).toContain(packageName)
+
+ atom.packages.disablePackage(packageName)
+ const packagesDisabled = atom.config.get('core.disabledPackages').filter(pack => pack === packageName)
+ expect(packagesDisabled.length).toEqual(1)
+ })
+ })
+
+ describe('with themes', () => {
+ beforeEach(() => atom.themes.activateThemes())
+ afterEach(() => atom.themes.deactivateThemes())
+
+ it('enables and disables a theme', async () => {
+ const packageName = 'theme-with-package-file'
+ expect(atom.config.get('core.themes')).not.toContain(packageName)
+ expect(atom.config.get('core.disabledPackages')).not.toContain(packageName)
+
+ // enabling of theme
+ const pack = atom.packages.enablePackage(packageName)
+ await new Promise(resolve => atom.packages.onDidActivatePackage(resolve))
+ expect(atom.packages.isPackageActive(packageName)).toBe(true)
+ expect(atom.config.get('core.themes')).toContain(packageName)
+ expect(atom.config.get('core.disabledPackages')).not.toContain(packageName)
+
+ await new Promise(resolve => {
+ atom.themes.onDidChangeActiveThemes(resolve)
+ atom.packages.disablePackage(packageName)
+ })
+
+ expect(atom.packages.getActivePackages()).not.toContain(pack)
+ expect(atom.config.get('core.themes')).not.toContain(packageName)
+ expect(atom.config.get('core.themes')).not.toContain(packageName)
+ expect(atom.config.get('core.disabledPackages')).not.toContain(packageName)
+ })
+ })
+ })
+})
diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee
index ff7634734..af34681a6 100644
--- a/spec/pane-element-spec.coffee
+++ b/spec/pane-element-spec.coffee
@@ -113,6 +113,53 @@ describe "PaneElement", ->
expect(paneElement.dataset.activeItemPath).toBeUndefined()
expect(paneElement.dataset.activeItemName).toBeUndefined()
+ describe "when the path of the item changes", ->
+ [item1, item2] = []
+
+ beforeEach ->
+ item1 = document.createElement('div')
+ item1.path = '/foo/bar.txt'
+ item1.changePathCallbacks = []
+ item1.setPath = (path) ->
+ @path = path
+ callback() for callback in @changePathCallbacks
+ return
+ item1.getPath = -> @path
+ item1.onDidChangePath = (callback) ->
+ @changePathCallbacks.push callback
+ return dispose: =>
+ @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback
+
+ item2 = document.createElement('div')
+
+ pane.addItem(item1)
+ pane.addItem(item2)
+
+ it "changes the file path and file name data attributes on the pane if the active item path is changed", ->
+
+ expect(paneElement.dataset.activeItemPath).toBe '/foo/bar.txt'
+ expect(paneElement.dataset.activeItemName).toBe 'bar.txt'
+
+ item1.setPath "/foo/bar1.txt"
+
+ expect(paneElement.dataset.activeItemPath).toBe '/foo/bar1.txt'
+ expect(paneElement.dataset.activeItemName).toBe 'bar1.txt'
+
+ pane.activateItem(item2)
+
+ expect(paneElement.dataset.activeItemPath).toBeUndefined()
+ expect(paneElement.dataset.activeItemName).toBeUndefined()
+
+ item1.setPath "/foo/bar2.txt"
+
+ expect(paneElement.dataset.activeItemPath).toBeUndefined()
+ expect(paneElement.dataset.activeItemName).toBeUndefined()
+
+ pane.activateItem(item1)
+
+ expect(paneElement.dataset.activeItemPath).toBe '/foo/bar2.txt'
+ expect(paneElement.dataset.activeItemName).toBe 'bar2.txt'
+
describe "when an item is removed from the pane", ->
describe "when the destroyed item is an element", ->
it "removes the item from the itemViews div", ->
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index 3fd40cdad..fa72e42ef 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -286,6 +286,31 @@ describe('TextEditorComponent', () => {
expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull()
})
+ it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => {
+ const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50)
+ const {component, element, editor} = buildComponent({text, height: 1000, width: 500})
+
+ element.addEventListener('scroll', (event) => {
+ event.stopPropagation()
+ }, true)
+
+ editor.setSoftWrapped(true)
+ jasmine.attachToDOM(element)
+ await component.getNextUpdatePromise()
+
+ const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length
+
+ setScrollTop(component, 620)
+ await component.getNextUpdatePromise()
+
+ editor.foldBufferRow(28)
+ await component.getNextUpdatePromise()
+
+ const firstLineElement = element.querySelector('.line')
+ expect(firstLineElement.dataset.screenRow).toBe('0')
+ expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar)
+ })
+
it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => {
const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true})
await setEditorWidthInCharacters(component, 5)
@@ -3343,9 +3368,9 @@ describe('TextEditorComponent', () => {
await component.getNextUpdatePromise()
expect(editor.isFoldedAtScreenRow(5)).toBe(true)
- target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right')
- component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)})
- expect(editor.isFoldedAtScreenRow(5)).toBe(false)
+ target = element.querySelectorAll('.line-number')[4].querySelector('.icon-right')
+ component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 4)})
+ expect(editor.isFoldedAtScreenRow(4)).toBe(false)
})
it('autoscrolls when dragging near the top or bottom of the gutter', async () => {
diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee
index cb70d030c..53011fdcc 100644
--- a/spec/text-editor-spec.coffee
+++ b/spec/text-editor-spec.coffee
@@ -1168,6 +1168,58 @@ describe "TextEditor", ->
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])
@@ -5272,37 +5324,6 @@ describe "TextEditor", ->
[[6, 3], [6, 4]],
])
- describe ".shouldPromptToSave()", ->
- it "returns true when buffer changed", ->
- jasmine.unspy(editor, 'shouldPromptToSave')
- expect(editor.shouldPromptToSave()).toBeFalsy()
- buffer.setText('changed')
- expect(editor.shouldPromptToSave()).toBeTruthy()
-
- it "returns false when an edit session's buffer is in use by more than one session", ->
- jasmine.unspy(editor, 'shouldPromptToSave')
- buffer.setText('changed')
-
- editor2 = null
- waitsForPromise ->
- atom.workspace.getActivePane().splitRight()
- atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o
-
- runs ->
- expect(editor.shouldPromptToSave()).toBeFalsy()
- editor2.destroy()
- expect(editor.shouldPromptToSave()).toBeTruthy()
-
- it "returns false when close of a window requested and edit session opened inside project", ->
- jasmine.unspy(editor, 'shouldPromptToSave')
- buffer.setText('changed')
- expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: true)).toBeFalsy()
-
- it "returns true when close of a window requested and edit session opened without project", ->
- jasmine.unspy(editor, 'shouldPromptToSave')
- buffer.setText('changed')
- expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: false)).toBeTruthy()
-
describe "when the editor contains surrogate pair characters", ->
it "correctly backspaces over them", ->
editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97')
diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js
new file mode 100644
index 000000000..c81df8089
--- /dev/null
+++ b/spec/text-editor-spec.js
@@ -0,0 +1,249 @@
+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')
+
+describe('TextEditor', () => {
+ let editor
+
+ afterEach(() => {
+ editor.destroy()
+ })
+
+ describe('.shouldPromptToSave()', () => {
+ beforeEach(async () => {
+ editor = await atom.workspace.open('sample.js')
+ jasmine.unspy(editor, 'shouldPromptToSave')
+ })
+
+ it('returns true when buffer has unsaved changes', () => {
+ expect(editor.shouldPromptToSave()).toBeFalsy()
+ editor.setText('changed')
+ expect(editor.shouldPromptToSave()).toBeTruthy()
+ })
+
+ it("returns false when an editor's buffer is in use by more than one buffer", async () => {
+ editor.setText('changed')
+
+ atom.workspace.getActivePane().splitRight()
+ const editor2 = await atom.workspace.open('sample.js', {autoIndent: false})
+ expect(editor.shouldPromptToSave()).toBeFalsy()
+
+ editor2.destroy()
+ expect(editor.shouldPromptToSave()).toBeTruthy()
+ })
+
+ it('returns true when the window is closing if the file has changed on disk', async () => {
+ jasmine.useRealClock()
+
+ editor.setText('initial stuff')
+ await editor.saveAs(temp.openSync('test-file').path)
+
+ editor.setText('other stuff')
+ fs.writeFileSync(editor.getPath(), 'new stuff')
+ expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy()
+
+ await new Promise(resolve => editor.onDidConflict(resolve))
+ expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeTruthy()
+ })
+
+ it('returns false when the window is closing and the project has one or more directory paths', () => {
+ editor.setText('changed')
+ expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy()
+ })
+
+ it('returns false when the window is closing and the project has no directory paths', () => {
+ editor.setText('changed')
+ expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: false})).toBeTruthy()
+ })
+ })
+
+ describe('folding', () => {
+ beforeEach(async () => {
+ await atom.packages.activatePackage('language-javascript')
+ })
+
+ it('maintains cursor buffer position when a folding/unfolding', async () => {
+ editor = await atom.workspace.open('sample.js', {autoIndent: false})
+ editor.setCursorBufferPosition([5, 5])
+ editor.foldAll()
+ expect(editor.getCursorBufferPosition()).toEqual([5, 5])
+ })
+
+ describe('.unfoldAll()', () => {
+ it('unfolds every folded line', async () => {
+ editor = await atom.workspace.open('sample.js', {autoIndent: false})
+
+ const initialScreenLineCount = editor.getScreenLineCount()
+ editor.foldBufferRow(0)
+ editor.foldBufferRow(1)
+ expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount)
+ editor.unfoldAll()
+ expect(editor.getScreenLineCount()).toBe(initialScreenLineCount)
+ })
+
+ it('unfolds every folded line with comments', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
+
+ const initialScreenLineCount = editor.getScreenLineCount()
+ editor.foldBufferRow(0)
+ editor.foldBufferRow(5)
+ expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount)
+ editor.unfoldAll()
+ expect(editor.getScreenLineCount()).toBe(initialScreenLineCount)
+ })
+ })
+
+ describe('.foldAll()', () => {
+ it('folds every foldable line', async () => {
+ editor = await atom.workspace.open('sample.js', {autoIndent: false})
+
+ editor.foldAll()
+ const [fold1, fold2, fold3] = editor.unfoldAll()
+ expect([fold1.start.row, fold1.end.row]).toEqual([0, 12])
+ expect([fold2.start.row, fold2.end.row]).toEqual([1, 9])
+ expect([fold3.start.row, fold3.end.row]).toEqual([4, 7])
+ })
+
+ it('works with multi-line comments', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
+
+ editor.foldAll()
+ const folds = editor.unfoldAll()
+ expect(folds.length).toBe(8)
+ expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30])
+ expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4])
+ expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27])
+ expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8])
+ expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16])
+ expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20])
+ expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22])
+ expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25])
+ })
+ })
+
+ describe('.foldBufferRow(bufferRow)', () => {
+ beforeEach(async () => {
+ editor = await atom.workspace.open('sample.js')
+ })
+
+ describe('when bufferRow can be folded', () => {
+ it('creates a fold based on the syntactic region starting at the given row', () => {
+ editor.foldBufferRow(1)
+ const [fold] = editor.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual([1, 9])
+ })
+ })
+
+ describe("when bufferRow can't be folded", () => {
+ it('searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)', () => {
+ editor.foldBufferRow(8)
+ const [fold] = editor.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual([1, 9])
+ })
+ })
+
+ describe('when the bufferRow is already folded', () => {
+ it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => {
+ editor.foldBufferRow(2)
+ expect(editor.isFoldedAtBufferRow(0)).toBe(false)
+ expect(editor.isFoldedAtBufferRow(1)).toBe(true)
+
+ editor.foldBufferRow(1)
+ expect(editor.isFoldedAtBufferRow(0)).toBe(true)
+ })
+ })
+
+ describe('when the bufferRow is in a multi-line comment', () => {
+ it('searches upward and downward for surrounding comment lines and folds them as a single fold', () => {
+ editor.buffer.insert([1, 0], ' //this is a comment\n // and\n //more docs\n\n//second comment')
+ editor.foldBufferRow(1)
+ const [fold] = editor.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual([1, 3])
+ })
+ })
+
+ describe('when the bufferRow is a single-line comment', () => {
+ it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => {
+ editor.buffer.insert([1, 0], ' //this is a single line comment\n')
+ editor.foldBufferRow(1)
+ const [fold] = editor.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual([0, 13])
+ })
+ })
+ })
+
+ describe('.foldAllAtIndentLevel(indentLevel)', () => {
+ it('folds blocks of text at the given indentation level', async () => {
+ editor = await atom.workspace.open('sample.js', {autoIndent: false})
+
+ editor.foldAllAtIndentLevel(0)
+ expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`)
+ expect(editor.getLastScreenRow()).toBe(0)
+
+ editor.foldAllAtIndentLevel(1)
+ expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
+ expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`)
+ expect(editor.getLastScreenRow()).toBe(4)
+
+ editor.foldAllAtIndentLevel(2)
+ expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
+ expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {')
+ expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;')
+ expect(editor.getLastScreenRow()).toBe(9)
+ })
+
+ it('folds every foldable range at a given indentLevel', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
+
+ editor.foldAllAtIndentLevel(2)
+ const folds = editor.unfoldAll()
+ expect(folds.length).toBe(5)
+ expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8])
+ expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16])
+ expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20])
+ expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22])
+ expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25])
+ })
+
+ it('does not fold anything but the indentLevel', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
+
+ editor.foldAllAtIndentLevel(0)
+ const folds = editor.unfoldAll()
+ expect(folds.length).toBe(1)
+ expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30])
+ })
+ })
+
+ describe('.isFoldableAtBufferRow(bufferRow)', () => {
+ it('returns true if the line starts a multi-line comment', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js')
+
+ expect(editor.isFoldableAtBufferRow(1)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(6)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(8)).toBe(false)
+ expect(editor.isFoldableAtBufferRow(11)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(15)).toBe(false)
+ expect(editor.isFoldableAtBufferRow(17)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(21)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(24)).toBe(true)
+ expect(editor.isFoldableAtBufferRow(28)).toBe(false)
+ })
+
+ it('returns true for lines that end with a comment and are followed by an indented line', async () => {
+ editor = await atom.workspace.open('sample-with-comments.js')
+
+ expect(editor.isFoldableAtBufferRow(5)).toBe(true)
+ })
+
+ it("does not return true for a line in the middle of a comment that's followed by an indented line", async () => {
+ editor = await atom.workspace.open('sample-with-comments.js')
+
+ expect(editor.isFoldableAtBufferRow(7)).toBe(false)
+ editor.buffer.insert([8, 0], ' ')
+ expect(editor.isFoldableAtBufferRow(7)).toBe(false)
+ })
+ })
+ })
+})
diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee
deleted file mode 100644
index 2c2379810..000000000
--- a/spec/tokenized-buffer-spec.coffee
+++ /dev/null
@@ -1,688 +0,0 @@
-NullGrammar = require '../src/null-grammar'
-TokenizedBuffer = require '../src/tokenized-buffer'
-{Point} = TextBuffer = require 'text-buffer'
-_ = require 'underscore-plus'
-
-describe "TokenizedBuffer", ->
- [tokenizedBuffer, buffer] = []
-
- beforeEach ->
- # enable async tokenization
- TokenizedBuffer.prototype.chunkSize = 5
- jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- tokenizedBuffer?.destroy()
-
- startTokenizing = (tokenizedBuffer) ->
- tokenizedBuffer.setVisible(true)
-
- fullyTokenize = (tokenizedBuffer) ->
- tokenizedBuffer.setVisible(true)
- advanceClock() while tokenizedBuffer.firstInvalidRow()?
-
- describe "serialization", ->
- describe "when the underlying buffer has a path", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- it "deserializes it searching among the buffers in the current project", ->
- tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
- tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
- expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
-
- describe "when the underlying buffer has no path", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync(null)
-
- it "deserializes it searching among the buffers in the current project", ->
- tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
- tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
- expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
-
- describe "when the buffer is destroyed", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
-
- it "stops tokenization", ->
- tokenizedBuffer.destroy()
- spyOn(tokenizedBuffer, 'tokenizeNextChunk')
- advanceClock()
- expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
-
- describe "when the buffer contains soft-tabs", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
-
- afterEach ->
- tokenizedBuffer.destroy()
- buffer.release()
-
- describe "on construction", ->
- it "tokenizes lines chunk at a time in the background", ->
- line0 = tokenizedBuffer.tokenizedLines[0]
- expect(line0).toBeUndefined()
-
- line11 = tokenizedBuffer.tokenizedLines[11]
- expect(line11).toBeUndefined()
-
- # tokenize chunk 1
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
-
- # tokenize chunk 2
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[9].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
-
- # tokenize last chunk
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[10].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[12].ruleStack?).toBeTruthy()
-
- describe "when the buffer is partially tokenized", ->
- beforeEach ->
- # tokenize chunk 1 only
- advanceClock()
-
- describe "when there is a buffer change inside the tokenized region", ->
- describe "when lines are added", ->
- it "pushes the invalid rows down", ->
- expect(tokenizedBuffer.firstInvalidRow()).toBe 5
- buffer.insert([1, 0], '\n\n')
- expect(tokenizedBuffer.firstInvalidRow()).toBe 7
-
- describe "when lines are removed", ->
- it "pulls the invalid rows up", ->
- expect(tokenizedBuffer.firstInvalidRow()).toBe 5
- buffer.delete([[1, 0], [3, 0]])
- expect(tokenizedBuffer.firstInvalidRow()).toBe 2
-
- describe "when the change invalidates all the lines before the current invalid region", ->
- it "retokenizes the invalidated lines and continues into the valid region", ->
- expect(tokenizedBuffer.firstInvalidRow()).toBe 5
- buffer.insert([2, 0], '/*')
- expect(tokenizedBuffer.firstInvalidRow()).toBe 3
- advanceClock()
- expect(tokenizedBuffer.firstInvalidRow()).toBe 8
-
- describe "when there is a buffer change surrounding an invalid row", ->
- it "pushes the invalid row to the end of the change", ->
- buffer.setTextInRange([[4, 0], [6, 0]], "\n\n\n")
- expect(tokenizedBuffer.firstInvalidRow()).toBe 8
-
- describe "when there is a buffer change inside an invalid region", ->
- it "does not attempt to tokenize the lines in the change, and preserves the existing invalid row", ->
- expect(tokenizedBuffer.firstInvalidRow()).toBe 5
- buffer.setTextInRange([[6, 0], [7, 0]], "\n\n\n")
- expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
- expect(tokenizedBuffer.firstInvalidRow()).toBe 5
-
- describe "when the buffer is fully tokenized", ->
- beforeEach ->
- fullyTokenize(tokenizedBuffer)
-
- describe "when there is a buffer change that is smaller than the chunk size", ->
- describe "when lines are updated, but none are added or removed", ->
- it "updates tokens to reflect the change", ->
- buffer.setTextInRange([[0, 0], [2, 0]], "foo()\n7\n")
-
- expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js'])
- expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js'])
- # line 2 is unchanged
- expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
-
- describe "when the change invalidates the tokenization of subsequent lines", ->
- it "schedules the invalidated lines to be tokenized in the background", ->
- buffer.insert([5, 30], '/* */')
- buffer.insert([2, 0], '/*')
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js']
-
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
- it "resumes highlighting with the state of the previous line", ->
- buffer.insert([0, 0], '/*')
- buffer.insert([5, 0], '*/')
-
- buffer.insert([1, 0], 'var ')
- expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
- describe "when lines are both updated and removed", ->
- it "updates tokens to reflect the change", ->
- buffer.setTextInRange([[1, 0], [3, 0]], "foo()")
-
- # previous line 0 remains
- expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.type.var.js'])
-
- # previous line 3 should be combined with input to form line 1
- expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
- expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
-
- # lines below deleted regions should be shifted upward
- expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
- expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
- expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
-
- describe "when the change invalidates the tokenization of subsequent lines", ->
- it "schedules the invalidated lines to be tokenized in the background", ->
- buffer.insert([5, 30], '/* */')
- buffer.setTextInRange([[2, 0], [3, 0]], '/*')
- expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js']
-
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
-
- describe "when lines are both updated and inserted", ->
- it "updates tokens to reflect the change", ->
- buffer.setTextInRange([[1, 0], [2, 0]], "foo()\nbar()\nbaz()\nquux()")
-
- # previous line 0 remains
- expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.type.var.js'])
-
- # 3 new lines inserted
- expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
- expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual(value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual(value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
-
- # previous line 2 is joined with quux() on line 4
- expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual(value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
- expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
-
- # previous line 3 is pushed down to become line 5
- expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
-
- describe "when the change invalidates the tokenization of subsequent lines", ->
- it "schedules the invalidated lines to be tokenized in the background", ->
- buffer.insert([5, 30], '/* */')
- buffer.insert([2, 0], '/*\nabcde\nabcder')
- expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
- expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js']
-
- advanceClock() # tokenize invalidated lines in background
- expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
- expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe ['source.js', 'comment.block.js']
-
- describe "when there is an insertion that is larger than the chunk size", ->
- it "tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background", ->
- commentBlock = _.multiplyString("// a comment\n", tokenizedBuffer.chunkSize + 2)
- buffer.insert([0, 0], commentBlock)
- expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
-
- advanceClock()
- expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLines[6].ruleStack?).toBeTruthy()
-
- it "does not break out soft tabs across a scope boundary", ->
- waitsForPromise ->
- atom.packages.activatePackage('language-gfm')
-
- runs ->
- tokenizedBuffer.setTabLength(4)
- tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
- buffer.setText(' 0
-
- expect(length).toBe 4
-
- describe "when the buffer contains hard-tabs", ->
- beforeEach ->
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- runs ->
- buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
- startTokenizing(tokenizedBuffer)
-
- afterEach ->
- tokenizedBuffer.destroy()
- buffer.release()
-
- describe "when the buffer is fully tokenized", ->
- beforeEach ->
- fullyTokenize(tokenizedBuffer)
-
- describe "when the grammar is tokenized", ->
- it "emits the `tokenized` event", ->
- editor = null
- tokenizedHandler = jasmine.createSpy("tokenized handler")
-
- waitsForPromise ->
- atom.workspace.open('sample.js').then (o) -> editor = o
-
- runs ->
- tokenizedBuffer = editor.tokenizedBuffer
- tokenizedBuffer.onDidTokenize tokenizedHandler
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedHandler.callCount).toBe(1)
-
- it "doesn't re-emit the `tokenized` event when it is re-tokenized", ->
- editor = null
- tokenizedHandler = jasmine.createSpy("tokenized handler")
-
- waitsForPromise ->
- atom.workspace.open('sample.js').then (o) -> editor = o
-
- runs ->
- tokenizedBuffer = editor.tokenizedBuffer
- fullyTokenize(tokenizedBuffer)
-
- tokenizedBuffer.onDidTokenize tokenizedHandler
- editor.getBuffer().insert([0, 0], "'")
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedHandler).not.toHaveBeenCalled()
-
- describe "when the grammar is updated because a grammar it includes is activated", ->
- it "re-emits the `tokenized` event", ->
- editor = null
- tokenizedBuffer = null
- tokenizedHandler = jasmine.createSpy("tokenized handler")
-
- waitsForPromise ->
- atom.workspace.open('coffee.coffee').then (o) -> editor = o
-
- runs ->
- tokenizedBuffer = editor.tokenizedBuffer
- tokenizedBuffer.onDidTokenize tokenizedHandler
- fullyTokenize(tokenizedBuffer)
- tokenizedHandler.reset()
-
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- runs ->
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedHandler.callCount).toBe(1)
-
- it "retokenizes the buffer", ->
- waitsForPromise ->
- atom.packages.activatePackage('language-ruby-on-rails')
-
- waitsForPromise ->
- atom.packages.activatePackage('language-ruby')
-
- runs ->
- buffer = atom.project.bufferForPathSync()
- buffer.setText "
<%= User.find(2).full_name %>
"
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- {tokens} = tokenizedBuffer.tokenizedLines[0]
- expect(tokens[0]).toEqual value: "", scopes: ["text.html.ruby"]
-
- waitsForPromise ->
- atom.packages.activatePackage('language-html')
-
- runs ->
- fullyTokenize(tokenizedBuffer)
- {tokens} = tokenizedBuffer.tokenizedLines[0]
- expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.div.html", "punctuation.definition.tag.begin.html"]
-
- describe ".tokenForPosition(position)", ->
- afterEach ->
- tokenizedBuffer.destroy()
- buffer.release()
-
- it "returns the correct token (regression)", ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"]
- expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"]
- expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.type.var.js"]
-
- describe ".bufferRangeForScopeAtPosition(selector, position)", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- describe "when the selector does not match the token at the position", ->
- it "returns a falsy value", ->
- expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined()
-
- describe "when the selector matches a single token at the position", ->
- it "returns the range covered by the token", ->
- expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual [[0, 0], [0, 3]]
- expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]]
-
- describe "when the selector matches a run of multiple tokens at the position", ->
- it "returns the range covered by all contiguous tokens (within a single line)", ->
- expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]]
-
- describe ".indentLevelForRow(row)", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- describe "when the line is non-empty", ->
- it "has an indent level based on the leading whitespace on the line", ->
- expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0
- expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1
- expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
- buffer.insert([2, 0], ' ')
- expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5
-
- describe "when the line is empty", ->
- it "assumes the indentation level of the first non-empty line below or above if one exists", ->
- buffer.insert([12, 0], ' ')
- buffer.insert([12, Infinity], '\n\n')
- expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2
-
- buffer.insert([1, Infinity], '\n\n')
- expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2
-
- buffer.setText('\n\n\n')
- expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0
-
- describe "when the changed lines are surrounded by whitespace-only lines", ->
- it "updates the indentLevel of empty lines that precede the change", ->
- expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0
-
- buffer.insert([12, 0], '\n')
- buffer.insert([13, 0], ' ')
- expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1
-
- it "updates empty line indent guides when the empty line is the last line", ->
- buffer.insert([12, 2], '\n')
-
- # The newline and the tab need to be in two different operations to surface the bug
- buffer.insert([12, 0], ' ')
- expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1
-
- buffer.insert([12, 0], ' ')
- expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
- expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined()
-
- it "updates the indentLevel of empty lines surrounding a change that inserts lines", ->
- buffer.insert([7, 0], '\n\n')
- buffer.insert([5, 0], '\n\n')
- expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3
- expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3
- expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3
- expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3
- expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2
-
- buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four')
- expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4
- expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4
- expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4
- expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4
- expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
-
- it "updates the indentLevel of empty lines surrounding a change that removes lines", ->
- buffer.insert([7, 0], '\n\n')
- buffer.insert([5, 0], '\n\n')
- buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
- expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text
- expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2
- expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # }
-
- describe "::isFoldableAtRow(row)", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- buffer.insert [10, 0], " // multi-line\n // comment\n // block\n"
- buffer.insert [0, 0], "// multi-line\n// comment\n// block\n"
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- it "includes the first line of multi-line comments", ->
- expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
- expect(tokenizedBuffer.isFoldableAtRow(13)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(14)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(15)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(16)).toBe false
-
- buffer.insert([0, Infinity], '\n')
-
- expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false
-
- buffer.undo()
-
- expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
-
- it "includes non-comment lines that precede an increase in indentation", ->
- buffer.insert([2, 0], ' ') # commented lines preceding an indent aren't foldable
-
- expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(4)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(5)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
-
- buffer.insert([7, 0], ' ')
-
- expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
-
- buffer.undo()
-
- expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
-
- buffer.insert([7, 0], " \n x\n")
-
- expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
-
- buffer.insert([9, 0], " ")
-
- expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
- expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
- expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
-
- describe "::tokenizedLineForRow(row)", ->
- it "returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", ->
- buffer = atom.project.bufferForPathSync('sample.js')
- grammar = atom.grammars.grammarForScopeName('source.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
- line0 = buffer.lineForRow(0)
-
- jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
- jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
- startTokenizing(tokenizedBuffer)
- expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
- advanceClock(1)
- expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
-
- nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
- nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
- tokenizedBuffer.setGrammar(NullGrammar)
- startTokenizing(tokenizedBuffer)
- expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
- advanceClock(1)
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
- expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
-
- it "returns undefined if the requested row is outside the buffer range", ->
- buffer = atom.project.bufferForPathSync('sample.js')
- grammar = atom.grammars.grammarForScopeName('source.js')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
-
- describe "when the buffer is configured with the null grammar", ->
- it "does not actually tokenize using the grammar", ->
- spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
- buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
- buffer.setText('a\nb\nc')
- tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
- tokenizeCallback = jasmine.createSpy('onDidTokenize')
- tokenizedBuffer.onDidTokenize(tokenizeCallback)
-
- expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
- expect(tokenizeCallback.callCount).toBe(0)
- expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
-
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
- expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
- expect(tokenizeCallback.callCount).toBe(0)
- expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
-
- describe "text decoration layer API", ->
- describe "iterator", ->
- it "iterates over the syntactic scope boundaries", ->
- buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n")
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.js"), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- iterator = tokenizedBuffer.buildIterator()
- iterator.seek(Point(0, 0))
-
- expectedBoundaries = [
- {position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]}
- {position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
- {position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
- {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
- {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
- {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
- {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]}
- {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []}
- {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]}
- {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
- {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
- {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
- {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
- {position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
- {position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
- ]
-
- loop
- boundary = {
- position: iterator.getPosition(),
- closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)),
- openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))
- }
-
- expect(boundary).toEqual(expectedBoundaries.shift())
- break unless iterator.moveToSuccessor()
-
- expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
- "syntax--source syntax--js",
- "syntax--storage syntax--type syntax--var syntax--js"
- ])
- expect(iterator.getPosition()).toEqual(Point(0, 3))
- expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
- "syntax--source syntax--js"
- ])
- expect(iterator.getPosition()).toEqual(Point(0, 8))
- expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
- "syntax--source syntax--js",
- "syntax--comment syntax--block syntax--js"
- ])
- expect(iterator.getPosition()).toEqual(Point(1, 0))
- expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
- "syntax--source syntax--js",
- "syntax--constant syntax--numeric syntax--decimal syntax--js"
- ])
- expect(iterator.getPosition()).toEqual(Point(1, 18))
-
- expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
- "syntax--source syntax--js"
- ])
- iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
-
- it "does not report columns beyond the length of the line", ->
- waitsForPromise ->
- atom.packages.activatePackage('language-coffee-script')
-
- runs ->
- buffer = new TextBuffer(text: "# hello\n# world")
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.coffee"), tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- iterator = tokenizedBuffer.buildIterator()
- iterator.seek(Point(0, 0))
- iterator.moveToSuccessor()
- iterator.moveToSuccessor()
- expect(iterator.getPosition().column).toBe(7)
-
- iterator.moveToSuccessor()
- expect(iterator.getPosition().column).toBe(0)
-
- iterator.seek(Point(0, 7))
- expect(iterator.getPosition().column).toBe(7)
-
- iterator.seek(Point(0, 8))
- expect(iterator.getPosition().column).toBe(7)
-
- it "correctly terminates scopes at the beginning of the line (regression)", ->
- grammar = atom.grammars.createGrammar('test', {
- 'scopeName': 'text.broken'
- 'name': 'Broken grammar'
- 'patterns': [
- {'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
- {'match': '.', 'name': 'yellow.broken'}
- ]
- })
-
- buffer = new TextBuffer(text: 'start x\nend x\nx')
- tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
- fullyTokenize(tokenizedBuffer)
-
- iterator = tokenizedBuffer.buildIterator()
- iterator.seek(Point(1, 0))
-
- expect(iterator.getPosition()).toEqual([1, 0])
- expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken']
- expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken']
diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js
new file mode 100644
index 000000000..ba43f9ff3
--- /dev/null
+++ b/spec/tokenized-buffer-spec.js
@@ -0,0 +1,1084 @@
+const NullGrammar = require('../src/null-grammar')
+const TokenizedBuffer = require('../src/tokenized-buffer')
+const TextBuffer = require('text-buffer')
+const {Point, Range} = TextBuffer
+const _ = require('underscore-plus')
+const dedent = require('dedent')
+const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
+const {ScopedSettingsDelegate} = require('../src/text-editor-registry')
+
+describe('TokenizedBuffer', () => {
+ let tokenizedBuffer, buffer
+
+ beforeEach(async () => {
+ // enable async tokenization
+ TokenizedBuffer.prototype.chunkSize = 5
+ jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
+ await atom.packages.activatePackage('language-javascript')
+ })
+
+ afterEach(() => {
+ buffer && buffer.destroy()
+ tokenizedBuffer && tokenizedBuffer.destroy()
+ })
+
+ function startTokenizing (tokenizedBuffer) {
+ tokenizedBuffer.setVisible(true)
+ }
+
+ function fullyTokenize (tokenizedBuffer) {
+ tokenizedBuffer.setVisible(true)
+ while (tokenizedBuffer.firstInvalidRow() != null) {
+ advanceClock()
+ }
+ }
+
+ describe('serialization', () => {
+ describe('when the underlying buffer has a path', () => {
+ beforeEach(async () => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ await atom.packages.activatePackage('language-coffee-script')
+ })
+
+ it('deserializes it searching among the buffers in the current project', () => {
+ const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
+ const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
+ expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
+ })
+ })
+
+ describe('when the underlying buffer has no path', () => {
+ beforeEach(() => buffer = atom.project.bufferForPathSync(null))
+
+ it('deserializes it searching among the buffers in the current project', () => {
+ const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
+ const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
+ expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
+ })
+ })
+ })
+
+ describe('tokenizing', () => {
+ describe('when the buffer is destroyed', () => {
+ beforeEach(() => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ startTokenizing(tokenizedBuffer)
+ })
+
+ it('stops tokenization', () => {
+ tokenizedBuffer.destroy()
+ spyOn(tokenizedBuffer, 'tokenizeNextChunk')
+ advanceClock()
+ expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('when the buffer contains soft-tabs', () => {
+ beforeEach(() => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ startTokenizing(tokenizedBuffer)
+ })
+
+ afterEach(() => {
+ tokenizedBuffer.destroy()
+ buffer.release()
+ })
+
+ describe('on construction', () =>
+ it('tokenizes lines chunk at a time in the background', () => {
+ const line0 = tokenizedBuffer.tokenizedLines[0]
+ expect(line0).toBeUndefined()
+
+ const line11 = tokenizedBuffer.tokenizedLines[11]
+ expect(line11).toBeUndefined()
+
+ // tokenize chunk 1
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
+
+ // tokenize chunk 2
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
+
+ // tokenize last chunk
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy()
+ })
+ )
+
+ describe('when the buffer is partially tokenized', () => {
+ beforeEach(() => {
+ // tokenize chunk 1 only
+ advanceClock()
+ })
+
+ describe('when there is a buffer change inside the tokenized region', () => {
+ describe('when lines are added', () => {
+ it('pushes the invalid rows down', () => {
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
+ buffer.insert([1, 0], '\n\n')
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(7)
+ })
+ })
+
+ describe('when lines are removed', () => {
+ it('pulls the invalid rows up', () => {
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
+ buffer.delete([[1, 0], [3, 0]])
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(2)
+ })
+ })
+
+ describe('when the change invalidates all the lines before the current invalid region', () => {
+ it('retokenizes the invalidated lines and continues into the valid region', () => {
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
+ buffer.insert([2, 0], '/*')
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(3)
+ advanceClock()
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
+ })
+ })
+ })
+
+ describe('when there is a buffer change surrounding an invalid row', () => {
+ it('pushes the invalid row to the end of the change', () => {
+ buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n')
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
+ })
+ })
+
+ describe('when there is a buffer change inside an invalid region', () => {
+ it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => {
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
+ buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n')
+ expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
+ expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
+ })
+ })
+ })
+
+ describe('when the buffer is fully tokenized', () => {
+ beforeEach(() => fullyTokenize(tokenizedBuffer))
+
+ describe('when there is a buffer change that is smaller than the chunk size', () => {
+ describe('when lines are updated, but none are added or removed', () => {
+ it('updates tokens to reflect the change', () => {
+ buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n')
+
+ expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']})
+ expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']})
+ // line 2 is unchanged
+ expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
+ })
+
+ describe('when the change invalidates the tokenization of subsequent lines', () => {
+ it('schedules the invalidated lines to be tokenized in the background', () => {
+ buffer.insert([5, 30], '/* */')
+ buffer.insert([2, 0], '/*')
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
+
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ })
+ })
+
+ it('resumes highlighting with the state of the previous line', () => {
+ buffer.insert([0, 0], '/*')
+ buffer.insert([5, 0], '*/')
+
+ buffer.insert([1, 0], 'var ')
+ expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ })
+ })
+
+ describe('when lines are both updated and removed', () => {
+ it('updates tokens to reflect the change', () => {
+ buffer.setTextInRange([[1, 0], [3, 0]], 'foo()')
+
+ // previous line 0 remains
+ expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']})
+
+ // previous line 3 should be combined with input to form line 1
+ expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
+ expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
+
+ // lines below deleted regions should be shifted upward
+ expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']})
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']})
+ })
+ })
+
+ describe('when the change invalidates the tokenization of subsequent lines', () => {
+ it('schedules the invalidated lines to be tokenized in the background', () => {
+ buffer.insert([5, 30], '/* */')
+ buffer.setTextInRange([[2, 0], [3, 0]], '/*')
+ expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
+
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ })
+ })
+
+ describe('when lines are both updated and inserted', () => {
+ it('updates tokens to reflect the change', () => {
+ buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()')
+
+ // previous line 0 remains
+ expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']})
+
+ // 3 new lines inserted
+ expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
+ expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
+
+ // previous line 2 is joined with quux() on line 4
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
+
+ // previous line 3 is pushed down to become line 5
+ expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
+ })
+ })
+
+ describe('when the change invalidates the tokenization of subsequent lines', () => {
+ it('schedules the invalidated lines to be tokenized in the background', () => {
+ buffer.insert([5, 30], '/* */')
+ buffer.insert([2, 0], '/*\nabcde\nabcder')
+ expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
+ expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js'])
+
+ advanceClock() // tokenize invalidated lines in background
+ expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
+ expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js'])
+ })
+ })
+ })
+
+ describe('when there is an insertion that is larger than the chunk size', () =>
+ it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
+ const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2)
+ buffer.insert([0, 0], commentBlock)
+ expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
+
+ advanceClock()
+ expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
+ expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy()
+ })
+ )
+
+ it('does not break out soft tabs across a scope boundary', async () => {
+ await atom.packages.activatePackage('language-gfm')
+
+ tokenizedBuffer.setTabLength(4)
+ tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
+ buffer.setText(' 0) length += tag
+ }
+
+ expect(length).toBe(4)
+ })
+ })
+ })
+
+ describe('when the buffer contains hard-tabs', () => {
+ beforeEach(async () => {
+ atom.packages.activatePackage('language-coffee-script')
+
+ buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
+ startTokenizing(tokenizedBuffer)
+ })
+
+ afterEach(() => {
+ tokenizedBuffer.destroy()
+ buffer.release()
+ })
+
+ describe('when the buffer is fully tokenized', () => {
+ beforeEach(() => fullyTokenize(tokenizedBuffer))
+ })
+ })
+
+ describe('when tokenization completes', () => {
+ it('emits the `tokenized` event', async () => {
+ const editor = await atom.workspace.open('sample.js')
+
+ const tokenizedHandler = jasmine.createSpy('tokenized handler')
+ editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
+ fullyTokenize(editor.tokenizedBuffer)
+ expect(tokenizedHandler.callCount).toBe(1)
+ })
+
+ it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => {
+ const editor = await atom.workspace.open('sample.js')
+ fullyTokenize(editor.tokenizedBuffer)
+
+ const tokenizedHandler = jasmine.createSpy('tokenized handler')
+ editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
+ editor.getBuffer().insert([0, 0], "'")
+ fullyTokenize(editor.tokenizedBuffer)
+ expect(tokenizedHandler).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('when the grammar is updated because a grammar it includes is activated', async () => {
+ it('re-emits the `tokenized` event', async () => {
+ const editor = await atom.workspace.open('coffee.coffee')
+
+ const tokenizedHandler = jasmine.createSpy('tokenized handler')
+ editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
+ fullyTokenize(editor.tokenizedBuffer)
+ tokenizedHandler.reset()
+
+ await atom.packages.activatePackage('language-coffee-script')
+ fullyTokenize(editor.tokenizedBuffer)
+ expect(tokenizedHandler.callCount).toBe(1)
+ })
+
+ it('retokenizes the buffer', async () => {
+ await atom.packages.activatePackage('language-ruby-on-rails')
+ await atom.packages.activatePackage('language-ruby')
+
+ buffer = atom.project.bufferForPathSync()
+ buffer.setText("
<%= User.find(2).full_name %>
")
+
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
+ value: "
",
+ scopes: ['text.html.ruby']
+ })
+
+ await atom.packages.activatePackage('language-html')
+ fullyTokenize(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
+ value: '<',
+ scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html']
+ })
+ })
+ })
+
+ describe('when the buffer is configured with the null grammar', () => {
+ it('does not actually tokenize using the grammar', () => {
+ spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
+ buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
+ buffer.setText('a\nb\nc')
+ tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
+ const tokenizeCallback = jasmine.createSpy('onDidTokenize')
+ tokenizedBuffer.onDidTokenize(tokenizeCallback)
+
+ expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
+ expect(tokenizeCallback.callCount).toBe(0)
+ expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
+
+ fullyTokenize(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
+ expect(tokenizeCallback.callCount).toBe(0)
+ expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
+ })
+ })
+ })
+
+ describe('.tokenForPosition(position)', () => {
+ afterEach(() => {
+ tokenizedBuffer.destroy()
+ buffer.release()
+ })
+
+ it('returns the correct token (regression)', () => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js'])
+ expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js'])
+ expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js'])
+ })
+ })
+
+ describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
+ beforeEach(() => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+ })
+
+ describe('when the selector does not match the token at the position', () =>
+ it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined())
+ )
+
+ describe('when the selector matches a single token at the position', () => {
+ it('returns the range covered by the token', () => {
+ expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]])
+ expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]])
+ })
+ })
+
+ describe('when the selector matches a run of multiple tokens at the position', () => {
+ it('returns the range covered by all contiguous tokens (within a single line)', () => {
+ expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]])
+ })
+ })
+ })
+
+ describe('.tokenizedLineForRow(row)', () => {
+ it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ const grammar = atom.grammars.grammarForScopeName('source.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ const line0 = buffer.lineForRow(0)
+
+ const jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
+ const jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
+ startTokenizing(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
+ expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
+ advanceClock(1)
+ expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
+ expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
+
+ const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
+ const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
+ tokenizedBuffer.setGrammar(NullGrammar)
+ startTokenizing(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
+ expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
+ expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
+ advanceClock(1)
+ expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
+ expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
+ })
+
+ it('returns undefined if the requested row is outside the buffer range', () => {
+ buffer = atom.project.bufferForPathSync('sample.js')
+ const grammar = atom.grammars.grammarForScopeName('source.js')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+ expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
+ })
+ })
+
+ describe('text decoration layer API', () => {
+ describe('iterator', () => {
+ it('iterates over the syntactic scope boundaries', () => {
+ buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'})
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+
+ const iterator = tokenizedBuffer.buildIterator()
+ iterator.seek(Point(0, 0))
+
+ const expectedBoundaries = [
+ {position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']},
+ {position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
+ {position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
+ {position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
+ {position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
+ {position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []},
+ {position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']},
+ {position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []},
+ {position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']},
+ {position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']},
+ {position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
+ {position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
+ {position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
+ {position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
+ {position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}
+ ]
+
+ while (true) {
+ const boundary = {
+ position: iterator.getPosition(),
+ closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)),
+ openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))
+ }
+
+ expect(boundary).toEqual(expectedBoundaries.shift())
+ if (!iterator.moveToSuccessor()) { break }
+ }
+
+ expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
+ 'syntax--source syntax--js',
+ 'syntax--storage syntax--type syntax--var syntax--js'
+ ])
+ expect(iterator.getPosition()).toEqual(Point(0, 3))
+ expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
+ 'syntax--source syntax--js'
+ ])
+ expect(iterator.getPosition()).toEqual(Point(0, 8))
+ expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
+ 'syntax--source syntax--js',
+ 'syntax--comment syntax--block syntax--js'
+ ])
+ expect(iterator.getPosition()).toEqual(Point(1, 0))
+ expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
+ 'syntax--source syntax--js',
+ 'syntax--constant syntax--numeric syntax--decimal syntax--js'
+ ])
+ expect(iterator.getPosition()).toEqual(Point(1, 18))
+
+ expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
+ 'syntax--source syntax--js'
+ ])
+ iterator.moveToSuccessor()
+ }) // ensure we don't infinitely loop (regression test)
+
+ it('does not report columns beyond the length of the line', async () => {
+ await atom.packages.activatePackage('language-coffee-script')
+
+ buffer = new TextBuffer({text: '# hello\n# world'})
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+
+ const iterator = tokenizedBuffer.buildIterator()
+ iterator.seek(Point(0, 0))
+ iterator.moveToSuccessor()
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition().column).toBe(7)
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition().column).toBe(0)
+
+ iterator.seek(Point(0, 7))
+ expect(iterator.getPosition().column).toBe(7)
+
+ iterator.seek(Point(0, 8))
+ expect(iterator.getPosition().column).toBe(7)
+ })
+
+ it('correctly terminates scopes at the beginning of the line (regression)', () => {
+ const grammar = atom.grammars.createGrammar('test', {
+ 'scopeName': 'text.broken',
+ 'name': 'Broken grammar',
+ 'patterns': [
+ {'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
+ {'match': '.', 'name': 'yellow.broken'}
+ ]
+ })
+
+ buffer = new TextBuffer({text: 'start x\nend x\nx'})
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+
+ const iterator = tokenizedBuffer.buildIterator()
+ iterator.seek(Point(1, 0))
+
+ expect(iterator.getPosition()).toEqual([1, 0])
+ expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken'])
+ expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken'])
+ })
+ })
+ })
+
+ describe('.suggestedIndentForBufferRow', () => {
+ let editor
+
+ describe('javascript', () => {
+ beforeEach(async () => {
+ editor = await atom.workspace.open('sample.js', {autoIndent: false})
+ await atom.packages.activatePackage('language-javascript')
+ })
+
+ it('bases indentation off of the previous non-blank line', () => {
+ expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
+ expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
+ expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
+ expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
+ expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
+ expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
+ expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
+ })
+
+ it('does not take invisibles into account', () => {
+ editor.update({showInvisibles: true})
+ expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
+ expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
+ expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
+ expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
+ expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
+ expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
+ expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
+ })
+ })
+
+ describe('css', () => {
+ beforeEach(async () => {
+ editor = await atom.workspace.open('css.css', {autoIndent: true})
+ await atom.packages.activatePackage('language-source')
+ await atom.packages.activatePackage('language-css')
+ })
+
+ it('does not return negative values (regression)', () => {
+ editor.setText('.test {\npadding: 0;\n}')
+ expect(editor.suggestedIndentForBufferRow(2)).toBe(0)
+ })
+ })
+ })
+
+ 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')
+ buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n')
+ buffer.insert([0, 0], '// multi-line\n// comment\n// block\n')
+ tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
+ fullyTokenize(tokenizedBuffer)
+ })
+
+ it('includes the first line of multi-line comments', () => {
+ expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent
+ expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false)
+
+ buffer.insert([0, Infinity], '\n')
+
+ expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false)
+
+ buffer.undo()
+
+ expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
+ }) // because of indent
+
+ it('includes non-comment lines that precede an increase in indentation', () => {
+ buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable
+
+ expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
+
+ buffer.insert([7, 0], ' ')
+
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
+
+ buffer.undo()
+
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
+
+ buffer.insert([7, 0], ' \n x\n')
+
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
+
+ buffer.insert([9, 0], ' ')
+
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
+ })
+ })
+
+ describe('.getFoldableRangesAtIndentLevel', () => {
+ it('returns the ranges that can be folded at the given indent level', () => {
+ buffer = new TextBuffer(dedent `
+ if (a) {
+ b();
+ if (c) {
+ d()
+ if (e) {
+ f()
+ }
+ g()
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ tokenizedBuffer = new TokenizedBuffer({buffer})
+
+ expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent `
+ if (a) {⋯
+ }
+ i()
+ if (j) {⋯
+ }
+ `)
+
+ expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent `
+ if (a) {
+ b();
+ if (c) {⋯
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent `
+ if (a) {
+ b();
+ if (c) {
+ d()
+ if (e) {⋯
+ }
+ g()
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+ })
+ })
+
+ describe('.getFoldableRanges', () => {
+ it('returns the ranges that can be folded', () => {
+ buffer = new TextBuffer(dedent `
+ if (a) {
+ b();
+ if (c) {
+ d()
+ if (e) {
+ f()
+ }
+ g()
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ tokenizedBuffer = new TokenizedBuffer({buffer})
+
+ expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([
+ ...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2),
+ ...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2),
+ ...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2),
+ ].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString()))
+ })
+ })
+
+ describe('.getFoldableRangeContainingPoint', () => {
+ it('returns the range for the smallest fold that contains the given range', () => {
+ buffer = new TextBuffer(dedent `
+ if (a) {
+ b();
+ if (c) {
+ d()
+ if (e) {
+ f()
+ }
+ g()
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ tokenizedBuffer = new TokenizedBuffer({buffer})
+
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull()
+
+ let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2)
+ expect(simulateFold([range])).toBe(dedent `
+ if (a) {⋯
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)
+ expect(simulateFold([range])).toBe(dedent `
+ if (a) {⋯
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+
+ range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2)
+ expect(simulateFold([range])).toBe(dedent `
+ if (a) {
+ b();
+ if (c) {⋯
+ }
+ h()
+ }
+ i()
+ if (j) {
+ k()
+ }
+ `)
+ })
+
+ it('works for coffee-script', async () => {
+ const editor = await atom.workspace.open('coffee.coffee')
+ await atom.packages.activatePackage('language-coffee-script')
+ buffer = editor.buffer
+ tokenizedBuffer = editor.tokenizedBuffer
+
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
+ expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]])
+ })
+
+ it('works for javascript', async () => {
+ const editor = await atom.workspace.open('sample.js')
+ await atom.packages.activatePackage('language-javascript')
+ buffer = editor.buffer
+ tokenizedBuffer = editor.tokenizedBuffer
+
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
+ expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]])
+ })
+ })
+
+ function simulateFold (ranges) {
+ buffer.transact(() => {
+ for (const range of ranges.reverse()) {
+ buffer.setTextInRange(range, '⋯')
+ }
+ })
+ let text = buffer.getText()
+ buffer.undo()
+ return text
+ }
+})
diff --git a/src/color.js b/src/color.js
index 6208d6837..2f2947e16 100644
--- a/src/color.js
+++ b/src/color.js
@@ -112,27 +112,15 @@ export default class Color {
function parseColor (colorString) {
const color = parseInt(colorString, 10)
- if (isNaN(color)) {
- return 0
- } else {
- return Math.min(Math.max(color, 0), 255)
- }
+ return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255)
}
function parseAlpha (alphaString) {
const alpha = parseFloat(alphaString)
- if (isNaN(alpha)) {
- return 1
- } else {
- return Math.min(Math.max(alpha, 0), 1)
- }
+ return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1)
}
function numberToHexString (number) {
const hex = number.toString(16)
- if (number < 16) {
- return `0${hex}`
- } else {
- return hex
- }
+ return number < 16 ? `0${hex}` : hex
}
diff --git a/src/cursor.coffee b/src/cursor.coffee
deleted file mode 100644
index 6273b0276..000000000
--- a/src/cursor.coffee
+++ /dev/null
@@ -1,659 +0,0 @@
-{Point, Range} = require 'text-buffer'
-{Emitter} = require 'event-kit'
-_ = require 'underscore-plus'
-Model = require './model'
-
-EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
-
-# Extended: The `Cursor` class represents the little blinking line identifying
-# where text can be inserted.
-#
-# Cursors belong to {TextEditor}s and have some metadata attached in the form
-# of a {DisplayMarker}.
-module.exports =
-class Cursor extends Model
- screenPosition: null
- bufferPosition: null
- goalColumn: null
-
- # Instantiated by a {TextEditor}
- constructor: ({@editor, @marker, id}) ->
- @emitter = new Emitter
- @assignId(id)
-
- destroy: ->
- @marker.destroy()
-
- ###
- Section: Event Subscription
- ###
-
- # Public: Calls your `callback` when the cursor has been moved.
- #
- # * `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.
- onDidChangePosition: (callback) ->
- @emitter.on 'did-change-position', callback
-
- # Public: Calls your `callback` when the cursor is destroyed
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.once 'did-destroy', callback
-
- ###
- Section: Managing Cursor Position
- ###
-
- # Public: Moves a cursor to a given screen position.
- #
- # * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
- # * `options` (optional) {Object} with the following keys:
- # * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
- # the cursor moves to.
- setScreenPosition: (screenPosition, options={}) ->
- @changePosition options, =>
- @marker.setHeadScreenPosition(screenPosition, options)
-
- # Public: Returns the screen position of the cursor as a {Point}.
- getScreenPosition: ->
- @marker.getHeadScreenPosition()
-
- # Public: Moves a cursor to a given buffer position.
- #
- # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
- # * `options` (optional) {Object} with the following keys:
- # * `autoscroll` {Boolean} indicating whether to autoscroll to the new
- # position. Defaults to `true` if this is the most recently added cursor,
- # `false` otherwise.
- setBufferPosition: (bufferPosition, options={}) ->
- @changePosition options, =>
- @marker.setHeadBufferPosition(bufferPosition, options)
-
- # Public: Returns the current buffer position as an Array.
- getBufferPosition: ->
- @marker.getHeadBufferPosition()
-
- # Public: Returns the cursor's current screen row.
- getScreenRow: ->
- @getScreenPosition().row
-
- # Public: Returns the cursor's current screen column.
- getScreenColumn: ->
- @getScreenPosition().column
-
- # Public: Retrieves the cursor's current buffer row.
- getBufferRow: ->
- @getBufferPosition().row
-
- # Public: Returns the cursor's current buffer column.
- getBufferColumn: ->
- @getBufferPosition().column
-
- # Public: Returns the cursor's current buffer row of text excluding its line
- # ending.
- getCurrentBufferLine: ->
- @editor.lineTextForBufferRow(@getBufferRow())
-
- # Public: Returns whether the cursor is at the start of a line.
- isAtBeginningOfLine: ->
- @getBufferPosition().column is 0
-
- # Public: Returns whether the cursor is on the line return character.
- isAtEndOfLine: ->
- @getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
-
- ###
- Section: Cursor Position Details
- ###
-
- # Public: Returns the underlying {DisplayMarker} for the cursor.
- # Useful with overlay {Decoration}s.
- getMarker: -> @marker
-
- # Public: Identifies if the cursor is surrounded by whitespace.
- #
- # "Surrounded" here means that the character directly before and after the
- # cursor are both whitespace.
- #
- # Returns a {Boolean}.
- isSurroundedByWhitespace: ->
- {row, column} = @getBufferPosition()
- range = [[row, column - 1], [row, column + 1]]
- /^\s+$/.test @editor.getTextInBufferRange(range)
-
- # Public: Returns whether the cursor is currently between a word and non-word
- # character. The non-word characters are defined by the
- # `editor.nonWordCharacters` config value.
- #
- # This method returns false if the character before or after the cursor is
- # whitespace.
- #
- # Returns a Boolean.
- isBetweenWordAndNonWord: ->
- return false if @isAtBeginningOfLine() or @isAtEndOfLine()
-
- {row, column} = @getBufferPosition()
- range = [[row, column - 1], [row, column + 1]]
- [before, after] = @editor.getTextInBufferRange(range)
- return false if /\s/.test(before) or /\s/.test(after)
-
- nonWordCharacters = @getNonWordCharacters()
- nonWordCharacters.includes(before) isnt nonWordCharacters.includes(after)
-
- # Public: Returns whether this cursor is between a word's start and end.
- #
- # * `options` (optional) {Object}
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp}).
- #
- # Returns a {Boolean}
- isInsideWord: (options) ->
- {row, column} = @getBufferPosition()
- range = [[row, column], [row, Infinity]]
- @editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0
-
- # Public: Returns the indentation level of the current line.
- getIndentLevel: ->
- if @editor.getSoftTabs()
- @getBufferColumn() / @editor.getTabLength()
- else
- @getBufferColumn()
-
- # Public: Retrieves the scope descriptor for the cursor's current position.
- #
- # Returns a {ScopeDescriptor}
- getScopeDescriptor: ->
- @editor.scopeDescriptorForBufferPosition(@getBufferPosition())
-
- # Public: Returns true if this cursor has no non-whitespace characters before
- # its current position.
- hasPrecedingCharactersOnLine: ->
- bufferPosition = @getBufferPosition()
- line = @editor.lineTextForBufferRow(bufferPosition.row)
- firstCharacterColumn = line.search(/\S/)
-
- if firstCharacterColumn is -1
- false
- else
- bufferPosition.column > firstCharacterColumn
-
- # Public: Identifies if this cursor is the last in the {TextEditor}.
- #
- # "Last" is defined as the most recently added cursor.
- #
- # Returns a {Boolean}.
- isLastCursor: ->
- this is @editor.getLastCursor()
-
- ###
- Section: Moving the Cursor
- ###
-
- # Public: Moves the cursor up one screen row.
- #
- # * `rowCount` (optional) {Number} number of rows to move (default: 1)
- # * `options` (optional) {Object} with the following keys:
- # * `moveToEndOfSelection` if true, move to the left of the selection if a
- # selection exists.
- moveUp: (rowCount=1, {moveToEndOfSelection}={}) ->
- range = @marker.getScreenRange()
- if moveToEndOfSelection and not range.isEmpty()
- {row, column} = range.start
- else
- {row, column} = @getScreenPosition()
-
- column = @goalColumn if @goalColumn?
- @setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true)
- @goalColumn = column
-
- # Public: Moves the cursor down one screen row.
- #
- # * `rowCount` (optional) {Number} number of rows to move (default: 1)
- # * `options` (optional) {Object} with the following keys:
- # * `moveToEndOfSelection` if true, move to the left of the selection if a
- # selection exists.
- moveDown: (rowCount=1, {moveToEndOfSelection}={}) ->
- range = @marker.getScreenRange()
- if moveToEndOfSelection and not range.isEmpty()
- {row, column} = range.end
- else
- {row, column} = @getScreenPosition()
-
- column = @goalColumn if @goalColumn?
- @setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true)
- @goalColumn = column
-
- # Public: Moves the cursor left one screen column.
- #
- # * `columnCount` (optional) {Number} number of columns to move (default: 1)
- # * `options` (optional) {Object} with the following keys:
- # * `moveToEndOfSelection` if true, move to the left of the selection if a
- # selection exists.
- moveLeft: (columnCount=1, {moveToEndOfSelection}={}) ->
- range = @marker.getScreenRange()
- if moveToEndOfSelection and not range.isEmpty()
- @setScreenPosition(range.start)
- else
- {row, column} = @getScreenPosition()
-
- while columnCount > column and row > 0
- columnCount -= column
- column = @editor.lineLengthForScreenRow(--row)
- columnCount-- # subtract 1 for the row move
-
- column = column - columnCount
- @setScreenPosition({row, column}, clipDirection: 'backward')
-
- # Public: Moves the cursor right one screen column.
- #
- # * `columnCount` (optional) {Number} number of columns to move (default: 1)
- # * `options` (optional) {Object} with the following keys:
- # * `moveToEndOfSelection` if true, move to the right of the selection if a
- # selection exists.
- moveRight: (columnCount=1, {moveToEndOfSelection}={}) ->
- range = @marker.getScreenRange()
- if moveToEndOfSelection and not range.isEmpty()
- @setScreenPosition(range.end)
- else
- {row, column} = @getScreenPosition()
- maxLines = @editor.getScreenLineCount()
- rowLength = @editor.lineLengthForScreenRow(row)
- columnsRemainingInLine = rowLength - column
-
- while columnCount > columnsRemainingInLine and row < maxLines - 1
- columnCount -= columnsRemainingInLine
- columnCount-- # subtract 1 for the row move
-
- column = 0
- rowLength = @editor.lineLengthForScreenRow(++row)
- columnsRemainingInLine = rowLength
-
- column = column + columnCount
- @setScreenPosition({row, column}, clipDirection: 'forward')
-
- # Public: Moves the cursor to the top of the buffer.
- moveToTop: ->
- @setBufferPosition([0, 0])
-
- # Public: Moves the cursor to the bottom of the buffer.
- moveToBottom: ->
- @setBufferPosition(@editor.getEofBufferPosition())
-
- # Public: Moves the cursor to the beginning of the line.
- moveToBeginningOfScreenLine: ->
- @setScreenPosition([@getScreenRow(), 0])
-
- # Public: Moves the cursor to the beginning of the buffer line.
- moveToBeginningOfLine: ->
- @setBufferPosition([@getBufferRow(), 0])
-
- # Public: Moves the cursor to the beginning of the first character in the
- # line.
- moveToFirstCharacterOfLine: ->
- screenRow = @getScreenRow()
- screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true)
- screenLineEnd = [screenRow, Infinity]
- screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
-
- firstCharacterColumn = null
- @editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) ->
- firstCharacterColumn = range.start.column
- stop()
-
- if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn()
- targetBufferColumn = firstCharacterColumn
- else
- targetBufferColumn = screenLineBufferRange.start.column
-
- @setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
-
- # Public: Moves the cursor to the end of the line.
- moveToEndOfScreenLine: ->
- @setScreenPosition([@getScreenRow(), Infinity])
-
- # Public: Moves the cursor to the end of the buffer line.
- moveToEndOfLine: ->
- @setBufferPosition([@getBufferRow(), Infinity])
-
- # Public: Moves the cursor to the beginning of the word.
- moveToBeginningOfWord: ->
- @setBufferPosition(@getBeginningOfCurrentWordBufferPosition())
-
- # Public: Moves the cursor to the end of the word.
- moveToEndOfWord: ->
- if position = @getEndOfCurrentWordBufferPosition()
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the beginning of the next word.
- moveToBeginningOfNextWord: ->
- if position = @getBeginningOfNextWordBufferPosition()
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the previous word boundary.
- moveToPreviousWordBoundary: ->
- if position = @getPreviousWordBoundaryBufferPosition()
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the next word boundary.
- moveToNextWordBoundary: ->
- if position = @getNextWordBoundaryBufferPosition()
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the previous subword boundary.
- moveToPreviousSubwordBoundary: ->
- options = {wordRegex: @subwordRegExp(backwards: true)}
- if position = @getPreviousWordBoundaryBufferPosition(options)
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the next subword boundary.
- moveToNextSubwordBoundary: ->
- options = {wordRegex: @subwordRegExp()}
- if position = @getNextWordBoundaryBufferPosition(options)
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the beginning of the buffer line, skipping all
- # whitespace.
- skipLeadingWhitespace: ->
- position = @getBufferPosition()
- scanRange = @getCurrentLineBufferRange()
- endOfLeadingWhitespace = null
- @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) ->
- endOfLeadingWhitespace = range.end
-
- @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position)
-
- # Public: Moves the cursor to the beginning of the next paragraph
- moveToBeginningOfNextParagraph: ->
- if position = @getBeginningOfNextParagraphBufferPosition()
- @setBufferPosition(position)
-
- # Public: Moves the cursor to the beginning of the previous paragraph
- moveToBeginningOfPreviousParagraph: ->
- if position = @getBeginningOfPreviousParagraphBufferPosition()
- @setBufferPosition(position)
-
- ###
- Section: Local Positions and Ranges
- ###
-
- # Public: Returns buffer position of previous word boundary. It might be on
- # the current word, or the previous word.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp})
- getPreviousWordBoundaryBufferPosition: (options = {}) ->
- currentBufferPosition = @getBufferPosition()
- previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row)
- scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition]
-
- beginningOfWordPosition = null
- @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
- if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0
- # force it to stop at the beginning of each line
- beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
- else if range.end.isLessThan(currentBufferPosition)
- beginningOfWordPosition = range.end
- else
- beginningOfWordPosition = range.start
-
- if not beginningOfWordPosition?.isEqual(currentBufferPosition)
- stop()
-
- beginningOfWordPosition or currentBufferPosition
-
- # Public: Returns buffer position of the next word boundary. It might be on
- # the current word, or the previous word.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp})
- getNextWordBoundaryBufferPosition: (options = {}) ->
- currentBufferPosition = @getBufferPosition()
- scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
-
- endOfWordPosition = null
- @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
- if range.start.row > currentBufferPosition.row
- # force it to stop at the beginning of each line
- endOfWordPosition = new Point(range.start.row, 0)
- else if range.start.isGreaterThan(currentBufferPosition)
- endOfWordPosition = range.start
- else
- endOfWordPosition = range.end
-
- if not endOfWordPosition?.isEqual(currentBufferPosition)
- stop()
-
- endOfWordPosition or currentBufferPosition
-
- # Public: Retrieves the buffer position of where the current word starts.
- #
- # * `options` (optional) An {Object} with the following keys:
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp}).
- # * `includeNonWordCharacters` A {Boolean} indicating whether to include
- # non-word characters in the default word regex.
- # Has no effect if wordRegex is set.
- # * `allowPrevious` A {Boolean} indicating whether the beginning of the
- # previous word can be returned.
- #
- # Returns a {Range}.
- getBeginningOfCurrentWordBufferPosition: (options = {}) ->
- allowPrevious = options.allowPrevious ? true
- currentBufferPosition = @getBufferPosition()
- previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0
- scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
-
- beginningOfWordPosition = null
- @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
- # Ignore 'empty line' matches between '\r' and '\n'
- return if matchText is '' and range.start.column isnt 0
-
- if range.start.isLessThan(currentBufferPosition)
- if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
- beginningOfWordPosition = range.start
- stop()
-
- if beginningOfWordPosition?
- beginningOfWordPosition
- else if allowPrevious
- new Point(0, 0)
- else
- currentBufferPosition
-
- # Public: Retrieves the buffer position of where the current word ends.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp})
- # * `includeNonWordCharacters` A Boolean indicating whether to include
- # non-word characters in the default word regex. Has no effect if
- # wordRegex is set.
- #
- # Returns a {Range}.
- getEndOfCurrentWordBufferPosition: (options = {}) ->
- allowNext = options.allowNext ? true
- currentBufferPosition = @getBufferPosition()
- scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
-
- endOfWordPosition = null
- @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
- # Ignore 'empty line' matches between '\r' and '\n'
- return if matchText is '' and range.start.column isnt 0
-
- if range.end.isGreaterThan(currentBufferPosition)
- if allowNext or range.start.isLessThanOrEqual(currentBufferPosition)
- endOfWordPosition = range.end
- stop()
-
- endOfWordPosition ? currentBufferPosition
-
- # Public: Retrieves the buffer position of where the next word starts.
- #
- # * `options` (optional) {Object}
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp}).
- #
- # Returns a {Range}
- getBeginningOfNextWordBufferPosition: (options = {}) ->
- currentBufferPosition = @getBufferPosition()
- start = if @isInsideWord(options) then @getEndOfCurrentWordBufferPosition(options) else currentBufferPosition
- scanRange = [start, @editor.getEofBufferPosition()]
-
- beginningOfNextWordPosition = null
- @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
- beginningOfNextWordPosition = range.start
- stop()
-
- beginningOfNextWordPosition or currentBufferPosition
-
- # Public: Returns the buffer Range occupied by the word located under the cursor.
- #
- # * `options` (optional) {Object}
- # * `wordRegex` A {RegExp} indicating what constitutes a "word"
- # (default: {::wordRegExp}).
- getCurrentWordBufferRange: (options={}) ->
- startOptions = Object.assign(_.clone(options), allowPrevious: false)
- endOptions = Object.assign(_.clone(options), allowNext: false)
- new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions))
-
- # Public: Returns the buffer Range for the current line.
- #
- # * `options` (optional) {Object}
- # * `includeNewline` A {Boolean} which controls whether the Range should
- # include the newline.
- getCurrentLineBufferRange: (options) ->
- @editor.bufferRangeForBufferRow(@getBufferRow(), options)
-
- # Public: Retrieves the range for the current paragraph.
- #
- # A paragraph is defined as a block of text surrounded by empty lines or comments.
- #
- # Returns a {Range}.
- getCurrentParagraphBufferRange: ->
- @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow())
-
- # Public: Returns the characters preceding the cursor in the current word.
- getCurrentWordPrefix: ->
- @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
-
- ###
- Section: Visibility
- ###
-
- ###
- Section: Comparing to another cursor
- ###
-
- # Public: Compare this cursor's buffer position to another cursor's buffer position.
- #
- # See {Point::compare} for more details.
- #
- # * `otherCursor`{Cursor} to compare against
- compare: (otherCursor) ->
- @getBufferPosition().compare(otherCursor.getBufferPosition())
-
- ###
- Section: Utilities
- ###
-
- # Public: Deselects the current selection.
- clearSelection: (options) ->
- @selection?.clear(options)
-
- # Public: Get the RegExp used by the cursor to determine what a "word" is.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `includeNonWordCharacters` A {Boolean} indicating whether to include
- # non-word characters in the regex. (default: true)
- #
- # Returns a {RegExp}.
- wordRegExp: (options) ->
- nonWordCharacters = _.escapeRegExp(@getNonWordCharacters())
- source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+"
- if options?.includeNonWordCharacters ? true
- source += "|" + "[#{nonWordCharacters}]+"
- new RegExp(source, "g")
-
- # Public: Get the RegExp used by the cursor to determine what a "subword" is.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `backwards` A {Boolean} indicating whether to look forwards or backwards
- # for the next subword. (default: false)
- #
- # Returns a {RegExp}.
- subwordRegExp: (options={}) ->
- nonWordCharacters = @getNonWordCharacters()
- lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
- uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
- snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+"
- segments = [
- "^[\t ]+",
- "[\t ]+$",
- "[#{uppercaseLetters}]+(?![#{lowercaseLetters}])",
- "\\d+"
- ]
- if options.backwards
- segments.push("#{snakeCamelSegment}_*")
- segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*")
- else
- segments.push("_*#{snakeCamelSegment}")
- segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+")
- segments.push("_+")
- new RegExp(segments.join("|"), "g")
-
- ###
- Section: Private
- ###
-
- getNonWordCharacters: ->
- @editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray())
-
- changePosition: (options, fn) ->
- @clearSelection(autoscroll: false)
- fn()
- @autoscroll() if options.autoscroll ? @isLastCursor()
-
- getScreenRange: ->
- {row, column} = @getScreenPosition()
- new Range(new Point(row, column), new Point(row, column + 1))
-
- autoscroll: (options = {}) ->
- options.clip = false
- @editor.scrollToScreenRange(@getScreenRange(), options)
-
- getBeginningOfNextParagraphBufferPosition: ->
- start = @getBufferPosition()
- eof = @editor.getEofBufferPosition()
- scanRange = [start, eof]
-
- {row, column} = eof
- position = new Point(row, column - 1)
-
- @editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
- position = range.start.traverse(Point(1, 0))
- stop() unless position.isEqual(start)
- position
-
- getBeginningOfPreviousParagraphBufferPosition: ->
- start = @getBufferPosition()
-
- {row, column} = start
- scanRange = [[row-1, column], [0, 0]]
- position = new Point(0, 0)
- @editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
- position = range.start.traverse(Point(1, 0))
- stop() unless position.isEqual(start)
- position
diff --git a/src/cursor.js b/src/cursor.js
new file mode 100644
index 000000000..1425f5b49
--- /dev/null
+++ b/src/cursor.js
@@ -0,0 +1,753 @@
+const {Point, Range} = require('text-buffer')
+const {Emitter} = require('event-kit')
+const _ = require('underscore-plus')
+const Model = require('./model')
+
+const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
+
+// Extended: The `Cursor` class represents the little blinking line identifying
+// where text can be inserted.
+//
+// Cursors belong to {TextEditor}s and have some metadata attached in the form
+// of a {DisplayMarker}.
+module.exports =
+class Cursor extends Model {
+ // Instantiated by a {TextEditor}
+ constructor (params) {
+ super(params)
+ this.editor = params.editor
+ this.marker = params.marker
+ this.emitter = new Emitter()
+ }
+
+ destroy () {
+ this.marker.destroy()
+ }
+
+ /*
+ Section: Event Subscription
+ */
+
+ // Public: Calls your `callback` when the cursor has been moved.
+ //
+ // * `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.
+ onDidChangePosition (callback) {
+ return this.emitter.on('did-change-position', callback)
+ }
+
+ // Public: Calls your `callback` when the cursor is 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 Cursor Position
+ */
+
+ // Public: Moves a cursor to a given screen position.
+ //
+ // * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
+ // * `options` (optional) {Object} with the following keys:
+ // * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
+ // the cursor moves to.
+ setScreenPosition (screenPosition, options = {}) {
+ this.changePosition(options, () => {
+ this.marker.setHeadScreenPosition(screenPosition, options)
+ })
+ }
+
+ // Public: Returns the screen position of the cursor as a {Point}.
+ getScreenPosition () {
+ return this.marker.getHeadScreenPosition()
+ }
+
+ // Public: Moves a cursor to a given buffer position.
+ //
+ // * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
+ // * `options` (optional) {Object} with the following keys:
+ // * `autoscroll` {Boolean} indicating whether to autoscroll to the new
+ // position. Defaults to `true` if this is the most recently added cursor,
+ // `false` otherwise.
+ setBufferPosition (bufferPosition, options = {}) {
+ this.changePosition(options, () => {
+ this.marker.setHeadBufferPosition(bufferPosition, options)
+ })
+ }
+
+ // Public: Returns the current buffer position as an Array.
+ getBufferPosition () {
+ return this.marker.getHeadBufferPosition()
+ }
+
+ // Public: Returns the cursor's current screen row.
+ getScreenRow () {
+ return this.getScreenPosition().row
+ }
+
+ // Public: Returns the cursor's current screen column.
+ getScreenColumn () {
+ return this.getScreenPosition().column
+ }
+
+ // Public: Retrieves the cursor's current buffer row.
+ getBufferRow () {
+ return this.getBufferPosition().row
+ }
+
+ // Public: Returns the cursor's current buffer column.
+ getBufferColumn () {
+ return this.getBufferPosition().column
+ }
+
+ // Public: Returns the cursor's current buffer row of text excluding its line
+ // ending.
+ getCurrentBufferLine () {
+ return this.editor.lineTextForBufferRow(this.getBufferRow())
+ }
+
+ // Public: Returns whether the cursor is at the start of a line.
+ isAtBeginningOfLine () {
+ return this.getBufferPosition().column === 0
+ }
+
+ // Public: Returns whether the cursor is on the line return character.
+ isAtEndOfLine () {
+ return this.getBufferPosition().isEqual(this.getCurrentLineBufferRange().end)
+ }
+
+ /*
+ Section: Cursor Position Details
+ */
+
+ // Public: Returns the underlying {DisplayMarker} for the cursor.
+ // Useful with overlay {Decoration}s.
+ getMarker () { return this.marker }
+
+ // Public: Identifies if the cursor is surrounded by whitespace.
+ //
+ // "Surrounded" here means that the character directly before and after the
+ // cursor are both whitespace.
+ //
+ // Returns a {Boolean}.
+ isSurroundedByWhitespace () {
+ const {row, column} = this.getBufferPosition()
+ const range = [[row, column - 1], [row, column + 1]]
+ return /^\s+$/.test(this.editor.getTextInBufferRange(range))
+ }
+
+ // Public: Returns whether the cursor is currently between a word and non-word
+ // character. The non-word characters are defined by the
+ // `editor.nonWordCharacters` config value.
+ //
+ // This method returns false if the character before or after the cursor is
+ // whitespace.
+ //
+ // Returns a Boolean.
+ isBetweenWordAndNonWord () {
+ if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false
+
+ const {row, column} = this.getBufferPosition()
+ const range = [[row, column - 1], [row, column + 1]]
+ const text = this.editor.getTextInBufferRange(range)
+ if (/\s/.test(text[0]) || /\s/.test(text[1])) return false
+
+ const nonWordCharacters = this.getNonWordCharacters()
+ return nonWordCharacters.includes(text[0]) !== nonWordCharacters.includes(text[1])
+ }
+
+ // Public: Returns whether this cursor is between a word's start and end.
+ //
+ // * `options` (optional) {Object}
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp}).
+ //
+ // Returns a {Boolean}
+ isInsideWord (options) {
+ const {row, column} = this.getBufferPosition()
+ const range = [[row, column], [row, Infinity]]
+ const text = this.editor.getTextInBufferRange(range)
+ return text.search((options && options.wordRegex) || this.wordRegExp()) === 0
+ }
+
+ // Public: Returns the indentation level of the current line.
+ getIndentLevel () {
+ if (this.editor.getSoftTabs()) {
+ return this.getBufferColumn() / this.editor.getTabLength()
+ } else {
+ return this.getBufferColumn()
+ }
+ }
+
+ // Public: Retrieves the scope descriptor for the cursor's current position.
+ //
+ // Returns a {ScopeDescriptor}
+ getScopeDescriptor () {
+ return this.editor.scopeDescriptorForBufferPosition(this.getBufferPosition())
+ }
+
+ // Public: Returns true if this cursor has no non-whitespace characters before
+ // its current position.
+ hasPrecedingCharactersOnLine () {
+ const bufferPosition = this.getBufferPosition()
+ const line = this.editor.lineTextForBufferRow(bufferPosition.row)
+ const firstCharacterColumn = line.search(/\S/)
+
+ if (firstCharacterColumn === -1) {
+ return false
+ } else {
+ return bufferPosition.column > firstCharacterColumn
+ }
+ }
+
+ // Public: Identifies if this cursor is the last in the {TextEditor}.
+ //
+ // "Last" is defined as the most recently added cursor.
+ //
+ // Returns a {Boolean}.
+ isLastCursor () {
+ return this === this.editor.getLastCursor()
+ }
+
+ /*
+ Section: Moving the Cursor
+ */
+
+ // Public: Moves the cursor up one screen row.
+ //
+ // * `rowCount` (optional) {Number} number of rows to move (default: 1)
+ // * `options` (optional) {Object} with the following keys:
+ // * `moveToEndOfSelection` if true, move to the left of the selection if a
+ // selection exists.
+ moveUp (rowCount = 1, {moveToEndOfSelection} = {}) {
+ let row, column
+ const range = this.marker.getScreenRange()
+ if (moveToEndOfSelection && !range.isEmpty()) {
+ ({row, column} = range.start)
+ } else {
+ ({row, column} = this.getScreenPosition())
+ }
+
+ if (this.goalColumn != null) column = this.goalColumn
+ this.setScreenPosition({row: row - rowCount, column}, {skipSoftWrapIndentation: true})
+ this.goalColumn = column
+ }
+
+ // Public: Moves the cursor down one screen row.
+ //
+ // * `rowCount` (optional) {Number} number of rows to move (default: 1)
+ // * `options` (optional) {Object} with the following keys:
+ // * `moveToEndOfSelection` if true, move to the left of the selection if a
+ // selection exists.
+ moveDown (rowCount = 1, {moveToEndOfSelection} = {}) {
+ let row, column
+ const range = this.marker.getScreenRange()
+ if (moveToEndOfSelection && !range.isEmpty()) {
+ ({row, column} = range.end)
+ } else {
+ ({row, column} = this.getScreenPosition())
+ }
+
+ if (this.goalColumn != null) column = this.goalColumn
+ this.setScreenPosition({row: row + rowCount, column}, {skipSoftWrapIndentation: true})
+ this.goalColumn = column
+ }
+
+ // Public: Moves the cursor left one screen column.
+ //
+ // * `columnCount` (optional) {Number} number of columns to move (default: 1)
+ // * `options` (optional) {Object} with the following keys:
+ // * `moveToEndOfSelection` if true, move to the left of the selection if a
+ // selection exists.
+ moveLeft (columnCount = 1, {moveToEndOfSelection} = {}) {
+ const range = this.marker.getScreenRange()
+ if (moveToEndOfSelection && !range.isEmpty()) {
+ this.setScreenPosition(range.start)
+ } else {
+ let {row, column} = this.getScreenPosition()
+
+ while (columnCount > column && row > 0) {
+ columnCount -= column
+ column = this.editor.lineLengthForScreenRow(--row)
+ columnCount-- // subtract 1 for the row move
+ }
+
+ column = column - columnCount
+ this.setScreenPosition({row, column}, {clipDirection: 'backward'})
+ }
+ }
+
+ // Public: Moves the cursor right one screen column.
+ //
+ // * `columnCount` (optional) {Number} number of columns to move (default: 1)
+ // * `options` (optional) {Object} with the following keys:
+ // * `moveToEndOfSelection` if true, move to the right of the selection if a
+ // selection exists.
+ moveRight (columnCount = 1, {moveToEndOfSelection} = {}) {
+ const range = this.marker.getScreenRange()
+ if (moveToEndOfSelection && !range.isEmpty()) {
+ this.setScreenPosition(range.end)
+ } else {
+ let {row, column} = this.getScreenPosition()
+ const maxLines = this.editor.getScreenLineCount()
+ let rowLength = this.editor.lineLengthForScreenRow(row)
+ let columnsRemainingInLine = rowLength - column
+
+ while (columnCount > columnsRemainingInLine && row < maxLines - 1) {
+ columnCount -= columnsRemainingInLine
+ columnCount-- // subtract 1 for the row move
+
+ column = 0
+ rowLength = this.editor.lineLengthForScreenRow(++row)
+ columnsRemainingInLine = rowLength
+ }
+
+ column = column + columnCount
+ this.setScreenPosition({row, column}, {clipDirection: 'forward'})
+ }
+ }
+
+ // Public: Moves the cursor to the top of the buffer.
+ moveToTop () {
+ this.setBufferPosition([0, 0])
+ }
+
+ // Public: Moves the cursor to the bottom of the buffer.
+ moveToBottom () {
+ this.setBufferPosition(this.editor.getEofBufferPosition())
+ }
+
+ // Public: Moves the cursor to the beginning of the line.
+ moveToBeginningOfScreenLine () {
+ this.setScreenPosition([this.getScreenRow(), 0])
+ }
+
+ // Public: Moves the cursor to the beginning of the buffer line.
+ moveToBeginningOfLine () {
+ this.setBufferPosition([this.getBufferRow(), 0])
+ }
+
+ // Public: Moves the cursor to the beginning of the first character in the
+ // line.
+ moveToFirstCharacterOfLine () {
+ let targetBufferColumn
+ const screenRow = this.getScreenRow()
+ const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {skipSoftWrapIndentation: true})
+ const screenLineEnd = [screenRow, Infinity]
+ const screenLineBufferRange = this.editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
+
+ let firstCharacterColumn = null
+ this.editor.scanInBufferRange(/\S/, screenLineBufferRange, ({range, stop}) => {
+ firstCharacterColumn = range.start.column
+ stop()
+ })
+
+ if (firstCharacterColumn != null && firstCharacterColumn !== this.getBufferColumn()) {
+ targetBufferColumn = firstCharacterColumn
+ } else {
+ targetBufferColumn = screenLineBufferRange.start.column
+ }
+
+ this.setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
+ }
+
+ // Public: Moves the cursor to the end of the line.
+ moveToEndOfScreenLine () {
+ this.setScreenPosition([this.getScreenRow(), Infinity])
+ }
+
+ // Public: Moves the cursor to the end of the buffer line.
+ moveToEndOfLine () {
+ this.setBufferPosition([this.getBufferRow(), Infinity])
+ }
+
+ // Public: Moves the cursor to the beginning of the word.
+ moveToBeginningOfWord () {
+ this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition())
+ }
+
+ // Public: Moves the cursor to the end of the word.
+ moveToEndOfWord () {
+ const position = this.getEndOfCurrentWordBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the beginning of the next word.
+ moveToBeginningOfNextWord () {
+ const position = this.getBeginningOfNextWordBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the previous word boundary.
+ moveToPreviousWordBoundary () {
+ const position = this.getPreviousWordBoundaryBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the next word boundary.
+ moveToNextWordBoundary () {
+ const position = this.getNextWordBoundaryBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the previous subword boundary.
+ moveToPreviousSubwordBoundary () {
+ const options = {wordRegex: this.subwordRegExp({backwards: true})}
+ const position = this.getPreviousWordBoundaryBufferPosition(options)
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the next subword boundary.
+ moveToNextSubwordBoundary () {
+ const options = {wordRegex: this.subwordRegExp()}
+ const position = this.getNextWordBoundaryBufferPosition(options)
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the beginning of the buffer line, skipping all
+ // whitespace.
+ skipLeadingWhitespace () {
+ const position = this.getBufferPosition()
+ const scanRange = this.getCurrentLineBufferRange()
+ let endOfLeadingWhitespace = null
+ this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({range}) => {
+ endOfLeadingWhitespace = range.end
+ })
+
+ if (endOfLeadingWhitespace.isGreaterThan(position)) this.setBufferPosition(endOfLeadingWhitespace)
+ }
+
+ // Public: Moves the cursor to the beginning of the next paragraph
+ moveToBeginningOfNextParagraph () {
+ const position = this.getBeginningOfNextParagraphBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ // Public: Moves the cursor to the beginning of the previous paragraph
+ moveToBeginningOfPreviousParagraph () {
+ const position = this.getBeginningOfPreviousParagraphBufferPosition()
+ if (position) this.setBufferPosition(position)
+ }
+
+ /*
+ Section: Local Positions and Ranges
+ */
+
+ // Public: Returns buffer position of previous word boundary. It might be on
+ // the current word, or the previous word.
+ //
+ // * `options` (optional) {Object} with the following keys:
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp})
+ getPreviousWordBoundaryBufferPosition (options = {}) {
+ const currentBufferPosition = this.getBufferPosition()
+ const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row)
+ const scanRange = [[previousNonBlankRow || 0, 0], currentBufferPosition]
+
+ let beginningOfWordPosition
+ this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => {
+ if (range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0) {
+ // force it to stop at the beginning of each line
+ beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
+ } else if (range.end.isLessThan(currentBufferPosition)) {
+ beginningOfWordPosition = range.end
+ } else {
+ beginningOfWordPosition = range.start
+ }
+
+ if (!beginningOfWordPosition.isEqual(currentBufferPosition)) stop()
+ })
+
+ return beginningOfWordPosition || currentBufferPosition
+ }
+
+ // Public: Returns buffer position of the next word boundary. It might be on
+ // the current word, or the previous word.
+ //
+ // * `options` (optional) {Object} with the following keys:
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp})
+ getNextWordBoundaryBufferPosition (options = {}) {
+ const currentBufferPosition = this.getBufferPosition()
+ const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()]
+
+ let endOfWordPosition
+ this.editor.scanInBufferRange((options.wordRegex != null ? options.wordRegex : this.wordRegExp()), scanRange, function ({range, stop}) {
+ if (range.start.row > currentBufferPosition.row) {
+ // force it to stop at the beginning of each line
+ endOfWordPosition = new Point(range.start.row, 0)
+ } else if (range.start.isGreaterThan(currentBufferPosition)) {
+ endOfWordPosition = range.start
+ } else {
+ endOfWordPosition = range.end
+ }
+
+ if (!endOfWordPosition.isEqual(currentBufferPosition)) stop()
+ })
+
+ return endOfWordPosition || currentBufferPosition
+ }
+
+ // Public: Retrieves the buffer position of where the current word starts.
+ //
+ // * `options` (optional) An {Object} with the following keys:
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp}).
+ // * `includeNonWordCharacters` A {Boolean} indicating whether to include
+ // non-word characters in the default word regex.
+ // Has no effect if wordRegex is set.
+ // * `allowPrevious` A {Boolean} indicating whether the beginning of the
+ // previous word can be returned.
+ //
+ // Returns a {Range}.
+ getBeginningOfCurrentWordBufferPosition (options = {}) {
+ const allowPrevious = options.allowPrevious !== false
+ const position = this.getBufferPosition()
+
+ const scanRange = allowPrevious
+ ? new Range(new Point(position.row - 1, 0), position)
+ : new Range(new Point(position.row, 0), position)
+
+ const ranges = this.editor.buffer.findAllInRangeSync(
+ options.wordRegex || this.wordRegExp(),
+ scanRange
+ )
+
+ let result
+ for (let range of ranges) {
+ if (position.isLessThanOrEqual(range.start)) break
+ if (allowPrevious || position.isLessThanOrEqual(range.end)) result = range.start
+ }
+
+ return result || (allowPrevious ? new Point(0, 0) : position)
+ }
+
+ // Public: Retrieves the buffer position of where the current word ends.
+ //
+ // * `options` (optional) {Object} with the following keys:
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp})
+ // * `includeNonWordCharacters` A Boolean indicating whether to include
+ // non-word characters in the default word regex. Has no effect if
+ // wordRegex is set.
+ //
+ // Returns a {Range}.
+ getEndOfCurrentWordBufferPosition (options = {}) {
+ const allowNext = options.allowNext !== false
+ const position = this.getBufferPosition()
+
+ const scanRange = allowNext
+ ? new Range(position, new Point(position.row + 2, 0))
+ : new Range(position, new Point(position.row, Infinity))
+
+ const ranges = this.editor.buffer.findAllInRangeSync(
+ options.wordRegex || this.wordRegExp(),
+ scanRange
+ )
+
+ for (let range of ranges) {
+ if (position.isLessThan(range.start) && !allowNext) break
+ if (position.isLessThan(range.end)) return range.end
+ }
+
+ return allowNext ? this.editor.getEofBufferPosition() : position
+ }
+
+ // Public: Retrieves the buffer position of where the next word starts.
+ //
+ // * `options` (optional) {Object}
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp}).
+ //
+ // Returns a {Range}
+ getBeginningOfNextWordBufferPosition (options = {}) {
+ const currentBufferPosition = this.getBufferPosition()
+ const start = this.isInsideWord(options) ? this.getEndOfCurrentWordBufferPosition(options) : currentBufferPosition
+ const scanRange = [start, this.editor.getEofBufferPosition()]
+
+ let beginningOfNextWordPosition
+ this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => {
+ beginningOfNextWordPosition = range.start
+ stop()
+ })
+
+ return beginningOfNextWordPosition || currentBufferPosition
+ }
+
+ // Public: Returns the buffer Range occupied by the word located under the cursor.
+ //
+ // * `options` (optional) {Object}
+ // * `wordRegex` A {RegExp} indicating what constitutes a "word"
+ // (default: {::wordRegExp}).
+ getCurrentWordBufferRange (options = {}) {
+ const position = this.getBufferPosition()
+ const ranges = this.editor.buffer.findAllInRangeSync(
+ options.wordRegex || this.wordRegExp(),
+ new Range(new Point(position.row, 0), new Point(position.row, Infinity))
+ )
+ return ranges.find(range =>
+ range.end.column >= position.column && range.start.column <= position.column
+ ) || new Range(position, position)
+ }
+
+ // Public: Returns the buffer Range for the current line.
+ //
+ // * `options` (optional) {Object}
+ // * `includeNewline` A {Boolean} which controls whether the Range should
+ // include the newline.
+ getCurrentLineBufferRange (options) {
+ return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options)
+ }
+
+ // Public: Retrieves the range for the current paragraph.
+ //
+ // A paragraph is defined as a block of text surrounded by empty lines or comments.
+ //
+ // Returns a {Range}.
+ getCurrentParagraphBufferRange () {
+ return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow())
+ }
+
+ // Public: Returns the characters preceding the cursor in the current word.
+ getCurrentWordPrefix () {
+ return this.editor.getTextInBufferRange([this.getBeginningOfCurrentWordBufferPosition(), this.getBufferPosition()])
+ }
+
+ /*
+ Section: Visibility
+ */
+
+ /*
+ Section: Comparing to another cursor
+ */
+
+ // Public: Compare this cursor's buffer position to another cursor's buffer position.
+ //
+ // See {Point::compare} for more details.
+ //
+ // * `otherCursor`{Cursor} to compare against
+ compare (otherCursor) {
+ return this.getBufferPosition().compare(otherCursor.getBufferPosition())
+ }
+
+ /*
+ Section: Utilities
+ */
+
+ // Public: Deselects the current selection.
+ clearSelection (options) {
+ if (this.selection) this.selection.clear(options)
+ }
+
+ // Public: Get the RegExp used by the cursor to determine what a "word" is.
+ //
+ // * `options` (optional) {Object} with the following keys:
+ // * `includeNonWordCharacters` A {Boolean} indicating whether to include
+ // non-word characters in the regex. (default: true)
+ //
+ // Returns a {RegExp}.
+ wordRegExp (options) {
+ const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters())
+ let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+`
+ if (!options || options.includeNonWordCharacters !== false) {
+ source += `|${`[${nonWordCharacters}]+`}`
+ }
+ return new RegExp(source, 'g')
+ }
+
+ // Public: Get the RegExp used by the cursor to determine what a "subword" is.
+ //
+ // * `options` (optional) {Object} with the following keys:
+ // * `backwards` A {Boolean} indicating whether to look forwards or backwards
+ // for the next subword. (default: false)
+ //
+ // Returns a {RegExp}.
+ subwordRegExp (options = {}) {
+ const nonWordCharacters = this.getNonWordCharacters()
+ const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
+ const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
+ const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+`
+ const segments = [
+ '^[\t ]+',
+ '[\t ]+$',
+ `[${uppercaseLetters}]+(?![${lowercaseLetters}])`,
+ '\\d+'
+ ]
+ if (options.backwards) {
+ segments.push(`${snakeCamelSegment}_*`)
+ segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`)
+ } else {
+ segments.push(`_*${snakeCamelSegment}`)
+ segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`)
+ }
+ segments.push('_+')
+ return new RegExp(segments.join('|'), 'g')
+ }
+
+ /*
+ Section: Private
+ */
+
+ getNonWordCharacters () {
+ return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray())
+ }
+
+ changePosition (options, fn) {
+ this.clearSelection({autoscroll: false})
+ fn()
+ const autoscroll = (options && options.autoscroll != null)
+ ? options.autoscroll
+ : this.isLastCursor()
+ if (autoscroll) this.autoscroll()
+ }
+
+ getScreenRange () {
+ const {row, column} = this.getScreenPosition()
+ return new Range(new Point(row, column), new Point(row, column + 1))
+ }
+
+ autoscroll (options = {}) {
+ options.clip = false
+ this.editor.scrollToScreenRange(this.getScreenRange(), options)
+ }
+
+ getBeginningOfNextParagraphBufferPosition () {
+ const start = this.getBufferPosition()
+ const eof = this.editor.getEofBufferPosition()
+ const scanRange = [start, eof]
+
+ const {row, column} = eof
+ let position = new Point(row, column - 1)
+
+ this.editor.scanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
+ position = range.start.traverse(Point(1, 0))
+ if (!position.isEqual(start)) stop()
+ })
+ return position
+ }
+
+ getBeginningOfPreviousParagraphBufferPosition () {
+ const start = this.getBufferPosition()
+
+ const {row, column} = start
+ const scanRange = [[row - 1, column], [0, 0]]
+ let position = new Point(0, 0)
+ this.editor.backwardsScanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
+ position = range.start.traverse(Point(1, 0))
+ if (!position.isEqual(start)) stop()
+ })
+ return position
+ }
+}
diff --git a/src/git-repository.coffee b/src/git-repository.coffee
deleted file mode 100644
index c7105baef..000000000
--- a/src/git-repository.coffee
+++ /dev/null
@@ -1,496 +0,0 @@
-{join} = require 'path'
-
-_ = require 'underscore-plus'
-{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
-fs = require 'fs-plus'
-path = require 'path'
-GitUtils = require 'git-utils'
-
-Task = require './task'
-
-# Extended: Represents the underlying git operations performed by Atom.
-#
-# This class shouldn't be instantiated directly but instead by accessing the
-# `atom.project` global and calling `getRepositories()`. Note that this will
-# only be available when the project is backed by a Git repository.
-#
-# This class handles submodules automatically by taking a `path` argument to many
-# of the methods. This `path` argument will determine which underlying
-# repository is used.
-#
-# For a repository with submodules this would have the following outcome:
-#
-# ```coffee
-# repo = atom.project.getRepositories()[0]
-# repo.getShortHead() # 'master'
-# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
-# ```
-#
-# ## Examples
-#
-# ### Logging the URL of the origin remote
-#
-# ```coffee
-# git = atom.project.getRepositories()[0]
-# console.log git.getOriginURL()
-# ```
-#
-# ### Requiring in packages
-#
-# ```coffee
-# {GitRepository} = require 'atom'
-# ```
-module.exports =
-class GitRepository
- @exists: (path) ->
- if git = @open(path)
- git.destroy()
- true
- else
- false
-
- ###
- Section: Construction and Destruction
- ###
-
- # Public: Creates a new GitRepository instance.
- #
- # * `path` The {String} path to the Git repository to open.
- # * `options` An optional {Object} with the following keys:
- # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
- # statuses when the window is focused.
- #
- # Returns a {GitRepository} instance or `null` if the repository could not be opened.
- @open: (path, options) ->
- return null unless path
- try
- new GitRepository(path, options)
- catch
- null
-
- constructor: (path, options={}) ->
- @emitter = new Emitter
- @subscriptions = new CompositeDisposable
-
- @repo = GitUtils.open(path)
- unless @repo?
- throw new Error("No Git repository found searching path: #{path}")
-
- @statuses = {}
- @upstream = {ahead: 0, behind: 0}
- for submodulePath, submoduleRepo of @repo.submodules
- submoduleRepo.upstream = {ahead: 0, behind: 0}
-
- {@project, @config, refreshOnWindowFocus} = options
-
- refreshOnWindowFocus ?= true
- if refreshOnWindowFocus
- onWindowFocus = =>
- @refreshIndex()
- @refreshStatus()
-
- window.addEventListener 'focus', onWindowFocus
- @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus)
-
- if @project?
- @project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer)
- @subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer)
-
- # Public: Destroy this {GitRepository} object.
- #
- # This destroys any tasks and subscriptions and releases the underlying
- # libgit2 repository handle. This method is idempotent.
- destroy: ->
- if @emitter?
- @emitter.emit 'did-destroy'
- @emitter.dispose()
- @emitter = null
-
- if @statusTask?
- @statusTask.terminate()
- @statusTask = null
-
- if @repo?
- @repo.release()
- @repo = null
-
- if @subscriptions?
- @subscriptions.dispose()
- @subscriptions = null
-
- # Public: Returns a {Boolean} indicating if this repository has been destroyed.
- isDestroyed: ->
- not @repo?
-
- # Public: Invoke the given callback when this GitRepository's destroy() method
- # is invoked.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.once 'did-destroy', callback
-
- ###
- Section: Event Subscription
- ###
-
- # Public: Invoke the given callback when a specific file's status has
- # changed. When a file is updated, reloaded, etc, and the status changes, this
- # will be fired.
- #
- # * `callback` {Function}
- # * `event` {Object}
- # * `path` {String} the old parameters the decoration used to have
- # * `pathStatus` {Number} representing the status. This value can be passed to
- # {::isStatusModified} or {::isStatusNew} to get more information.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeStatus: (callback) ->
- @emitter.on 'did-change-status', callback
-
- # Public: Invoke the given callback when a multiple files' statuses have
- # changed. For example, on window focus, the status of all the paths in the
- # repo is checked. If any of them have changed, this will be fired. Call
- # {::getPathStatus(path)} to get the status for your path of choice.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeStatuses: (callback) ->
- @emitter.on 'did-change-statuses', callback
-
- ###
- Section: Repository Details
- ###
-
- # Public: A {String} indicating the type of version control system used by
- # this repository.
- #
- # Returns `"git"`.
- getType: -> 'git'
-
- # Public: Returns the {String} path of the repository.
- getPath: ->
- @path ?= fs.absolute(@getRepo().getPath())
-
- # Public: Returns the {String} working directory path of the repository.
- getWorkingDirectory: -> @getRepo().getWorkingDirectory()
-
- # Public: Returns true if at the root, false if in a subfolder of the
- # repository.
- isProjectAtRoot: ->
- @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is ''
-
- # Public: Makes a path relative to the repository's working directory.
- relativize: (path) -> @getRepo().relativize(path)
-
- # Public: Returns true if the given branch exists.
- hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
-
- # Public: Retrieves a shortened version of the HEAD reference value.
- #
- # This removes the leading segments of `refs/heads`, `refs/tags`, or
- # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
- # characters.
- #
- # * `path` An optional {String} path in the repository to get this information
- # for, only needed if the repository contains submodules.
- #
- # Returns a {String}.
- getShortHead: (path) -> @getRepo(path).getShortHead()
-
- # Public: Is the given path a submodule in the repository?
- #
- # * `path` The {String} path to check.
- #
- # Returns a {Boolean}.
- isSubmodule: (path) ->
- return false unless path
-
- repo = @getRepo(path)
- if repo.isSubmodule(repo.relativize(path))
- true
- else
- # Check if the path is a working directory in a repo that isn't the root.
- repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir'
-
- # Public: Returns the number of commits behind the current branch is from the
- # its upstream remote branch.
- #
- # * `reference` The {String} branch reference name.
- # * `path` The {String} path in the repository to get this information for,
- # only needed if the repository contains submodules.
- getAheadBehindCount: (reference, path) ->
- @getRepo(path).getAheadBehindCount(reference)
-
- # Public: Get the cached ahead/behind commit counts for the current branch's
- # upstream branch.
- #
- # * `path` An optional {String} path in the repository to get this information
- # for, only needed if the repository has submodules.
- #
- # Returns an {Object} with the following keys:
- # * `ahead` The {Number} of commits ahead.
- # * `behind` The {Number} of commits behind.
- getCachedUpstreamAheadBehindCount: (path) ->
- @getRepo(path).upstream ? @upstream
-
- # Public: Returns the git configuration value specified by the key.
- #
- # * `key` The {String} key for the configuration to lookup.
- # * `path` An optional {String} path in the repository to get this information
- # for, only needed if the repository has submodules.
- getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key)
-
- # Public: Returns the origin url of the repository.
- #
- # * `path` (optional) {String} path in the repository to get this information
- # for, only needed if the repository has submodules.
- getOriginURL: (path) -> @getConfigValue('remote.origin.url', path)
-
- # Public: Returns the upstream branch for the current HEAD, or null if there
- # is no upstream branch for the current HEAD.
- #
- # * `path` An optional {String} path in the repo to get this information for,
- # only needed if the repository contains submodules.
- #
- # Returns a {String} branch name such as `refs/remotes/origin/master`.
- getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch()
-
- # Public: Gets all the local and remote references.
- #
- # * `path` An optional {String} path in the repository to get this information
- # for, only needed if the repository has submodules.
- #
- # Returns an {Object} with the following keys:
- # * `heads` An {Array} of head reference names.
- # * `remotes` An {Array} of remote reference names.
- # * `tags` An {Array} of tag reference names.
- getReferences: (path) -> @getRepo(path).getReferences()
-
- # Public: Returns the current {String} SHA for the given reference.
- #
- # * `reference` The {String} reference to get the target of.
- # * `path` An optional {String} path in the repo to get the reference target
- # for. Only needed if the repository contains submodules.
- getReferenceTarget: (reference, path) ->
- @getRepo(path).getReferenceTarget(reference)
-
- ###
- Section: Reading Status
- ###
-
- # Public: Returns true if the given path is modified.
- #
- # * `path` The {String} path to check.
- #
- # Returns a {Boolean} that's true if the `path` is modified.
- isPathModified: (path) -> @isStatusModified(@getPathStatus(path))
-
- # Public: Returns true if the given path is new.
- #
- # * `path` The {String} path to check.
- #
- # Returns a {Boolean} that's true if the `path` is new.
- isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
-
- # Public: Is the given path ignored?
- #
- # * `path` The {String} path to check.
- #
- # Returns a {Boolean} that's true if the `path` is ignored.
- isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path))
-
- # Public: Get the status of a directory in the repository's working directory.
- #
- # * `path` The {String} path to check.
- #
- # Returns a {Number} representing the status. This value can be passed to
- # {::isStatusModified} or {::isStatusNew} to get more information.
- getDirectoryStatus: (directoryPath) ->
- directoryPath = "#{@relativize(directoryPath)}/"
- directoryStatus = 0
- for statusPath, status of @statuses
- directoryStatus |= status if statusPath.indexOf(directoryPath) is 0
- directoryStatus
-
- # Public: Get the status of a single path in the repository.
- #
- # * `path` A {String} repository-relative path.
- #
- # Returns a {Number} representing the status. This value can be passed to
- # {::isStatusModified} or {::isStatusNew} to get more information.
- getPathStatus: (path) ->
- repo = @getRepo(path)
- relativePath = @relativize(path)
- currentPathStatus = @statuses[relativePath] ? 0
- pathStatus = repo.getStatus(repo.relativize(path)) ? 0
- pathStatus = 0 if repo.isStatusIgnored(pathStatus)
- if pathStatus > 0
- @statuses[relativePath] = pathStatus
- else
- delete @statuses[relativePath]
- if currentPathStatus isnt pathStatus
- @emitter.emit 'did-change-status', {path, pathStatus}
-
- pathStatus
-
- # Public: Get the cached status for the given path.
- #
- # * `path` A {String} path in the repository, relative or absolute.
- #
- # Returns a status {Number} or null if the path is not in the cache.
- getCachedPathStatus: (path) ->
- @statuses[@relativize(path)]
-
- # Public: Returns true if the given status indicates modification.
- #
- # * `status` A {Number} representing the status.
- #
- # Returns a {Boolean} that's true if the `status` indicates modification.
- isStatusModified: (status) -> @getRepo().isStatusModified(status)
-
- # Public: Returns true if the given status indicates a new path.
- #
- # * `status` A {Number} representing the status.
- #
- # Returns a {Boolean} that's true if the `status` indicates a new path.
- isStatusNew: (status) -> @getRepo().isStatusNew(status)
-
- ###
- Section: Retrieving Diffs
- ###
-
- # Public: Retrieves the number of lines added and removed to a path.
- #
- # This compares the working directory contents of the path to the `HEAD`
- # version.
- #
- # * `path` The {String} path to check.
- #
- # Returns an {Object} with the following keys:
- # * `added` The {Number} of added lines.
- # * `deleted` The {Number} of deleted lines.
- getDiffStats: (path) ->
- repo = @getRepo(path)
- repo.getDiffStats(repo.relativize(path))
-
- # Public: Retrieves the line diffs comparing the `HEAD` version of the given
- # path and the given text.
- #
- # * `path` The {String} path relative to the repository.
- # * `text` The {String} to compare against the `HEAD` contents
- #
- # Returns an {Array} of hunk {Object}s with the following keys:
- # * `oldStart` The line {Number} of the old hunk.
- # * `newStart` The line {Number} of the new hunk.
- # * `oldLines` The {Number} of lines in the old hunk.
- # * `newLines` The {Number} of lines in the new hunk
- getLineDiffs: (path, text) ->
- # Ignore eol of line differences on windows so that files checked in as
- # LF don't report every line modified when the text contains CRLF endings.
- options = ignoreEolWhitespace: process.platform is 'win32'
- repo = @getRepo(path)
- repo.getLineDiffs(repo.relativize(path), text, options)
-
- ###
- Section: Checking Out
- ###
-
- # Public: Restore the contents of a path in the working directory and index
- # to the version at `HEAD`.
- #
- # This is essentially the same as running:
- #
- # ```sh
- # git reset HEAD --
- # git checkout HEAD --
- # ```
- #
- # * `path` The {String} path to checkout.
- #
- # Returns a {Boolean} that's true if the method was successful.
- checkoutHead: (path) ->
- repo = @getRepo(path)
- headCheckedOut = repo.checkoutHead(repo.relativize(path))
- @getPathStatus(path) if headCheckedOut
- headCheckedOut
-
- # Public: Checks out a branch in your repository.
- #
- # * `reference` The {String} reference to checkout.
- # * `create` A {Boolean} value which, if true creates the new reference if
- # it doesn't exist.
- #
- # Returns a Boolean that's true if the method was successful.
- checkoutReference: (reference, create) ->
- @getRepo().checkoutReference(reference, create)
-
- ###
- Section: Private
- ###
-
- # Subscribes to buffer events.
- subscribeToBuffer: (buffer) ->
- getBufferPathStatus = =>
- if bufferPath = buffer.getPath()
- @getPathStatus(bufferPath)
-
- getBufferPathStatus()
- bufferSubscriptions = new CompositeDisposable
- bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus)
- bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus)
- bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus)
- bufferSubscriptions.add buffer.onDidDestroy =>
- bufferSubscriptions.dispose()
- @subscriptions.remove(bufferSubscriptions)
- @subscriptions.add(bufferSubscriptions)
- return
-
- # Subscribes to editor view event.
- checkoutHeadForEditor: (editor) ->
- buffer = editor.getBuffer()
- if filePath = buffer.getPath()
- @checkoutHead(filePath)
- buffer.reload()
-
- # Returns the corresponding {Repository}
- getRepo: (path) ->
- if @repo?
- @repo.submoduleForPath(path) ? @repo
- else
- throw new Error("Repository has been destroyed")
-
- # Reread the index to update any values that have changed since the
- # last time the index was read.
- refreshIndex: -> @getRepo().refreshIndex()
-
- # Refreshes the current git status in an outside process and asynchronously
- # updates the relevant properties.
- refreshStatus: ->
- @handlerPath ?= require.resolve('./repository-status-handler')
-
- relativeProjectPaths = @project?.getPaths()
- .map (projectPath) => @relativize(projectPath)
- .filter (projectPath) -> projectPath.length > 0 and not path.isAbsolute(projectPath)
-
- @statusTask?.terminate()
- new Promise (resolve) =>
- @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
- statusesUnchanged = _.isEqual(statuses, @statuses) and
- _.isEqual(upstream, @upstream) and
- _.isEqual(branch, @branch) and
- _.isEqual(submodules, @submodules)
-
- @statuses = statuses
- @upstream = upstream
- @branch = branch
- @submodules = submodules
-
- for submodulePath, submoduleRepo of @getRepo().submodules
- submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
-
- unless statusesUnchanged
- @emitter.emit 'did-change-statuses'
- resolve()
diff --git a/src/git-repository.js b/src/git-repository.js
new file mode 100644
index 000000000..057c5fcb7
--- /dev/null
+++ b/src/git-repository.js
@@ -0,0 +1,603 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS104: Avoid inline assignments
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const {join} = require('path')
+const _ = require('underscore-plus')
+const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
+const fs = require('fs-plus')
+const path = require('path')
+const GitUtils = require('git-utils')
+
+let nextId = 0
+
+// Extended: Represents the underlying git operations performed by Atom.
+//
+// This class shouldn't be instantiated directly but instead by accessing the
+// `atom.project` global and calling `getRepositories()`. Note that this will
+// only be available when the project is backed by a Git repository.
+//
+// This class handles submodules automatically by taking a `path` argument to many
+// of the methods. This `path` argument will determine which underlying
+// repository is used.
+//
+// For a repository with submodules this would have the following outcome:
+//
+// ```coffee
+// repo = atom.project.getRepositories()[0]
+// repo.getShortHead() # 'master'
+// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
+// ```
+//
+// ## Examples
+//
+// ### Logging the URL of the origin remote
+//
+// ```coffee
+// git = atom.project.getRepositories()[0]
+// console.log git.getOriginURL()
+// ```
+//
+// ### Requiring in packages
+//
+// ```coffee
+// {GitRepository} = require 'atom'
+// ```
+module.exports =
+class GitRepository {
+ static exists (path) {
+ const git = this.open(path)
+ if (git) {
+ git.destroy()
+ return true
+ } else {
+ return false
+ }
+ }
+
+ /*
+ Section: Construction and Destruction
+ */
+
+ // Public: Creates a new GitRepository instance.
+ //
+ // * `path` The {String} path to the Git repository to open.
+ // * `options` An optional {Object} with the following keys:
+ // * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
+ // statuses when the window is focused.
+ //
+ // Returns a {GitRepository} instance or `null` if the repository could not be opened.
+ static open (path, options) {
+ if (!path) { return null }
+ try {
+ return new GitRepository(path, options)
+ } catch (error) {
+ return null
+ }
+ }
+
+ constructor (path, options = {}) {
+ this.id = nextId++
+ this.emitter = new Emitter()
+ this.subscriptions = new CompositeDisposable()
+ this.repo = GitUtils.open(path)
+ if (this.repo == null) {
+ throw new Error(`No Git repository found searching path: ${path}`)
+ }
+
+ this.statusRefreshCount = 0
+ this.statuses = {}
+ this.upstream = {ahead: 0, behind: 0}
+ for (let submodulePath in this.repo.submodules) {
+ const submoduleRepo = this.repo.submodules[submodulePath]
+ submoduleRepo.upstream = {ahead: 0, behind: 0}
+ }
+
+ this.project = options.project
+ this.config = options.config
+
+ if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) {
+ const onWindowFocus = () => {
+ this.refreshIndex()
+ this.refreshStatus()
+ }
+
+ window.addEventListener('focus', onWindowFocus)
+ this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus)))
+ }
+
+ if (this.project != null) {
+ this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer))
+ this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer)))
+ }
+ }
+
+ // Public: Destroy this {GitRepository} object.
+ //
+ // This destroys any tasks and subscriptions and releases the underlying
+ // libgit2 repository handle. This method is idempotent.
+ destroy () {
+ this.repo = null
+
+ if (this.emitter) {
+ this.emitter.emit('did-destroy')
+ this.emitter.dispose()
+ this.emitter = null
+ }
+
+ if (this.subscriptions) {
+ this.subscriptions.dispose()
+ this.subscriptions = null
+ }
+ }
+
+ // Public: Returns a {Boolean} indicating if this repository has been destroyed.
+ isDestroyed () {
+ return this.repo == null
+ }
+
+ // Public: Invoke the given callback when this GitRepository's destroy() method
+ // is invoked.
+ //
+ // * `callback` {Function}
+ //
+ // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+ onDidDestroy (callback) {
+ return this.emitter.once('did-destroy', callback)
+ }
+
+ /*
+ Section: Event Subscription
+ */
+
+ // Public: Invoke the given callback when a specific file's status has
+ // changed. When a file is updated, reloaded, etc, and the status changes, this
+ // will be fired.
+ //
+ // * `callback` {Function}
+ // * `event` {Object}
+ // * `path` {String} the old parameters the decoration used to have
+ // * `pathStatus` {Number} representing the status. This value can be passed to
+ // {::isStatusModified} or {::isStatusNew} to get more information.
+ //
+ // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+ onDidChangeStatus (callback) {
+ return this.emitter.on('did-change-status', callback)
+ }
+
+ // Public: Invoke the given callback when a multiple files' statuses have
+ // changed. For example, on window focus, the status of all the paths in the
+ // repo is checked. If any of them have changed, this will be fired. Call
+ // {::getPathStatus(path)} to get the status for your path of choice.
+ //
+ // * `callback` {Function}
+ //
+ // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+ onDidChangeStatuses (callback) {
+ return this.emitter.on('did-change-statuses', callback)
+ }
+
+ /*
+ Section: Repository Details
+ */
+
+ // Public: A {String} indicating the type of version control system used by
+ // this repository.
+ //
+ // Returns `"git"`.
+ getType () { return 'git' }
+
+ // Public: Returns the {String} path of the repository.
+ getPath () {
+ if (this.path == null) {
+ this.path = fs.absolute(this.getRepo().getPath())
+ }
+ return this.path
+ }
+
+ // Public: Returns the {String} working directory path of the repository.
+ getWorkingDirectory () {
+ return this.getRepo().getWorkingDirectory()
+ }
+
+ // Public: Returns true if at the root, false if in a subfolder of the
+ // repository.
+ isProjectAtRoot () {
+ if (this.projectAtRoot == null) {
+ this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === ''
+ }
+ return this.projectAtRoot
+ }
+
+ // Public: Makes a path relative to the repository's working directory.
+ relativize (path) {
+ return this.getRepo().relativize(path)
+ }
+
+ // Public: Returns true if the given branch exists.
+ hasBranch (branch) {
+ return this.getReferenceTarget(`refs/heads/${branch}`) != null
+ }
+
+ // Public: Retrieves a shortened version of the HEAD reference value.
+ //
+ // This removes the leading segments of `refs/heads`, `refs/tags`, or
+ // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
+ // characters.
+ //
+ // * `path` An optional {String} path in the repository to get this information
+ // for, only needed if the repository contains submodules.
+ //
+ // Returns a {String}.
+ getShortHead (path) {
+ return this.getRepo(path).getShortHead()
+ }
+
+ // Public: Is the given path a submodule in the repository?
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns a {Boolean}.
+ isSubmodule (path) {
+ if (!path) return false
+
+ const repo = this.getRepo(path)
+ if (repo.isSubmodule(repo.relativize(path))) {
+ return true
+ } else {
+ // Check if the path is a working directory in a repo that isn't the root.
+ return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir'
+ }
+ }
+
+ // Public: Returns the number of commits behind the current branch is from the
+ // its upstream remote branch.
+ //
+ // * `reference` The {String} branch reference name.
+ // * `path` The {String} path in the repository to get this information for,
+ // only needed if the repository contains submodules.
+ getAheadBehindCount (reference, path) {
+ return this.getRepo(path).getAheadBehindCount(reference)
+ }
+
+ // Public: Get the cached ahead/behind commit counts for the current branch's
+ // upstream branch.
+ //
+ // * `path` An optional {String} path in the repository to get this information
+ // for, only needed if the repository has submodules.
+ //
+ // Returns an {Object} with the following keys:
+ // * `ahead` The {Number} of commits ahead.
+ // * `behind` The {Number} of commits behind.
+ getCachedUpstreamAheadBehindCount (path) {
+ return this.getRepo(path).upstream || this.upstream
+ }
+
+ // Public: Returns the git configuration value specified by the key.
+ //
+ // * `key` The {String} key for the configuration to lookup.
+ // * `path` An optional {String} path in the repository to get this information
+ // for, only needed if the repository has submodules.
+ getConfigValue (key, path) {
+ return this.getRepo(path).getConfigValue(key)
+ }
+
+ // Public: Returns the origin url of the repository.
+ //
+ // * `path` (optional) {String} path in the repository to get this information
+ // for, only needed if the repository has submodules.
+ getOriginURL (path) {
+ return this.getConfigValue('remote.origin.url', path)
+ }
+
+ // Public: Returns the upstream branch for the current HEAD, or null if there
+ // is no upstream branch for the current HEAD.
+ //
+ // * `path` An optional {String} path in the repo to get this information for,
+ // only needed if the repository contains submodules.
+ //
+ // Returns a {String} branch name such as `refs/remotes/origin/master`.
+ getUpstreamBranch (path) {
+ return this.getRepo(path).getUpstreamBranch()
+ }
+
+ // Public: Gets all the local and remote references.
+ //
+ // * `path` An optional {String} path in the repository to get this information
+ // for, only needed if the repository has submodules.
+ //
+ // Returns an {Object} with the following keys:
+ // * `heads` An {Array} of head reference names.
+ // * `remotes` An {Array} of remote reference names.
+ // * `tags` An {Array} of tag reference names.
+ getReferences (path) {
+ return this.getRepo(path).getReferences()
+ }
+
+ // Public: Returns the current {String} SHA for the given reference.
+ //
+ // * `reference` The {String} reference to get the target of.
+ // * `path` An optional {String} path in the repo to get the reference target
+ // for. Only needed if the repository contains submodules.
+ getReferenceTarget (reference, path) {
+ return this.getRepo(path).getReferenceTarget(reference)
+ }
+
+ /*
+ Section: Reading Status
+ */
+
+ // Public: Returns true if the given path is modified.
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns a {Boolean} that's true if the `path` is modified.
+ isPathModified (path) {
+ return this.isStatusModified(this.getPathStatus(path))
+ }
+
+ // Public: Returns true if the given path is new.
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns a {Boolean} that's true if the `path` is new.
+ isPathNew (path) {
+ return this.isStatusNew(this.getPathStatus(path))
+ }
+
+ // Public: Is the given path ignored?
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns a {Boolean} that's true if the `path` is ignored.
+ isPathIgnored (path) {
+ return this.getRepo().isIgnored(this.relativize(path))
+ }
+
+ // Public: Get the status of a directory in the repository's working directory.
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns a {Number} representing the status. This value can be passed to
+ // {::isStatusModified} or {::isStatusNew} to get more information.
+ getDirectoryStatus (directoryPath) {
+ directoryPath = `${this.relativize(directoryPath)}/`
+ let directoryStatus = 0
+ for (let statusPath in this.statuses) {
+ const status = this.statuses[statusPath]
+ if (statusPath.startsWith(directoryPath)) directoryStatus |= status
+ }
+ return directoryStatus
+ }
+
+ // Public: Get the status of a single path in the repository.
+ //
+ // * `path` A {String} repository-relative path.
+ //
+ // Returns a {Number} representing the status. This value can be passed to
+ // {::isStatusModified} or {::isStatusNew} to get more information.
+ getPathStatus (path) {
+ const repo = this.getRepo(path)
+ const relativePath = this.relativize(path)
+ const currentPathStatus = this.statuses[relativePath] || 0
+ let pathStatus = repo.getStatus(repo.relativize(path)) || 0
+ if (repo.isStatusIgnored(pathStatus)) pathStatus = 0
+ if (pathStatus > 0) {
+ this.statuses[relativePath] = pathStatus
+ } else {
+ delete this.statuses[relativePath]
+ }
+ if (currentPathStatus !== pathStatus) {
+ this.emitter.emit('did-change-status', {path, pathStatus})
+ }
+
+ return pathStatus
+ }
+
+ // Public: Get the cached status for the given path.
+ //
+ // * `path` A {String} path in the repository, relative or absolute.
+ //
+ // Returns a status {Number} or null if the path is not in the cache.
+ getCachedPathStatus (path) {
+ return this.statuses[this.relativize(path)]
+ }
+
+ // Public: Returns true if the given status indicates modification.
+ //
+ // * `status` A {Number} representing the status.
+ //
+ // Returns a {Boolean} that's true if the `status` indicates modification.
+ isStatusModified (status) { return this.getRepo().isStatusModified(status) }
+
+ // Public: Returns true if the given status indicates a new path.
+ //
+ // * `status` A {Number} representing the status.
+ //
+ // Returns a {Boolean} that's true if the `status` indicates a new path.
+ isStatusNew (status) {
+ return this.getRepo().isStatusNew(status)
+ }
+
+ /*
+ Section: Retrieving Diffs
+ */
+
+ // Public: Retrieves the number of lines added and removed to a path.
+ //
+ // This compares the working directory contents of the path to the `HEAD`
+ // version.
+ //
+ // * `path` The {String} path to check.
+ //
+ // Returns an {Object} with the following keys:
+ // * `added` The {Number} of added lines.
+ // * `deleted` The {Number} of deleted lines.
+ getDiffStats (path) {
+ const repo = this.getRepo(path)
+ return repo.getDiffStats(repo.relativize(path))
+ }
+
+ // Public: Retrieves the line diffs comparing the `HEAD` version of the given
+ // path and the given text.
+ //
+ // * `path` The {String} path relative to the repository.
+ // * `text` The {String} to compare against the `HEAD` contents
+ //
+ // Returns an {Array} of hunk {Object}s with the following keys:
+ // * `oldStart` The line {Number} of the old hunk.
+ // * `newStart` The line {Number} of the new hunk.
+ // * `oldLines` The {Number} of lines in the old hunk.
+ // * `newLines` The {Number} of lines in the new hunk
+ getLineDiffs (path, text) {
+ // Ignore eol of line differences on windows so that files checked in as
+ // LF don't report every line modified when the text contains CRLF endings.
+ const options = {ignoreEolWhitespace: process.platform === 'win32'}
+ const repo = this.getRepo(path)
+ return repo.getLineDiffs(repo.relativize(path), text, options)
+ }
+
+ /*
+ Section: Checking Out
+ */
+
+ // Public: Restore the contents of a path in the working directory and index
+ // to the version at `HEAD`.
+ //
+ // This is essentially the same as running:
+ //
+ // ```sh
+ // git reset HEAD --
+ // git checkout HEAD --
+ // ```
+ //
+ // * `path` The {String} path to checkout.
+ //
+ // Returns a {Boolean} that's true if the method was successful.
+ checkoutHead (path) {
+ const repo = this.getRepo(path)
+ const headCheckedOut = repo.checkoutHead(repo.relativize(path))
+ if (headCheckedOut) this.getPathStatus(path)
+ return headCheckedOut
+ }
+
+ // Public: Checks out a branch in your repository.
+ //
+ // * `reference` The {String} reference to checkout.
+ // * `create` A {Boolean} value which, if true creates the new reference if
+ // it doesn't exist.
+ //
+ // Returns a Boolean that's true if the method was successful.
+ checkoutReference (reference, create) {
+ return this.getRepo().checkoutReference(reference, create)
+ }
+
+ /*
+ Section: Private
+ */
+
+ // Subscribes to buffer events.
+ subscribeToBuffer (buffer) {
+ const getBufferPathStatus = () => {
+ const bufferPath = buffer.getPath()
+ if (bufferPath) this.getPathStatus(bufferPath)
+ }
+
+ getBufferPathStatus()
+ const bufferSubscriptions = new CompositeDisposable()
+ bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus))
+ bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus))
+ bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus))
+ bufferSubscriptions.add(buffer.onDidDestroy(() => {
+ bufferSubscriptions.dispose()
+ return this.subscriptions.remove(bufferSubscriptions)
+ }))
+ this.subscriptions.add(bufferSubscriptions)
+ }
+
+ // Subscribes to editor view event.
+ checkoutHeadForEditor (editor) {
+ const buffer = editor.getBuffer()
+ const bufferPath = buffer.getPath()
+ if (bufferPath) {
+ this.checkoutHead(bufferPath)
+ return buffer.reload()
+ }
+ }
+
+ // Returns the corresponding {Repository}
+ getRepo (path) {
+ if (this.repo) {
+ return this.repo.submoduleForPath(path) || this.repo
+ } else {
+ throw new Error('Repository has been destroyed')
+ }
+ }
+
+ // Reread the index to update any values that have changed since the
+ // last time the index was read.
+ refreshIndex () {
+ return this.getRepo().refreshIndex()
+ }
+
+ // Refreshes the current git status in an outside process and asynchronously
+ // updates the relevant properties.
+ async refreshStatus () {
+ const statusRefreshCount = ++this.statusRefreshCount
+ const repo = this.getRepo()
+
+ const relativeProjectPaths = this.project && this.project.getPaths()
+ .map(projectPath => this.relativize(projectPath))
+ .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath))
+
+ const branch = await repo.getHeadAsync()
+ const upstream = await repo.getAheadBehindCountAsync()
+
+ const statuses = {}
+ const repoStatus = relativeProjectPaths.length > 0
+ ? await repo.getStatusAsync(relativeProjectPaths)
+ : await repo.getStatusAsync()
+ for (let filePath in repoStatus) {
+ statuses[filePath] = repoStatus[filePath]
+ }
+
+ const submodules = {}
+ for (let submodulePath in repo.submodules) {
+ const submoduleRepo = repo.submodules[submodulePath]
+ submodules[submodulePath] = {
+ branch: await submoduleRepo.getHeadAsync(),
+ upstream: await submoduleRepo.getAheadBehindCountAsync()
+ }
+
+ const workingDirectoryPath = submoduleRepo.getWorkingDirectory()
+ const submoduleStatus = await submoduleRepo.getStatusAsync()
+ for (let filePath in submoduleStatus) {
+ const absolutePath = path.join(workingDirectoryPath, filePath)
+ const relativizePath = repo.relativize(absolutePath)
+ statuses[relativizePath] = submoduleStatus[filePath]
+ }
+ }
+
+ if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return
+
+ const statusesUnchanged =
+ _.isEqual(branch, this.branch) &&
+ _.isEqual(statuses, this.statuses) &&
+ _.isEqual(upstream, this.upstream) &&
+ _.isEqual(submodules, this.submodules)
+
+ this.branch = branch
+ this.statuses = statuses
+ this.upstream = upstream
+ this.submodules = submodules
+
+ for (let submodulePath in repo.submodules) {
+ repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream
+ }
+
+ if (!statusesUnchanged) this.emitter.emit('did-change-statuses')
+ }
+}
diff --git a/src/language-mode.coffee b/src/language-mode.coffee
deleted file mode 100644
index 1839f1c59..000000000
--- a/src/language-mode.coffee
+++ /dev/null
@@ -1,350 +0,0 @@
-{Range} = require 'text-buffer'
-_ = require 'underscore-plus'
-{OnigRegExp} = require 'oniguruma'
-ScopeDescriptor = require './scope-descriptor'
-NullGrammar = require './null-grammar'
-
-module.exports =
-class LanguageMode
- # Sets up a `LanguageMode` for the given {TextEditor}.
- #
- # editor - The {TextEditor} to associate with
- constructor: (@editor) ->
- {@buffer} = @editor
- @regexesByPattern = {}
-
- destroy: ->
-
- toggleLineCommentForBufferRow: (row) ->
- @toggleLineCommentsForBufferRows(row, row)
-
- # Wraps the lines between two rows in comments.
- #
- # If the language doesn't have comment, nothing happens.
- #
- # startRow - The row {Number} to start at
- # endRow - The row {Number} to end at
- toggleLineCommentsForBufferRows: (start, end) ->
- scope = @editor.scopeDescriptorForBufferPosition([start, 0])
- commentStrings = @editor.getCommentStrings(scope)
- return unless commentStrings?.commentStartString
- {commentStartString, commentEndString} = commentStrings
-
- buffer = @editor.buffer
- commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
- commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
-
- if commentEndString
- shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start))
- if shouldUncomment
- commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
- commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$")
- startMatch = commentStartRegex.searchSync(buffer.lineForRow(start))
- endMatch = commentEndRegex.searchSync(buffer.lineForRow(end))
- if startMatch and endMatch
- buffer.transact ->
- columnStart = startMatch[1].length
- columnEnd = columnStart + startMatch[2].length
- buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "")
-
- endLength = buffer.lineLengthForRow(end) - endMatch[2].length
- endColumn = endLength - endMatch[1].length
- buffer.setTextInRange([[end, endColumn], [end, endLength]], "")
- else
- buffer.transact ->
- indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0
- buffer.insert([start, indentLength], commentStartString)
- buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString)
- else
- allBlank = true
- allBlankOrCommented = true
-
- for row in [start..end] by 1
- line = buffer.lineForRow(row)
- blank = line?.match(/^\s*$/)
-
- allBlank = false unless blank
- allBlankOrCommented = false unless blank or commentStartRegex.testSync(line)
-
- shouldUncomment = allBlankOrCommented and not allBlank
-
- if shouldUncomment
- for row in [start..end] by 1
- if match = commentStartRegex.searchSync(buffer.lineForRow(row))
- columnStart = match[1].length
- columnEnd = columnStart + match[2].length
- buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "")
- else
- if start is end
- indent = @editor.indentationForBufferRow(start)
- else
- indent = @minIndentLevelForRowRange(start, end)
- indentString = @editor.buildIndentString(indent)
- tabLength = @editor.getTabLength()
- indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}")
- for row in [start..end] by 1
- line = buffer.lineForRow(row)
- if indentLength = line.match(indentRegex)?[0].length
- buffer.insert([row, indentLength], commentStartString)
- else
- buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString)
- return
-
- # Folds all the foldable lines in the buffer.
- foldAll: ->
- @unfoldAll()
- foldedRowRanges = {}
- for currentRow in [0..@buffer.getLastRow()] by 1
- rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
- continue unless startRow?
- continue if foldedRowRanges[rowRange]
-
- @editor.foldBufferRowRange(startRow, endRow)
- foldedRowRanges[rowRange] = true
- return
-
- # Unfolds all the foldable lines in the buffer.
- unfoldAll: ->
- @editor.displayLayer.destroyAllFolds()
-
- # Fold all comment and code blocks at a given indentLevel
- #
- # indentLevel - A {Number} indicating indentLevel; 0 based.
- foldAllAtIndentLevel: (indentLevel) ->
- @unfoldAll()
- foldedRowRanges = {}
- for currentRow in [0..@buffer.getLastRow()] by 1
- rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
- continue unless startRow?
- continue if foldedRowRanges[rowRange]
-
- # assumption: startRow will always be the min indent level for the entire range
- if @editor.indentationForBufferRow(startRow) is indentLevel
- @editor.foldBufferRowRange(startRow, endRow)
- foldedRowRanges[rowRange] = true
- return
-
- # Given a buffer row, creates a fold at it.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns the new {Fold}.
- foldBufferRow: (bufferRow) ->
- for currentRow in [bufferRow..0] by -1
- [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
- continue unless startRow? and startRow <= bufferRow <= endRow
- unless @editor.isFoldedAtBufferRow(startRow)
- return @editor.foldBufferRowRange(startRow, endRow)
-
- # Find the row range for a fold at a given bufferRow. Will handle comments
- # and code.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns an {Array} of the [startRow, endRow]. Returns null if no range.
- rowRangeForFoldAtBufferRow: (bufferRow) ->
- rowRange = @rowRangeForCommentAtBufferRow(bufferRow)
- rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow)
- rowRange
-
- rowRangeForCommentAtBufferRow: (bufferRow) ->
- return unless @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment()
-
- startRow = bufferRow
- endRow = bufferRow
-
- if bufferRow > 0
- for currentRow in [bufferRow-1..0] by -1
- break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment()
- startRow = currentRow
-
- if bufferRow < @buffer.getLastRow()
- for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
- break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment()
- endRow = currentRow
-
- return [startRow, endRow] if startRow isnt endRow
-
- rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
- return null unless @isFoldableAtBufferRow(bufferRow)
-
- startIndentLevel = @editor.indentationForBufferRow(bufferRow)
- scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
- for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1
- continue if @editor.isBufferRowBlank(row)
- indentation = @editor.indentationForBufferRow(row)
- if indentation <= startIndentLevel
- includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
- foldEndRow = row if includeRowInFold
- break
-
- foldEndRow = row
-
- [bufferRow, foldEndRow]
-
- isFoldableAtBufferRow: (bufferRow) ->
- @editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
-
- # Returns a {Boolean} indicating whether the line at the given buffer
- # row is a comment.
- isLineCommentedAtBufferRow: (bufferRow) ->
- return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
- @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false
-
- # Find a row range for a 'paragraph' around specified bufferRow. A paragraph
- # is a block of text bounded by and empty line or a block of text that is not
- # the same type (comments next to source code).
- rowRangeForParagraphAtBufferRow: (bufferRow) ->
- scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
- commentStrings = @editor.getCommentStrings(scope)
- commentStartRegex = null
- if commentStrings?.commentStartString? and not commentStrings.commentEndString?
- commentStartRegexString = _.escapeRegExp(commentStrings.commentStartString).replace(/(\s+)$/, '(?:$1)?')
- commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
-
- filterCommentStart = (line) ->
- if commentStartRegex?
- matches = commentStartRegex.searchSync(line)
- line = line.substring(matches[0].end) if matches?.length
- line
-
- return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow)))
-
- if @isLineCommentedAtBufferRow(bufferRow)
- isOriginalRowComment = true
- range = @rowRangeForCommentAtBufferRow(bufferRow)
- [firstRow, lastRow] = range or [bufferRow, bufferRow]
- else
- isOriginalRowComment = false
- [firstRow, lastRow] = [0, @editor.getLastBufferRow()-1]
-
- startRow = bufferRow
- while startRow > firstRow
- break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment
- break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
- startRow--
-
- endRow = bufferRow
- lastRow = @editor.getLastBufferRow()
- while endRow < lastRow
- break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment
- break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1)))
- endRow++
-
- new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
-
- # Given a buffer row, this returns a suggested indentation level.
- #
- # The indentation level provided is based on the current {LanguageMode}.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns a {Number}.
- suggestedIndentForBufferRow: (bufferRow, options) ->
- line = @buffer.lineForRow(bufferRow)
- tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
- @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
-
- suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
- tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
- @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
-
- suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->
- iterator = tokenizedLine.getTokenIterator()
- iterator.next()
- scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes())
-
- increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
- decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
- decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
-
- if options?.skipBlankLines ? true
- precedingRow = @buffer.previousNonBlankRow(bufferRow)
- return 0 unless precedingRow?
- else
- precedingRow = bufferRow - 1
- return 0 if precedingRow < 0
-
- desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
- return desiredIndentLevel unless increaseIndentRegex
-
- unless @editor.isBufferRowCommented(precedingRow)
- precedingLine = @buffer.lineForRow(precedingRow)
- desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine)
- desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine)
-
- unless @buffer.isRowBlank(precedingRow)
- desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line)
-
- Math.max(desiredIndentLevel, 0)
-
- # Calculate a minimum indent level for a range of lines excluding empty lines.
- #
- # startRow - The row {Number} to start at
- # endRow - The row {Number} to end at
- #
- # Returns a {Number} of the indent level of the block of lines.
- minIndentLevelForRowRange: (startRow, endRow) ->
- indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row))
- indents = [0] unless indents.length
- Math.min(indents...)
-
- # 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) ->
- @autoIndentBufferRow(row) for row in [startRow..endRow] by 1
- return
-
- # Given a buffer row, this indents it.
- #
- # bufferRow - The row {Number}.
- # options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
- autoIndentBufferRow: (bufferRow, options) ->
- indentLevel = @suggestedIndentForBufferRow(bufferRow, options)
- @editor.setIndentationForBufferRow(bufferRow, indentLevel, options)
-
- # Given a buffer row, this decreases the indentation.
- #
- # bufferRow - The row {Number}
- autoDecreaseIndentForBufferRow: (bufferRow) ->
- scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
- return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
-
- line = @buffer.lineForRow(bufferRow)
- return unless decreaseIndentRegex.testSync(line)
-
- currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
- return if currentIndentLevel is 0
-
- precedingRow = @buffer.previousNonBlankRow(bufferRow)
- return unless precedingRow?
-
- precedingLine = @buffer.lineForRow(precedingRow)
- desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
-
- if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
- desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine)
-
- if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
- desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine)
-
- if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
- @editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
-
- cacheRegex: (pattern) ->
- if pattern
- @regexesByPattern[pattern] ?= new OnigRegExp(pattern)
-
- increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
- @cacheRegex(@editor.getIncreaseIndentPattern(scopeDescriptor))
-
- decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
- @cacheRegex(@editor.getDecreaseIndentPattern(scopeDescriptor))
-
- decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) ->
- @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor))
-
- foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
- @cacheRegex(@editor.getFoldEndPattern(scopeDescriptor))
diff --git a/src/package.coffee b/src/package.coffee
index d5e13b6b9..42647acb5 100644
--- a/src/package.coffee
+++ b/src/package.coffee
@@ -534,7 +534,7 @@ class Package
console.error "Error deactivating package '#{@name}'", e.stack
# We support then-able async promises as well as sync ones from deactivate
- if deactivationResult?.then is 'function'
+ if typeof deactivationResult?.then is 'function'
deactivationResult.then => @afterDeactivation()
else
@afterDeactivation()
diff --git a/src/pane-element.coffee b/src/pane-element.coffee
index c4866816a..d68b3b834 100644
--- a/src/pane-element.coffee
+++ b/src/pane-element.coffee
@@ -79,6 +79,7 @@ class PaneElement extends HTMLElement
activeItemChanged: (item) ->
delete @dataset.activeItemName
delete @dataset.activeItemPath
+ @changePathDisposable?.dispose()
return unless item?
@@ -89,6 +90,12 @@ class PaneElement extends HTMLElement
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
+ if item.onDidChangePath?
+ @changePathDisposable = item.onDidChangePath =>
+ itemPath = item.getPath()
+ @dataset.activeItemName = path.basename(itemPath)
+ @dataset.activeItemPath = itemPath
+
unless @itemViews.contains(itemView)
@itemViews.appendChild(itemView)
@@ -119,6 +126,7 @@ class PaneElement extends HTMLElement
paneDestroyed: ->
@subscriptions.dispose()
+ @changePathDisposable?.dispose()
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale
diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee
deleted file mode 100644
index 2fda9a335..000000000
--- a/src/repository-status-handler.coffee
+++ /dev/null
@@ -1,36 +0,0 @@
-Git = require 'git-utils'
-path = require 'path'
-
-module.exports = (repoPath, paths = []) ->
- repo = Git.open(repoPath)
-
- upstream = {}
- statuses = {}
- submodules = {}
- branch = null
-
- if repo?
- # Statuses in main repo
- workingDirectoryPath = repo.getWorkingDirectory()
- repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus())
- for filePath, status of repoStatus
- statuses[filePath] = status
-
- # Statuses in submodules
- for submodulePath, submoduleRepo of repo.submodules
- submodules[submodulePath] =
- branch: submoduleRepo.getHead()
- upstream: submoduleRepo.getAheadBehindCount()
-
- workingDirectoryPath = submoduleRepo.getWorkingDirectory()
- for filePath, status of submoduleRepo.getStatus()
- absolutePath = path.join(workingDirectoryPath, filePath)
- # Make path relative to parent repository
- relativePath = repo.relativize(absolutePath)
- statuses[relativePath] = status
-
- upstream = repo.getAheadBehindCount()
- branch = repo.getHead()
- repo.release()
-
- {statuses, upstream, branch, submodules}
diff --git a/src/selection.coffee b/src/selection.coffee
index e361d0b5c..4d3fe8882 100644
--- a/src/selection.coffee
+++ b/src/selection.coffee
@@ -381,7 +381,7 @@ class Selection extends Model
if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
autoIndentFirstLine = true
firstLine = precedingText + firstInsertedLine
- desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
+ desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine)
@adjustIndent(remainingLines, indentAdjustment)
diff --git a/src/text-editor-component.js b/src/text-editor-component.js
index 093f2590e..3060b6857 100644
--- a/src/text-editor-component.js
+++ b/src/text-editor-component.js
@@ -362,7 +362,7 @@ class TextEditorComponent {
this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
}
- this.populateVisibleRowRange()
+ this.populateVisibleRowRange(this.getRenderedStartRow())
this.populateVisibleTiles()
this.queryScreenLinesToRender()
this.queryLongestLine()
@@ -1883,7 +1883,7 @@ class TextEditorComponent {
function didMouseUp () {
window.removeEventListener('mousemove', didMouseMove)
- window.removeEventListener('mouseup', didMouseUp)
+ window.removeEventListener('mouseup', didMouseUp, {capture: true})
bufferWillChangeDisposable.dispose()
if (dragging) {
dragging = false
@@ -2096,14 +2096,29 @@ class TextEditorComponent {
return marginInBaseCharacters * this.getBaseCharacterWidth()
}
+ // This method is called at the beginning of a frame render to relay any
+ // potential changes in the editor's width into the model before proceeding.
updateModelSoftWrapColumn () {
const {model} = this.props
const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters()
if (newEditorWidthInChars !== model.getEditorWidthInChars()) {
this.suppressUpdates = true
+
+ const renderedStartRow = this.getRenderedStartRow()
this.props.model.setEditorWidthInChars(newEditorWidthInChars)
- // Wrapping may cause a vertical scrollbar to appear, which will change the width again.
+
+ // Relaying a change in to the editor's client width may cause the
+ // vertical scrollbar to appear or disappear, which causes the editor's
+ // client width to change *again*. Make sure the display layer is fully
+ // populated for the visible area before recalculating the editor's
+ // width in characters. Then update the display layer *again* just in
+ // case a change in scrollbar visibility causes lines to wrap
+ // differently. We capture the renderedStartRow before resetting the
+ // display layer because once it has been reset, we can't compute the
+ // rendered start row accurately. 😥
+ this.populateVisibleRowRange(renderedStartRow)
this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters())
+
this.suppressUpdates = false
}
}
@@ -2867,12 +2882,11 @@ class TextEditorComponent {
}
}
- // Ensure the spatial index is populated with rows that are currently
- // visible so we *at least* get the longest row in the visible range.
- populateVisibleRowRange () {
+ // Ensure the spatial index is populated with rows that are currently visible
+ populateVisibleRowRange (renderedStartRow) {
const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight()
const visibleTileCount = Math.ceil(editorHeightInTiles) + 1
- const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile())
+ const lastRenderedRow = renderedStartRow + (visibleTileCount * this.getRowsPerTile())
this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow)
}
diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js
index 35be27fd1..2cbf3093c 100644
--- a/src/text-editor-registry.js
+++ b/src/text-editor-registry.js
@@ -429,3 +429,5 @@ class ScopedSettingsDelegate {
}
}
}
+
+TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
index a84f6f631..c00508f09 100644
--- a/src/text-editor.coffee
+++ b/src/text-editor.coffee
@@ -4,7 +4,6 @@ fs = require 'fs-plus'
Grim = require 'grim'
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
{Point, Range} = TextBuffer = require 'text-buffer'
-LanguageMode = require './language-mode'
DecorationManager = require './decoration-manager'
TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
@@ -16,6 +15,7 @@ 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
@@ -78,7 +78,6 @@ class TextEditor extends Model
serializationVersion: 1
buffer: null
- languageMode: null
cursors: null
showCursorOnSelection: null
selections: null
@@ -122,6 +121,8 @@ class TextEditor extends Model
this
)
+ Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer)
+
@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?
@@ -243,8 +244,6 @@ class TextEditor extends Model
initialColumn = Math.max(parseInt(initialColumn) or 0, 0)
@addCursorAtBufferPosition([initialLine, initialColumn])
- @languageMode = new LanguageMode(this)
-
@gutterContainer = new GutterContainer(this)
@lineNumberGutter = @gutterContainer.addGutter
name: 'line-number'
@@ -482,7 +481,6 @@ class TextEditor extends Model
@tokenizedBuffer.destroy()
selection.destroy() for selection in @selections.slice()
@buffer.release()
- @languageMode.destroy()
@gutterContainer.destroy()
@emitter.emit 'did-destroy'
@emitter.clear()
@@ -963,7 +961,7 @@ class TextEditor extends Model
# this editor.
shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) ->
if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected()
- false
+ @buffer.isInConflict()
else
@isModified() and not @buffer.hasMultipleEditors()
@@ -2210,7 +2208,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtBufferPosition: (bufferPosition, options) ->
- @selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options))
+ @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -3311,13 +3309,15 @@ class TextEditor extends Model
# indentation level up to the nearest following row with a lower indentation
# level.
foldCurrentRow: ->
- bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
- @foldBufferRow(bufferRow)
+ {row} = @getCursorBufferPosition()
+ range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity))
+ @displayLayer.foldBufferRange(range)
# Essential: Unfold the most recent cursor's row by one level.
unfoldCurrentRow: ->
- bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
- @unfoldBufferRow(bufferRow)
+ {row} = @getCursorBufferPosition()
+ position = Point(row, Infinity)
+ @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position))
# Essential: Fold the given row in buffer coordinates based on its indentation
# level.
@@ -3327,13 +3327,26 @@ class TextEditor extends Model
#
# * `bufferRow` A {Number}.
foldBufferRow: (bufferRow) ->
- @languageMode.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) ->
- @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity)))
+ position = Point(bufferRow, Infinity)
+ @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position))
# Extended: For each selection, fold the rows it intersects.
foldSelectedLines: ->
@@ -3342,18 +3355,25 @@ class TextEditor extends Model
# Extended: Fold all foldable lines.
foldAll: ->
- @languageMode.foldAll()
+ @displayLayer.destroyAllFolds()
+ for range in @tokenizedBuffer.getFoldableRanges(@getTabLength())
+ @displayLayer.foldBufferRange(range)
+ return
# Extended: Unfold all existing folds.
unfoldAll: ->
- @languageMode.unfoldAll()
+ result = @displayLayer.destroyAllFolds()
@scrollToCursorPosition()
+ result
# Extended: Fold all foldable lines at the given indent level.
#
# * `level` A {Number}.
foldAllAtIndentLevel: (level) ->
- @languageMode.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.
#
@@ -3547,6 +3567,7 @@ class TextEditor extends Model
# 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.
@@ -3603,18 +3624,6 @@ class TextEditor extends Model
getCommentStrings: (scopes) ->
@scopedSettingsDelegate?.getCommentStrings?(scopes)
- getIncreaseIndentPattern: (scopes) ->
- @scopedSettingsDelegate?.getIncreaseIndentPattern?(scopes)
-
- getDecreaseIndentPattern: (scopes) ->
- @scopedSettingsDelegate?.getDecreaseIndentPattern?(scopes)
-
- getDecreaseNextIndentPattern: (scopes) ->
- @scopedSettingsDelegate?.getDecreaseNextIndentPattern?(scopes)
-
- getFoldEndPattern: (scopes) ->
- @scopedSettingsDelegate?.getFoldEndPattern?(scopes)
-
###
Section: Event Handlers
###
@@ -3850,14 +3859,51 @@ class TextEditor extends Model
Section: Language Mode Delegated Methods
###
- suggestedIndentForBufferRow: (bufferRow, options) -> @languageMode.suggestedIndentForBufferRow(bufferRow, options)
+ suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options)
- autoIndentBufferRow: (bufferRow, options) -> @languageMode.autoIndentBufferRow(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)
- autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow)
+ # 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) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow)
+ autoDecreaseIndentForBufferRow: (bufferRow) ->
+ indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow)
+ @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel?
- toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row)
+ toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row)
- toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end)
+ toggleLineCommentsForBufferRows: (start, end) -> @tokenizedBuffer.toggleLineCommentsForBufferRows(start, end)
+
+ 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)))
diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee
deleted file mode 100644
index e4d954a59..000000000
--- a/src/tokenized-buffer.coffee
+++ /dev/null
@@ -1,455 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = require 'text-buffer'
-Model = require './model'
-TokenizedLine = require './tokenized-line'
-TokenIterator = require './token-iterator'
-ScopeDescriptor = require './scope-descriptor'
-TokenizedBufferIterator = require './tokenized-buffer-iterator'
-NullGrammar = require './null-grammar'
-{toFirstMateScopeId} = require './first-mate-helpers'
-
-prefixedScopes = new Map()
-
-module.exports =
-class TokenizedBuffer extends Model
- grammar: null
- buffer: null
- tabLength: null
- tokenizedLines: null
- chunkSize: 50
- invalidRows: null
- visible: false
- changeCount: 0
-
- @deserialize: (state, atomEnvironment) ->
- buffer = null
- if state.bufferId
- buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
- else
- # TODO: remove this fallback after everyone transitions to the latest version.
- buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
- return null unless buffer?
-
- state.buffer = buffer
- state.assert = atomEnvironment.assert
- new this(state)
-
- constructor: (params) ->
- {grammar, @buffer, @tabLength, @largeFileMode, @assert} = params
-
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @tokenIterator = new TokenIterator(this)
-
- @disposables.add @buffer.registerTextDecorationLayer(this)
-
- @setGrammar(grammar ? NullGrammar)
-
- destroyed: ->
- @disposables.dispose()
- @tokenizedLines.length = 0
-
- buildIterator: ->
- new TokenizedBufferIterator(this)
-
- classNameForScopeId: (id) ->
- scope = @grammar.scopeForId(toFirstMateScopeId(id))
- if scope
- prefixedScope = prefixedScopes.get(scope)
- if prefixedScope
- prefixedScope
- else
- prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}"
- prefixedScopes.set(scope, prefixedScope)
- prefixedScope
- else
- null
-
- getInvalidatedRanges: ->
- []
-
- onDidInvalidateRange: (fn) ->
- @emitter.on 'did-invalidate-range', fn
-
- serialize: ->
- {
- deserializer: 'TokenizedBuffer'
- bufferPath: @buffer.getPath()
- bufferId: @buffer.getId()
- tabLength: @tabLength
- largeFileMode: @largeFileMode
- }
-
- observeGrammar: (callback) ->
- callback(@grammar)
- @onDidChangeGrammar(callback)
-
- onDidChangeGrammar: (callback) ->
- @emitter.on 'did-change-grammar', callback
-
- onDidTokenize: (callback) ->
- @emitter.on 'did-tokenize', callback
-
- setGrammar: (grammar) ->
- return unless grammar? and grammar isnt @grammar
-
- @grammar = grammar
- @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
-
- @grammarUpdateDisposable?.dispose()
- @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
- @disposables.add(@grammarUpdateDisposable)
-
- @retokenizeLines()
-
- @emitter.emit 'did-change-grammar', grammar
-
- getGrammarSelectionContent: ->
- @buffer.getTextInRange([[0, 0], [10, 0]])
-
- hasTokenForSelector: (selector) ->
- for tokenizedLine in @tokenizedLines when tokenizedLine?
- for token in tokenizedLine.tokens
- return true if selector.matches(token.scopes)
- false
-
- retokenizeLines: ->
- return unless @alive
- @fullyTokenized = false
- @tokenizedLines = new Array(@buffer.getLineCount())
- @invalidRows = []
- if @largeFileMode or @grammar.name is 'Null Grammar'
- @markTokenizationComplete()
- else
- @invalidateRow(0)
-
- setVisible: (@visible) ->
- if @visible and @grammar.name isnt 'Null Grammar' and not @largeFileMode
- @tokenizeInBackground()
-
- getTabLength: -> @tabLength
-
- setTabLength: (@tabLength) ->
-
- tokenizeInBackground: ->
- return if not @visible or @pendingChunk or not @isAlive()
-
- @pendingChunk = true
- _.defer =>
- @pendingChunk = false
- @tokenizeNextChunk() if @isAlive() and @buffer.isAlive()
-
- tokenizeNextChunk: ->
- rowsRemaining = @chunkSize
-
- while @firstInvalidRow()? and rowsRemaining > 0
- startRow = @invalidRows.shift()
- lastRow = @getLastRow()
- continue if startRow > lastRow
-
- row = startRow
- loop
- previousStack = @stackForRow(row)
- @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
- if --rowsRemaining is 0
- filledRegion = false
- endRow = row
- break
- if row is lastRow or _.isEqual(@stackForRow(row), previousStack)
- filledRegion = true
- endRow = row
- break
- row++
-
- @validateRow(endRow)
- @invalidateRow(endRow + 1) unless filledRegion
-
- @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))
-
- if @firstInvalidRow()?
- @tokenizeInBackground()
- else
- @markTokenizationComplete()
-
- markTokenizationComplete: ->
- unless @fullyTokenized
- @emitter.emit 'did-tokenize'
- @fullyTokenized = true
-
- firstInvalidRow: ->
- @invalidRows[0]
-
- validateRow: (row) ->
- @invalidRows.shift() while @invalidRows[0] <= row
- return
-
- invalidateRow: (row) ->
- @invalidRows.push(row)
- @invalidRows.sort (a, b) -> a - b
- @tokenizeInBackground()
-
- updateInvalidRows: (start, end, delta) ->
- @invalidRows = @invalidRows.map (row) ->
- if row < start
- row
- else if start <= row <= end
- end + delta + 1
- else if row > end
- row + delta
-
- bufferDidChange: (e) ->
- @changeCount = @buffer.changeCount
-
- {oldRange, newRange} = e
- start = oldRange.start.row
- end = oldRange.end.row
- delta = newRange.end.row - oldRange.end.row
- oldLineCount = oldRange.end.row - oldRange.start.row + 1
- newLineCount = newRange.end.row - newRange.start.row + 1
-
- @updateInvalidRows(start, end, delta)
- previousEndStack = @stackForRow(end) # used in spill detection below
- if @largeFileMode or @grammar.name is 'Null Grammar'
- _.spliceWithArray(@tokenizedLines, start, oldLineCount, new Array(newLineCount))
- else
- newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start))
- _.spliceWithArray(@tokenizedLines, start, oldLineCount, newTokenizedLines)
- newEndStack = @stackForRow(end + delta)
- if newEndStack and not _.isEqual(newEndStack, previousEndStack)
- @invalidateRow(end + delta + 1)
-
- isFoldableAtRow: (row) ->
- @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
-
- # Returns a {Boolean} indicating whether the given buffer row starts
- # a a foldable row range due to the code's indentation patterns.
- isFoldableCodeAtRow: (row) ->
- if 0 <= row <= @buffer.getLastRow()
- nextRow = @buffer.nextNonBlankRow(row)
- tokenizedLine = @tokenizedLines[row]
- if @buffer.isRowBlank(row) or tokenizedLine?.isComment() or not nextRow?
- false
- else
- @indentLevelForRow(nextRow) > @indentLevelForRow(row)
- else
- false
-
- isFoldableCommentAtRow: (row) ->
- previousRow = row - 1
- nextRow = row + 1
- if nextRow > @buffer.getLastRow()
- false
- else
- Boolean(
- not (@tokenizedLines[previousRow]?.isComment()) and
- @tokenizedLines[row]?.isComment() and
- @tokenizedLines[nextRow]?.isComment()
- )
-
- buildTokenizedLinesForRows: (startRow, endRow, startingStack, startingopenScopes) ->
- ruleStack = startingStack
- openScopes = startingopenScopes
- stopTokenizingAt = startRow + @chunkSize
- tokenizedLines = for row in [startRow..endRow] by 1
- if (ruleStack or row is 0) and row < stopTokenizingAt
- tokenizedLine = @buildTokenizedLineForRow(row, ruleStack, openScopes)
- ruleStack = tokenizedLine.ruleStack
- openScopes = @scopesFromTags(openScopes, tokenizedLine.tags)
- else
- tokenizedLine = undefined
- tokenizedLine
-
- if endRow >= stopTokenizingAt
- @invalidateRow(stopTokenizingAt)
- @tokenizeInBackground()
-
- tokenizedLines
-
- buildTokenizedLineForRow: (row, ruleStack, openScopes) ->
- @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes)
-
- buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
- lineEnding = @buffer.lineEndingForRow(row)
- {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
- new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar})
-
- tokenizedLineForRow: (bufferRow) ->
- if 0 <= bufferRow <= @buffer.getLastRow()
- if tokenizedLine = @tokenizedLines[bufferRow]
- tokenizedLine
- else
- text = @buffer.lineForRow(bufferRow)
- lineEnding = @buffer.lineEndingForRow(bufferRow)
- tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)]
- @tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar})
-
- tokenizedLinesForRows: (startRow, endRow) ->
- for row in [startRow..endRow] by 1
- @tokenizedLineForRow(row)
-
- stackForRow: (bufferRow) ->
- @tokenizedLines[bufferRow]?.ruleStack
-
- openScopesForRow: (bufferRow) ->
- if precedingLine = @tokenizedLines[bufferRow - 1]
- @scopesFromTags(precedingLine.openScopes, precedingLine.tags)
- else
- []
-
- scopesFromTags: (startingScopes, tags) ->
- scopes = startingScopes.slice()
- for tag in tags when tag < 0
- if (tag % 2) is -1
- scopes.push(tag)
- else
- matchingStartTag = tag + 1
- loop
- break if scopes.pop() is matchingStartTag
- if scopes.length is 0
- @assert false, "Encountered an unmatched scope end tag.", (error) =>
- error.metadata = {
- grammarScopeName: @grammar.scopeName
- unmatchedEndTag: @grammar.scopeForId(tag)
- }
- path = require 'path'
- error.privateMetadataDescription = "The contents of `#{path.basename(@buffer.getPath())}`"
- error.privateMetadata = {
- filePath: @buffer.getPath()
- fileContents: @buffer.getText()
- }
- break
- scopes
-
- indentLevelForRow: (bufferRow) ->
- line = @buffer.lineForRow(bufferRow)
- indentLevel = 0
-
- if line is ''
- nextRow = bufferRow + 1
- lineCount = @getLineCount()
- while nextRow < lineCount
- nextLine = @buffer.lineForRow(nextRow)
- unless nextLine is ''
- indentLevel = Math.ceil(@indentLevelForLine(nextLine))
- break
- nextRow++
-
- previousRow = bufferRow - 1
- while previousRow >= 0
- previousLine = @buffer.lineForRow(previousRow)
- unless previousLine is ''
- indentLevel = Math.max(Math.ceil(@indentLevelForLine(previousLine)), indentLevel)
- break
- previousRow--
-
- indentLevel
- else
- @indentLevelForLine(line)
-
- indentLevelForLine: (line) ->
- indentLength = 0
- for char in line
- if char is '\t'
- indentLength += @getTabLength() - (indentLength % @getTabLength())
- else if char is ' '
- indentLength++
- else
- break
-
- indentLength / @getTabLength()
-
- scopeDescriptorForPosition: (position) ->
- {row, column} = @buffer.clipPosition(Point.fromObject(position))
-
- iterator = @tokenizedLineForRow(row).getTokenIterator()
- while iterator.next()
- if iterator.getBufferEnd() > column
- scopes = iterator.getScopes()
- break
-
- # rebuild scope of last token if we iterated off the end
- unless scopes?
- scopes = iterator.getScopes()
- scopes.push(iterator.getScopeEnds().reverse()...)
-
- new ScopeDescriptor({scopes})
-
- tokenForPosition: (position) ->
- {row, column} = Point.fromObject(position)
- @tokenizedLineForRow(row).tokenAtBufferColumn(column)
-
- tokenStartPositionForPosition: (position) ->
- {row, column} = Point.fromObject(position)
- column = @tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column)
- new Point(row, column)
-
- bufferRangeForScopeAtPosition: (selector, position) ->
- position = Point.fromObject(position)
-
- {openScopes, tags} = @tokenizedLineForRow(position.row)
- scopes = openScopes.map (tag) => @grammar.scopeForId(tag)
-
- startColumn = 0
- for tag, tokenIndex in tags
- if tag < 0
- if tag % 2 is -1
- scopes.push(@grammar.scopeForId(tag))
- else
- scopes.pop()
- else
- endColumn = startColumn + tag
- if endColumn >= position.column
- break
- else
- startColumn = endColumn
-
-
- return unless selectorMatchesAnyScope(selector, scopes)
-
- startScopes = scopes.slice()
- for startTokenIndex in [(tokenIndex - 1)..0] by -1
- tag = tags[startTokenIndex]
- if tag < 0
- if tag % 2 is -1
- startScopes.pop()
- else
- startScopes.push(@grammar.scopeForId(tag))
- else
- break unless selectorMatchesAnyScope(selector, startScopes)
- startColumn -= tag
-
- endScopes = scopes.slice()
- for endTokenIndex in [(tokenIndex + 1)...tags.length] by 1
- tag = tags[endTokenIndex]
- if tag < 0
- if tag % 2 is -1
- endScopes.push(@grammar.scopeForId(tag))
- else
- endScopes.pop()
- else
- break unless selectorMatchesAnyScope(selector, endScopes)
- endColumn += tag
-
- new Range(new Point(position.row, startColumn), new Point(position.row, endColumn))
-
- # Gets the row number of the last line.
- #
- # Returns a {Number}.
- getLastRow: ->
- @buffer.getLastRow()
-
- getLineCount: ->
- @buffer.getLineCount()
-
- logLines: (start=0, end=@buffer.getLastRow()) ->
- for row in [start..end]
- line = @tokenizedLines[row].text
- console.log row, line, line.length
- return
-
-selectorMatchesAnyScope = (selector, scopes) ->
- targetClasses = selector.replace(/^\./, '').split('.')
- _.any scopes, (scope) ->
- scopeClasses = scope.split('.')
- _.isSubset(targetClasses, scopeClasses)
diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js
new file mode 100644
index 000000000..b4bc0d41c
--- /dev/null
+++ b/src/tokenized-buffer.js
@@ -0,0 +1,875 @@
+const _ = require('underscore-plus')
+const {CompositeDisposable, Emitter} = require('event-kit')
+const {Point, Range} = require('text-buffer')
+const TokenizedLine = require('./tokenized-line')
+const TokenIterator = require('./token-iterator')
+const ScopeDescriptor = require('./scope-descriptor')
+const TokenizedBufferIterator = require('./tokenized-buffer-iterator')
+const NullGrammar = require('./null-grammar')
+const {OnigRegExp} = require('oniguruma')
+const {toFirstMateScopeId} = require('./first-mate-helpers')
+
+const NON_WHITESPACE_REGEX = /\S/
+
+let nextId = 0
+const prefixedScopes = new Map()
+
+module.exports =
+class TokenizedBuffer {
+ static deserialize (state, atomEnvironment) {
+ const buffer = atomEnvironment.project.bufferForIdSync(state.bufferId)
+ if (!buffer) return null
+
+ state.buffer = buffer
+ state.assert = atomEnvironment.assert
+ return new TokenizedBuffer(state)
+ }
+
+ constructor (params) {
+ this.emitter = new Emitter()
+ this.disposables = new CompositeDisposable()
+ this.tokenIterator = new TokenIterator(this)
+ this.regexesByPattern = {}
+
+ this.alive = true
+ this.visible = false
+ this.id = params.id != null ? params.id : nextId++
+ this.buffer = params.buffer
+ this.tabLength = params.tabLength
+ this.largeFileMode = params.largeFileMode
+ this.assert = params.assert
+ this.scopedSettingsDelegate = params.scopedSettingsDelegate
+
+ this.setGrammar(params.grammar || NullGrammar)
+ this.disposables.add(this.buffer.registerTextDecorationLayer(this))
+ }
+
+ destroy () {
+ if (!this.alive) return
+ this.alive = false
+ this.disposables.dispose()
+ this.tokenizedLines.length = 0
+ }
+
+ isAlive () {
+ return this.alive
+ }
+
+ isDestroyed () {
+ return !this.alive
+ }
+
+ /*
+ Section - auto-indent
+ */
+
+ // Get the suggested indentation level for an existing line in the buffer.
+ //
+ // * bufferRow - A {Number} indicating the buffer row
+ //
+ // Returns a {Number}.
+ suggestedIndentForBufferRow (bufferRow, options) {
+ const line = this.buffer.lineForRow(bufferRow)
+ const tokenizedLine = this.tokenizedLineForRow(bufferRow)
+ return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
+ }
+
+ // Get the suggested indentation level for a given line of text, if it were inserted at the given
+ // row in the buffer.
+ //
+ // * bufferRow - A {Number} indicating the buffer row
+ //
+ // Returns a {Number}.
+ suggestedIndentForLineAtBufferRow (bufferRow, line, options) {
+ const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line)
+ return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
+ }
+
+ // Get the suggested indentation level for a line in the buffer on which the user is currently
+ // typing. This may return a different result from {::suggestedIndentForBufferRow} in order
+ // to avoid unexpected changes in indentation. It may also return undefined if no change should
+ // be made.
+ //
+ // * bufferRow - The row {Number}
+ //
+ // Returns a {Number}.
+ suggestedIndentForEditedBufferRow (bufferRow) {
+ const line = this.buffer.lineForRow(bufferRow)
+ const currentIndentLevel = this.indentLevelForLine(line)
+ if (currentIndentLevel === 0) return
+
+ const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0])
+ const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
+ if (!decreaseIndentRegex) return
+
+ if (!decreaseIndentRegex.testSync(line)) return
+
+ const precedingRow = this.buffer.previousNonBlankRow(bufferRow)
+ if (precedingRow == null) return
+
+ const precedingLine = this.buffer.lineForRow(precedingRow)
+ let desiredIndentLevel = this.indentLevelForLine(precedingLine)
+
+ const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
+ if (increaseIndentRegex) {
+ if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
+ }
+
+ const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
+ if (decreaseNextIndentRegex) {
+ if (decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
+ }
+
+ if (desiredIndentLevel < 0) return 0
+ if (desiredIndentLevel >= currentIndentLevel) return
+ return desiredIndentLevel
+ }
+
+ _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, options) {
+ const iterator = tokenizedLine.getTokenIterator()
+ iterator.next()
+ const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
+
+ const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
+ const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
+ const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
+
+ let precedingRow
+ if (!options || options.skipBlankLines !== false) {
+ precedingRow = this.buffer.previousNonBlankRow(bufferRow)
+ if (precedingRow == null) return 0
+ } else {
+ precedingRow = bufferRow - 1
+ if (precedingRow < 0) return 0
+ }
+
+ const precedingLine = this.buffer.lineForRow(precedingRow)
+ let desiredIndentLevel = this.indentLevelForLine(precedingLine)
+ if (!increaseIndentRegex) return desiredIndentLevel
+
+ if (!this.isRowCommented(precedingRow)) {
+ if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel += 1
+ if (decreaseNextIndentRegex && decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
+ }
+
+ if (!this.buffer.isRowBlank(precedingRow)) {
+ if (decreaseIndentRegex && decreaseIndentRegex.testSync(line)) desiredIndentLevel -= 1
+ }
+
+ return Math.max(desiredIndentLevel, 0)
+ }
+
+ /*
+ Section - Comments
+ */
+
+ toggleLineCommentsForBufferRows (start, end) {
+ const scope = this.scopeDescriptorForPosition([start, 0])
+ const commentStrings = this.commentStringsForScopeDescriptor(scope)
+ if (!commentStrings) return
+ const {commentStartString, commentEndString} = commentStrings
+ if (!commentStartString) return
+
+ const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
+ const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`)
+
+ 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) {
+ 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]], '')
+ })
+ }
+ } 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 (commentStartRegex.testSync(line)) {
+ hasCommentedLines = true
+ } else {
+ hasUncommentedLines = true
+ }
+ }
+ }
+
+ const shouldUncomment = hasCommentedLines && !hasUncommentedLines
+
+ 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]], '')
+ }
+ }
+ } 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
+ )
+ }
+ }
+ }
+ }
+ }
+
+ buildIterator () {
+ return new TokenizedBufferIterator(this)
+ }
+
+ classNameForScopeId (id) {
+ const scope = this.grammar.scopeForId(toFirstMateScopeId(id))
+ if (scope) {
+ let prefixedScope = prefixedScopes.get(scope)
+ if (prefixedScope) {
+ return prefixedScope
+ } else {
+ prefixedScope = `syntax--${scope.replace(/\./g, ' syntax--')}`
+ prefixedScopes.set(scope, prefixedScope)
+ return prefixedScope
+ }
+ } else {
+ return null
+ }
+ }
+
+ getInvalidatedRanges () {
+ return []
+ }
+
+ onDidInvalidateRange (fn) {
+ return this.emitter.on('did-invalidate-range', fn)
+ }
+
+ serialize () {
+ return {
+ deserializer: 'TokenizedBuffer',
+ bufferPath: this.buffer.getPath(),
+ bufferId: this.buffer.getId(),
+ tabLength: this.tabLength,
+ largeFileMode: this.largeFileMode
+ }
+ }
+
+ observeGrammar (callback) {
+ callback(this.grammar)
+ return this.onDidChangeGrammar(callback)
+ }
+
+ onDidChangeGrammar (callback) {
+ return this.emitter.on('did-change-grammar', callback)
+ }
+
+ onDidTokenize (callback) {
+ return this.emitter.on('did-tokenize', callback)
+ }
+
+ setGrammar (grammar) {
+ if (!grammar || grammar === this.grammar) return
+
+ this.grammar = grammar
+ this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
+
+ if (this.grammarUpdateDisposable) this.grammarUpdateDisposable.dispose()
+ this.grammarUpdateDisposable = this.grammar.onDidUpdate(() => this.retokenizeLines())
+ this.disposables.add(this.grammarUpdateDisposable)
+
+ this.retokenizeLines()
+ this.emitter.emit('did-change-grammar', grammar)
+ }
+
+ getGrammarSelectionContent () {
+ return this.buffer.getTextInRange([[0, 0], [10, 0]])
+ }
+
+ hasTokenForSelector (selector) {
+ for (const tokenizedLine of this.tokenizedLines) {
+ if (tokenizedLine) {
+ for (let token of tokenizedLine.tokens) {
+ if (selector.matches(token.scopes)) return true
+ }
+ }
+ }
+ return false
+ }
+
+ retokenizeLines () {
+ if (!this.alive) return
+ this.fullyTokenized = false
+ this.tokenizedLines = new Array(this.buffer.getLineCount())
+ this.invalidRows = []
+ if (this.largeFileMode || this.grammar.name === 'Null Grammar') {
+ this.markTokenizationComplete()
+ } else {
+ this.invalidateRow(0)
+ }
+ }
+
+ setVisible (visible) {
+ this.visible = visible
+ if (this.visible && this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
+ this.tokenizeInBackground()
+ }
+ }
+
+ getTabLength () { return this.tabLength }
+
+ setTabLength (tabLength) {
+ this.tabLength = tabLength
+ }
+
+ tokenizeInBackground () {
+ if (!this.visible || this.pendingChunk || !this.alive) return
+
+ this.pendingChunk = true
+ _.defer(() => {
+ this.pendingChunk = false
+ if (this.isAlive() && this.buffer.isAlive()) this.tokenizeNextChunk()
+ })
+ }
+
+ tokenizeNextChunk () {
+ let rowsRemaining = this.chunkSize
+
+ while (this.firstInvalidRow() != null && rowsRemaining > 0) {
+ var endRow, filledRegion
+ const startRow = this.invalidRows.shift()
+ const lastRow = this.buffer.getLastRow()
+ if (startRow > lastRow) continue
+
+ let row = startRow
+ while (true) {
+ const previousStack = this.stackForRow(row)
+ this.tokenizedLines[row] = this.buildTokenizedLineForRow(row, this.stackForRow(row - 1), this.openScopesForRow(row))
+ if (--rowsRemaining === 0) {
+ filledRegion = false
+ endRow = row
+ break
+ }
+ if (row === lastRow || _.isEqual(this.stackForRow(row), previousStack)) {
+ filledRegion = true
+ endRow = row
+ break
+ }
+ row++
+ }
+
+ this.validateRow(endRow)
+ if (!filledRegion) this.invalidateRow(endRow + 1)
+
+ this.emitter.emit('did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)))
+ }
+
+ if (this.firstInvalidRow() != null) {
+ this.tokenizeInBackground()
+ } else {
+ this.markTokenizationComplete()
+ }
+ }
+
+ markTokenizationComplete () {
+ if (!this.fullyTokenized) {
+ this.emitter.emit('did-tokenize')
+ }
+ this.fullyTokenized = true
+ }
+
+ firstInvalidRow () {
+ return this.invalidRows[0]
+ }
+
+ validateRow (row) {
+ while (this.invalidRows[0] <= row) this.invalidRows.shift()
+ }
+
+ invalidateRow (row) {
+ this.invalidRows.push(row)
+ this.invalidRows.sort((a, b) => a - b)
+ this.tokenizeInBackground()
+ }
+
+ updateInvalidRows (start, end, delta) {
+ this.invalidRows = this.invalidRows.map((row) => {
+ if (row < start) {
+ return row
+ } else if (start <= row && row <= end) {
+ return end + delta + 1
+ } else if (row > end) {
+ return row + delta
+ }
+ })
+ }
+
+ bufferDidChange (e) {
+ this.changeCount = this.buffer.changeCount
+
+ const {oldRange, newRange} = e
+ const start = oldRange.start.row
+ const end = oldRange.end.row
+ const delta = newRange.end.row - oldRange.end.row
+ const oldLineCount = (oldRange.end.row - oldRange.start.row) + 1
+ const newLineCount = (newRange.end.row - newRange.start.row) + 1
+
+ this.updateInvalidRows(start, end, delta)
+ const previousEndStack = this.stackForRow(end) // used in spill detection below
+ if (this.largeFileMode || (this.grammar.name === 'Null Grammar')) {
+ _.spliceWithArray(this.tokenizedLines, start, oldLineCount, new Array(newLineCount))
+ } else {
+ const newTokenizedLines = this.buildTokenizedLinesForRows(start, end + delta, this.stackForRow(start - 1), this.openScopesForRow(start))
+ _.spliceWithArray(this.tokenizedLines, start, oldLineCount, newTokenizedLines)
+ const newEndStack = this.stackForRow(end + delta)
+ if (newEndStack && !_.isEqual(newEndStack, previousEndStack)) {
+ this.invalidateRow(end + delta + 1)
+ }
+ }
+ }
+
+ isFoldableAtRow (row) {
+ return this.endRowForFoldAtRow(row, 1, true) != null
+ }
+
+ buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) {
+ let ruleStack = startingStack
+ let openScopes = startingopenScopes
+ const stopTokenizingAt = startRow + this.chunkSize
+ const tokenizedLines = []
+ for (let row = startRow, end = endRow; row <= end; row++) {
+ let tokenizedLine
+ if ((ruleStack || (row === 0)) && row < stopTokenizingAt) {
+ tokenizedLine = this.buildTokenizedLineForRow(row, ruleStack, openScopes)
+ ruleStack = tokenizedLine.ruleStack
+ openScopes = this.scopesFromTags(openScopes, tokenizedLine.tags)
+ }
+ tokenizedLines.push(tokenizedLine)
+ }
+
+ if (endRow >= stopTokenizingAt) {
+ this.invalidateRow(stopTokenizingAt)
+ this.tokenizeInBackground()
+ }
+
+ return tokenizedLines
+ }
+
+ buildTokenizedLineForRow (row, ruleStack, openScopes) {
+ return this.buildTokenizedLineForRowWithText(row, this.buffer.lineForRow(row), ruleStack, openScopes)
+ }
+
+ buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) {
+ const lineEnding = this.buffer.lineEndingForRow(row)
+ const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false)
+ return new TokenizedLine({
+ openScopes,
+ text,
+ tags,
+ ruleStack,
+ lineEnding,
+ tokenIterator: this.tokenIterator,
+ grammar: this.grammar
+ })
+ }
+
+ tokenizedLineForRow (bufferRow) {
+ if (bufferRow >= 0 && bufferRow <= this.buffer.getLastRow()) {
+ const tokenizedLine = this.tokenizedLines[bufferRow]
+ if (tokenizedLine) {
+ return tokenizedLine
+ } else {
+ const text = this.buffer.lineForRow(bufferRow)
+ const lineEnding = this.buffer.lineEndingForRow(bufferRow)
+ const tags = [
+ this.grammar.startIdForScope(this.grammar.scopeName),
+ text.length,
+ this.grammar.endIdForScope(this.grammar.scopeName)
+ ]
+ this.tokenizedLines[bufferRow] = new TokenizedLine({
+ openScopes: [],
+ text,
+ tags,
+ lineEnding,
+ tokenIterator: this.tokenIterator,
+ grammar: this.grammar
+ })
+ return this.tokenizedLines[bufferRow]
+ }
+ }
+ }
+
+ tokenizedLinesForRows (startRow, endRow) {
+ const result = []
+ for (let row = startRow, end = endRow; row <= end; row++) {
+ result.push(this.tokenizedLineForRow(row))
+ }
+ return result
+ }
+
+ stackForRow (bufferRow) {
+ return this.tokenizedLines[bufferRow] && this.tokenizedLines[bufferRow].ruleStack
+ }
+
+ openScopesForRow (bufferRow) {
+ const precedingLine = this.tokenizedLines[bufferRow - 1]
+ if (precedingLine) {
+ return this.scopesFromTags(precedingLine.openScopes, precedingLine.tags)
+ } else {
+ return []
+ }
+ }
+
+ scopesFromTags (startingScopes, tags) {
+ const scopes = startingScopes.slice()
+ for (const tag of tags) {
+ if (tag < 0) {
+ if (tag % 2 === -1) {
+ scopes.push(tag)
+ } else {
+ const matchingStartTag = tag + 1
+ while (true) {
+ if (scopes.pop() === matchingStartTag) break
+ if (scopes.length === 0) {
+ this.assert(false, 'Encountered an unmatched scope end tag.', error => {
+ error.metadata = {
+ grammarScopeName: this.grammar.scopeName,
+ unmatchedEndTag: this.grammar.scopeForId(tag)
+ }
+ const path = require('path')
+ error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\``
+ error.privateMetadata = {
+ filePath: this.buffer.getPath(),
+ fileContents: this.buffer.getText()
+ }
+ })
+ break
+ }
+ }
+ }
+ }
+ }
+ 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++) {
+ const char = line[i]
+ if (char === '\t') {
+ indentLength += tabLength - (indentLength % tabLength)
+ } else if (char === ' ') {
+ indentLength++
+ } else {
+ break
+ }
+ }
+ return indentLength / tabLength
+ }
+
+ scopeDescriptorForPosition (position) {
+ let scopes
+ const {row, column} = this.buffer.clipPosition(Point.fromObject(position))
+
+ const iterator = this.tokenizedLineForRow(row).getTokenIterator()
+ while (iterator.next()) {
+ if (iterator.getBufferEnd() > column) {
+ scopes = iterator.getScopes()
+ break
+ }
+ }
+
+ // rebuild scope of last token if we iterated off the end
+ if (!scopes) {
+ scopes = iterator.getScopes()
+ scopes.push(...iterator.getScopeEnds().reverse())
+ }
+
+ return new ScopeDescriptor({scopes})
+ }
+
+ tokenForPosition (position) {
+ const {row, column} = Point.fromObject(position)
+ return this.tokenizedLineForRow(row).tokenAtBufferColumn(column)
+ }
+
+ tokenStartPositionForPosition (position) {
+ let {row, column} = Point.fromObject(position)
+ column = this.tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column)
+ return new Point(row, column)
+ }
+
+ bufferRangeForScopeAtPosition (selector, position) {
+ let endColumn, tag, tokenIndex
+ position = Point.fromObject(position)
+
+ const {openScopes, tags} = this.tokenizedLineForRow(position.row)
+ const scopes = openScopes.map(tag => this.grammar.scopeForId(tag))
+
+ let startColumn = 0
+ for (tokenIndex = 0; tokenIndex < tags.length; tokenIndex++) {
+ tag = tags[tokenIndex]
+ if (tag < 0) {
+ if ((tag % 2) === -1) {
+ scopes.push(this.grammar.scopeForId(tag))
+ } else {
+ scopes.pop()
+ }
+ } else {
+ endColumn = startColumn + tag
+ if (endColumn >= position.column) {
+ break
+ } else {
+ startColumn = endColumn
+ }
+ }
+ }
+
+ if (!selectorMatchesAnyScope(selector, scopes)) return
+
+ const startScopes = scopes.slice()
+ for (let startTokenIndex = tokenIndex - 1; startTokenIndex >= 0; startTokenIndex--) {
+ tag = tags[startTokenIndex]
+ if (tag < 0) {
+ if ((tag % 2) === -1) {
+ startScopes.pop()
+ } else {
+ startScopes.push(this.grammar.scopeForId(tag))
+ }
+ } else {
+ if (!selectorMatchesAnyScope(selector, startScopes)) { break }
+ startColumn -= tag
+ }
+ }
+
+ const endScopes = scopes.slice()
+ for (let endTokenIndex = tokenIndex + 1, end = tags.length; endTokenIndex < end; endTokenIndex++) {
+ tag = tags[endTokenIndex]
+ if (tag < 0) {
+ if ((tag % 2) === -1) {
+ endScopes.push(this.grammar.scopeForId(tag))
+ } else {
+ endScopes.pop()
+ }
+ } else {
+ if (!selectorMatchesAnyScope(selector, endScopes)) { break }
+ endColumn += tag
+ }
+ }
+
+ return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn))
+ }
+
+ isRowCommented (row) {
+ return this.tokenizedLines[row] && this.tokenizedLines[row].isComment()
+ }
+
+ getFoldableRangeContainingPoint (point, tabLength) {
+ if (point.column >= this.buffer.lineLengthForRow(point.row)) {
+ const endRow = this.endRowForFoldAtRow(point.row, tabLength)
+ if (endRow != null) {
+ return Range(Point(point.row, Infinity), Point(endRow, Infinity))
+ }
+ }
+
+ for (let row = point.row - 1; row >= 0; row--) {
+ const endRow = this.endRowForFoldAtRow(row, tabLength)
+ if (endRow != null && endRow > point.row) {
+ return Range(Point(row, Infinity), Point(endRow, Infinity))
+ }
+ }
+ return null
+ }
+
+ getFoldableRangesAtIndentLevel (indentLevel, tabLength) {
+ const result = []
+ let row = 0
+ const lineCount = this.buffer.getLineCount()
+ while (row < lineCount) {
+ if (this.indentLevelForLine(this.buffer.lineForRow(row), tabLength) === indentLevel) {
+ const endRow = this.endRowForFoldAtRow(row, tabLength)
+ if (endRow != null) {
+ result.push(Range(Point(row, Infinity), Point(endRow, Infinity)))
+ row = endRow + 1
+ continue
+ }
+ }
+ row++
+ }
+ return result
+ }
+
+ getFoldableRanges (tabLength) {
+ const result = []
+ let row = 0
+ const lineCount = this.buffer.getLineCount()
+ while (row < lineCount) {
+ const endRow = this.endRowForFoldAtRow(row, tabLength)
+ if (endRow != null) {
+ result.push(Range(Point(row, Infinity), Point(endRow, Infinity)))
+ }
+ row++
+ }
+ return result
+ }
+
+ endRowForFoldAtRow (row, tabLength, existenceOnly = false) {
+ if (this.isRowCommented(row)) {
+ return this.endRowForCommentFoldAtRow(row, existenceOnly)
+ } else {
+ return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly)
+ }
+ }
+
+ endRowForCommentFoldAtRow (row, existenceOnly) {
+ if (this.isRowCommented(row - 1)) return
+
+ let endRow
+ for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) {
+ if (!this.isRowCommented(nextRow)) break
+ endRow = nextRow
+ if (existenceOnly) break
+ }
+
+ return endRow
+ }
+
+ endRowForCodeFoldAtRow (row, tabLength, existenceOnly) {
+ let foldEndRow
+ const line = this.buffer.lineForRow(row)
+ if (!NON_WHITESPACE_REGEX.test(line)) return
+ const startIndentLevel = this.indentLevelForLine(line, tabLength)
+ const scopeDescriptor = this.scopeDescriptorForPosition([row, 0])
+ const foldEndRegex = this.foldEndRegexForScopeDescriptor(scopeDescriptor)
+ for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) {
+ const line = this.buffer.lineForRow(nextRow)
+ if (!NON_WHITESPACE_REGEX.test(line)) continue
+ const indentation = this.indentLevelForLine(line, tabLength)
+ if (indentation < startIndentLevel) {
+ break
+ } else if (indentation === startIndentLevel) {
+ if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow
+ break
+ }
+ foldEndRow = nextRow
+ if (existenceOnly) break
+ }
+ return foldEndRow
+ }
+
+ increaseIndentRegexForScopeDescriptor (scopeDescriptor) {
+ if (this.scopedSettingsDelegate) {
+ return this.regexForPattern(this.scopedSettingsDelegate.getIncreaseIndentPattern(scopeDescriptor))
+ }
+ }
+
+ decreaseIndentRegexForScopeDescriptor (scopeDescriptor) {
+ if (this.scopedSettingsDelegate) {
+ return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseIndentPattern(scopeDescriptor))
+ }
+ }
+
+ decreaseNextIndentRegexForScopeDescriptor (scopeDescriptor) {
+ if (this.scopedSettingsDelegate) {
+ return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseNextIndentPattern(scopeDescriptor))
+ }
+ }
+
+ foldEndRegexForScopeDescriptor (scopes) {
+ if (this.scopedSettingsDelegate) {
+ return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes))
+ }
+ }
+
+ commentStringsForScopeDescriptor (scopes) {
+ if (this.scopedSettingsDelegate) {
+ return this.scopedSettingsDelegate.getCommentStrings(scopes)
+ }
+ }
+
+ regexForPattern (pattern) {
+ if (pattern) {
+ if (!this.regexesByPattern[pattern]) {
+ this.regexesByPattern[pattern] = new OnigRegExp(pattern)
+ }
+ return this.regexesByPattern[pattern]
+ }
+ }
+
+ logLines (start = 0, end = this.buffer.getLastRow()) {
+ for (let row = start; row <= end; row++) {
+ const line = this.tokenizedLines[row].text
+ console.log(row, line, line.length)
+ }
+ }
+}
+
+module.exports.prototype.chunkSize = 50
+
+function selectorMatchesAnyScope (selector, scopes) {
+ const targetClasses = selector.replace(/^\./, '').split('.')
+ return scopes.some((scope) => {
+ const scopeClasses = scope.split('.')
+ return _.isSubset(targetClasses, scopeClasses)
+ })
+}