mirror of
https://github.com/atom/atom.git
synced 2026-01-24 22:38:20 -05:00
Merge remote-tracking branch 'origin/master' into mkt-url-based-command-dispatch
This commit is contained in:
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.7"
|
||||
"atom-package-manager": "1.18.8"
|
||||
}
|
||||
}
|
||||
|
||||
40
package.json
40
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",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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') ||
|
||||
|
||||
@@ -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")
|
||||
103
spec/git-repository-provider-spec.js
Normal file
103
spec/git-repository-provider-spec.js
Normal file
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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("<!-- test -->")
|
||||
{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
|
||||
File diff suppressed because it is too large
Load Diff
1339
spec/package-manager-spec.js
Normal file
1339
spec/package-manager-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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", ->
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
249
spec/text-editor-spec.js
Normal file
249
spec/text-editor-spec.js
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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(' <![]()\n ')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
length = 0
|
||||
for tag in tokenizedBuffer.tokenizedLines[1].tags
|
||||
length += tag if tag > 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 "<div class='name'><%= User.find(2).full_name %></div>"
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
{tokens} = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(tokens[0]).toEqual value: "<div class='name'>", 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']
|
||||
1084
spec/tokenized-buffer-spec.js
Normal file
1084
spec/tokenized-buffer-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
18
src/color.js
18
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
753
src/cursor.js
Normal file
753
src/cursor.js
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 -- <path>
|
||||
# git checkout HEAD -- <path>
|
||||
# ```
|
||||
#
|
||||
# * `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()
|
||||
603
src/git-repository.js
Normal file
603
src/git-repository.js
Normal file
@@ -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 -- <path>
|
||||
// git checkout HEAD -- <path>
|
||||
// ```
|
||||
//
|
||||
// * `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')
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -429,3 +429,5 @@ class ScopedSettingsDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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)
|
||||
875
src/tokenized-buffer.js
Normal file
875
src/tokenized-buffer.js
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user