mirror of
https://github.com/atom/atom.git
synced 2026-02-07 13:14:55 -05:00
Merge remote-tracking branch 'upstream/master' into tests
This commit is contained in:
@@ -322,6 +322,44 @@ describe "AtomEnvironment", ->
|
||||
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
|
||||
atom2.destroy()
|
||||
|
||||
describe "deserialization failures", ->
|
||||
|
||||
it "propagates project state restoration failures", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open project directory',
|
||||
{description: 'Project directory `/foo` is no longer on disk.'}
|
||||
|
||||
it "accumulates and reports two errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 2 project directories',
|
||||
{description: 'Project directories `/foo` and `/wat` are no longer on disk.'}
|
||||
|
||||
it "accumulates and reports three+ errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 4 project directories',
|
||||
{description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'}
|
||||
|
||||
describe "openInitialEmptyEditorIfNecessary", ->
|
||||
describe "when there are no paths set", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -13,6 +13,8 @@ describe "Config", ->
|
||||
dotAtomPath = temp.path('atom-spec-config')
|
||||
atom.config.configDirPath = dotAtomPath
|
||||
atom.config.enablePersistence = true
|
||||
atom.config.settingsLoaded = true
|
||||
atom.config.pendingOperations = []
|
||||
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
|
||||
|
||||
afterEach ->
|
||||
@@ -877,7 +879,7 @@ describe "Config", ->
|
||||
|
||||
beforeEach ->
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
|
||||
spyOn(fs, "existsSync").andCallFake ->
|
||||
spyOn(fs, "makeTreeSync").andCallFake ->
|
||||
error = new Error()
|
||||
error.code = 'EPERM'
|
||||
throw error
|
||||
@@ -895,16 +897,15 @@ describe "Config", ->
|
||||
describe ".observeUserConfig()", ->
|
||||
updatedHandler = null
|
||||
|
||||
writeConfigFile = (data) ->
|
||||
previousSetTimeoutCallCount = setTimeout.callCount
|
||||
runs ->
|
||||
fs.writeFileSync(atom.config.configFilePath, data)
|
||||
waitsFor "debounced config file load", ->
|
||||
setTimeout.callCount > previousSetTimeoutCallCount
|
||||
runs ->
|
||||
advanceClock(1000)
|
||||
writeConfigFile = (data, secondsInFuture = 0) ->
|
||||
fs.writeFileSync(atom.config.configFilePath, data)
|
||||
|
||||
future = (Date.now() / 1000) + secondsInFuture
|
||||
fs.utimesSync(atom.config.configFilePath, future, future)
|
||||
|
||||
beforeEach ->
|
||||
jasmine.useRealClock()
|
||||
|
||||
atom.config.setSchema 'foo',
|
||||
type: 'object'
|
||||
properties:
|
||||
@@ -920,7 +921,7 @@ describe "Config", ->
|
||||
default: 12
|
||||
|
||||
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
|
||||
fs.writeFileSync atom.config.configFilePath, """
|
||||
writeConfigFile """
|
||||
'*':
|
||||
foo:
|
||||
bar: 'baz'
|
||||
@@ -930,26 +931,32 @@ describe "Config", ->
|
||||
scoped: true
|
||||
"""
|
||||
atom.config.loadUserConfig()
|
||||
atom.config.observeUserConfig()
|
||||
updatedHandler = jasmine.createSpy("updatedHandler")
|
||||
atom.config.onDidChange updatedHandler
|
||||
|
||||
waitsForPromise -> atom.config.observeUserConfig()
|
||||
|
||||
runs ->
|
||||
updatedHandler = jasmine.createSpy "updatedHandler"
|
||||
atom.config.onDidChange updatedHandler
|
||||
|
||||
afterEach ->
|
||||
atom.config.unobserveUserConfig()
|
||||
fs.removeSync(dotAtomPath)
|
||||
|
||||
describe "when the config file changes to contain valid cson", ->
|
||||
|
||||
it "updates the config data", ->
|
||||
writeConfigFile("foo: { bar: 'quux', baz: 'bar'}")
|
||||
writeConfigFile "foo: { bar: 'quux', baz: 'bar'}", 2
|
||||
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBe 'quux'
|
||||
expect(atom.config.get('foo.baz')).toBe 'bar'
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy()
|
||||
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy "unchanged"
|
||||
|
||||
writeConfigFile("foo: { bar: 'baz', baz: 'ok'}")
|
||||
writeConfigFile "foo: { bar: 'baz', baz: 'ok'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@@ -964,15 +971,16 @@ describe "Config", ->
|
||||
items:
|
||||
type: 'string'
|
||||
|
||||
writeConfigFile("foo: { bar: ['baz', 'ok']}")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "foo: { bar: ['baz', 'ok']}", 4
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
runs -> updatedHandler.reset()
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
noChangeSpy = jasmine.createSpy()
|
||||
noChangeSpy = jasmine.createSpy "unchanged"
|
||||
atom.config.onDidChange('foo.bar', noChangeSpy)
|
||||
|
||||
writeConfigFile("foo: { bar: ['baz', 'ok'], baz: 'another'}")
|
||||
writeConfigFile "foo: { bar: ['baz', 'ok'], baz: 'another'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@@ -989,7 +997,7 @@ describe "Config", ->
|
||||
'*':
|
||||
foo:
|
||||
scoped: false
|
||||
"""
|
||||
""", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@@ -997,7 +1005,7 @@ describe "Config", ->
|
||||
expect(atom.config.get('foo.scoped', scope: ['.source.ruby'])).toBe false
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
noChangeSpy = jasmine.createSpy()
|
||||
noChangeSpy = jasmine.createSpy "no change"
|
||||
atom.config.onDidChange('foo.scoped', scope: ['.source.ruby'], noChangeSpy)
|
||||
|
||||
writeConfigFile """
|
||||
@@ -1007,7 +1015,7 @@ describe "Config", ->
|
||||
'.source.ruby':
|
||||
foo:
|
||||
scoped: true
|
||||
"""
|
||||
""", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@@ -1017,7 +1025,7 @@ describe "Config", ->
|
||||
|
||||
describe "when the config file changes to omit a setting with a default", ->
|
||||
it "resets the setting back to the default", ->
|
||||
writeConfigFile("foo: { baz: 'new'}")
|
||||
writeConfigFile "foo: { baz: 'new'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBe 'def'
|
||||
@@ -1025,20 +1033,20 @@ describe "Config", ->
|
||||
|
||||
describe "when the config file changes to be empty", ->
|
||||
beforeEach ->
|
||||
writeConfigFile("")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "", 4
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "resets all settings back to the defaults", ->
|
||||
expect(updatedHandler.callCount).toBe 1
|
||||
expect(atom.config.get('foo.bar')).toBe 'def'
|
||||
atom.config.set("hair", "blonde") # trigger a save
|
||||
advanceClock(500)
|
||||
expect(atom.config.save).toHaveBeenCalled()
|
||||
waitsFor 'save', -> atom.config.save.callCount > 0
|
||||
|
||||
describe "when the config file subsequently changes again to contain configuration", ->
|
||||
beforeEach ->
|
||||
updatedHandler.reset()
|
||||
writeConfigFile("foo: bar: 'newVal'")
|
||||
writeConfigFile "foo: bar: 'newVal'", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "sets the setting to the value specified in the config file", ->
|
||||
@@ -1047,25 +1055,26 @@ describe "Config", ->
|
||||
describe "when the config file changes to contain invalid cson", ->
|
||||
addErrorHandler = null
|
||||
beforeEach ->
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
|
||||
writeConfigFile("}}}")
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy "error handler"
|
||||
writeConfigFile "}}}", 4
|
||||
waitsFor "error to be logged", -> addErrorHandler.callCount > 0
|
||||
|
||||
it "logs a warning and does not update config data", ->
|
||||
expect(updatedHandler.callCount).toBe 0
|
||||
expect(atom.config.get('foo.bar')).toBe 'baz'
|
||||
|
||||
atom.config.set("hair", "blonde") # trigger a save
|
||||
expect(atom.config.save).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the config file subsequently changes again to contain valid cson", ->
|
||||
beforeEach ->
|
||||
writeConfigFile("foo: bar: 'newVal'")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "foo: bar: 'newVal'", 6
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "updates the config data and resumes saving", ->
|
||||
atom.config.set("hair", "blonde")
|
||||
advanceClock(500)
|
||||
expect(atom.config.save).toHaveBeenCalled()
|
||||
waitsFor 'save', -> atom.config.save.callCount > 0
|
||||
|
||||
describe ".initializeConfigDirectory()", ->
|
||||
beforeEach ->
|
||||
@@ -1741,3 +1750,35 @@ describe "Config", ->
|
||||
|
||||
expect(atom.config.set('foo.bar.str_options', 'One')).toBe false
|
||||
expect(atom.config.get('foo.bar.str_options')).toEqual 'two'
|
||||
|
||||
describe "when .set/.unset is called prior to .loadUserConfig", ->
|
||||
beforeEach ->
|
||||
atom.config.settingsLoaded = false
|
||||
fs.writeFileSync atom.config.configFilePath, """
|
||||
'*':
|
||||
foo:
|
||||
bar: 'baz'
|
||||
do:
|
||||
ray: 'me'
|
||||
"""
|
||||
|
||||
it "ensures that early set and unset calls are replayed after the config is loaded from disk", ->
|
||||
atom.config.unset 'foo.bar'
|
||||
atom.config.set 'foo.qux', 'boo'
|
||||
|
||||
expect(atom.config.get('foo.bar')).toBeUndefined()
|
||||
expect(atom.config.get('foo.qux')).toBe 'boo'
|
||||
expect(atom.config.get('do.ray')).toBeUndefined()
|
||||
|
||||
advanceClock 100
|
||||
expect(atom.config.save).not.toHaveBeenCalled()
|
||||
|
||||
atom.config.loadUserConfig()
|
||||
|
||||
advanceClock 100
|
||||
waitsFor -> atom.config.save.callCount > 0
|
||||
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBeUndefined()
|
||||
expect(atom.config.get('foo.qux')).toBe 'boo'
|
||||
expect(atom.config.get('do.ray')).toBe 'me'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'name': 'Test Ruby'
|
||||
'scopeName': 'test.rb'
|
||||
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
|
||||
'fileTypes': [
|
||||
'rb'
|
||||
]
|
||||
|
||||
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
activate: () => null,
|
||||
deactivate: () => null,
|
||||
handleURI: () => null,
|
||||
}
|
||||
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-with-uri-handler",
|
||||
"uriHandler": {
|
||||
"method": "handleURI"
|
||||
}
|
||||
}
|
||||
@@ -1,101 +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 ->
|
||||
if provider?
|
||||
provider.pathToRepository[key].destroy() for key in Object.keys(provider.pathToRepository)
|
||||
|
||||
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")
|
||||
111
spec/git-repository-provider-spec.js
Normal file
111
spec/git-repository-provider-spec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (provider) {
|
||||
Object.keys(provider.pathToRepository).forEach(key => {
|
||||
provider.pathToRepository[key].destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@ describe "the `grammars` global", ->
|
||||
atom.packages.activatePackage('language-git')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
waitsForPromise ->
|
||||
atom.packages.deactivatePackages()
|
||||
runs ->
|
||||
atom.packages.unloadPackages()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".selectGrammar(filePath)", ->
|
||||
it "always returns a grammar", ->
|
||||
@@ -118,6 +120,8 @@ describe "the `grammars` global", ->
|
||||
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
|
||||
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
|
||||
|
||||
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
|
||||
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
|
||||
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
|
||||
|
||||
describe "when there is no file path", ->
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
GutterContainer = require '../src/gutter-container'
|
||||
|
||||
describe 'GutterContainer', ->
|
||||
gutterContainer = null
|
||||
fakeTextEditor = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
|
||||
describe 'when initialized', ->
|
||||
it 'it has no gutters', ->
|
||||
expect(gutterContainer.getGutters().length).toBe 0
|
||||
|
||||
describe '::addGutter', ->
|
||||
it 'creates a new gutter', ->
|
||||
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
|
||||
expect(gutterContainer.getGutters()).toEqual [newGutter]
|
||||
expect(newGutter.priority).toBe 1
|
||||
|
||||
it 'throws an error if the provided gutter name is already in use', ->
|
||||
name = 'test-gutter'
|
||||
gutterContainer.addGutter {name}
|
||||
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
|
||||
|
||||
it 'keeps added gutters sorted by ascending priority', ->
|
||||
gutter1 = gutterContainer.addGutter {name: 'first', priority: 1}
|
||||
gutter3 = gutterContainer.addGutter {name: 'third', priority: 3}
|
||||
gutter2 = gutterContainer.addGutter {name: 'second', priority: 2}
|
||||
expect(gutterContainer.getGutters()).toEqual [gutter1, gutter2, gutter3]
|
||||
|
||||
describe '::removeGutter', ->
|
||||
removedGutters = null
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
removedGutters = []
|
||||
gutterContainer.onDidRemoveGutter (gutterName) ->
|
||||
removedGutters.push gutterName
|
||||
|
||||
it 'removes the gutter if it is contained by this GutterContainer', ->
|
||||
gutter = gutterContainer.addGutter {'test-gutter'}
|
||||
expect(gutterContainer.getGutters()).toEqual [gutter]
|
||||
gutterContainer.removeGutter gutter
|
||||
expect(gutterContainer.getGutters().length).toBe 0
|
||||
expect(removedGutters).toEqual [gutter.name]
|
||||
|
||||
it 'throws an error if the gutter is not within this GutterContainer', ->
|
||||
fakeOtherTextEditor = {}
|
||||
otherGutterContainer = new GutterContainer fakeOtherTextEditor
|
||||
gutter = new Gutter 'gutter-name', otherGutterContainer
|
||||
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
|
||||
|
||||
describe '::destroy', ->
|
||||
it 'clears its array of gutters and destroys custom gutters', ->
|
||||
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
|
||||
newGutterSpy = jasmine.createSpy()
|
||||
newGutter.onDidDestroy(newGutterSpy)
|
||||
|
||||
gutterContainer.destroy()
|
||||
expect(newGutterSpy).toHaveBeenCalled()
|
||||
expect(gutterContainer.getGutters()).toEqual []
|
||||
77
spec/gutter-container-spec.js
Normal file
77
spec/gutter-container-spec.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const Gutter = require('../src/gutter')
|
||||
const GutterContainer = require('../src/gutter-container')
|
||||
|
||||
describe('GutterContainer', () => {
|
||||
let gutterContainer = null
|
||||
const fakeTextEditor = {
|
||||
scheduleComponentUpdate () {}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
gutterContainer = new GutterContainer(fakeTextEditor)
|
||||
})
|
||||
|
||||
describe('when initialized', () =>
|
||||
it('it has no gutters', () => {
|
||||
expect(gutterContainer.getGutters().length).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::addGutter', () => {
|
||||
it('creates a new gutter', () => {
|
||||
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
|
||||
expect(gutterContainer.getGutters()).toEqual([newGutter])
|
||||
expect(newGutter.priority).toBe(1)
|
||||
})
|
||||
|
||||
it('throws an error if the provided gutter name is already in use', () => {
|
||||
const name = 'test-gutter'
|
||||
gutterContainer.addGutter({name})
|
||||
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
|
||||
})
|
||||
|
||||
it('keeps added gutters sorted by ascending priority', () => {
|
||||
const gutter1 = gutterContainer.addGutter({name: 'first', priority: 1})
|
||||
const gutter3 = gutterContainer.addGutter({name: 'third', priority: 3})
|
||||
const gutter2 = gutterContainer.addGutter({name: 'second', priority: 2})
|
||||
expect(gutterContainer.getGutters()).toEqual([gutter1, gutter2, gutter3])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::removeGutter', () => {
|
||||
let removedGutters
|
||||
|
||||
beforeEach(function () {
|
||||
gutterContainer = new GutterContainer(fakeTextEditor)
|
||||
removedGutters = []
|
||||
gutterContainer.onDidRemoveGutter(gutterName => removedGutters.push(gutterName))
|
||||
})
|
||||
|
||||
it('removes the gutter if it is contained by this GutterContainer', () => {
|
||||
const gutter = gutterContainer.addGutter({'test-gutter': 'test-gutter'})
|
||||
expect(gutterContainer.getGutters()).toEqual([gutter])
|
||||
gutterContainer.removeGutter(gutter)
|
||||
expect(gutterContainer.getGutters().length).toBe(0)
|
||||
expect(removedGutters).toEqual([gutter.name])
|
||||
})
|
||||
|
||||
it('throws an error if the gutter is not within this GutterContainer', () => {
|
||||
const fakeOtherTextEditor = {}
|
||||
const otherGutterContainer = new GutterContainer(fakeOtherTextEditor)
|
||||
const gutter = new Gutter('gutter-name', otherGutterContainer)
|
||||
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::destroy', () =>
|
||||
it('clears its array of gutters and destroys custom gutters', () => {
|
||||
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
|
||||
const newGutterSpy = jasmine.createSpy()
|
||||
newGutter.onDidDestroy(newGutterSpy)
|
||||
|
||||
gutterContainer.destroy()
|
||||
expect(newGutterSpy).toHaveBeenCalled()
|
||||
expect(gutterContainer.getGutters()).toEqual([])
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe 'Gutter', ->
|
||||
fakeGutterContainer = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
name = 'name'
|
||||
|
||||
describe '::hide', ->
|
||||
it 'hides the gutter if it is visible.', ->
|
||||
options =
|
||||
name: name
|
||||
visible: true
|
||||
gutter = new Gutter fakeGutterContainer, options
|
||||
events = []
|
||||
gutter.onDidChangeVisible (gutter) ->
|
||||
events.push gutter.isVisible()
|
||||
|
||||
expect(gutter.isVisible()).toBe true
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe false
|
||||
expect(events).toEqual [false]
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe false
|
||||
# An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe 1
|
||||
|
||||
describe '::show', ->
|
||||
it 'shows the gutter if it is hidden.', ->
|
||||
options =
|
||||
name: name
|
||||
visible: false
|
||||
gutter = new Gutter fakeGutterContainer, options
|
||||
events = []
|
||||
gutter.onDidChangeVisible (gutter) ->
|
||||
events.push gutter.isVisible()
|
||||
|
||||
expect(gutter.isVisible()).toBe false
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe true
|
||||
expect(events).toEqual [true]
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe true
|
||||
# An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe 1
|
||||
|
||||
describe '::destroy', ->
|
||||
[mockGutterContainer, mockGutterContainerRemovedGutters] = []
|
||||
|
||||
beforeEach ->
|
||||
mockGutterContainerRemovedGutters = []
|
||||
mockGutterContainer = removeGutter: (destroyedGutter) ->
|
||||
mockGutterContainerRemovedGutters.push destroyedGutter
|
||||
|
||||
it 'removes the gutter from its container.', ->
|
||||
gutter = new Gutter mockGutterContainer, {name}
|
||||
gutter.destroy()
|
||||
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
|
||||
|
||||
it 'calls all callbacks registered on ::onDidDestroy.', ->
|
||||
gutter = new Gutter mockGutterContainer, {name}
|
||||
didDestroy = false
|
||||
gutter.onDidDestroy ->
|
||||
didDestroy = true
|
||||
gutter.destroy()
|
||||
expect(didDestroy).toBe true
|
||||
|
||||
it 'does not allow destroying the line-number gutter', ->
|
||||
gutter = new Gutter mockGutterContainer, {name: 'line-number'}
|
||||
expect(gutter.destroy).toThrow()
|
||||
82
spec/gutter-spec.js
Normal file
82
spec/gutter-spec.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const Gutter = require('../src/gutter')
|
||||
|
||||
describe('Gutter', () => {
|
||||
const fakeGutterContainer = {
|
||||
scheduleComponentUpdate () {}
|
||||
}
|
||||
const name = 'name'
|
||||
|
||||
describe('::hide', () =>
|
||||
it('hides the gutter if it is visible.', () => {
|
||||
const options = {
|
||||
name,
|
||||
visible: true
|
||||
}
|
||||
const gutter = new Gutter(fakeGutterContainer, options)
|
||||
const events = []
|
||||
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
|
||||
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
expect(events).toEqual([false])
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
// An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe(1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::show', () =>
|
||||
it('shows the gutter if it is hidden.', () => {
|
||||
const options = {
|
||||
name,
|
||||
visible: false
|
||||
}
|
||||
const gutter = new Gutter(fakeGutterContainer, options)
|
||||
const events = []
|
||||
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
|
||||
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
expect(events).toEqual([true])
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
// An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe(1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::destroy', () => {
|
||||
let mockGutterContainer, mockGutterContainerRemovedGutters
|
||||
|
||||
beforeEach(() => {
|
||||
mockGutterContainerRemovedGutters = []
|
||||
mockGutterContainer = {
|
||||
removeGutter (destroyedGutter) {
|
||||
mockGutterContainerRemovedGutters.push(destroyedGutter)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('removes the gutter from its container.', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name})
|
||||
gutter.destroy()
|
||||
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
|
||||
})
|
||||
|
||||
it('calls all callbacks registered on ::onDidDestroy.', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name})
|
||||
let didDestroy = false
|
||||
gutter.onDidDestroy(() => { didDestroy = true })
|
||||
gutter.destroy()
|
||||
expect(didDestroy).toBe(true)
|
||||
})
|
||||
|
||||
it('does not allow destroying the line-number gutter', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
expect(gutter.destroy).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
42
spec/helpers/random.js
Normal file
42
spec/helpers/random.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const WORDS = require('./words')
|
||||
const {Point, Range} = require('text-buffer')
|
||||
|
||||
exports.getRandomBufferRange = function getRandomBufferRange (random, buffer) {
|
||||
const endRow = random(buffer.getLineCount())
|
||||
const startRow = random.intBetween(0, endRow)
|
||||
const startColumn = random(buffer.lineForRow(startRow).length + 1)
|
||||
const endColumn = random(buffer.lineForRow(endRow).length + 1)
|
||||
return Range(Point(startRow, startColumn), Point(endRow, endColumn))
|
||||
}
|
||||
|
||||
exports.buildRandomLines = function buildRandomLines (random, maxLines) {
|
||||
const lines = []
|
||||
|
||||
for (let i = 0; i < random(maxLines); i++) {
|
||||
lines.push(buildRandomLine(random))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildRandomLine (random) {
|
||||
const line = []
|
||||
|
||||
for (let i = 0; i < random(5); i++) {
|
||||
const n = random(10)
|
||||
|
||||
if (n < 2) {
|
||||
line.push('\t')
|
||||
} else if (n < 4) {
|
||||
line.push(' ')
|
||||
} else {
|
||||
if (line.length > 0 && !/\s/.test(line[line.length - 1])) {
|
||||
line.push(' ')
|
||||
}
|
||||
|
||||
line.push(WORDS[random(WORDS.length)])
|
||||
}
|
||||
}
|
||||
|
||||
return line.join('')
|
||||
}
|
||||
46891
spec/helpers/words.js
Normal file
46891
spec/helpers/words.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,490 +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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 syntatic 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 syntatic 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 syntatic 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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
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 ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe "suggestedIndentForBufferRow", ->
|
||||
it "does not return negative values (regression)", ->
|
||||
editor.setText('.test {\npadding: 0;\n}')
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe 0
|
||||
27
spec/main-process/parse-command-line.test.js
Normal file
27
spec/main-process/parse-command-line.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @babel */
|
||||
|
||||
import parseCommandLine from '../../src/main-process/parse-command-line'
|
||||
|
||||
describe('parseCommandLine', function () {
|
||||
describe('when --uri-handler is not passed', function () {
|
||||
it('parses arguments as normal', function () {
|
||||
const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
|
||||
assert.isTrue(args.devMode)
|
||||
assert.isTrue(args.safeMode)
|
||||
assert.isTrue(args.test)
|
||||
assert.deepEqual(args.urlsToOpen, ['atom://test/url', 'atom://other/url'])
|
||||
assert.deepEqual(args.pathsToOpen, ['/some/path'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when --uri-handler is passed', function () {
|
||||
it('ignores other arguments and limits to one URL', function () {
|
||||
const args = parseCommandLine(['-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
|
||||
assert.isUndefined(args.devMode)
|
||||
assert.isUndefined(args.safeMode)
|
||||
assert.isUndefined(args.test)
|
||||
assert.deepEqual(args.urlsToOpen, ['atom://test/url'])
|
||||
assert.deepEqual(args.pathsToOpen, [])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
NotificationManager = require '../src/notification-manager'
|
||||
|
||||
describe "NotificationManager", ->
|
||||
[manager] = []
|
||||
|
||||
beforeEach ->
|
||||
manager = new NotificationManager
|
||||
|
||||
describe "the atom global", ->
|
||||
it "has a notifications instance", ->
|
||||
expect(atom.notifications instanceof NotificationManager).toBe true
|
||||
|
||||
describe "adding events", ->
|
||||
addSpy = null
|
||||
|
||||
beforeEach ->
|
||||
addSpy = jasmine.createSpy()
|
||||
manager.onDidAddNotification(addSpy)
|
||||
|
||||
it "emits an event when a notification has been added", ->
|
||||
manager.add('error', 'Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'error'
|
||||
expect(notification.getMessage()).toBe 'Some error!'
|
||||
expect(notification.getIcon()).toBe 'someIcon'
|
||||
|
||||
it "emits a fatal error ::addFatalError has been called", ->
|
||||
manager.addFatalError('Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'fatal'
|
||||
|
||||
it "emits an error ::addError has been called", ->
|
||||
manager.addError('Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'error'
|
||||
|
||||
it "emits a warning notification ::addWarning has been called", ->
|
||||
manager.addWarning('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'warning'
|
||||
|
||||
it "emits an info notification ::addInfo has been called", ->
|
||||
manager.addInfo('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'info'
|
||||
|
||||
it "emits a success notification ::addSuccess has been called", ->
|
||||
manager.addSuccess('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'success'
|
||||
69
spec/notification-manager-spec.js
Normal file
69
spec/notification-manager-spec.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const NotificationManager = require('../src/notification-manager')
|
||||
|
||||
describe('NotificationManager', () => {
|
||||
let manager
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new NotificationManager()
|
||||
})
|
||||
|
||||
describe('the atom global', () =>
|
||||
it('has a notifications instance', () => {
|
||||
expect(atom.notifications instanceof NotificationManager).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('adding events', () => {
|
||||
let addSpy
|
||||
|
||||
beforeEach(() => {
|
||||
addSpy = jasmine.createSpy()
|
||||
manager.onDidAddNotification(addSpy)
|
||||
})
|
||||
|
||||
it('emits an event when a notification has been added', () => {
|
||||
manager.add('error', 'Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('error')
|
||||
expect(notification.getMessage()).toBe('Some error!')
|
||||
expect(notification.getIcon()).toBe('someIcon')
|
||||
})
|
||||
|
||||
it('emits a fatal error when ::addFatalError has been called', () => {
|
||||
manager.addFatalError('Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('fatal')
|
||||
})
|
||||
|
||||
it('emits an error when ::addError has been called', () => {
|
||||
manager.addError('Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('error')
|
||||
})
|
||||
|
||||
it('emits a warning notification when ::addWarning has been called', () => {
|
||||
manager.addWarning('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('warning')
|
||||
})
|
||||
|
||||
it('emits an info notification when ::addInfo has been called', () => {
|
||||
manager.addInfo('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('info')
|
||||
})
|
||||
|
||||
it('emits a success notification when ::addSuccess has been called', () => {
|
||||
manager.addSuccess('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('success')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
Notification = require '../src/notification'
|
||||
|
||||
describe "Notification", ->
|
||||
[notification] = []
|
||||
|
||||
it "throws an error when created with a non-string message", ->
|
||||
expect(-> new Notification('error', null)).toThrow()
|
||||
expect(-> new Notification('error', 3)).toThrow()
|
||||
expect(-> new Notification('error', {})).toThrow()
|
||||
expect(-> new Notification('error', false)).toThrow()
|
||||
expect(-> new Notification('error', [])).toThrow()
|
||||
|
||||
it "throws an error when created with non-object options", ->
|
||||
expect(-> new Notification('error', 'message', 'foo')).toThrow()
|
||||
expect(-> new Notification('error', 'message', 3)).toThrow()
|
||||
expect(-> new Notification('error', 'message', false)).toThrow()
|
||||
expect(-> new Notification('error', 'message', [])).toThrow()
|
||||
|
||||
describe "::getTimestamp()", ->
|
||||
it "returns a Date object", ->
|
||||
notification = new Notification('error', 'message!')
|
||||
expect(notification.getTimestamp() instanceof Date).toBe true
|
||||
|
||||
describe "::getIcon()", ->
|
||||
it "returns a default when no icon specified", ->
|
||||
notification = new Notification('error', 'message!')
|
||||
expect(notification.getIcon()).toBe 'flame'
|
||||
|
||||
it "returns the icon specified", ->
|
||||
notification = new Notification('error', 'message!', icon: 'my-icon')
|
||||
expect(notification.getIcon()).toBe 'my-icon'
|
||||
|
||||
describe "dismissing notifications", ->
|
||||
describe "when the notfication is dismissable", ->
|
||||
it "calls a callback when the notification is dismissed", ->
|
||||
dismissedSpy = jasmine.createSpy()
|
||||
notification = new Notification('error', 'message', dismissable: true)
|
||||
notification.onDidDismiss dismissedSpy
|
||||
|
||||
expect(notification.isDismissable()).toBe true
|
||||
expect(notification.isDismissed()).toBe false
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe true
|
||||
|
||||
describe "when the notfication is not dismissable", ->
|
||||
it "does nothing when ::dismiss() is called", ->
|
||||
dismissedSpy = jasmine.createSpy()
|
||||
notification = new Notification('error', 'message')
|
||||
notification.onDidDismiss dismissedSpy
|
||||
|
||||
expect(notification.isDismissable()).toBe false
|
||||
expect(notification.isDismissed()).toBe true
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).not.toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe true
|
||||
71
spec/notification-spec.js
Normal file
71
spec/notification-spec.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const Notification = require('../src/notification')
|
||||
|
||||
describe('Notification', () => {
|
||||
it('throws an error when created with a non-string message', () => {
|
||||
expect(() => new Notification('error', null)).toThrow()
|
||||
expect(() => new Notification('error', 3)).toThrow()
|
||||
expect(() => new Notification('error', {})).toThrow()
|
||||
expect(() => new Notification('error', false)).toThrow()
|
||||
expect(() => new Notification('error', [])).toThrow()
|
||||
})
|
||||
|
||||
it('throws an error when created with non-object options', () => {
|
||||
expect(() => new Notification('error', 'message', 'foo')).toThrow()
|
||||
expect(() => new Notification('error', 'message', 3)).toThrow()
|
||||
expect(() => new Notification('error', 'message', false)).toThrow()
|
||||
expect(() => new Notification('error', 'message', [])).toThrow()
|
||||
})
|
||||
|
||||
describe('::getTimestamp()', () =>
|
||||
it('returns a Date object', () => {
|
||||
const notification = new Notification('error', 'message!')
|
||||
expect(notification.getTimestamp() instanceof Date).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::getIcon()', () => {
|
||||
it('returns a default when no icon specified', () => {
|
||||
const notification = new Notification('error', 'message!')
|
||||
expect(notification.getIcon()).toBe('flame')
|
||||
})
|
||||
|
||||
it('returns the icon specified', () => {
|
||||
const notification = new Notification('error', 'message!', {icon: 'my-icon'})
|
||||
expect(notification.getIcon()).toBe('my-icon')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dismissing notifications', () => {
|
||||
describe('when the notfication is dismissable', () =>
|
||||
it('calls a callback when the notification is dismissed', () => {
|
||||
const dismissedSpy = jasmine.createSpy()
|
||||
const notification = new Notification('error', 'message', {dismissable: true})
|
||||
notification.onDidDismiss(dismissedSpy)
|
||||
|
||||
expect(notification.isDismissable()).toBe(true)
|
||||
expect(notification.isDismissed()).toBe(false)
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the notfication is not dismissable', () =>
|
||||
it('does nothing when ::dismiss() is called', () => {
|
||||
const dismissedSpy = jasmine.createSpy()
|
||||
const notification = new Notification('error', 'message')
|
||||
notification.onDidDismiss(dismissedSpy)
|
||||
|
||||
expect(notification.isDismissable()).toBe(false)
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).not.toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
1354
spec/package-manager-spec.js
Normal file
1354
spec/package-manager-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -138,7 +138,8 @@ describe "Package", ->
|
||||
jasmine.attachToDOM(editorElement)
|
||||
|
||||
afterEach ->
|
||||
theme.deactivate() if theme?
|
||||
waitsForPromise ->
|
||||
Promise.resolve(theme.deactivate()) if theme?
|
||||
|
||||
describe "when the theme contains a single style file", ->
|
||||
it "loads and applies css", ->
|
||||
@@ -200,8 +201,10 @@ describe "Package", ->
|
||||
|
||||
it "deactivated event fires on .deactivate()", ->
|
||||
theme.onDidDeactivate spy = jasmine.createSpy()
|
||||
theme.deactivate()
|
||||
expect(spy).toHaveBeenCalled()
|
||||
waitsForPromise ->
|
||||
Promise.resolve(theme.deactivate())
|
||||
runs ->
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
describe ".loadMetadata()", ->
|
||||
[packagePath, metadata] = []
|
||||
|
||||
@@ -172,7 +172,7 @@ describe "PaneContainerElement", ->
|
||||
lowerPane = leftPane.splitDown()
|
||||
expectPaneScale [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5]
|
||||
|
||||
# dynamically close pane, the pane's flexscale will recorver to origin value
|
||||
# dynamically close pane, the pane's flexscale will recover to origin value
|
||||
waitsForPromise -> lowerPane.close()
|
||||
runs -> expectPaneScale [leftPane, 0.5], [rightPane, 1.5]
|
||||
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
PaneContainer = require '../src/pane-container'
|
||||
Pane = require '../src/pane'
|
||||
|
||||
describe "PaneContainer", ->
|
||||
[confirm, params] = []
|
||||
|
||||
beforeEach ->
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
|
||||
describe "serialization", ->
|
||||
[containerA, pane1A, pane2A, pane3A] = []
|
||||
|
||||
beforeEach ->
|
||||
# This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item
|
||||
atom.deserializers.add(this)
|
||||
@deserialize: -> new this
|
||||
serialize: -> deserializer: 'Item'
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item)
|
||||
pane2A = pane1A.splitRight(items: [new Item])
|
||||
pane3A = pane2A.splitDown(items: [new Item])
|
||||
pane3A.focus()
|
||||
|
||||
it "preserves the focused pane across serialization", ->
|
||||
expect(pane3A.focused).toBe true
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(pane3B.focused).toBe true
|
||||
|
||||
it "preserves the active pane across serialization, independent of focus", ->
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe pane3A
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(containerB.getActivePane()).toBe pane3B
|
||||
|
||||
it "makes the first pane active if no pane exists for the activePaneId", ->
|
||||
pane3A.activate()
|
||||
state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe containerB.getPanes()[0]
|
||||
|
||||
describe "if there are empty panes after deserialization", ->
|
||||
beforeEach ->
|
||||
pane3A.getItems()[0].serialize = -> deserializer: 'Bogus'
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is false (the default)", ->
|
||||
it "leaves the empty panes intact", ->
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, column] = containerB.getRoot().getChildren()
|
||||
[topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(topPane.getItems().length).toBe 1
|
||||
expect(bottomPane.getItems().length).toBe 0
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is true", ->
|
||||
it "removes empty panes on deserialization", ->
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(rightPane.getItems().length).toBe 1
|
||||
|
||||
it "does not allow the root pane to be destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe false
|
||||
|
||||
describe "::getActivePane()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
|
||||
it "returns the first pane if no pane has been made active", ->
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
it "returns the most pane on which ::activate() was most recently called", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe pane2
|
||||
expect(pane1.isActive()).toBe false
|
||||
expect(pane2.isActive()).toBe true
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
expect(pane2.isActive()).toBe false
|
||||
|
||||
it "returns the next pane if the current active pane is destroyed", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
describe "::onDidChangeActivePane()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane (pane) -> observed.push(pane)
|
||||
|
||||
it "invokes observers when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual [pane1, pane2]
|
||||
|
||||
describe "::onDidChangeActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePaneItem (item) -> observed.push(item)
|
||||
|
||||
it "invokes observers when the active item of the active pane changes", ->
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual [pane2.itemAtIndex(1), pane2.itemAtIndex(0)]
|
||||
|
||||
it "invokes observers when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual [pane1.itemAtIndex(0), pane2.itemAtIndex(0)]
|
||||
|
||||
describe "::onDidStopChangingActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem (item) -> observed.push(item)
|
||||
|
||||
it "invokes observers once when the active item of the active pane changes", ->
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
it "invokes observers once when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
describe "::onDidActivatePane", ->
|
||||
it "invokes observers when a pane is activated (even if it was already active)", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
activatedPanes = []
|
||||
container.onDidActivatePane (pane) -> activatedPanes.push(pane)
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
|
||||
describe "::observePanes()", ->
|
||||
it "invokes observers with all current and future panes", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.observePanes (pane) -> observed.push(pane)
|
||||
|
||||
pane3 = pane2.splitDown()
|
||||
pane4 = pane2.splitRight()
|
||||
|
||||
expect(observed).toEqual [pane1, pane2, pane3, pane4]
|
||||
|
||||
describe "::observePaneItems()", ->
|
||||
it "invokes observers with all current and future pane items", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
observed = []
|
||||
container.observePaneItems (pane) -> observed.push(pane)
|
||||
|
||||
pane3 = pane2.splitDown(items: [new Object])
|
||||
pane3.addItems([new Object, new Object])
|
||||
|
||||
expect(observed).toEqual container.getPaneItems()
|
||||
|
||||
describe "::confirmClose()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
shouldPromptToSave: -> true
|
||||
getURI: -> 'test'
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem)
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
it "returns true if the user saves all modified files when prompted", ->
|
||||
confirm.andReturn(0)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
|
||||
it "returns false if the user cancels saving any modified file", ->
|
||||
confirm.andReturn(1)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
|
||||
describe "::onDidAddPane(callback)", ->
|
||||
it "invokes the given callback when panes are added", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidAddPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe true
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPane(callback)", ->
|
||||
it "invokes the given callback before panes or their items are destroyed", ->
|
||||
class TestItem
|
||||
constructor: -> @_isDestroyed = false
|
||||
destroy: -> @_isDestroyed = true
|
||||
isDestroyed: -> @_isDestroyed
|
||||
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onWillDestroyPane (event) ->
|
||||
itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems())
|
||||
events.push([event, itemsDestroyed: itemsDestroyed])
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual [[{pane: pane2}, itemsDestroyed: [false]]]
|
||||
|
||||
describe "::onDidDestroyPane(callback)", ->
|
||||
it "invokes the given callback when panes are destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
it "invokes the given callback when the container is destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane1}, {pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", ->
|
||||
it "invokes the given callbacks when an item will be destroyed on any pane", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new Object
|
||||
item2 = new Object
|
||||
item3 = new Object
|
||||
|
||||
pane1.addItem(item1)
|
||||
events = []
|
||||
container.onWillDestroyPaneItem (event) -> events.push(['will', event])
|
||||
container.onDidDestroyPaneItem (event) -> events.push(['did', event])
|
||||
pane2 = pane1.splitRight(items: [item2, item3])
|
||||
|
||||
pane1.destroyItem(item1)
|
||||
pane2.destroyItem(item3)
|
||||
pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual [
|
||||
['will', {item: item1, pane: pane1, index: 0}]
|
||||
['did', {item: item1, pane: pane1, index: 0}]
|
||||
['will', {item: item3, pane: pane2, index: 1}]
|
||||
['did', {item: item3, pane: pane2, index: 1}]
|
||||
['will', {item: item2, pane: pane2, index: 0}]
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
]
|
||||
|
||||
describe "::saveAll()", ->
|
||||
it "saves all modified pane items", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
pane2 = pane1.splitRight()
|
||||
|
||||
item1 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item2 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> false,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item3 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe true
|
||||
expect(item2.saved).toBe false
|
||||
expect(item3.saved).toBe true
|
||||
|
||||
describe "::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)", ->
|
||||
[container, pane1, pane2, item1] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
constructor: (id) -> @id = id
|
||||
copy: -> new TestItem(@id)
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight(items: [item1])
|
||||
|
||||
describe "::::moveActiveItemToPane(destPane)", ->
|
||||
it "moves active item to given pane and focuses it", ->
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe item1
|
||||
|
||||
describe "::::copyActiveItemToPane(destPane)", ->
|
||||
it "copies active item to given pane and focuses it", ->
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe pane2
|
||||
expect(pane1.getActiveItem().id).toBe item1.id
|
||||
472
spec/pane-container-spec.js
Normal file
472
spec/pane-container-spec.js
Normal file
@@ -0,0 +1,472 @@
|
||||
const PaneContainer = require('../src/pane-container')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('PaneContainer', () => {
|
||||
let confirm, params
|
||||
|
||||
beforeEach(() => {
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers,
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
})
|
||||
|
||||
describe('serialization', () => {
|
||||
let containerA, pane1A, pane2A, pane3A
|
||||
|
||||
beforeEach(() => {
|
||||
// This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item {
|
||||
static deserialize () { return new (this)() }
|
||||
serialize () { return {deserializer: 'Item'} }
|
||||
}
|
||||
atom.deserializers.add(Item)
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item())
|
||||
pane2A = pane1A.splitRight({items: [new Item()]})
|
||||
pane3A = pane2A.splitDown({items: [new Item()]})
|
||||
pane3A.focus()
|
||||
})
|
||||
|
||||
it('preserves the focused pane across serialization', () => {
|
||||
expect(pane3A.focused).toBe(true)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(pane3B.focused).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves the active pane across serialization, independent of focus', () => {
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe(pane3A)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(containerB.getActivePane()).toBe(pane3B)
|
||||
})
|
||||
|
||||
it('makes the first pane active if no pane exists for the activePaneId', () => {
|
||||
pane3A.activate()
|
||||
const state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe(containerB.getPanes()[0])
|
||||
})
|
||||
|
||||
describe('if there are empty panes after deserialization', () => {
|
||||
beforeEach(() => {
|
||||
pane3A.getItems()[0].serialize = () => ({deserializer: 'Bogus'})
|
||||
})
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is false (the default)", () =>
|
||||
it('leaves the empty panes intact', () => {
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, column] = containerB.getRoot().getChildren()
|
||||
const [topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(topPane.getItems().length).toBe(1)
|
||||
expect(bottomPane.getItems().length).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is true", () =>
|
||||
it('removes empty panes on deserialization', () => {
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(rightPane.getItems().length).toBe(1)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the root pane to be destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe(false)
|
||||
})
|
||||
|
||||
describe('::getActivePane()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
})
|
||||
|
||||
it('returns the first pane if no pane has been made active', () => {
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns the most pane on which ::activate() was most recently called', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe(pane2)
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(true)
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
expect(pane2.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('returns the next pane if the current active pane is destroyed', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePane()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane(pane => observed.push(pane))
|
||||
})
|
||||
|
||||
it('invokes observers when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([pane1, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePaneItem(item => observed.push(item))
|
||||
})
|
||||
|
||||
it('invokes observers when the active item of the active pane changes', () => {
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual([pane2.itemAtIndex(1), pane2.itemAtIndex(0)])
|
||||
})
|
||||
|
||||
it('invokes observers when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([pane1.itemAtIndex(0), pane2.itemAtIndex(0)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidStopChangingActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem(item => observed.push(item))
|
||||
})
|
||||
|
||||
it('invokes observers once when the active item of the active pane changes', () => {
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
|
||||
it('invokes observers once when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidActivatePane', () => {
|
||||
it('invokes observers when a pane is activated (even if it was already active)', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const activatedPanes = []
|
||||
container.onDidActivatePane(pane => activatedPanes.push(pane))
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePanes()', () => {
|
||||
it('invokes observers with all current and future panes', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const observed = []
|
||||
container.observePanes(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown()
|
||||
const pane4 = pane2.splitRight()
|
||||
|
||||
expect(observed).toEqual([pane1, pane2, pane3, pane4])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePaneItems()', () =>
|
||||
it('invokes observers with all current and future pane items', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}]})
|
||||
const pane2 = container.getPanes()[1]
|
||||
const observed = []
|
||||
container.observePaneItems(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown({items: [{}]})
|
||||
pane3.addItems([{}, {}])
|
||||
|
||||
expect(observed).toEqual(container.getPaneItems())
|
||||
})
|
||||
)
|
||||
|
||||
describe('::confirmClose()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
shouldPromptToSave () { return true }
|
||||
getURI () { return 'test' }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight();
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem())
|
||||
pane2.addItem(new TestItem())
|
||||
})
|
||||
|
||||
it('returns true if the user saves all modified files when prompted', async () => {
|
||||
confirm.andReturn(0)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false if the user cancels saving any modified file', async () => {
|
||||
confirm.andReturn(1)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAddPane(callback)', () => {
|
||||
it('invokes the given callback when panes are added', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidAddPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(true)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPane(callback)', () => {
|
||||
it('invokes the given callback before panes or their items are destroyed', () => {
|
||||
class TestItem {
|
||||
constructor () { this._isDestroyed = false }
|
||||
destroy () { this._isDestroyed = true }
|
||||
isDestroyed () { return this._isDestroyed }
|
||||
}
|
||||
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onWillDestroyPane((event) => {
|
||||
const itemsDestroyed = event.pane.getItems().map((item) => item.isDestroyed())
|
||||
events.push([event, {itemsDestroyed}])
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem())
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual([[{pane: pane2}, {itemsDestroyed: [false]}]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidDestroyPane(callback)', () => {
|
||||
it('invokes the given callback when panes are destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
|
||||
it('invokes the given callback when the container is destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane1}, {pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPaneItem() and ::onDidDestroyPaneItem', () => {
|
||||
it('invokes the given callbacks when an item will be destroyed on any pane', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
const item1 = {}
|
||||
const item2 = {}
|
||||
const item3 = {}
|
||||
|
||||
pane1.addItem(item1)
|
||||
const events = []
|
||||
container.onWillDestroyPaneItem(event => events.push(['will', event]))
|
||||
container.onDidDestroyPaneItem(event => events.push(['did', event]))
|
||||
const pane2 = pane1.splitRight({items: [item2, item3]})
|
||||
|
||||
await pane1.destroyItem(item1)
|
||||
await pane2.destroyItem(item3)
|
||||
await pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual([
|
||||
['will', {item: item1, pane: pane1, index: 0}],
|
||||
['did', {item: item1, pane: pane1, index: 0}],
|
||||
['will', {item: item3, pane: pane2, index: 1}],
|
||||
['did', {item: item3, pane: pane2, index: 1}],
|
||||
['will', {item: item2, pane: pane2, index: 0}],
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::saveAll()', () =>
|
||||
it('saves all modified pane items', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
pane1.splitRight()
|
||||
|
||||
const item1 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item2 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return false },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item3 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe(true)
|
||||
expect(item2.saved).toBe(false)
|
||||
expect(item3.saved).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)', () => {
|
||||
let container, pane1, pane2, item1
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
constructor (id) { this.id = id }
|
||||
copy () { return new TestItem(this.id) }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight({items: [item1]})
|
||||
})
|
||||
|
||||
describe('::::moveActiveItemToPane(destPane)', () =>
|
||||
it('moves active item to given pane and focuses it', () => {
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe(item1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::::copyActiveItemToPane(destPane)', () =>
|
||||
it('copies active item to given pane and focuses it', () => {
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe(pane2)
|
||||
expect(pane1.getActiveItem().id).toBe(item1.id)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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", ->
|
||||
|
||||
@@ -3,7 +3,7 @@ const {Emitter} = require('event-kit')
|
||||
const Grim = require('grim')
|
||||
const Pane = require('../src/pane')
|
||||
const PaneContainer = require('../src/pane-container')
|
||||
const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
|
||||
const {it, fit, ffit, fffit, beforeEach, timeoutPromise} = require('./async-spec-helpers')
|
||||
|
||||
describe('Pane', () => {
|
||||
let confirm, showSaveDialog, deserializerDisposable
|
||||
@@ -491,16 +491,31 @@ describe('Pane', () => {
|
||||
expect(pane.getActiveItem()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('invokes ::onWillDestroyItem() observers before destroying the item', () => {
|
||||
it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => {
|
||||
jasmine.useRealClock()
|
||||
pane.container = new PaneContainer({config: atom.config, confirm})
|
||||
const events = []
|
||||
pane.onWillDestroyItem(function (event) {
|
||||
|
||||
pane.onWillDestroyItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(event)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(['will-destroy-item', event])
|
||||
})
|
||||
|
||||
pane.destroyItem(item2)
|
||||
pane.container.onWillDestroyPaneItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(['will-destroy-pane-item', event])
|
||||
})
|
||||
|
||||
await pane.destroyItem(item2)
|
||||
expect(item2.isDestroyed()).toBe(true)
|
||||
expect(events).toEqual([{item: item2, index: 1}])
|
||||
expect(events).toEqual([
|
||||
['will-destroy-item', {item: item2, index: 1}],
|
||||
['will-destroy-pane-item', {item: item2, index: 1, pane}]
|
||||
])
|
||||
})
|
||||
|
||||
it('invokes ::onWillRemoveItem() observers', () => {
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('Panel', () => {
|
||||
expect(spy).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('initially renders panel created with visibile: false', () => {
|
||||
it('initially renders panel created with visible: false', () => {
|
||||
const panel = new Panel({visible: false, item: new TestPanelItem()}, atom.views)
|
||||
const element = panel.getElement()
|
||||
expect(element.style.display).toBe('none')
|
||||
@@ -91,7 +91,7 @@ describe('Panel', () => {
|
||||
})
|
||||
|
||||
describe('when a class name is specified', () => {
|
||||
it('initially renders panel created with visibile: false', () => {
|
||||
it('initially renders panel created with visible: false', () => {
|
||||
const panel = new Panel({className: 'some classes', item: new TestPanelItem()}, atom.views)
|
||||
const element = panel.getElement()
|
||||
|
||||
|
||||
@@ -1,716 +0,0 @@
|
||||
temp = require('temp').track()
|
||||
TextBuffer = require('text-buffer')
|
||||
Project = require '../src/project'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{Directory} = require 'pathwatcher'
|
||||
{stopAllWatchers} = require '../src/path-watcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
|
||||
describe "Project", ->
|
||||
beforeEach ->
|
||||
atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')])
|
||||
|
||||
# Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
|
||||
describe "serialization", ->
|
||||
deserializedProject = null
|
||||
|
||||
afterEach ->
|
||||
deserializedProject?.destroy()
|
||||
|
||||
it "does not deserialize paths to non directories", ->
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
state = atom.project.serialize()
|
||||
state.paths.push('/directory/that/does/not/exist')
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
|
||||
|
||||
it "does not include unretained buffers in the serialized state", ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath('a')
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a')
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 1
|
||||
deserializedProject.getBuffers()[0].destroy()
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is a directory that exists", ->
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.mkdirSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is inaccessible", ->
|
||||
return if process.platform is 'win32' # chmod not supported on win32
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.chmodSync(pathToOpen, '000')
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "serializes marker layers and history only if Atom is quitting", ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a')
|
||||
|
||||
notQuittingProject = null
|
||||
quittingProject = null
|
||||
bufferA = null
|
||||
layerA = null
|
||||
markerA = null
|
||||
|
||||
runs ->
|
||||
bufferA = atom.project.getBuffers()[0]
|
||||
layerA = bufferA.addMarkerLayer(persistent: true)
|
||||
markerA = layerA.markPosition([0, 3])
|
||||
bufferA.append('!')
|
||||
|
||||
waitsForPromise ->
|
||||
notQuittingProject?.destroy()
|
||||
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})).then ->
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
|
||||
waitsForPromise ->
|
||||
quittingProject?.destroy()
|
||||
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
quittingProject.deserialize(atom.project.serialize({isUnloading: true})).then ->
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
|
||||
describe "when an editor is saved and the project has no path", ->
|
||||
it "sets the project's path to the saved file's parent directory", ->
|
||||
tempFile = temp.openSync().path
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()[0]).toBeUndefined()
|
||||
editor = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open().then (o) -> editor = o
|
||||
|
||||
waitsForPromise ->
|
||||
editor.saveAs(tempFile)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)
|
||||
|
||||
describe "before and after saving a buffer", ->
|
||||
[buffer] = []
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) ->
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
|
||||
afterEach ->
|
||||
buffer.release()
|
||||
|
||||
it "emits save events on the main process", ->
|
||||
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
|
||||
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
|
||||
|
||||
waitsForPromise -> buffer.save()
|
||||
|
||||
runs ->
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
|
||||
describe "when a watch error is thrown from the TextBuffer", ->
|
||||
editor = null
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o
|
||||
|
||||
it "creates a warning notification", ->
|
||||
atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy()
|
||||
|
||||
error = new Error('SomeError')
|
||||
error.eventType = 'resurrect'
|
||||
editor.buffer.emitter.emit 'will-throw-watch-error',
|
||||
handle: jasmine.createSpy()
|
||||
error: error
|
||||
|
||||
expect(noteSpy).toHaveBeenCalled()
|
||||
|
||||
notification = noteSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'warning'
|
||||
expect(notification.getDetail()).toBe 'SomeError'
|
||||
expect(notification.getMessage()).toContain '`resurrect`'
|
||||
expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a')
|
||||
|
||||
describe "when a custom repository-provider service is provided", ->
|
||||
[fakeRepositoryProvider, fakeRepository] = []
|
||||
|
||||
beforeEach ->
|
||||
fakeRepository = {destroy: -> null}
|
||||
fakeRepositoryProvider = {
|
||||
repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository)
|
||||
repositoryForDirectorySync: (directory) -> fakeRepository
|
||||
}
|
||||
|
||||
it "uses it to create repositories for any directories that need one", ->
|
||||
projectPath = temp.mkdirSync('atom-project')
|
||||
atom.project.setPaths([projectPath])
|
||||
expect(atom.project.getRepositories()).toEqual [null]
|
||||
|
||||
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs -> atom.project.getRepositories()[0] is fakeRepository
|
||||
|
||||
it "does not create any new repositories if every directory has a repository", ->
|
||||
repositories = atom.project.getRepositories()
|
||||
expect(repositories.length).toEqual 1
|
||||
expect(repositories[0]).toBeTruthy()
|
||||
|
||||
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs -> expect(atom.project.getRepositories()).toBe repositories
|
||||
|
||||
it "stops using it to create repositories when the service is removed", ->
|
||||
atom.project.setPaths([])
|
||||
|
||||
disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs ->
|
||||
disposable.dispose()
|
||||
atom.project.addPath(temp.mkdirSync('atom-project'))
|
||||
expect(atom.project.getRepositories()).toEqual [null]
|
||||
|
||||
describe "when a custom directory-provider service is provided", ->
|
||||
class DummyDirectory
|
||||
constructor: (@path) ->
|
||||
getPath: -> @path
|
||||
getFile: -> {existsSync: -> false}
|
||||
getSubdirectory: -> {existsSync: -> false}
|
||||
isRoot: -> true
|
||||
existsSync: -> @path.endsWith('does-exist')
|
||||
contains: (filePath) -> filePath.startsWith(@path)
|
||||
|
||||
serviceDisposable = null
|
||||
|
||||
beforeEach ->
|
||||
serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
|
||||
directoryForURISync: (uri) ->
|
||||
if uri.startsWith("ssh://")
|
||||
new DummyDirectory(uri)
|
||||
else
|
||||
null
|
||||
})
|
||||
|
||||
waitsFor ->
|
||||
atom.project.directoryProviders.length > 0
|
||||
|
||||
it "uses the provider's custom directories for any paths that it handles", ->
|
||||
localPath = temp.mkdirSync('local-path')
|
||||
remotePath = "ssh://foreign-directory:8080/does-exist"
|
||||
|
||||
atom.project.setPaths([localPath, remotePath])
|
||||
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[0].getPath()).toBe localPath
|
||||
expect(directories[0] instanceof Directory).toBe true
|
||||
expect(directories[1].getPath()).toBe remotePath
|
||||
expect(directories[1] instanceof DummyDirectory).toBe true
|
||||
|
||||
# It does not add new remote paths that do not exist
|
||||
nonExistentRemotePath = "ssh://another-directory:8080/does-not-exist"
|
||||
atom.project.addPath(nonExistentRemotePath)
|
||||
expect(atom.project.getDirectories().length).toBe 2
|
||||
|
||||
# It adds new remote paths if their directories exist.
|
||||
newRemotePath = "ssh://another-directory:8080/does-exist"
|
||||
atom.project.addPath(newRemotePath)
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[2].getPath()).toBe newRemotePath
|
||||
expect(directories[2] instanceof DummyDirectory).toBe true
|
||||
|
||||
it "stops using the provider when the service is removed", ->
|
||||
serviceDisposable.dispose()
|
||||
atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"])
|
||||
expect(atom.project.getDirectories().length).toBe(0)
|
||||
|
||||
describe ".open(path)", ->
|
||||
[absolutePath, newBufferHandler] = []
|
||||
|
||||
beforeEach ->
|
||||
absolutePath = require.resolve('./fixtures/dir/a')
|
||||
newBufferHandler = jasmine.createSpy('newBufferHandler')
|
||||
atom.project.onDidAddBuffer(newBufferHandler)
|
||||
|
||||
describe "when given an absolute path that isn't currently open", ->
|
||||
it "returns a new edit session for the given path and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBe absolutePath
|
||||
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
|
||||
|
||||
describe "when given a relative path that isn't currently opened", ->
|
||||
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBe absolutePath
|
||||
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
|
||||
|
||||
describe "when passed the path to a buffer that is currently opened", ->
|
||||
it "returns a new edit session containing currently opened buffer", ->
|
||||
editor = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
newBufferHandler.reset()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then ({buffer}) ->
|
||||
expect(buffer).toBe editor.buffer
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a').then ({buffer}) ->
|
||||
expect(buffer).toBe editor.buffer
|
||||
expect(newBufferHandler).not.toHaveBeenCalled()
|
||||
|
||||
describe "when not passed a path", ->
|
||||
it "returns a new edit session and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open().then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBeUndefined()
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
|
||||
describe ".bufferForPath(path)", ->
|
||||
buffer = null
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("a").then (o) ->
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
|
||||
afterEach ->
|
||||
buffer.release()
|
||||
|
||||
describe "when opening a previously opened path", ->
|
||||
it "does not create a new buffer", ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("a").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).toBe buffer
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("b").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).not.toBe buffer
|
||||
|
||||
waitsForPromise ->
|
||||
Promise.all([
|
||||
atom.project.bufferForPath('c'),
|
||||
atom.project.bufferForPath('c')
|
||||
]).then ([buffer1, buffer2]) ->
|
||||
expect(buffer1).toBe(buffer2)
|
||||
|
||||
it "retries loading the buffer if it previously failed", ->
|
||||
waitsForPromise shouldReject: true, ->
|
||||
spyOn(TextBuffer, 'load').andCallFake ->
|
||||
Promise.reject(new Error('Could not open file'))
|
||||
atom.project.bufferForPath('b')
|
||||
|
||||
waitsForPromise shouldReject: false, ->
|
||||
TextBuffer.load.andCallThrough()
|
||||
atom.project.bufferForPath('b')
|
||||
|
||||
it "creates a new buffer if the previous buffer was destroyed", ->
|
||||
buffer.release()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("b").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).not.toBe buffer
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
it "resolves to null when the directory does not have a repository", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory("/tmp")
|
||||
atom.project.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeNull()
|
||||
expect(atom.project.repositoryProviders.length).toBeGreaterThan 0
|
||||
expect(atom.project.repositoryPromisesByPath.size).toBe 0
|
||||
|
||||
it "resolves to a GitRepository and is cached when the given directory is a Git repo", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory(path.join(__dirname, '..'))
|
||||
promise = atom.project.repositoryForDirectory(directory)
|
||||
promise.then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
dirPath = directory.getRealPathSync()
|
||||
expect(result.getPath()).toBe path.join(dirPath, '.git')
|
||||
|
||||
# Verify that the result is cached.
|
||||
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
|
||||
|
||||
it "creates a new repository if a previous one with the same directory had been destroyed", ->
|
||||
repository = null
|
||||
directory = new Directory(path.join(__dirname, '..'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
|
||||
|
||||
runs ->
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
repository.destroy()
|
||||
expect(repository.isDestroyed()).toBe(true)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
|
||||
|
||||
runs ->
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
|
||||
describe ".setPaths(paths)", ->
|
||||
describe "when path is a file", ->
|
||||
it "sets its path to the files parent directory and updates the root directory", ->
|
||||
filePath = require.resolve('./fixtures/dir/a')
|
||||
atom.project.setPaths([filePath])
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath)
|
||||
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath)
|
||||
|
||||
describe "when path is a directory", ->
|
||||
it "assigns the directories and repositories", ->
|
||||
directory1 = temp.mkdirSync("non-git-repo")
|
||||
directory2 = temp.mkdirSync("git-repo1")
|
||||
directory3 = temp.mkdirSync("git-repo2")
|
||||
|
||||
gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory2, ".git"))
|
||||
fs.copySync(gitDirPath, path.join(directory3, ".git"))
|
||||
|
||||
atom.project.setPaths([directory1, directory2, directory3])
|
||||
|
||||
[repo1, repo2, repo3] = atom.project.getRepositories()
|
||||
expect(repo1).toBeNull()
|
||||
expect(repo2.getShortHead()).toBe "master"
|
||||
expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git"))
|
||||
expect(repo3.getShortHead()).toBe "master"
|
||||
expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git"))
|
||||
|
||||
it "calls callbacks registered with ::onDidChangePaths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe 1
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
|
||||
|
||||
describe "when no paths are given", ->
|
||||
it "clears its path", ->
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
expect(atom.project.getDirectories()).toEqual []
|
||||
|
||||
it "normalizes the path to remove consecutive slashes, ., and .. segments", ->
|
||||
atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."])
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
|
||||
describe ".addPath(path)", ->
|
||||
it "calls callbacks registered with ::onDidChangePaths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
[oldPath] = atom.project.getPaths()
|
||||
|
||||
newPath = temp.mkdirSync("dir")
|
||||
atom.project.addPath(newPath)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe 1
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
|
||||
|
||||
it "doesn't add redundant paths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
[oldPath] = atom.project.getPaths()
|
||||
|
||||
# Doesn't re-add an existing root directory
|
||||
atom.project.addPath(oldPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
# Doesn't add an entry for a file-path within an existing root directory
|
||||
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
# Does add an entry for a directory within an existing directory
|
||||
newPath = path.join(oldPath, "a-dir")
|
||||
atom.project.addPath(newPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
|
||||
it "doesn't add non-existent directories", ->
|
||||
previousPaths = atom.project.getPaths()
|
||||
atom.project.addPath('/this-definitely/does-not-exist')
|
||||
expect(atom.project.getPaths()).toEqual(previousPaths)
|
||||
|
||||
describe ".removePath(path)", ->
|
||||
onDidChangePathsSpy = null
|
||||
|
||||
beforeEach ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
it "removes the directory and repository for the path", ->
|
||||
result = atom.project.removePath(atom.project.getPaths()[0])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
expect(atom.project.getRepositories()).toEqual([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(result).toBe true
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
|
||||
it "does nothing if the path is not one of the project's root paths", ->
|
||||
originalPaths = atom.project.getPaths()
|
||||
result = atom.project.removePath(originalPaths[0] + "xyz")
|
||||
expect(result).toBe false
|
||||
expect(atom.project.getPaths()).toEqual(originalPaths)
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "doesn't destroy the repository if it is shared by another root directory", ->
|
||||
atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")])
|
||||
atom.project.removePath(__dirname)
|
||||
expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")])
|
||||
expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false
|
||||
|
||||
it "removes a path that is represented as a URI", ->
|
||||
atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
|
||||
directoryForURISync: (uri) ->
|
||||
{
|
||||
getPath: -> uri
|
||||
getSubdirectory: -> {}
|
||||
isRoot: -> true
|
||||
existsSync: -> true
|
||||
off: ->
|
||||
}
|
||||
})
|
||||
|
||||
ftpURI = "ftp://example.com/some/folder"
|
||||
|
||||
atom.project.setPaths([ftpURI])
|
||||
expect(atom.project.getPaths()).toEqual [ftpURI]
|
||||
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
|
||||
describe ".onDidChangeFiles()", ->
|
||||
sub = []
|
||||
events = []
|
||||
checkCallback = ->
|
||||
|
||||
beforeEach ->
|
||||
sub = atom.project.onDidChangeFiles (incoming) ->
|
||||
events.push incoming...
|
||||
checkCallback()
|
||||
|
||||
afterEach ->
|
||||
sub.dispose()
|
||||
|
||||
waitForEvents = (paths) ->
|
||||
remaining = new Set(fs.realpathSync(p) for p in paths)
|
||||
new Promise (resolve, reject) ->
|
||||
checkCallback = ->
|
||||
remaining.delete(event.path) for event in events
|
||||
resolve() if remaining.size is 0
|
||||
|
||||
expire = ->
|
||||
checkCallback = ->
|
||||
console.error "Paths not seen:", Array.from(remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
|
||||
checkCallback()
|
||||
setTimeout expire, 2000
|
||||
|
||||
it "reports filesystem changes within project paths", ->
|
||||
dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
fileOne = path.join(dirOne, 'file-one.txt')
|
||||
fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
# Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise -> stopAllWatchers()
|
||||
|
||||
runs -> atom.project.setPaths([dirOne])
|
||||
waitsForPromise -> atom.project.getWatcherPromise dirOne
|
||||
|
||||
runs ->
|
||||
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual undefined
|
||||
|
||||
fs.writeFileSync fileThree, "three\n"
|
||||
fs.writeFileSync fileTwo, "two\n"
|
||||
fs.writeFileSync fileOne, "one\n"
|
||||
|
||||
waitsForPromise -> waitForEvents [fileOne, fileTwo]
|
||||
|
||||
runs ->
|
||||
expect(events.some (event) -> event.path is fileThree).toBeFalsy()
|
||||
|
||||
describe ".onDidAddBuffer()", ->
|
||||
it "invokes the callback with added text buffers", ->
|
||||
buffers = []
|
||||
added = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 1
|
||||
atom.project.onDidAddBuffer (buffer) -> added.push(buffer)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 2
|
||||
expect(added).toEqual [buffers[1]]
|
||||
|
||||
describe ".observeBuffers()", ->
|
||||
it "invokes the observer with current and future text buffers", ->
|
||||
buffers = []
|
||||
observed = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 2
|
||||
atom.project.observeBuffers (buffer) -> observed.push(buffer)
|
||||
expect(observed).toEqual buffers
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(observed.length).toBe 3
|
||||
expect(buffers.length).toBe 3
|
||||
expect(observed).toEqual buffers
|
||||
|
||||
describe ".relativize(path)", ->
|
||||
it "returns the path, relative to whichever root directory it is inside of", ->
|
||||
atom.project.addPath(temp.mkdirSync("another-path"))
|
||||
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
|
||||
|
||||
it "returns the given path if it is not in any of the root directories", ->
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.relativize(randomPath)).toBe randomPath
|
||||
|
||||
describe ".relativizePath(path)", ->
|
||||
it "returns the root path that contains the given path, and the path relativized to that root path", ->
|
||||
atom.project.addPath(temp.mkdirSync("another-path"))
|
||||
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
|
||||
|
||||
describe "when the given path isn't inside of any of the project's path", ->
|
||||
it "returns null for the root path, and the given path unchanged", ->
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath]
|
||||
|
||||
describe "when the given path is a URL", ->
|
||||
it "returns null for the root path, and the given path unchanged", ->
|
||||
url = "http://the-path"
|
||||
expect(atom.project.relativizePath(url)).toEqual [null, url]
|
||||
|
||||
describe "when the given path is inside more than one root folder", ->
|
||||
it "uses the root folder that is closest to the given path", ->
|
||||
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
|
||||
|
||||
inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
|
||||
|
||||
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true
|
||||
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
|
||||
expect(atom.project.relativizePath(inputPath)).toEqual [
|
||||
atom.project.getPaths()[1],
|
||||
path.join('somewhere', 'something.txt')
|
||||
]
|
||||
|
||||
describe ".contains(path)", ->
|
||||
it "returns whether or not the given path is in one of the root directories", ->
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.contains(childPath)).toBe true
|
||||
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.contains(randomPath)).toBe false
|
||||
|
||||
describe ".resolvePath(uri)", ->
|
||||
it "normalizes disk drive letter in passed path on #win32", ->
|
||||
expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt"
|
||||
927
spec/project-spec.js
Normal file
927
spec/project-spec.js
Normal file
@@ -0,0 +1,927 @@
|
||||
const temp = require('temp').track()
|
||||
const TextBuffer = require('text-buffer')
|
||||
const Project = require('../src/project')
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
const {Directory} = require('pathwatcher')
|
||||
const {stopAllWatchers} = require('../src/path-watcher')
|
||||
const GitRepository = require('../src/git-repository')
|
||||
|
||||
describe('Project', () => {
|
||||
beforeEach(() => {
|
||||
const directory = atom.project.getDirectories()[0]
|
||||
const paths = directory ? [directory.resolve('dir')] : [null]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
// Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
})
|
||||
|
||||
describe('serialization', () => {
|
||||
let deserializedProject = null
|
||||
let notQuittingProject = null
|
||||
let quittingProject = null
|
||||
|
||||
afterEach(() => {
|
||||
if (deserializedProject != null) {
|
||||
deserializedProject.destroy()
|
||||
}
|
||||
if (notQuittingProject != null) {
|
||||
notQuittingProject.destroy()
|
||||
}
|
||||
if (quittingProject != null) {
|
||||
quittingProject.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it("does not deserialize paths to directories that don't exist", () => {
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
const state = atom.project.serialize()
|
||||
state.paths.push('/directory/that/does/not/exist')
|
||||
|
||||
let err = null
|
||||
waitsForPromise(() =>
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch(e => { err = e })
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
|
||||
expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist'])
|
||||
})
|
||||
})
|
||||
|
||||
it('does not deserialize paths that are now files', () => {
|
||||
const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
|
||||
fs.mkdirSync(childPath)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
atom.project.setPaths([childPath])
|
||||
const state = atom.project.serialize()
|
||||
|
||||
fs.rmdirSync(childPath)
|
||||
fs.writeFileSync(childPath, 'surprise!\n')
|
||||
|
||||
let err = null
|
||||
waitsForPromise(() =>
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch(e => { err = e })
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getPaths()).toEqual([])
|
||||
expect(err.missingProjectPaths).toEqual([childPath])
|
||||
})
|
||||
})
|
||||
|
||||
it('does not include unretained buffers in the serialized state', () => {
|
||||
waitsForPromise(() => atom.project.bufferForPath('a'))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => {
|
||||
waitsForPromise(() => atom.workspace.open('a'))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getBuffers().length).toBe(1)
|
||||
deserializedProject.getBuffers()[0].destroy()
|
||||
expect(deserializedProject.getBuffers().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not deserialize buffers when their path is now a directory', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.mkdirSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('does not deserialize buffers when their path is inaccessible', () => {
|
||||
if (process.platform === 'win32') { return } // chmod not supported on win32
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.chmodSync(pathToOpen, '000')
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('does not deserialize buffers with their path is no longer present', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.unlinkSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('deserializes buffers that have never been saved before', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
atom.workspace.getActiveTextEditor().setText('unsaved\n')
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getBuffers().length).toBe(1)
|
||||
expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen)
|
||||
expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n')
|
||||
})
|
||||
})
|
||||
|
||||
it('serializes marker layers and history only if Atom is quitting', () => {
|
||||
waitsForPromise(() => atom.workspace.open('a'))
|
||||
|
||||
let bufferA = null
|
||||
let layerA = null
|
||||
let markerA = null
|
||||
|
||||
runs(() => {
|
||||
bufferA = atom.project.getBuffers()[0]
|
||||
layerA = bufferA.addMarkerLayer({persistent: true})
|
||||
markerA = layerA.markPosition([0, 3])
|
||||
bufferA.append('!')
|
||||
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
|
||||
|
||||
runs(() => {
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an editor is saved and the project has no path', () =>
|
||||
it("sets the project's path to the saved file's parent directory", () => {
|
||||
const tempFile = temp.openSync().path
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()[0]).toBeUndefined()
|
||||
let editor = null
|
||||
|
||||
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
|
||||
|
||||
waitsForPromise(() => editor.saveAs(tempFile))
|
||||
|
||||
runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
|
||||
})
|
||||
)
|
||||
|
||||
describe('before and after saving a buffer', () => {
|
||||
let buffer
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => {
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
afterEach(() => buffer.release())
|
||||
|
||||
it('emits save events on the main process', () => {
|
||||
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
|
||||
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
|
||||
|
||||
waitsForPromise(() => buffer.save())
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a watch error is thrown from the TextBuffer', () => {
|
||||
let editor = null
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o }))
|
||||
)
|
||||
|
||||
it('creates a warning notification', () => {
|
||||
let noteSpy
|
||||
atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy())
|
||||
|
||||
const error = new Error('SomeError')
|
||||
error.eventType = 'resurrect'
|
||||
editor.buffer.emitter.emit('will-throw-watch-error', {
|
||||
handle: jasmine.createSpy(),
|
||||
error
|
||||
}
|
||||
)
|
||||
|
||||
expect(noteSpy).toHaveBeenCalled()
|
||||
|
||||
const notification = noteSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('warning')
|
||||
expect(notification.getDetail()).toBe('SomeError')
|
||||
expect(notification.getMessage()).toContain('`resurrect`')
|
||||
expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a custom repository-provider service is provided', () => {
|
||||
let fakeRepositoryProvider, fakeRepository
|
||||
|
||||
beforeEach(() => {
|
||||
fakeRepository = {destroy () { return null }}
|
||||
fakeRepositoryProvider = {
|
||||
repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) },
|
||||
repositoryForDirectorySync (directory) { return fakeRepository }
|
||||
}
|
||||
})
|
||||
|
||||
it('uses it to create repositories for any directories that need one', () => {
|
||||
const projectPath = temp.mkdirSync('atom-project')
|
||||
atom.project.setPaths([projectPath])
|
||||
expect(atom.project.getRepositories()).toEqual([null])
|
||||
|
||||
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => atom.project.getRepositories()[0] === fakeRepository)
|
||||
})
|
||||
|
||||
it('does not create any new repositories if every directory has a repository', () => {
|
||||
const repositories = atom.project.getRepositories()
|
||||
expect(repositories.length).toEqual(1)
|
||||
expect(repositories[0]).toBeTruthy()
|
||||
|
||||
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => expect(atom.project.getRepositories()).toBe(repositories))
|
||||
})
|
||||
|
||||
it('stops using it to create repositories when the service is removed', () => {
|
||||
atom.project.setPaths([])
|
||||
|
||||
const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => {
|
||||
disposable.dispose()
|
||||
atom.project.addPath(temp.mkdirSync('atom-project'))
|
||||
expect(atom.project.getRepositories()).toEqual([null])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a custom directory-provider service is provided', () => {
|
||||
class DummyDirectory {
|
||||
constructor (aPath) {
|
||||
this.path = aPath
|
||||
}
|
||||
getPath () { return this.path }
|
||||
getFile () { return {existsSync () { return false }} }
|
||||
getSubdirectory () { return {existsSync () { return false }} }
|
||||
isRoot () { return true }
|
||||
existsSync () { return this.path.endsWith('does-exist') }
|
||||
contains (filePath) { return filePath.startsWith(this.path) }
|
||||
}
|
||||
|
||||
let serviceDisposable = null
|
||||
|
||||
beforeEach(() => {
|
||||
serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
|
||||
directoryForURISync (uri) {
|
||||
if (uri.startsWith('ssh://')) {
|
||||
return new DummyDirectory(uri)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
waitsFor(() => atom.project.directoryProviders.length > 0)
|
||||
})
|
||||
|
||||
it("uses the provider's custom directories for any paths that it handles", () => {
|
||||
const localPath = temp.mkdirSync('local-path')
|
||||
const remotePath = 'ssh://foreign-directory:8080/does-exist'
|
||||
|
||||
atom.project.setPaths([localPath, remotePath])
|
||||
|
||||
let directories = atom.project.getDirectories()
|
||||
expect(directories[0].getPath()).toBe(localPath)
|
||||
expect(directories[0] instanceof Directory).toBe(true)
|
||||
expect(directories[1].getPath()).toBe(remotePath)
|
||||
expect(directories[1] instanceof DummyDirectory).toBe(true)
|
||||
|
||||
// It does not add new remote paths that do not exist
|
||||
const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist'
|
||||
atom.project.addPath(nonExistentRemotePath)
|
||||
expect(atom.project.getDirectories().length).toBe(2)
|
||||
|
||||
// It adds new remote paths if their directories exist.
|
||||
const newRemotePath = 'ssh://another-directory:8080/does-exist'
|
||||
atom.project.addPath(newRemotePath)
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[2].getPath()).toBe(newRemotePath)
|
||||
expect(directories[2] instanceof DummyDirectory).toBe(true)
|
||||
})
|
||||
|
||||
it('stops using the provider when the service is removed', () => {
|
||||
serviceDisposable.dispose()
|
||||
atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
|
||||
expect(atom.project.getDirectories().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.open(path)', () => {
|
||||
let absolutePath, newBufferHandler
|
||||
|
||||
beforeEach(() => {
|
||||
absolutePath = require.resolve('./fixtures/dir/a')
|
||||
newBufferHandler = jasmine.createSpy('newBufferHandler')
|
||||
atom.project.onDidAddBuffer(newBufferHandler)
|
||||
})
|
||||
|
||||
describe("when given an absolute path that isn't currently open", () =>
|
||||
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBe(absolutePath)
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe("when given a relative path that isn't currently opened", () =>
|
||||
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBe(absolutePath)
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed the path to a buffer that is currently opened', () =>
|
||||
it('returns a new edit session containing currently opened buffer', () => {
|
||||
let editor = null
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => newBufferHandler.reset())
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open('a').then(({buffer}) => {
|
||||
expect(buffer).toBe(editor.buffer)
|
||||
expect(newBufferHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when not passed a path', () =>
|
||||
it("returns a new edit session and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBeUndefined()
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('.bufferForPath(path)', () => {
|
||||
let buffer = null
|
||||
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('a').then((o) => {
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
afterEach(() => buffer.release())
|
||||
|
||||
describe('when opening a previously opened path', () => {
|
||||
it('does not create a new buffer', () => {
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
Promise.all([
|
||||
atom.project.bufferForPath('c'),
|
||||
atom.project.bufferForPath('c')
|
||||
]).then(([buffer1, buffer2]) => {
|
||||
expect(buffer1).toBe(buffer2)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('retries loading the buffer if it previously failed', () => {
|
||||
waitsForPromise({shouldReject: true}, () => {
|
||||
spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file')))
|
||||
return atom.project.bufferForPath('b')
|
||||
})
|
||||
|
||||
waitsForPromise({shouldReject: false}, () => {
|
||||
TextBuffer.load.andCallThrough()
|
||||
return atom.project.bufferForPath('b')
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a new buffer if the previous buffer was destroyed', () => {
|
||||
buffer.release()
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.repositoryForDirectory(directory)', () => {
|
||||
it('resolves to null when the directory does not have a repository', () =>
|
||||
waitsForPromise(() => {
|
||||
const directory = new Directory('/tmp')
|
||||
return atom.project.repositoryForDirectory(directory).then((result) => {
|
||||
expect(result).toBeNull()
|
||||
expect(atom.project.repositoryProviders.length).toBeGreaterThan(0)
|
||||
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
|
||||
waitsForPromise(() => {
|
||||
const directory = new Directory(path.join(__dirname, '..'))
|
||||
const promise = atom.project.repositoryForDirectory(directory)
|
||||
return promise.then((result) => {
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
const dirPath = directory.getRealPathSync()
|
||||
expect(result.getPath()).toBe(path.join(dirPath, '.git'))
|
||||
|
||||
// Verify that the result is cached.
|
||||
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
|
||||
let repository = null
|
||||
const directory = new Directory(path.join(__dirname, '..'))
|
||||
|
||||
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
|
||||
|
||||
runs(() => {
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
repository.destroy()
|
||||
expect(repository.isDestroyed()).toBe(true)
|
||||
})
|
||||
|
||||
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
|
||||
|
||||
runs(() => expect(repository.isDestroyed()).toBe(false))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.setPaths(paths, options)', () => {
|
||||
describe('when path is a file', () =>
|
||||
it("sets its path to the file's parent directory and updates the root directory", () => {
|
||||
const filePath = require.resolve('./fixtures/dir/a')
|
||||
atom.project.setPaths([filePath])
|
||||
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
|
||||
})
|
||||
)
|
||||
|
||||
describe('when path is a directory', () => {
|
||||
it('assigns the directories and repositories', () => {
|
||||
const directory1 = temp.mkdirSync('non-git-repo')
|
||||
const directory2 = temp.mkdirSync('git-repo1')
|
||||
const directory3 = temp.mkdirSync('git-repo2')
|
||||
|
||||
const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory2, '.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory3, '.git'))
|
||||
|
||||
atom.project.setPaths([directory1, directory2, directory3])
|
||||
|
||||
const [repo1, repo2, repo3] = atom.project.getRepositories()
|
||||
expect(repo1).toBeNull()
|
||||
expect(repo2.getShortHead()).toBe('master')
|
||||
expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git')))
|
||||
expect(repo3.getShortHead()).toBe('master')
|
||||
expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git')))
|
||||
})
|
||||
|
||||
it('calls callbacks registered with ::onDidChangePaths', () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe(1)
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
|
||||
})
|
||||
|
||||
it('optionally throws an error with any paths that did not exist', () => {
|
||||
const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1']
|
||||
|
||||
try {
|
||||
atom.project.setPaths(paths, {mustExist: true})
|
||||
expect('no exception thrown').toBeUndefined()
|
||||
} catch (e) {
|
||||
expect(e.missingProjectPaths).toEqual([paths[1], paths[3]])
|
||||
}
|
||||
|
||||
expect(atom.project.getPaths()).toEqual([paths[0], paths[2]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no paths are given', () =>
|
||||
it('clears its path', () => {
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
})
|
||||
)
|
||||
|
||||
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
|
||||
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
|
||||
expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.addPath(path, options)', () => {
|
||||
it('calls callbacks registered with ::onDidChangePaths', () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
const [oldPath] = atom.project.getPaths()
|
||||
|
||||
const newPath = temp.mkdirSync('dir')
|
||||
atom.project.addPath(newPath)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe(1)
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
|
||||
})
|
||||
|
||||
it("doesn't add redundant paths", () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
const [oldPath] = atom.project.getPaths()
|
||||
|
||||
// Doesn't re-add an existing root directory
|
||||
atom.project.addPath(oldPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Doesn't add an entry for a file-path within an existing root directory
|
||||
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Does add an entry for a directory within an existing directory
|
||||
const newPath = path.join(oldPath, 'a-dir')
|
||||
atom.project.addPath(newPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't add non-existent directories", () => {
|
||||
const previousPaths = atom.project.getPaths()
|
||||
atom.project.addPath('/this-definitely/does-not-exist')
|
||||
expect(atom.project.getPaths()).toEqual(previousPaths)
|
||||
})
|
||||
|
||||
it('optionally throws on non-existent directories', () =>
|
||||
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
|
||||
)
|
||||
})
|
||||
|
||||
describe('.removePath(path)', () => {
|
||||
let onDidChangePathsSpy = null
|
||||
|
||||
beforeEach(() => {
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
})
|
||||
|
||||
it('removes the directory and repository for the path', () => {
|
||||
const result = atom.project.removePath(atom.project.getPaths()[0])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
expect(atom.project.getRepositories()).toEqual([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(result).toBe(true)
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does nothing if the path is not one of the project's root paths", () => {
|
||||
const originalPaths = atom.project.getPaths()
|
||||
const result = atom.project.removePath(originalPaths[0] + 'xyz')
|
||||
expect(result).toBe(false)
|
||||
expect(atom.project.getPaths()).toEqual(originalPaths)
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't destroy the repository if it is shared by another root directory", () => {
|
||||
atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')])
|
||||
atom.project.removePath(__dirname)
|
||||
expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')])
|
||||
expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false)
|
||||
})
|
||||
|
||||
it('removes a path that is represented as a URI', () => {
|
||||
atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
|
||||
directoryForURISync (uri) {
|
||||
return {
|
||||
getPath () { return uri },
|
||||
getSubdirectory () { return {} },
|
||||
isRoot () { return true },
|
||||
existsSync () { return true },
|
||||
off () {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ftpURI = 'ftp://example.com/some/folder'
|
||||
|
||||
atom.project.setPaths([ftpURI])
|
||||
expect(atom.project.getPaths()).toEqual([ftpURI])
|
||||
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.onDidChangeFiles()', () => {
|
||||
let sub = []
|
||||
const events = []
|
||||
let checkCallback = () => {}
|
||||
|
||||
beforeEach(() => {
|
||||
sub = atom.project.onDidChangeFiles((incoming) => {
|
||||
events.push(...incoming)
|
||||
checkCallback()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => sub.dispose())
|
||||
|
||||
const waitForEvents = (paths) => {
|
||||
const remaining = new Set(paths.map((p) => fs.realpathSync(p)))
|
||||
return new Promise((resolve, reject) => {
|
||||
checkCallback = () => {
|
||||
for (let event of events) { remaining.delete(event.path) }
|
||||
if (remaining.size === 0) { resolve() }
|
||||
}
|
||||
|
||||
const expire = () => {
|
||||
checkCallback = () => {}
|
||||
console.error('Paths not seen:', remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
}
|
||||
|
||||
checkCallback()
|
||||
setTimeout(expire, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
it('reports filesystem changes within project paths', () => {
|
||||
const dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
const fileOne = path.join(dirOne, 'file-one.txt')
|
||||
const fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
const dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
const fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
// Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise(() => stopAllWatchers())
|
||||
|
||||
runs(() => atom.project.setPaths([dirOne]))
|
||||
waitsForPromise(() => atom.project.getWatcherPromise(dirOne))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined)
|
||||
|
||||
fs.writeFileSync(fileThree, 'three\n')
|
||||
fs.writeFileSync(fileTwo, 'two\n')
|
||||
fs.writeFileSync(fileOne, 'one\n')
|
||||
})
|
||||
|
||||
waitsForPromise(() => waitForEvents([fileOne, fileTwo]))
|
||||
|
||||
runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy())
|
||||
})
|
||||
})
|
||||
|
||||
describe('.onDidAddBuffer()', () =>
|
||||
it('invokes the callback with added text buffers', () => {
|
||||
const buffers = []
|
||||
const added = []
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(1)
|
||||
atom.project.onDidAddBuffer(buffer => added.push(buffer))
|
||||
})
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(2)
|
||||
expect(added).toEqual([buffers[1]])
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('.observeBuffers()', () =>
|
||||
it('invokes the observer with current and future text buffers', () => {
|
||||
const buffers = []
|
||||
const observed = []
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(2)
|
||||
atom.project.observeBuffers(buffer => observed.push(buffer))
|
||||
expect(observed).toEqual(buffers)
|
||||
})
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(observed.length).toBe(3)
|
||||
expect(buffers.length).toBe(3)
|
||||
expect(observed).toEqual(buffers)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('.relativize(path)', () => {
|
||||
it('returns the path, relative to whichever root directory it is inside of', () => {
|
||||
atom.project.addPath(temp.mkdirSync('another-path'))
|
||||
|
||||
let rootPath = atom.project.getPaths()[0]
|
||||
let childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
|
||||
})
|
||||
|
||||
it('returns the given path if it is not in any of the root directories', () => {
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.relativize(randomPath)).toBe(randomPath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.relativizePath(path)', () => {
|
||||
it('returns the root path that contains the given path, and the path relativized to that root path', () => {
|
||||
atom.project.addPath(temp.mkdirSync('another-path'))
|
||||
|
||||
let rootPath = atom.project.getPaths()[0]
|
||||
let childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
|
||||
})
|
||||
|
||||
describe("when the given path isn't inside of any of the project's path", () =>
|
||||
it('returns null for the root path, and the given path unchanged', () => {
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the given path is a URL', () =>
|
||||
it('returns null for the root path, and the given path unchanged', () => {
|
||||
const url = 'http://the-path'
|
||||
expect(atom.project.relativizePath(url)).toEqual([null, url])
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the given path is inside more than one root folder', () =>
|
||||
it('uses the root folder that is closest to the given path', () => {
|
||||
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
|
||||
|
||||
const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
|
||||
|
||||
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true)
|
||||
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true)
|
||||
expect(atom.project.relativizePath(inputPath)).toEqual([
|
||||
atom.project.getPaths()[1],
|
||||
path.join('somewhere', 'something.txt')
|
||||
])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('.contains(path)', () =>
|
||||
it('returns whether or not the given path is in one of the root directories', () => {
|
||||
const rootPath = atom.project.getPaths()[0]
|
||||
const childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.contains(childPath)).toBe(true)
|
||||
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.contains(randomPath)).toBe(false)
|
||||
})
|
||||
)
|
||||
|
||||
describe('.resolvePath(uri)', () =>
|
||||
it('normalizes disk drive letter in passed path on #win32', () => {
|
||||
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -222,7 +222,7 @@ describe("ReopenProjectMenuManager", () => {
|
||||
expect(label).toBe('https://launch.pad/apollo/11')
|
||||
})
|
||||
|
||||
it("returns a comma-seperated list of base names if there are multiple", () => {
|
||||
it("returns a comma-separated list of base names if there are multiple", () => {
|
||||
const project = { paths: [ '/var/one', '/usr/bin/two', '/etc/mission/control/three' ] }
|
||||
const label = ReopenProjectMenuManager.createLabel(project)
|
||||
expect(label).toBe('one, two, three')
|
||||
|
||||
@@ -103,6 +103,11 @@ describe "Selection", ->
|
||||
selection.insertText("\r\n", autoIndent: true)
|
||||
expect(buffer.lineForRow(2)).toBe " "
|
||||
|
||||
it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", ->
|
||||
selection.setBufferRange [[5, 0], [5, 0]]
|
||||
selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1)
|
||||
expect(buffer.lineForRow(6)).toBe(' bar')
|
||||
|
||||
describe ".fold()", ->
|
||||
it "folds the buffer range spanned by the selection", ->
|
||||
selection.setBufferRange([[0, 3], [1, 6]])
|
||||
|
||||
@@ -58,7 +58,7 @@ if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json')
|
||||
if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures')
|
||||
specProjectPath = path.join(specDirectory, 'fixtures')
|
||||
else
|
||||
specProjectPath = path.join(__dirname, 'fixtures')
|
||||
specProjectPath = require('os').tmpdir()
|
||||
|
||||
beforeEach ->
|
||||
atom.project.setPaths([specProjectPath])
|
||||
@@ -108,10 +108,14 @@ beforeEach ->
|
||||
afterEach ->
|
||||
ensureNoDeprecatedFunctionCalls()
|
||||
ensureNoDeprecatedStylesheets()
|
||||
atom.reset()
|
||||
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
|
||||
warnIfLeakingPathSubscriptions()
|
||||
waits(0) # yield to ui thread to make screen update more frequently
|
||||
|
||||
waitsForPromise ->
|
||||
atom.reset()
|
||||
|
||||
runs ->
|
||||
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
|
||||
warnIfLeakingPathSubscriptions()
|
||||
waits(0) # yield to ui thread to make screen update more frequently
|
||||
|
||||
warnIfLeakingPathSubscriptions = ->
|
||||
watchedPaths = pathwatcher.getWatchedPaths()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
|
||||
|
||||
const Random = require('../script/node_modules/random-seed')
|
||||
const {getRandomBufferRange, buildRandomLines} = require('./helpers/random')
|
||||
const TextEditorComponent = require('../src/text-editor-component')
|
||||
const TextEditorElement = require('../src/text-editor-element')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
@@ -12,7 +14,6 @@ const electron = require('electron')
|
||||
const clipboard = require('../src/safe-clipboard')
|
||||
|
||||
const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8')
|
||||
const NBSP_CHARACTER = '\u00a0'
|
||||
|
||||
document.registerElement('text-editor-component-test-element', {
|
||||
prototype: Object.create(HTMLElement.prototype, {
|
||||
@@ -286,6 +287,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)
|
||||
@@ -378,35 +404,50 @@ describe('TextEditorComponent', () => {
|
||||
expect(horizontalScrollbar.style.visibility).toBe('')
|
||||
})
|
||||
|
||||
it('updates the bottom/right of dummy scrollbars and client height/width measurements without forgetting the previous scroll top/left when scrollbar styles change', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 100, width: 100})
|
||||
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10)
|
||||
setScrollTop(component, 20)
|
||||
setScrollLeft(component, 10)
|
||||
await component.getNextUpdatePromise()
|
||||
describe('when scrollbar styles change or the editor element is detached and then reattached', () => {
|
||||
it('updates the bottom/right of dummy scrollbars and client height/width measurements', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 100, width: 100})
|
||||
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10)
|
||||
setScrollTop(component, 20)
|
||||
setScrollLeft(component, 10)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const style = document.createElement('style')
|
||||
style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'
|
||||
jasmine.attachToDOM(style)
|
||||
// Updating scrollbar styles.
|
||||
const style = document.createElement('style')
|
||||
style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'
|
||||
jasmine.attachToDOM(style)
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
await component.getNextUpdatePromise()
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
// Detaching and re-attaching the editor element.
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
// Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors.
|
||||
await editor.update({mini: true})
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
|
||||
// Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors.
|
||||
await editor.update({mini: true})
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders cursors within the visible row range', async () => {
|
||||
@@ -854,6 +895,97 @@ describe('TextEditorComponent', () => {
|
||||
expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth)
|
||||
expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth)
|
||||
})
|
||||
|
||||
describe('randomized tests', () => {
|
||||
let originalTimeout
|
||||
|
||||
beforeEach(() => {
|
||||
originalTimeout = jasmine.getEnv().defaultTimeoutInterval
|
||||
jasmine.getEnv().defaultTimeoutInterval = 60 * 1000
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.getEnv().defaultTimeoutInterval = originalTimeout
|
||||
})
|
||||
|
||||
it('renders the visible rows correctly after randomly mutating the editor', async () => {
|
||||
const initialSeed = Date.now()
|
||||
for (var i = 0; i < 20; i++) {
|
||||
let seed = initialSeed + i
|
||||
// seed = 1507224195357
|
||||
const failureMessage = 'Randomized test failed with seed: ' + seed
|
||||
const random = Random(seed)
|
||||
|
||||
const rowsPerTile = random.intBetween(1, 6)
|
||||
const {component, element, editor} = buildComponent({rowsPerTile, autoHeight: false})
|
||||
editor.setSoftWrapped(Boolean(random(2)))
|
||||
await setEditorWidthInCharacters(component, random(20))
|
||||
await setEditorHeightInLines(component, random(10))
|
||||
element.focus()
|
||||
|
||||
for (var j = 0; j < 5; j++) {
|
||||
const k = random(100)
|
||||
const range = getRandomBufferRange(random, editor.buffer)
|
||||
|
||||
if (k < 10) {
|
||||
editor.setSoftWrapped(!editor.isSoftWrapped())
|
||||
} else if (k < 15) {
|
||||
if (random(2)) setEditorWidthInCharacters(component, random(20))
|
||||
if (random(2)) setEditorHeightInLines(component, random(10))
|
||||
} else if (k < 40) {
|
||||
editor.setSelectedBufferRange(range)
|
||||
editor.backspace()
|
||||
} else if (k < 80) {
|
||||
const linesToInsert = buildRandomLines(random, 5)
|
||||
editor.setCursorBufferPosition(range.start)
|
||||
editor.insertText(linesToInsert)
|
||||
} else if (k < 90) {
|
||||
if (random(2)) {
|
||||
editor.foldBufferRange(range)
|
||||
} else {
|
||||
editor.destroyFoldsIntersectingBufferRange(range)
|
||||
}
|
||||
} else if (k < 95) {
|
||||
editor.setSelectedBufferRange(range)
|
||||
} else {
|
||||
if (random(2)) component.setScrollTop(random(component.getScrollHeight()))
|
||||
if (random(2)) component.setScrollLeft(random(component.getScrollWidth()))
|
||||
}
|
||||
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const renderedLines = queryOnScreenLineElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
|
||||
const renderedLineNumbers = queryOnScreenLineNumberElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
|
||||
const renderedStartRow = component.getRenderedStartRow()
|
||||
const expectedLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow())
|
||||
|
||||
expect(renderedLines.length).toBe(expectedLines.length, failureMessage)
|
||||
expect(renderedLineNumbers.length).toBe(expectedLines.length, failureMessage)
|
||||
for (let k = 0; k < renderedLines.length; k++) {
|
||||
const expectedLine = expectedLines[k]
|
||||
const expectedText = expectedLine.lineText || ' '
|
||||
|
||||
const renderedLine = renderedLines[k]
|
||||
const renderedLineNumber = renderedLineNumbers[k]
|
||||
let renderedText = renderedLine.textContent
|
||||
// We append zero width NBSPs after folds at the end of the
|
||||
// line in order to support measurement.
|
||||
if (expectedText.endsWith(editor.displayLayer.foldCharacter)) {
|
||||
renderedText = renderedText.substring(0, renderedText.length - 1)
|
||||
}
|
||||
|
||||
expect(renderedText).toBe(expectedText, failureMessage)
|
||||
expect(parseInt(renderedLine.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage)
|
||||
expect(parseInt(renderedLineNumber.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
element.remove()
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mini editors', () => {
|
||||
@@ -1142,7 +1274,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(component.getScrollTopRow()).toBe(4)
|
||||
expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight()))
|
||||
|
||||
// Preserves the scrollTopRow when sdetached
|
||||
// Preserves the scrollTopRow when detached
|
||||
element.remove()
|
||||
expect(component.getScrollTopRow()).toBe(4)
|
||||
expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight()))
|
||||
@@ -1601,7 +1733,7 @@ describe('TextEditorComponent', () => {
|
||||
const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'})
|
||||
decoration.flash('b', 10)
|
||||
|
||||
// Flash on initial appearence of highlight
|
||||
// Flash on initial appearance of highlight
|
||||
await component.getNextUpdatePromise()
|
||||
const highlights = element.querySelectorAll('.highlight.a')
|
||||
expect(highlights.length).toBe(1)
|
||||
@@ -1764,6 +1896,8 @@ describe('TextEditorComponent', () => {
|
||||
const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'})
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const overlayComponent = component.overlayComponents.values().next().value
|
||||
|
||||
const overlayWrapper = overlayElement.parentElement
|
||||
expect(overlayWrapper.classList.contains('a')).toBe(true)
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
@@ -1794,12 +1928,12 @@ describe('TextEditorComponent', () => {
|
||||
await setScrollTop(component, 20)
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
overlayElement.style.height = 60 + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
await overlayComponent.getNextUpdatePromise()
|
||||
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
|
||||
|
||||
// Does not flip the overlay vertically if it would overflow the top of the window
|
||||
overlayElement.style.height = 80 + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
await overlayComponent.getNextUpdatePromise()
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
|
||||
// Can update overlay wrapper class
|
||||
@@ -2284,6 +2418,27 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('removes block decorations whose markers have been destroyed', async () => {
|
||||
const {editor, component, element} = buildComponent({rowsPerTile: 3})
|
||||
const {marker} = createBlockDecorationAtScreenRow(editor, 2, {height: 5, position: 'before'})
|
||||
await component.getNextUpdatePromise()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight() + 5},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
marker.destroy()
|
||||
await component.getNextUpdatePromise()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
})
|
||||
|
||||
it('removes block decorations whose markers are invalidated, and adds them back when they become valid again', async () => {
|
||||
const editor = buildEditor({rowsPerTile: 3, autoHeight: false})
|
||||
const {item, decoration, marker} = createBlockDecorationAtScreenRow(editor, 3, {height: 44, position: 'before', invalidate: 'touch'})
|
||||
@@ -2388,6 +2543,49 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('does not throw exceptions when destroying a block decoration inside a marker change event (regression)', async () => {
|
||||
const {editor, component} = buildComponent({rowsPerTile: 3})
|
||||
|
||||
const marker = editor.markScreenPosition([2, 0])
|
||||
marker.onDidChange(() => { marker.destroy() })
|
||||
const item = document.createElement('div')
|
||||
editor.decorateMarker(marker, {type: 'block', item})
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2))
|
||||
|
||||
marker.setBufferRange([[0, 0], [0, 0]])
|
||||
expect(marker.isDestroyed()).toBe(true)
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
})
|
||||
|
||||
it('does not attempt to render block decorations located outside the visible range', async () => {
|
||||
const {editor, component} = buildComponent({autoHeight: false, rowsPerTile: 2})
|
||||
await setEditorHeightInLines(component, 2)
|
||||
expect(component.getRenderedStartRow()).toBe(0)
|
||||
expect(component.getRenderedEndRow()).toBe(4)
|
||||
|
||||
const marker1 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: false})
|
||||
const item1 = document.createElement('div')
|
||||
editor.decorateMarker(marker1, {type: 'block', item: item1})
|
||||
|
||||
const marker2 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: true})
|
||||
const item2 = document.createElement('div')
|
||||
editor.decorateMarker(marker2, {type: 'block', item: item2})
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item1.parentElement).toBeNull()
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3))
|
||||
|
||||
await setScrollTop(component, 4 * component.getLineHeight())
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
expect(component.getRenderedEndRow()).toBe(8)
|
||||
expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 5))
|
||||
expect(item2.parentElement).toBeNull()
|
||||
})
|
||||
|
||||
it('measures block decorations correctly when they are added before the component width has been updated', async () => {
|
||||
{
|
||||
const {editor, component, element} = buildComponent({autoHeight: false, width: 500, attach: false})
|
||||
@@ -2706,6 +2904,8 @@ describe('TextEditorComponent', () => {
|
||||
clientY: clientTopForLine(component, 3) + lineHeight / 2
|
||||
})
|
||||
expect(editor.getCursorScreenPosition()).toEqual([3, 16])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('selects words on double-click', () => {
|
||||
@@ -2714,6 +2914,7 @@ describe('TextEditorComponent', () => {
|
||||
component.didMouseDownOnContent({detail: 1, button: 0, clientX, clientY})
|
||||
component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[1, 13], [1, 21]])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('selects lines on triple-click', () => {
|
||||
@@ -2723,6 +2924,7 @@ describe('TextEditorComponent', () => {
|
||||
component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY})
|
||||
component.didMouseDownOnContent({detail: 3, button: 0, clientX, clientY})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [2, 0]])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds or removes cursors when holding cmd or ctrl when single-clicking', () => {
|
||||
@@ -2760,7 +2962,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[1, 16]])
|
||||
|
||||
// cmd-clicking within a selection destroys it
|
||||
editor.addSelectionForScreenRange([[2, 10], [2, 15]])
|
||||
editor.addSelectionForScreenRange([[2, 10], [2, 15]], {autoscroll: false})
|
||||
expect(editor.getSelectedScreenRanges()).toEqual([
|
||||
[[1, 16], [1, 16]],
|
||||
[[2, 10], [2, 15]]
|
||||
@@ -2790,7 +2992,7 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// ctrl-click adds cursors on platforms *other* than macOS
|
||||
component.props.platform = 'win32'
|
||||
editor.setCursorScreenPosition([1, 4])
|
||||
editor.setCursorScreenPosition([1, 4], {autoscroll: false})
|
||||
component.didMouseDownOnContent(
|
||||
Object.assign(clientPositionForCharacter(component, 1, 16), {
|
||||
detail: 1,
|
||||
@@ -2799,11 +3001,13 @@ describe('TextEditorComponent', () => {
|
||||
})
|
||||
)
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds word selections when holding cmd or ctrl when double-clicking', () => {
|
||||
const {component, editor} = buildComponent()
|
||||
editor.addCursorAtScreenPosition([1, 16])
|
||||
editor.addCursorAtScreenPosition([1, 16], {autoscroll: false})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]])
|
||||
|
||||
component.didMouseDownOnContent(
|
||||
@@ -2824,11 +3028,12 @@ describe('TextEditorComponent', () => {
|
||||
[[0, 0], [0, 0]],
|
||||
[[1, 13], [1, 21]]
|
||||
])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds line selections when holding cmd or ctrl when triple-clicking', () => {
|
||||
const {component, editor} = buildComponent()
|
||||
editor.addCursorAtScreenPosition([1, 16])
|
||||
editor.addCursorAtScreenPosition([1, 16], {autoscroll: false})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]])
|
||||
|
||||
const {clientX, clientY} = clientPositionForCharacter(component, 1, 16)
|
||||
@@ -2840,12 +3045,13 @@ describe('TextEditorComponent', () => {
|
||||
[[0, 0], [0, 0]],
|
||||
[[1, 0], [2, 0]]
|
||||
])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('expands the last selection on shift-click', () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@@ -2862,8 +3068,8 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// reorients word-wise selections to keep the word selected regardless of
|
||||
// where the subsequent shift-click occurs
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.getLastSelection().selectWord()
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
editor.getLastSelection().selectWord({autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@@ -2880,8 +3086,8 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// reorients line-wise selections to keep the word selected regardless of
|
||||
// where the subsequent shift-click occurs
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.getLastSelection().selectLine()
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
editor.getLastSelection().selectLine(null, {autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@@ -2895,6 +3101,8 @@ describe('TextEditorComponent', () => {
|
||||
shiftKey: true
|
||||
}, clientPositionForCharacter(component, 3, 11)))
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [4, 0]])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('expands the last selection on drag', () => {
|
||||
@@ -3272,9 +3480,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 () => {
|
||||
@@ -4216,7 +4424,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(dragEvents).toEqual([])
|
||||
})
|
||||
|
||||
it('calls `didStopDragging` if the buffer changes while dragging', async () => {
|
||||
it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => {
|
||||
const {component, editor} = buildComponent()
|
||||
|
||||
let dragging = false
|
||||
@@ -4229,8 +4437,14 @@ describe('TextEditorComponent', () => {
|
||||
await getNextAnimationFramePromise()
|
||||
expect(dragging).toBe(true)
|
||||
|
||||
editor.delete()
|
||||
// Buffer changes don't cause dragging to be stopped.
|
||||
editor.insertText('X')
|
||||
expect(dragging).toBe(true)
|
||||
|
||||
// Keyboard interaction prevents users from dragging further.
|
||||
component.didKeydown({code: 'KeyX'})
|
||||
expect(dragging).toBe(false)
|
||||
|
||||
window.dispatchEvent(new MouseEvent('mousemove'))
|
||||
await getNextAnimationFramePromise()
|
||||
expect(dragging).toBe(false)
|
||||
@@ -4250,7 +4464,10 @@ function buildEditor (params = {}) {
|
||||
for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity']) {
|
||||
if (params[paramName] != null) editorParams[paramName] = params[paramName]
|
||||
}
|
||||
return new TextEditor(editorParams)
|
||||
const editor = new TextEditor(editorParams)
|
||||
editor.testAutoscrollRequests = []
|
||||
editor.onDidRequestAutoscroll((request) => { editor.testAutoscrollRequests.push(request) })
|
||||
return editor
|
||||
}
|
||||
|
||||
function buildComponent (params = {}) {
|
||||
|
||||
@@ -544,6 +544,21 @@ describe('TextEditorRegistry', function () {
|
||||
expect(editor.getSoftWrapColumn()).toBe(80)
|
||||
})
|
||||
|
||||
it('allows for custom definition of maximum soft wrap based on config', async function () {
|
||||
editor.update({
|
||||
softWrapped: false,
|
||||
maxScreenLineLength: 1500,
|
||||
})
|
||||
|
||||
expect(editor.getSoftWrapColumn()).toBe(1500)
|
||||
|
||||
atom.config.set('editor.softWrap', false)
|
||||
atom.config.set('editor.maxScreenLineLength', 500)
|
||||
registry.maintainConfig(editor)
|
||||
await initialPackageActivation
|
||||
expect(editor.getSoftWrapColumn()).toBe(500)
|
||||
})
|
||||
|
||||
it('sets the preferred line length based on the config', async function () {
|
||||
editor.update({preferredLineLength: 80})
|
||||
expect(editor.getPreferredLineLength()).toBe(80)
|
||||
@@ -685,7 +700,7 @@ describe('TextEditorRegistry', function () {
|
||||
registry.setGrammarOverride(editor, 'source.c')
|
||||
registry.setGrammarOverride(editor2, 'source.js')
|
||||
|
||||
atom.packages.deactivatePackage('language-javascript')
|
||||
await atom.packages.deactivatePackage('language-javascript')
|
||||
|
||||
const editorCopy = TextEditor.deserialize(editor.serialize(), atom)
|
||||
const editor2Copy = TextEditor.deserialize(editor2.serialize(), atom)
|
||||
|
||||
@@ -74,6 +74,16 @@ describe "TextEditor", ->
|
||||
expect(editor2.getInvisibles()).toEqual(editor.getInvisibles())
|
||||
expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars())
|
||||
expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength())
|
||||
expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn())
|
||||
|
||||
it "ignores buffers with retired IDs", ->
|
||||
editor2 = TextEditor.deserialize(editor.serialize(), {
|
||||
assert: atom.assert,
|
||||
textEditors: atom.textEditors,
|
||||
project: {bufferForIdSync: -> null}
|
||||
})
|
||||
|
||||
expect(editor2).toBeNull()
|
||||
|
||||
describe "when the editor is constructed with the largeFileMode option set to true", ->
|
||||
it "loads the editor but doesn't tokenize", ->
|
||||
@@ -145,7 +155,7 @@ describe "TextEditor", ->
|
||||
returnedPromise = editor.update({
|
||||
tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40,
|
||||
showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true,
|
||||
autoHeight: false
|
||||
autoHeight: false, maxScreenLineLength: 1000
|
||||
})
|
||||
|
||||
expect(returnedPromise).toBe(element.component.getNextUpdatePromise())
|
||||
@@ -620,7 +630,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getCursorBufferPosition()).toEqual [0, 0]
|
||||
|
||||
describe ".moveToBottom()", ->
|
||||
it "moves the cusor to the bottom of the buffer", ->
|
||||
it "moves the cursor to the bottom of the buffer", ->
|
||||
editor.setCursorScreenPosition [0, 0]
|
||||
editor.addCursorAtScreenPosition [1, 0]
|
||||
editor.moveToBottom()
|
||||
@@ -1158,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])
|
||||
@@ -1364,7 +1426,7 @@ describe "TextEditor", ->
|
||||
expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]]
|
||||
|
||||
describe ".selectToBeginningOfPreviousParagraph()", ->
|
||||
it "selects from the cursor to the first line of the pevious paragraph", ->
|
||||
it "selects from the cursor to the first line of the previous paragraph", ->
|
||||
editor.setSelectedBufferRange([[3, 0], [4, 5]])
|
||||
editor.addCursorAtScreenPosition([5, 6])
|
||||
editor.selectToScreenPosition([6, 2])
|
||||
@@ -1397,7 +1459,7 @@ describe "TextEditor", ->
|
||||
expect(selection1.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToTop()", ->
|
||||
it "selects text from cusor position to the top of the buffer", ->
|
||||
it "selects text from cursor position to the top of the buffer", ->
|
||||
editor.setCursorScreenPosition [11, 2]
|
||||
editor.addCursorAtScreenPosition [10, 0]
|
||||
editor.selectToTop()
|
||||
@@ -1407,7 +1469,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getLastSelection().isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToBottom()", ->
|
||||
it "selects text from cusor position to the bottom of the buffer", ->
|
||||
it "selects text from cursor position to the bottom of the buffer", ->
|
||||
editor.setCursorScreenPosition [10, 0]
|
||||
editor.addCursorAtScreenPosition [9, 3]
|
||||
editor.selectToBottom()
|
||||
@@ -1422,7 +1484,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange()
|
||||
|
||||
describe ".selectToBeginningOfLine()", ->
|
||||
it "selects text from cusor position to beginning of line", ->
|
||||
it "selects text from cursor position to beginning of line", ->
|
||||
editor.setCursorScreenPosition [12, 2]
|
||||
editor.addCursorAtScreenPosition [11, 3]
|
||||
|
||||
@@ -1441,7 +1503,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToEndOfLine()", ->
|
||||
it "selects text from cusor position to end of line", ->
|
||||
it "selects text from cursor position to end of line", ->
|
||||
editor.setCursorScreenPosition [12, 0]
|
||||
editor.addCursorAtScreenPosition [11, 3]
|
||||
|
||||
@@ -1483,7 +1545,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]]
|
||||
|
||||
describe ".selectToBeginningOfWord()", ->
|
||||
it "selects text from cusor position to beginning of word", ->
|
||||
it "selects text from cursor position to beginning of word", ->
|
||||
editor.setCursorScreenPosition [0, 13]
|
||||
editor.addCursorAtScreenPosition [3, 49]
|
||||
|
||||
@@ -1502,7 +1564,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToEndOfWord()", ->
|
||||
it "selects text from cusor position to end of word", ->
|
||||
it "selects text from cursor position to end of word", ->
|
||||
editor.setCursorScreenPosition [0, 4]
|
||||
editor.addCursorAtScreenPosition [3, 48]
|
||||
|
||||
@@ -1521,7 +1583,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeFalsy()
|
||||
|
||||
describe ".selectToBeginningOfNextWord()", ->
|
||||
it "selects text from cusor position to beginning of next word", ->
|
||||
it "selects text from cursor position to beginning of next word", ->
|
||||
editor.setCursorScreenPosition [0, 4]
|
||||
editor.addCursorAtScreenPosition [3, 48]
|
||||
|
||||
@@ -1800,7 +1862,7 @@ describe "TextEditor", ->
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]])
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]]
|
||||
|
||||
it "recyles existing selection instances", ->
|
||||
it "recycles existing selection instances", ->
|
||||
selection = editor.getLastSelection()
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]])
|
||||
|
||||
@@ -1849,7 +1911,7 @@ describe "TextEditor", ->
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]])
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]]
|
||||
|
||||
it "recyles existing selection instances", ->
|
||||
it "recycles existing selection instances", ->
|
||||
selection = editor.getLastSelection()
|
||||
editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]])
|
||||
|
||||
@@ -2258,7 +2320,7 @@ describe "TextEditor", ->
|
||||
|
||||
|
||||
describe "when the preceding row consists of folded code", ->
|
||||
it "moves the line above the folded row and preseveres the correct folds", ->
|
||||
it "moves the line above the folded row and perseveres the correct folds", ->
|
||||
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
|
||||
expect(editor.lineTextForBufferRow(9)).toBe " };"
|
||||
|
||||
@@ -3517,7 +3579,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;'
|
||||
|
||||
describe "when text is selected", ->
|
||||
it "still deletes all text to begginning of the line", ->
|
||||
it "still deletes all text to beginning of the line", ->
|
||||
editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]])
|
||||
editor.deleteToBeginningOfLine()
|
||||
expect(buffer.lineForRow(1)).toBe 'ems) {'
|
||||
@@ -3704,7 +3766,7 @@ describe "TextEditor", ->
|
||||
describe "when autoIndent is enabled", ->
|
||||
describe "when the cursor's column is less than the suggested level of indentation", ->
|
||||
describe "when 'softTabs' is true (the default)", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation", ->
|
||||
buffer.insert([5, 0], " \n")
|
||||
editor.setCursorBufferPosition [5, 0]
|
||||
editor.indent(autoIndent: true)
|
||||
@@ -3727,7 +3789,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(13).length).toBe 8
|
||||
|
||||
describe "when 'softTabs' is false", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentaion", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation", ->
|
||||
convertToHardTabs(buffer)
|
||||
editor.setSoftTabs(false)
|
||||
buffer.insert([5, 0], "\t\n")
|
||||
@@ -4160,6 +4222,19 @@ describe "TextEditor", ->
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;")
|
||||
expect(editor.getCursorBufferPosition()).toEqual([3, 13])
|
||||
|
||||
it "respects options that preserve the formatting of the pasted text", ->
|
||||
editor.update({autoIndentOnPaste: true})
|
||||
atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0)
|
||||
editor.setCursorBufferPosition([5, 0])
|
||||
editor.insertText(' ')
|
||||
editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false})
|
||||
|
||||
expect(editor.lineTextForBufferRow(5)).toBe " a(x);"
|
||||
expect(editor.lineTextForBufferRow(6)).toBe " b(x);"
|
||||
expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n"
|
||||
expect(editor.lineTextForBufferRow(7)).toBe "c(x);"
|
||||
expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();"
|
||||
|
||||
describe ".indentSelectedRows()", ->
|
||||
describe "when nothing is selected", ->
|
||||
describe "when softTabs is enabled", ->
|
||||
@@ -4301,108 +4376,6 @@ describe "TextEditor", ->
|
||||
expect(editor.lineTextForBufferRow(4)).toBe " }"
|
||||
expect(editor.lineTextForBufferRow(5)).toBe " i=1"
|
||||
|
||||
describe ".toggleLineCommentsInSelection()", ->
|
||||
it "toggles comments on the selected lines", ->
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " // }"
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]]
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " }"
|
||||
|
||||
it "does not comment the last line of a non-empty selection if it ends at column 0", ->
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " }"
|
||||
|
||||
it "uncomments lines if all lines match the comment regex", ->
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 1]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
|
||||
expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe " var sort = function(items) {"
|
||||
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
||||
|
||||
it "uncomments commented lines separated by an empty line", ->
|
||||
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
|
||||
|
||||
buffer.insert([0, Infinity], '\n')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe ""
|
||||
expect(buffer.lineForRow(2)).toBe " var sort = function(items) {"
|
||||
|
||||
it "preserves selection emptiness", ->
|
||||
editor.setCursorBufferPosition([4, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
|
||||
|
||||
it "does not explode if the current language mode has no comment regex", ->
|
||||
editor = new TextEditor(buffer: new TextBuffer(text: 'hello'))
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe "hello"
|
||||
|
||||
it "does nothing for empty lines and null grammar", ->
|
||||
runs ->
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.buffer.lineForRow(10)).toBe ""
|
||||
|
||||
it "uncomments when the line lacks the trailing whitespace in the comment regex", ->
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(10)).toBe "// "
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]]
|
||||
editor.backspace()
|
||||
expect(buffer.lineForRow(10)).toBe "//"
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(10)).toBe ""
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]]
|
||||
|
||||
it "uncomments when the line has leading whitespace", ->
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(10)).toBe "// "
|
||||
editor.moveToBeginningOfLine()
|
||||
editor.insertText(" ")
|
||||
editor.setSelectedBufferRange([[10, 0], [10, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(10)).toBe " "
|
||||
|
||||
describe ".undo() and .redo()", ->
|
||||
it "undoes/redoes the last change", ->
|
||||
editor.insertText("foo")
|
||||
@@ -4820,7 +4793,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(6)).toBe(line7)
|
||||
expect(buffer.getLineCount()).toBe(count - 1)
|
||||
|
||||
describe "when the line being deleted preceeds a fold, and the command is undone", ->
|
||||
describe "when the line being deleted precedes a fold, and the command is undone", ->
|
||||
it "restores the line and preserves the fold", ->
|
||||
editor.setCursorBufferPosition([4])
|
||||
editor.foldCurrentRow()
|
||||
@@ -4992,7 +4965,7 @@ describe "TextEditor", ->
|
||||
editor.insertText('\n')
|
||||
expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1
|
||||
|
||||
describe "when the line preceding the newline does't add a level of indentation", ->
|
||||
describe "when the line preceding the newline doesn't add a level of indentation", ->
|
||||
it "indents the new line to the same level as the preceding line", ->
|
||||
editor.setCursorBufferPosition([5, 14])
|
||||
editor.insertText('\n')
|
||||
@@ -5262,37 +5235,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')
|
||||
@@ -5918,3 +5860,11 @@ describe "TextEditor", ->
|
||||
describe "::getElement", ->
|
||||
it "returns an element", ->
|
||||
expect(editor.getElement() instanceof HTMLElement).toBe(true)
|
||||
|
||||
describe 'setMaxScreenLineLength', ->
|
||||
it "sets the maximum line length in the editor before soft wrapping is forced", ->
|
||||
expect(editor.getSoftWrapColumn()).toBe(500)
|
||||
editor.update({
|
||||
maxScreenLineLength: 1500
|
||||
})
|
||||
expect(editor.getSoftWrapColumn()).toBe(1500)
|
||||
|
||||
541
spec/text-editor-spec.js
Normal file
541
spec/text-editor-spec.js
Normal file
@@ -0,0 +1,541 @@
|
||||
const fs = require('fs')
|
||||
const temp = require('temp').track()
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
|
||||
describe('TextEditor', () => {
|
||||
let editor
|
||||
|
||||
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('.toggleLineCommentsInSelection()', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
})
|
||||
|
||||
it('toggles comments on the selected lines', () => {
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[4, 8], [7, 8]])
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' }')
|
||||
})
|
||||
|
||||
it('does not comment the last line of a non-empty selection if it ends at column 0', () => {
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' }')
|
||||
})
|
||||
|
||||
it('uncomments lines if all lines match the comment regex', () => {
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 1]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// // var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe('// if (items.length <= 1) return items;')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' var sort = function(items) {')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
|
||||
})
|
||||
|
||||
it('uncomments commented lines separated by an empty line', () => {
|
||||
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
|
||||
|
||||
editor.getBuffer().insert([0, Infinity], '\n')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {')
|
||||
})
|
||||
|
||||
it('preserves selection emptiness', () => {
|
||||
editor.setCursorBufferPosition([4, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not explode if the current language mode has no comment regex', () => {
|
||||
const editor = new TextEditor({buffer: new TextBuffer({text: 'hello'})})
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('hello')
|
||||
})
|
||||
|
||||
it('does nothing for empty lines and null grammar', () => {
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('')
|
||||
})
|
||||
|
||||
it('uncomments when the line lacks the trailing whitespace in the comment regex', () => {
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('// ')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[10, 3], [10, 3]])
|
||||
editor.backspace()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('//')
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]])
|
||||
})
|
||||
|
||||
it('uncomments when the line has leading whitespace', () => {
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('// ')
|
||||
editor.moveToBeginningOfLine()
|
||||
editor.insertText(' ')
|
||||
editor.setSelectedBufferRange([[10, 0], [10, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe(' ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.toggleLineCommentsForBufferRows', () => {
|
||||
describe('xml', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-xml')
|
||||
editor = await atom.workspace.open('test.xml')
|
||||
editor.setText('<!-- test -->')
|
||||
})
|
||||
|
||||
it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('less', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-less')
|
||||
await atom.packages.activatePackage('language-css')
|
||||
editor = await atom.workspace.open('sample.less')
|
||||
})
|
||||
|
||||
it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;')
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-css')
|
||||
editor = await atom.workspace.open('css.css')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
})
|
||||
|
||||
it('uncomments lines with leading whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
|
||||
})
|
||||
|
||||
it('uncomments lines with trailing whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ')
|
||||
})
|
||||
|
||||
it('uncomments lines with leading and trailing whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('coffeescript', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
editor = await atom.workspace.open('coffee.coffee')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 6)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
})
|
||||
|
||||
it('comments/uncomments empty lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('javascript', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
|
||||
editor.setText('\tvar i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;')
|
||||
|
||||
editor.setText('var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var i;')
|
||||
|
||||
editor.setText(' var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // var i;')
|
||||
|
||||
editor.setText(' ')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // ')
|
||||
|
||||
editor.setText(' a\n \n b')
|
||||
editor.toggleLineCommentsForBufferRows(0, 2)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // a')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' // ')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' // b')
|
||||
|
||||
editor.setText(' \n // var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' ')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' var i;')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('folding', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
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('.foldCurrentRow()', () => {
|
||||
it('creates a fold at the location of the last cursor', async () => {
|
||||
editor = await atom.workspace.open()
|
||||
editor.setText('\nif (x) {\n y()\n}')
|
||||
editor.setCursorBufferPosition([1, 0])
|
||||
expect(editor.getScreenLineCount()).toBe(4)
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
})
|
||||
|
||||
it('does nothing when the current row cannot be folded', async () => {
|
||||
editor = await atom.workspace.open()
|
||||
editor.setText('var x;\nx++\nx++')
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.foldAllAtIndentLevel(indentLevel)', () => {
|
||||
it('folds blocks of text at the given indentation level', async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,9 +8,11 @@ describe "atom.themes", ->
|
||||
spyOn(console, 'warn')
|
||||
|
||||
afterEach ->
|
||||
atom.themes.deactivateThemes()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
waitsForPromise ->
|
||||
atom.themes.deactivateThemes()
|
||||
runs ->
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "theme getters and setters", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -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.any.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 contigous 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']
|
||||
904
spec/tokenized-buffer-spec.js
Normal file
904
spec/tokenized-buffer-spec.js
Normal file
@@ -0,0 +1,904 @@
|
||||
const NullGrammar = require('../src/null-grammar')
|
||||
const TokenizedBuffer = require('../src/tokenized-buffer')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const {Point, Range} = TextBuffer
|
||||
const _ = require('underscore-plus')
|
||||
const dedent = require('dedent')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
const {ScopedSettingsDelegate} = require('../src/text-editor-registry')
|
||||
|
||||
describe('TokenizedBuffer', () => {
|
||||
let tokenizedBuffer, buffer
|
||||
|
||||
beforeEach(async () => {
|
||||
// enable async tokenization
|
||||
TokenizedBuffer.prototype.chunkSize = 5
|
||||
jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
buffer && buffer.destroy()
|
||||
tokenizedBuffer && tokenizedBuffer.destroy()
|
||||
})
|
||||
|
||||
function startTokenizing (tokenizedBuffer) {
|
||||
tokenizedBuffer.setVisible(true)
|
||||
}
|
||||
|
||||
function fullyTokenize (tokenizedBuffer) {
|
||||
tokenizedBuffer.setVisible(true)
|
||||
while (tokenizedBuffer.firstInvalidRow() != null) {
|
||||
advanceClock()
|
||||
}
|
||||
}
|
||||
|
||||
describe('serialization', () => {
|
||||
describe('when the underlying buffer has a path', () => {
|
||||
beforeEach(async () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
})
|
||||
|
||||
it('deserializes it searching among the buffers in the current project', () => {
|
||||
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the underlying buffer has no path', () => {
|
||||
beforeEach(() => buffer = atom.project.bufferForPathSync(null))
|
||||
|
||||
it('deserializes it searching among the buffers in the current project', () => {
|
||||
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('tokenizing', () => {
|
||||
describe('when the buffer is destroyed', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
it('stops tokenization', () => {
|
||||
tokenizedBuffer.destroy()
|
||||
spyOn(tokenizedBuffer, 'tokenizeNextChunk')
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer contains soft-tabs', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
describe('on construction', () =>
|
||||
it('tokenizes lines chunk at a time in the background', () => {
|
||||
const line0 = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(line0).toBeUndefined()
|
||||
|
||||
const line11 = tokenizedBuffer.tokenizedLines[11]
|
||||
expect(line11).toBeUndefined()
|
||||
|
||||
// tokenize chunk 1
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
// tokenize chunk 2
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
|
||||
|
||||
// tokenize last chunk
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the buffer is partially tokenized', () => {
|
||||
beforeEach(() => {
|
||||
// tokenize chunk 1 only
|
||||
advanceClock()
|
||||
})
|
||||
|
||||
describe('when there is a buffer change inside the tokenized region', () => {
|
||||
describe('when lines are added', () => {
|
||||
it('pushes the invalid rows down', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.insert([1, 0], '\n\n')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are removed', () => {
|
||||
it('pulls the invalid rows up', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.delete([[1, 0], [3, 0]])
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates all the lines before the current invalid region', () => {
|
||||
it('retokenizes the invalidated lines and continues into the valid region', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(3)
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a buffer change surrounding an invalid row', () => {
|
||||
it('pushes the invalid row to the end of the change', () => {
|
||||
buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a buffer change inside an invalid region', () => {
|
||||
it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n')
|
||||
expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer is fully tokenized', () => {
|
||||
beforeEach(() => fullyTokenize(tokenizedBuffer))
|
||||
|
||||
describe('when there is a buffer change that is smaller than the chunk size', () => {
|
||||
describe('when lines are updated, but none are added or removed', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n')
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']})
|
||||
// line 2 is unchanged
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
it('resumes highlighting with the state of the previous line', () => {
|
||||
buffer.insert([0, 0], '/*')
|
||||
buffer.insert([5, 0], '*/')
|
||||
|
||||
buffer.insert([1, 0], 'var ')
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are both updated and removed', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[1, 0], [3, 0]], 'foo()')
|
||||
|
||||
// previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']})
|
||||
|
||||
// previous line 3 should be combined with input to form line 1
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
|
||||
// lines below deleted regions should be shifted upward
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.setTextInRange([[2, 0], [3, 0]], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are both updated and inserted', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()')
|
||||
|
||||
// previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']})
|
||||
|
||||
// 3 new lines inserted
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
|
||||
// previous line 2 is joined with quux() on line 4
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
|
||||
|
||||
// previous line 3 is pushed down to become line 5
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*\nabcde\nabcder')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock() // tokenize invalidated lines in background
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an insertion that is larger than the chunk size', () =>
|
||||
it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
|
||||
const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2)
|
||||
buffer.insert([0, 0], commentBlock)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy()
|
||||
})
|
||||
)
|
||||
|
||||
it('does not break out soft tabs across a scope boundary', async () => {
|
||||
await atom.packages.activatePackage('language-gfm')
|
||||
|
||||
tokenizedBuffer.setTabLength(4)
|
||||
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
|
||||
buffer.setText(' <![]()\n ')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
let length = 0
|
||||
for (let tag of tokenizedBuffer.tokenizedLines[1].tags) {
|
||||
if (tag > 0) length += tag
|
||||
}
|
||||
|
||||
expect(length).toBe(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer contains hard-tabs', () => {
|
||||
beforeEach(async () => {
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
describe('when the buffer is fully tokenized', () => {
|
||||
beforeEach(() => fullyTokenize(tokenizedBuffer))
|
||||
})
|
||||
})
|
||||
|
||||
describe('when tokenization completes', () => {
|
||||
it('emits the `tokenized` event', async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
})
|
||||
|
||||
it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
editor.getBuffer().insert([0, 0], "'")
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the grammar is updated because a grammar it includes is activated', async () => {
|
||||
it('re-emits the `tokenized` event', async () => {
|
||||
const editor = await atom.workspace.open('coffee.coffee')
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
tokenizedHandler.reset()
|
||||
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
})
|
||||
|
||||
it('retokenizes the buffer', async () => {
|
||||
await atom.packages.activatePackage('language-ruby-on-rails')
|
||||
await atom.packages.activatePackage('language-ruby')
|
||||
|
||||
buffer = atom.project.bufferForPathSync()
|
||||
buffer.setText("<div class='name'><%= User.find(2).full_name %></div>")
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
|
||||
value: "<div class='name'>",
|
||||
scopes: ['text.html.ruby']
|
||||
})
|
||||
|
||||
await atom.packages.activatePackage('language-html')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
|
||||
value: '<',
|
||||
scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html']
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer is configured with the null grammar', () => {
|
||||
it('does not actually tokenize using the grammar', () => {
|
||||
spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
|
||||
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
|
||||
buffer.setText('a\nb\nc')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizeCallback = jasmine.createSpy('onDidTokenize')
|
||||
tokenizedBuffer.onDidTokenize(tokenizeCallback)
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.tokenForPosition(position)', () => {
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
it('returns the correct token (regression)', () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js'])
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js'])
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
})
|
||||
|
||||
describe('when the selector does not match the token at the position', () =>
|
||||
it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined())
|
||||
)
|
||||
|
||||
describe('when the selector matches a single token at the position', () => {
|
||||
it('returns the range covered by the token', () => {
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]])
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the selector matches a run of multiple tokens at the position', () => {
|
||||
it('returns the range covered by all contiguous tokens (within a single line)', () => {
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.tokenizedLineForRow(row)', () => {
|
||||
it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
const grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
const line0 = buffer.lineForRow(0)
|
||||
|
||||
const jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
|
||||
const jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
|
||||
const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
|
||||
const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
|
||||
tokenizedBuffer.setGrammar(NullGrammar)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
})
|
||||
|
||||
it('returns undefined if the requested row is outside the buffer range', () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
const grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('text decoration layer API', () => {
|
||||
describe('iterator', () => {
|
||||
it('iterates over the syntactic scope boundaries', () => {
|
||||
buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(0, 0))
|
||||
|
||||
const expectedBoundaries = [
|
||||
{position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']},
|
||||
{position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
|
||||
{position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
|
||||
{position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
|
||||
{position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
|
||||
{position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []},
|
||||
{position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']},
|
||||
{position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []},
|
||||
{position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']},
|
||||
{position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']},
|
||||
{position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
|
||||
{position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
|
||||
{position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
|
||||
{position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
|
||||
{position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}
|
||||
]
|
||||
|
||||
while (true) {
|
||||
const boundary = {
|
||||
position: iterator.getPosition(),
|
||||
closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)),
|
||||
openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))
|
||||
}
|
||||
|
||||
expect(boundary).toEqual(expectedBoundaries.shift())
|
||||
if (!iterator.moveToSuccessor()) { break }
|
||||
}
|
||||
|
||||
expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--storage syntax--type syntax--var syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 8))
|
||||
expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--comment syntax--block syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--constant syntax--numeric syntax--decimal syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 18))
|
||||
|
||||
expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js'
|
||||
])
|
||||
iterator.moveToSuccessor()
|
||||
}) // ensure we don't infinitely loop (regression test)
|
||||
|
||||
it('does not report columns beyond the length of the line', async () => {
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
buffer = new TextBuffer({text: '# hello\n# world'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(0, 0))
|
||||
iterator.moveToSuccessor()
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(0)
|
||||
|
||||
iterator.seek(Point(0, 7))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.seek(Point(0, 8))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
})
|
||||
|
||||
it('correctly terminates scopes at the beginning of the line (regression)', () => {
|
||||
const grammar = atom.grammars.createGrammar('test', {
|
||||
'scopeName': 'text.broken',
|
||||
'name': 'Broken grammar',
|
||||
'patterns': [
|
||||
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
|
||||
{'match': '.', 'name': 'yellow.broken'}
|
||||
]
|
||||
})
|
||||
|
||||
buffer = new TextBuffer({text: 'start x\nend x\nx'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(1, 0))
|
||||
|
||||
expect(iterator.getPosition()).toEqual([1, 0])
|
||||
expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken'])
|
||||
expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.suggestedIndentForBufferRow', () => {
|
||||
let editor
|
||||
|
||||
describe('javascript', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
it('bases indentation off of the previous non-blank line', () => {
|
||||
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
|
||||
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
|
||||
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
|
||||
})
|
||||
|
||||
it('does not take invisibles into account', () => {
|
||||
editor.update({showInvisibles: true})
|
||||
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
|
||||
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
|
||||
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('css.css', {autoIndent: true})
|
||||
await atom.packages.activatePackage('language-source')
|
||||
await atom.packages.activatePackage('language-css')
|
||||
})
|
||||
|
||||
it('does not return negative values (regression)', () => {
|
||||
editor.setText('.test {\npadding: 0;\n}')
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isFoldableAtRow(row)', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n')
|
||||
buffer.insert([0, 0], '// multi-line\n// comment\n// block\n')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
})
|
||||
|
||||
it('includes the first line of multi-line comments', () => {
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent
|
||||
expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false)
|
||||
|
||||
buffer.insert([0, Infinity], '\n')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false)
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
|
||||
}) // because of indent
|
||||
|
||||
it('includes non-comment lines that precede an increase in indentation', () => {
|
||||
buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([7, 0], ' ')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([7, 0], ' \n x\n')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([9, 0], ' ')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRangesAtIndentLevel', () => {
|
||||
it('returns the ranges that can be folded at the given indent level', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {⋯
|
||||
}
|
||||
`)
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {⋯
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {⋯
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRanges', () => {
|
||||
it('returns the ranges that can be folded', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2),
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2),
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2),
|
||||
].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString()))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRangeContainingPoint', () => {
|
||||
it('returns the range for the smallest fold that contains the given range', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull()
|
||||
|
||||
let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {⋯
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('works for coffee-script', async () => {
|
||||
const editor = await atom.workspace.open('coffee.coffee')
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
buffer = editor.buffer
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]])
|
||||
})
|
||||
|
||||
it('works for javascript', async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
buffer = editor.buffer
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]])
|
||||
})
|
||||
})
|
||||
|
||||
function simulateFold (ranges) {
|
||||
buffer.transact(() => {
|
||||
for (const range of ranges.reverse()) {
|
||||
buffer.setTextInRange(range, '⋯')
|
||||
}
|
||||
})
|
||||
let text = buffer.getText()
|
||||
buffer.undo()
|
||||
return text
|
||||
}
|
||||
})
|
||||
@@ -204,7 +204,7 @@ describe "TooltipManager", ->
|
||||
disposable2.dispose()
|
||||
expect(manager.findTooltips(element).length).toBe(0)
|
||||
|
||||
it "lets us hide tooltips programatically", ->
|
||||
it "lets us hide tooltips programmatically", ->
|
||||
disposable = manager.add element, title: "Title"
|
||||
hover element, ->
|
||||
expect(document.body.querySelector(".tooltip")).not.toBeNull()
|
||||
|
||||
75
spec/uri-handler-registry-spec.js
Normal file
75
spec/uri-handler-registry-spec.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/** @babel */
|
||||
|
||||
import url from 'url'
|
||||
|
||||
import {it} from './async-spec-helpers'
|
||||
|
||||
import URIHandlerRegistry from '../src/uri-handler-registry'
|
||||
|
||||
describe('URIHandlerRegistry', () => {
|
||||
let registry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new URIHandlerRegistry(5)
|
||||
})
|
||||
|
||||
it('handles URIs on a per-host basis', () => {
|
||||
const testPackageSpy = jasmine.createSpy()
|
||||
const otherPackageSpy = jasmine.createSpy()
|
||||
registry.registerHostHandler('test-package', testPackageSpy)
|
||||
registry.registerHostHandler('other-package', otherPackageSpy)
|
||||
|
||||
registry.handleURI('atom://yet-another-package/path')
|
||||
expect(testPackageSpy).not.toHaveBeenCalled()
|
||||
expect(otherPackageSpy).not.toHaveBeenCalled()
|
||||
|
||||
registry.handleURI('atom://test-package/path')
|
||||
expect(testPackageSpy).toHaveBeenCalledWith(url.parse('atom://test-package/path', true), 'atom://test-package/path')
|
||||
expect(otherPackageSpy).not.toHaveBeenCalled()
|
||||
|
||||
registry.handleURI('atom://other-package/path')
|
||||
expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path')
|
||||
})
|
||||
|
||||
it('keeps track of the most recent URIs', () => {
|
||||
const spy1 = jasmine.createSpy()
|
||||
const spy2 = jasmine.createSpy()
|
||||
const changeSpy = jasmine.createSpy()
|
||||
registry.registerHostHandler('one', spy1)
|
||||
registry.registerHostHandler('two', spy2)
|
||||
registry.onHistoryChange(changeSpy)
|
||||
|
||||
const uris = [
|
||||
'atom://one/something?asdf=1',
|
||||
'atom://fake/nothing',
|
||||
'atom://two/other/stuff',
|
||||
'atom://one/more/thing',
|
||||
'atom://two/more/stuff'
|
||||
]
|
||||
|
||||
uris.forEach(u => registry.handleURI(u))
|
||||
|
||||
expect(changeSpy.callCount).toBe(5)
|
||||
expect(registry.getRecentlyHandledURIs()).toEqual(uris.map((u, idx) => {
|
||||
return {id: idx + 1, uri: u, handled: !u.match(/fake/), host: url.parse(u).host}
|
||||
}).reverse())
|
||||
|
||||
registry.handleURI('atom://another/url')
|
||||
expect(changeSpy.callCount).toBe(6)
|
||||
const history = registry.getRecentlyHandledURIs()
|
||||
expect(history.length).toBe(5)
|
||||
expect(history[0].uri).toBe('atom://another/url')
|
||||
expect(history[4].uri).toBe(uris[1])
|
||||
})
|
||||
|
||||
it('refuses to handle bad URLs', () => {
|
||||
[
|
||||
'atom:package/path',
|
||||
'atom:8080://package/path',
|
||||
'user:pass@atom://package/path',
|
||||
'smth://package/path'
|
||||
].forEach(uri => {
|
||||
expect(() => registry.handleURI(uri)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,163 +0,0 @@
|
||||
ViewRegistry = require '../src/view-registry'
|
||||
|
||||
describe "ViewRegistry", ->
|
||||
registry = null
|
||||
|
||||
beforeEach ->
|
||||
registry = new ViewRegistry
|
||||
|
||||
afterEach ->
|
||||
registry.clearDocumentRequests()
|
||||
|
||||
describe "::getView(object)", ->
|
||||
describe "when passed a DOM node", ->
|
||||
it "returns the given DOM node", ->
|
||||
node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe node
|
||||
|
||||
describe "when passed an object with an element property", ->
|
||||
it "returns the element property if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
constructor: -> @element = document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.element
|
||||
|
||||
describe "when passed an object with a getElement function", ->
|
||||
it "returns the return value of getElement if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
getElement: ->
|
||||
@myElement ?= document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.myElement
|
||||
|
||||
describe "when passed a model object", ->
|
||||
describe "when a view provider is registered matching the object's constructor", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
class TestModel
|
||||
|
||||
class TestModelSubclass extends TestModel
|
||||
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
model = new TestModel
|
||||
|
||||
registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe true
|
||||
expect(view.model).toBe model
|
||||
|
||||
subclassModel = new TestModelSubclass
|
||||
view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe true
|
||||
expect(view2.model).toBe subclassModel
|
||||
|
||||
describe "when a view provider is registered generically, and works with the object", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
model = {a: 'b'}
|
||||
|
||||
registry.addViewProvider (model) ->
|
||||
if model.a is 'b'
|
||||
element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
element
|
||||
|
||||
view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe 'test-element'
|
||||
|
||||
expect(-> registry.getView({a: 'c'})).toThrow()
|
||||
|
||||
describe "when no view provider is registered for the object's constructor", ->
|
||||
it "throws an exception", ->
|
||||
expect(-> registry.getView(new Object)).toThrow()
|
||||
|
||||
describe "::addViewProvider(providerSpec)", ->
|
||||
it "returns a disposable that can be used to remove the provider", ->
|
||||
class TestModel
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
disposable = registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
expect(registry.getView(new TestModel) instanceof TestView).toBe true
|
||||
disposable.dispose()
|
||||
expect(-> registry.getView(new TestModel)).toThrow()
|
||||
|
||||
describe "::updateDocument(fn) and ::readDocument(fn)", ->
|
||||
frameRequests = null
|
||||
|
||||
beforeEach ->
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
|
||||
|
||||
it "performs all pending writes before all pending reads on the next animation frame", ->
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument -> events.push('read 1')
|
||||
registry.readDocument -> events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(events).toEqual []
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
disposable = registry.updateDocument -> events.push('write 3')
|
||||
registry.updateDocument -> events.push('write 4')
|
||||
registry.readDocument -> events.push('read 3')
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 4', 'read 3']
|
||||
|
||||
it "performs writes requested from read callbacks in the same animation frame", ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 1')
|
||||
events.push('read 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 2')
|
||||
events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe 1
|
||||
|
||||
expect(events).toEqual [
|
||||
'write 1'
|
||||
'write 2'
|
||||
'read 1'
|
||||
'read 2'
|
||||
'write from read 1'
|
||||
'write from read 2'
|
||||
]
|
||||
|
||||
describe "::getNextUpdatePromise()", ->
|
||||
it "returns a promise that resolves at the end of the next update cycle", ->
|
||||
updateCalled = false
|
||||
readCalled = false
|
||||
|
||||
waitsFor 'getNextUpdatePromise to resolve', (done) ->
|
||||
registry.getNextUpdatePromise().then ->
|
||||
expect(updateCalled).toBe true
|
||||
expect(readCalled).toBe true
|
||||
done()
|
||||
|
||||
registry.updateDocument -> updateCalled = true
|
||||
registry.readDocument -> readCalled = true
|
||||
216
spec/view-registry-spec.js
Normal file
216
spec/view-registry-spec.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const ViewRegistry = require('../src/view-registry')
|
||||
|
||||
describe('ViewRegistry', () => {
|
||||
let registry = null
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ViewRegistry()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
registry.clearDocumentRequests()
|
||||
})
|
||||
|
||||
describe('::getView(object)', () => {
|
||||
describe('when passed a DOM node', () =>
|
||||
it('returns the given DOM node', () => {
|
||||
const node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe(node)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with an element property', () =>
|
||||
it("returns the element property if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
constructor () {
|
||||
this.element = document.createElement('div')
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.element)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with a getElement function', () =>
|
||||
it("returns the return value of getElement if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
getElement () {
|
||||
if (this.myElement == null) {
|
||||
this.myElement = document.createElement('div')
|
||||
}
|
||||
return this.myElement
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.myElement)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed a model object', () => {
|
||||
describe("when a view provider is registered matching the object's constructor", () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
class TestModel {}
|
||||
|
||||
class TestModelSubclass extends TestModel {}
|
||||
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const model = new TestModel()
|
||||
|
||||
registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
const view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe(true)
|
||||
expect(view.model).toBe(model)
|
||||
|
||||
const subclassModel = new TestModelSubclass()
|
||||
const view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe(true)
|
||||
expect(view2.model).toBe(subclassModel)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a view provider is registered generically, and works with the object', () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
registry.addViewProvider((model) => {
|
||||
if (model.a === 'b') {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
return element
|
||||
}
|
||||
})
|
||||
|
||||
const view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe('test-element')
|
||||
|
||||
expect(() => registry.getView({a: 'c'})).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe("when no view provider is registered for the object's constructor", () =>
|
||||
it('throws an exception', () => {
|
||||
expect(() => registry.getView({})).toThrow()
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::addViewProvider(providerSpec)', () =>
|
||||
it('returns a disposable that can be used to remove the provider', () => {
|
||||
class TestModel {}
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const disposable = registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
expect(registry.getView(new TestModel()) instanceof TestView).toBe(true)
|
||||
disposable.dispose()
|
||||
expect(() => registry.getView(new TestModel())).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe('::updateDocument(fn) and ::readDocument(fn)', () => {
|
||||
let frameRequests = null
|
||||
|
||||
beforeEach(() => {
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn))
|
||||
})
|
||||
|
||||
it('performs all pending writes before all pending reads on the next animation frame', () => {
|
||||
let events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => events.push('read 1'))
|
||||
registry.readDocument(() => events.push('read 2'))
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(events).toEqual([])
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2'])
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
const disposable = registry.updateDocument(() => events.push('write 3'))
|
||||
registry.updateDocument(() => events.push('write 4'))
|
||||
registry.readDocument(() => events.push('read 3'))
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 4', 'read 3'])
|
||||
})
|
||||
|
||||
it('performs writes requested from read callbacks in the same animation frame', () => {
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
const events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 1'))
|
||||
events.push('read 1')
|
||||
})
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 2'))
|
||||
events.push('read 2')
|
||||
})
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe(1)
|
||||
|
||||
expect(events).toEqual([
|
||||
'write 1',
|
||||
'write 2',
|
||||
'read 1',
|
||||
'read 2',
|
||||
'write from read 1',
|
||||
'write from read 2'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::getNextUpdatePromise()', () =>
|
||||
it('returns a promise that resolves at the end of the next update cycle', () => {
|
||||
let updateCalled = false
|
||||
let readCalled = false
|
||||
|
||||
waitsFor('getNextUpdatePromise to resolve', (done) => {
|
||||
registry.getNextUpdatePromise().then(() => {
|
||||
expect(updateCalled).toBe(true)
|
||||
expect(readCalled).toBe(true)
|
||||
done()
|
||||
})
|
||||
|
||||
registry.updateDocument(() => { updateCalled = true })
|
||||
registry.readDocument(() => { readCalled = true })
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -1,209 +0,0 @@
|
||||
KeymapManager = require 'atom-keymap'
|
||||
TextEditor = require '../src/text-editor'
|
||||
WindowEventHandler = require '../src/window-event-handler'
|
||||
{ipcRenderer} = require 'electron'
|
||||
|
||||
describe "WindowEventHandler", ->
|
||||
[windowEventHandler] = []
|
||||
|
||||
beforeEach ->
|
||||
atom.uninstallWindowEventHandler()
|
||||
spyOn(atom, 'hide')
|
||||
initialPath = atom.project.getPaths()[0]
|
||||
spyOn(atom, 'getLoadSettings').andCallFake ->
|
||||
loadSettings = atom.getLoadSettings.originalValue.call(atom)
|
||||
loadSettings.initialPath = initialPath
|
||||
loadSettings
|
||||
atom.project.destroy()
|
||||
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
|
||||
windowEventHandler.initialize(window, document)
|
||||
|
||||
afterEach ->
|
||||
windowEventHandler.unsubscribe()
|
||||
atom.installWindowEventHandler()
|
||||
|
||||
describe "when the window is loaded", ->
|
||||
it "doesn't have .is-blurred on the body tag", ->
|
||||
return if process.platform is 'win32' #Win32TestFailures - can not steal focus
|
||||
expect(document.body.className).not.toMatch("is-blurred")
|
||||
|
||||
describe "when the window is blurred", ->
|
||||
beforeEach ->
|
||||
window.dispatchEvent(new CustomEvent('blur'))
|
||||
|
||||
afterEach ->
|
||||
document.body.classList.remove('is-blurred')
|
||||
|
||||
it "adds the .is-blurred class on the body", ->
|
||||
expect(document.body.className).toMatch("is-blurred")
|
||||
|
||||
describe "when the window is focused again", ->
|
||||
it "removes the .is-blurred class from the body", ->
|
||||
window.dispatchEvent(new CustomEvent('focus'))
|
||||
expect(document.body.className).not.toMatch("is-blurred")
|
||||
|
||||
describe "window:close event", ->
|
||||
it "closes the window", ->
|
||||
spyOn(atom, 'close')
|
||||
window.dispatchEvent(new CustomEvent('window:close'))
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
describe "when a link is clicked", ->
|
||||
it "opens the http/https links in an external application", ->
|
||||
{shell} = require 'electron'
|
||||
spyOn(shell, 'openExternal')
|
||||
|
||||
link = document.createElement('a')
|
||||
linkChild = document.createElement('span')
|
||||
link.appendChild(linkChild)
|
||||
link.href = 'http://github.com'
|
||||
jasmine.attachToDOM(link)
|
||||
fakeEvent = {target: linkChild, currentTarget: link, preventDefault: (->)}
|
||||
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe "http://github.com"
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = 'https://github.com'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe "https://github.com"
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = ''
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = '#scroll-me'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
|
||||
describe "when a form is submitted", ->
|
||||
it "prevents the default so that the window's URL isn't changed", ->
|
||||
form = document.createElement('form')
|
||||
jasmine.attachToDOM(form)
|
||||
|
||||
defaultPrevented = false
|
||||
event = new CustomEvent('submit', bubbles: true)
|
||||
event.preventDefault = -> defaultPrevented = true
|
||||
form.dispatchEvent(event)
|
||||
expect(defaultPrevented).toBe(true)
|
||||
|
||||
describe "core:focus-next and core:focus-previous", ->
|
||||
describe "when there is no currently focused element", ->
|
||||
it "focuses the element with the lowest/highest tabindex", ->
|
||||
wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = """
|
||||
<div>
|
||||
<button tabindex="2"></button>
|
||||
<input tabindex="1">
|
||||
</div>
|
||||
"""
|
||||
elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
document.body.focus()
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
describe "when a tabindex is set on the currently focused element", ->
|
||||
it "focuses the element with the next highest/lowest tabindex, skipping disabled elements", ->
|
||||
wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = """
|
||||
<div>
|
||||
<input tabindex="1">
|
||||
<button tabindex="2"></button>
|
||||
<button tabindex="5"></button>
|
||||
<input tabindex="-1">
|
||||
<input tabindex="3">
|
||||
<button tabindex="7"></button>
|
||||
<input tabindex="9" disabled>
|
||||
</div>
|
||||
"""
|
||||
elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.querySelector('[tabindex="1"]').focus()
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 3
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 5
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 5
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 3
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
describe "when keydown events occur on the document", ->
|
||||
it "dispatches the event via the KeymapManager and CommandRegistry", ->
|
||||
dispatchedCommands = []
|
||||
atom.commands.onWillDispatch (command) -> dispatchedCommands.push(command)
|
||||
atom.commands.add '*', 'foo-command': ->
|
||||
atom.keymaps.add 'source-name', '*': {'x': 'foo-command'}
|
||||
|
||||
event = KeymapManager.buildKeydownEvent('x', target: document.createElement('div'))
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(dispatchedCommands.length).toBe 1
|
||||
expect(dispatchedCommands[0].type).toBe 'foo-command'
|
||||
|
||||
describe "native key bindings", ->
|
||||
it "correctly dispatches them to active elements with the '.native-key-bindings' class", ->
|
||||
webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"])
|
||||
spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({
|
||||
webContents: webContentsSpy
|
||||
on: ->
|
||||
})
|
||||
|
||||
nativeKeyBindingsInput = document.createElement("input")
|
||||
nativeKeyBindingsInput.classList.add("native-key-bindings")
|
||||
jasmine.attachToDOM(nativeKeyBindingsInput)
|
||||
nativeKeyBindingsInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand("core:copy")
|
||||
atom.dispatchApplicationMenuCommand("core:paste")
|
||||
|
||||
expect(webContentsSpy.copy).toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).toHaveBeenCalled()
|
||||
|
||||
webContentsSpy.copy.reset()
|
||||
webContentsSpy.paste.reset()
|
||||
|
||||
normalInput = document.createElement("input")
|
||||
jasmine.attachToDOM(normalInput)
|
||||
normalInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand("core:copy")
|
||||
atom.dispatchApplicationMenuCommand("core:paste")
|
||||
|
||||
expect(webContentsSpy.copy).not.toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).not.toHaveBeenCalled()
|
||||
228
spec/window-event-handler-spec.js
Normal file
228
spec/window-event-handler-spec.js
Normal file
@@ -0,0 +1,228 @@
|
||||
const KeymapManager = require('atom-keymap')
|
||||
const WindowEventHandler = require('../src/window-event-handler')
|
||||
|
||||
describe('WindowEventHandler', () => {
|
||||
let windowEventHandler
|
||||
|
||||
beforeEach(() => {
|
||||
atom.uninstallWindowEventHandler()
|
||||
spyOn(atom, 'hide')
|
||||
const initialPath = atom.project.getPaths()[0]
|
||||
spyOn(atom, 'getLoadSettings').andCallFake(() => {
|
||||
const loadSettings = atom.getLoadSettings.originalValue.call(atom)
|
||||
loadSettings.initialPath = initialPath
|
||||
return loadSettings
|
||||
})
|
||||
atom.project.destroy()
|
||||
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
|
||||
windowEventHandler.initialize(window, document)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
windowEventHandler.unsubscribe()
|
||||
atom.installWindowEventHandler()
|
||||
})
|
||||
|
||||
describe('when the window is loaded', () =>
|
||||
it("doesn't have .is-blurred on the body tag", () => {
|
||||
if (process.platform === 'win32') { return } // Win32TestFailures - can not steal focus
|
||||
expect(document.body.className).not.toMatch('is-blurred')
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the window is blurred', () => {
|
||||
beforeEach(() => window.dispatchEvent(new CustomEvent('blur')))
|
||||
|
||||
afterEach(() => document.body.classList.remove('is-blurred'))
|
||||
|
||||
it('adds the .is-blurred class on the body', () => expect(document.body.className).toMatch('is-blurred'))
|
||||
|
||||
describe('when the window is focused again', () =>
|
||||
it('removes the .is-blurred class from the body', () => {
|
||||
window.dispatchEvent(new CustomEvent('focus'))
|
||||
expect(document.body.className).not.toMatch('is-blurred')
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('window:close event', () =>
|
||||
it('closes the window', () => {
|
||||
spyOn(atom, 'close')
|
||||
window.dispatchEvent(new CustomEvent('window:close'))
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a link is clicked', () =>
|
||||
it('opens the http/https links in an external application', () => {
|
||||
const {shell} = require('electron')
|
||||
spyOn(shell, 'openExternal')
|
||||
|
||||
const link = document.createElement('a')
|
||||
const linkChild = document.createElement('span')
|
||||
link.appendChild(linkChild)
|
||||
link.href = 'http://github.com'
|
||||
jasmine.attachToDOM(link)
|
||||
const fakeEvent = {target: linkChild, currentTarget: link, preventDefault: () => {}}
|
||||
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com')
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = 'https://github.com'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe('https://github.com')
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = ''
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = '#scroll-me'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a form is submitted', () =>
|
||||
it("prevents the default so that the window's URL isn't changed", () => {
|
||||
const form = document.createElement('form')
|
||||
jasmine.attachToDOM(form)
|
||||
|
||||
let defaultPrevented = false
|
||||
const event = new CustomEvent('submit', {bubbles: true})
|
||||
event.preventDefault = () => { defaultPrevented = true }
|
||||
form.dispatchEvent(event)
|
||||
expect(defaultPrevented).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('core:focus-next and core:focus-previous', () => {
|
||||
describe('when there is no currently focused element', () =>
|
||||
it('focuses the element with the lowest/highest tabindex', () => {
|
||||
const wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = `
|
||||
<div>
|
||||
<button tabindex="2"></button>
|
||||
<input tabindex="1">
|
||||
</div>
|
||||
`.trim()
|
||||
const elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
document.body.focus()
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a tabindex is set on the currently focused element', () =>
|
||||
it('focuses the element with the next highest/lowest tabindex, skipping disabled elements', () => {
|
||||
const wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = `
|
||||
<div>
|
||||
<input tabindex="1">
|
||||
<button tabindex="2"></button>
|
||||
<button tabindex="5"></button>
|
||||
<input tabindex="-1">
|
||||
<input tabindex="3">
|
||||
<button tabindex="7"></button>
|
||||
<input tabindex="9" disabled>
|
||||
</div>
|
||||
`.trim()
|
||||
const elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.querySelector('[tabindex="1"]').focus()
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(3)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(5)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(5)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(3)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('when keydown events occur on the document', () =>
|
||||
it('dispatches the event via the KeymapManager and CommandRegistry', () => {
|
||||
const dispatchedCommands = []
|
||||
atom.commands.onWillDispatch(command => dispatchedCommands.push(command))
|
||||
atom.commands.add('*', {'foo-command': () => {}})
|
||||
atom.keymaps.add('source-name', {'*': {'x': 'foo-command'}})
|
||||
|
||||
const event = KeymapManager.buildKeydownEvent('x', {target: document.createElement('div')})
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(dispatchedCommands.length).toBe(1)
|
||||
expect(dispatchedCommands[0].type).toBe('foo-command')
|
||||
})
|
||||
)
|
||||
|
||||
describe('native key bindings', () =>
|
||||
it("correctly dispatches them to active elements with the '.native-key-bindings' class", () => {
|
||||
const webContentsSpy = jasmine.createSpyObj('webContents', ['copy', 'paste'])
|
||||
spyOn(atom.applicationDelegate, 'getCurrentWindow').andReturn({
|
||||
webContents: webContentsSpy,
|
||||
on: () => {}
|
||||
})
|
||||
|
||||
const nativeKeyBindingsInput = document.createElement('input')
|
||||
nativeKeyBindingsInput.classList.add('native-key-bindings')
|
||||
jasmine.attachToDOM(nativeKeyBindingsInput)
|
||||
nativeKeyBindingsInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand('core:copy')
|
||||
atom.dispatchApplicationMenuCommand('core:paste')
|
||||
|
||||
expect(webContentsSpy.copy).toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).toHaveBeenCalled()
|
||||
|
||||
webContentsSpy.copy.reset()
|
||||
webContentsSpy.paste.reset()
|
||||
|
||||
const normalInput = document.createElement('input')
|
||||
jasmine.attachToDOM(normalInput)
|
||||
normalInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand('core:copy')
|
||||
atom.dispatchApplicationMenuCommand('core:paste')
|
||||
|
||||
expect(webContentsSpy.copy).not.toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -1585,15 +1585,15 @@ i = /test/; #FIXME\
|
||||
atom2.project.deserialize(atom.project.serialize())
|
||||
atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers)
|
||||
|
||||
expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([
|
||||
'CoffeeScript',
|
||||
'CoffeeScript (Literate)',
|
||||
'JSDoc',
|
||||
'JavaScript',
|
||||
'Null Grammar',
|
||||
'Regular Expression Replacement (JavaScript)',
|
||||
'Regular Expressions (JavaScript)',
|
||||
'TODO'
|
||||
expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([
|
||||
'source.coffee',
|
||||
'source.js',
|
||||
'source.js.regexp',
|
||||
'source.js.regexp.replacement',
|
||||
'source.jsdoc',
|
||||
'source.litcoffee',
|
||||
'text.plain.null-grammar',
|
||||
'text.todo'
|
||||
])
|
||||
|
||||
atom2.destroy()
|
||||
@@ -2773,7 +2773,7 @@ i = /test/; #FIXME\
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the core.allowPendingPaneItems option is falsey', () => {
|
||||
describe('when the core.allowPendingPaneItems option is falsy', () => {
|
||||
it('does not open item with `pending: true` option as pending', () => {
|
||||
let pane = null
|
||||
atom.config.set('core.allowPendingPaneItems', false)
|
||||
|
||||
Reference in New Issue
Block a user