Merge remote-tracking branch 'upstream/master' into tests

This commit is contained in:
Steven Hobson-Campbell
2017-10-25 14:14:12 -07:00
129 changed files with 59480 additions and 9912 deletions

View File

@@ -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 ->

View File

@@ -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'

View File

@@ -1,5 +1,6 @@
'name': 'Test Ruby'
'scopeName': 'test.rb'
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
'fileTypes': [
'rb'
]

View File

@@ -0,0 +1,5 @@
module.exports = {
activate: () => null,
deactivate: () => null,
handleURI: () => null,
}

View File

@@ -0,0 +1,6 @@
{
"name": "package-with-uri-handler",
"uriHandler": {
"method": "handleURI"
}
}

View File

@@ -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")

View 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')
})
})
})
})

View File

@@ -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()

View File

@@ -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", ->

View File

@@ -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 []

View 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([])
})
)
})

View File

@@ -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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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, [])
})
})
})

View File

@@ -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'

View 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')
})
})
})

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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] = []

View File

@@ -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]

View File

@@ -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
View 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)
})
)
})
})

View File

@@ -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", ->

View File

@@ -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', () => {

View File

@@ -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()

View File

@@ -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
View 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')
})
)
})

View File

@@ -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')

View File

@@ -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]])

View File

@@ -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()

View File

@@ -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 = {}) {

View File

@@ -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)

View File

@@ -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
View 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)
})
})
})
})

View File

@@ -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 ->

View File

@@ -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']

View 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
}
})

View File

@@ -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()

View 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()
})
})
})

View File

@@ -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
View 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 })
})
})
)
})

View File

@@ -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()

View 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()
})
)
})

View File

@@ -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)