Merge branch 'master' into mb-use-language-mode-api

This commit is contained in:
Max Brunsfeld 2017-11-06 11:32:52 -08:00
commit 4ec1d85aad
23 changed files with 1244 additions and 989 deletions

View File

@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.23.0-dev",
"version": "1.24.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@ -101,15 +101,15 @@
"background-tips": "0.27.1",
"bookmarks": "0.44.4",
"bracket-matcher": "0.88.0",
"command-palette": "0.41.1",
"command-palette": "0.42.0",
"dalek": "0.2.1",
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.47.1",
"encoding-selector": "0.23.7",
"exception-reporting": "0.41.5",
"find-and-replace": "0.213.0",
"fuzzy-finder": "1.7.2",
"github": "0.8.0",
"fuzzy-finder": "1.7.3",
"github": "0.8.1",
"git-diff": "1.3.6",
"go-to-line": "0.32.1",
"grammar-selector": "0.49.8",
@ -121,17 +121,17 @@
"markdown-preview": "0.159.18",
"metrics": "1.2.6",
"notifications": "0.69.2",
"open-on-github": "1.2.1",
"open-on-github": "1.3.0",
"package-generator": "1.1.1",
"settings-view": "0.252.2",
"settings-view": "0.253.0",
"snippets": "1.1.9",
"spell-check": "0.72.3",
"status-bar": "1.8.14",
"styleguide": "0.49.8",
"status-bar": "1.8.15",
"styleguide": "0.49.9",
"symbols-view": "0.118.1",
"tabs": "0.109.1",
"timecop": "0.36.0",
"tree-view": "0.221.1",
"timecop": "0.36.2",
"tree-view": "0.221.2",
"update-package-dependencies": "0.12.0",
"welcome": "0.36.5",
"whitespace": "0.37.5",

View File

@ -34,7 +34,7 @@ export function afterEach (fn) {
}
})
export async function conditionPromise (condition) {
export async function conditionPromise (condition, description = 'anonymous condition') {
const startTime = Date.now()
while (true) {
@ -45,7 +45,7 @@ export async function conditionPromise (condition) {
}
if (Date.now() - startTime > 5000) {
throw new Error('Timed out waiting on condition')
throw new Error('Timed out waiting on ' + description)
}
}
}

View File

@ -1,377 +0,0 @@
temp = require('temp').track()
GitRepository = require '../src/git-repository'
fs = require 'fs-plus'
path = require 'path'
Project = require '../src/project'
copyRepository = ->
workingDirPath = temp.mkdirSync('atom-spec-git')
fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
workingDirPath
describe "GitRepository", ->
repo = null
beforeEach ->
gitPath = path.join(temp.dir, '.git')
fs.removeSync(gitPath) if fs.isDirectorySync(gitPath)
afterEach ->
repo.destroy() if repo?.repo?
try
temp.cleanupSync() # These tests sometimes lag at shutting down resources
describe "@open(path)", ->
it "returns null when no repository is found", ->
expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull()
describe "new GitRepository(path)", ->
it "throws an exception when no repository is found", ->
expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow()
describe ".getPath()", ->
it "returns the repository path for a .git directory path with a directory", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git')
it "returns the repository path for a repository path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git')
describe ".isPathIgnored(path)", ->
it "returns true for an ignored path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('a.txt')).toBeTruthy()
it "returns false for a non-ignored path", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('b.txt')).toBeFalsy()
describe ".isPathModified(path)", ->
[repo, filePath, newPath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
describe "when the path is unstaged", ->
it "returns false if the path has not been modified", ->
expect(repo.isPathModified(filePath)).toBeFalsy()
it "returns true if the path is modified", ->
fs.writeFileSync(filePath, "change")
expect(repo.isPathModified(filePath)).toBeTruthy()
it "returns true if the path is deleted", ->
fs.removeSync(filePath)
expect(repo.isPathModified(filePath)).toBeTruthy()
it "returns false if the path is new", ->
expect(repo.isPathModified(newPath)).toBeFalsy()
describe ".isPathNew(path)", ->
[filePath, newPath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
fs.writeFileSync(newPath, "i'm new here")
describe "when the path is unstaged", ->
it "returns true if the path is new", ->
expect(repo.isPathNew(newPath)).toBeTruthy()
it "returns false if the path isn't new", ->
expect(repo.isPathNew(filePath)).toBeFalsy()
describe ".checkoutHead(path)", ->
[filePath] = []
beforeEach ->
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
it "no longer reports a path as modified after checkout", ->
expect(repo.isPathModified(filePath)).toBeFalsy()
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.isPathModified(filePath)).toBeTruthy()
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(repo.isPathModified(filePath)).toBeFalsy()
it "restores the contents of the path to the original text", ->
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
it "fires a status-changed event if the checkout completes successfully", ->
fs.writeFileSync(filePath, 'ch ch changes')
repo.getPathStatus(filePath)
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus statusHandler
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0}
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe 1
describe ".checkoutHeadForEditor(editor)", ->
[filePath, editor] = []
beforeEach ->
spyOn(atom, "confirm")
workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm})
filePath = path.join(workingDirPath, 'a.txt')
fs.writeFileSync(filePath, 'ch ch changes')
waitsForPromise ->
atom.workspace.open(filePath)
runs ->
editor = atom.workspace.getActiveTextEditor()
it "displays a confirmation dialog by default", ->
return if process.platform is 'win32' # Permissions issues with this test on Windows
atom.confirm.andCallFake ({buttons}) -> buttons.OK()
atom.config.set('editor.confirmCheckoutHeadRevision', true)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
it "does not display a dialog when confirmation is disabled", ->
return if process.platform is 'win32' # Flakey EPERM opening a.txt on Win32
atom.config.set('editor.confirmCheckoutHeadRevision', false)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe ''
expect(atom.confirm).not.toHaveBeenCalled()
describe ".destroy()", ->
it "throws an exception when any method is called after it is called", ->
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
repo.destroy()
expect(-> repo.getShortHead()).toThrow()
describe ".getPathStatus(path)", ->
[filePath] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
filePath = path.join(workingDirectory, 'file.txt')
it "trigger a status-changed event when the new status differs from the last cached one", ->
statusHandler = jasmine.createSpy("statusHandler")
repo.onDidChangeStatus statusHandler
fs.writeFileSync(filePath, '')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status}
fs.writeFileSync(filePath, 'abc')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe 1
describe ".getDirectoryStatus(path)", ->
[directoryPath, filePath] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
directoryPath = path.join(workingDirectory, 'dir')
filePath = path.join(directoryPath, 'b.txt')
it "gets the status based on the files inside the directory", ->
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false
fs.writeFileSync(filePath, 'abc')
repo.getPathStatus(filePath)
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true
describe ".refreshStatus()", ->
[newPath, modifiedPath, cleanPath, workingDirectory] = []
beforeEach ->
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config})
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
cleanPath = path.join(workingDirectory, 'other.txt')
fs.writeFileSync(cleanPath, 'Full of text')
fs.writeFileSync(newPath, '')
newPath = fs.absolute newPath # specs could be running under symbol path.
it "returns status information for all new and modified files", ->
fs.writeFileSync(modifiedPath, 'making this path modified')
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
it 'caches the proper statuses when a subdir is open', ->
subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
waitsForPromise ->
atom.workspace.open('b.txt')
statusHandler = null
runs ->
repo = atom.project.getRepositories()[0]
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe false
expect(repo.isStatusNew(status)).toBe false
it "works correctly when the project has multiple folders (regression)", ->
atom.project.addPath(workingDirectory)
atom.project.addPath(path.join(__dirname, 'fixtures', 'dir'))
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
it 'caches statuses that were looked up synchronously', ->
originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
waitsForPromise -> repo.refreshStatus()
runs ->
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
describe "buffer events", ->
[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()
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
waitsForPromise ->
editor.save()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
it "emits a status-changed event when a buffer is reloaded", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
waitsForPromise ->
editor.getBuffer().reload()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
waitsForPromise ->
editor.getBuffer().reload()
runs ->
expect(statusHandler.callCount).toBe 1
it "emits a status-changed event when a buffer's path changes", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
editor.getBuffer().emitter.emit 'did-change-path'
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
editor.getBuffer().emitter.emit 'did-change-path'
expect(statusHandler.callCount).toBe 1
it "stops listening to the buffer when the repository is destroyed (regression)", ->
atom.project.getRepositories()[0].destroy()
expect(-> editor.save()).not.toThrow()
describe "when a project is deserialized", ->
[buffer, project2, statusHandler] = []
afterEach ->
project2?.destroy()
it "subscribes to all the serialized buffers in the project", ->
atom.project.setPaths([copyRepository()])
waitsForPromise ->
atom.workspace.open('file.txt')
waitsForPromise ->
project2 = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
applicationDelegate: atom.applicationDelegate,
grammarRegistry: atom.grammars
})
project2.deserialize(atom.project.serialize({isUnloading: false}))
waitsFor ->
buffer = project2.getBuffers()[0]
waitsForPromise ->
originalContent = buffer.getText()
buffer.append('changes')
statusHandler = jasmine.createSpy('statusHandler')
project2.getRepositories()[0].onDidChangeStatus statusHandler
buffer.save()
runs ->
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256}

394
spec/git-repository-spec.js Normal file
View File

@ -0,0 +1,394 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const GitRepository = require('../src/git-repository')
const Project = require('../src/project')
describe('GitRepository', () => {
let repo
beforeEach(() => {
const gitPath = path.join(temp.dir, '.git')
if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath)
})
afterEach(() => {
if (repo && !repo.isDestroyed()) repo.destroy()
// These tests sometimes lag at shutting down resources
try {
temp.cleanupSync()
} catch (error) {}
})
describe('@open(path)', () => {
it('returns null when no repository is found', () => {
expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull()
})
})
describe('new GitRepository(path)', () => {
it('throws an exception when no repository is found', () => {
expect(() => new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow()
})
})
describe('.getPath()', () => {
it('returns the repository path for a .git directory path with a directory', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
it('returns the repository path for a repository path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
})
describe('.isPathIgnored(path)', () => {
it('returns true for an ignored path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('a.txt')).toBeTruthy()
})
it('returns false for a non-ignored path', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git'))
expect(repo.isPathIgnored('b.txt')).toBeFalsy()
})
})
describe('.isPathModified(path)', () => {
let filePath, newPath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
})
describe('when the path is unstaged', () => {
it('returns false if the path has not been modified', () => {
expect(repo.isPathModified(filePath)).toBeFalsy()
})
it('returns true if the path is modified', () => {
fs.writeFileSync(filePath, 'change')
expect(repo.isPathModified(filePath)).toBeTruthy()
})
it('returns true if the path is deleted', () => {
fs.removeSync(filePath)
expect(repo.isPathModified(filePath)).toBeTruthy()
})
it('returns false if the path is new', () => {
expect(repo.isPathModified(newPath)).toBeFalsy()
})
})
})
describe('.isPathNew(path)', () => {
let filePath, newPath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
newPath = path.join(workingDirPath, 'new-path.txt')
fs.writeFileSync(newPath, "i'm new here")
})
describe('when the path is unstaged', () => {
it('returns true if the path is new', () => {
expect(repo.isPathNew(newPath)).toBeTruthy()
})
it("returns false if the path isn't new", () => {
expect(repo.isPathNew(filePath)).toBeFalsy()
})
})
})
describe('.checkoutHead(path)', () => {
let filePath
beforeEach(() => {
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath)
filePath = path.join(workingDirPath, 'a.txt')
})
it('no longer reports a path as modified after checkout', () => {
expect(repo.isPathModified(filePath)).toBeFalsy()
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.isPathModified(filePath)).toBeTruthy()
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(repo.isPathModified(filePath)).toBeFalsy()
})
it('restores the contents of the path to the original text', () => {
fs.writeFileSync(filePath, 'ch ch changes')
expect(repo.checkoutHead(filePath)).toBeTruthy()
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
})
it('fires a status-changed event if the checkout completes successfully', () => {
fs.writeFileSync(filePath, 'ch ch changes')
repo.getPathStatus(filePath)
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus(statusHandler)
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe(1)
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0})
repo.checkoutHead(filePath)
expect(statusHandler.callCount).toBe(1)
})
})
describe('.checkoutHeadForEditor(editor)', () => {
let filePath, editor
beforeEach(async () => {
spyOn(atom, 'confirm')
const workingDirPath = copyRepository()
repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm})
filePath = path.join(workingDirPath, 'a.txt')
fs.writeFileSync(filePath, 'ch ch changes')
editor = await atom.workspace.open(filePath)
})
it('displays a confirmation dialog by default', () => {
// Permissions issues with this test on Windows
if (process.platform === 'win32') return
atom.confirm.andCallFake(({buttons}) => buttons.OK())
atom.config.set('editor.confirmCheckoutHeadRevision', true)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
})
it('does not display a dialog when confirmation is disabled', () => {
// Flakey EPERM opening a.txt on Win32
if (process.platform === 'win32') return
atom.config.set('editor.confirmCheckoutHeadRevision', false)
repo.checkoutHeadForEditor(editor)
expect(fs.readFileSync(filePath, 'utf8')).toBe('')
expect(atom.confirm).not.toHaveBeenCalled()
})
})
describe('.destroy()', () => {
it('throws an exception when any method is called after it is called', () => {
repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git'))
repo.destroy()
expect(() => repo.getShortHead()).toThrow()
})
})
describe('.getPathStatus(path)', () => {
let filePath
beforeEach(() => {
const workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
filePath = path.join(workingDirectory, 'file.txt')
})
it('trigger a status-changed event when the new status differs from the last cached one', () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatus(statusHandler)
fs.writeFileSync(filePath, '')
let status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status})
fs.writeFileSync(filePath, 'abc')
status = repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
})
})
describe('.getDirectoryStatus(path)', () => {
let directoryPath, filePath
beforeEach(() => {
const workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory)
directoryPath = path.join(workingDirectory, 'dir')
filePath = path.join(directoryPath, 'b.txt')
})
it('gets the status based on the files inside the directory', () => {
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(false)
fs.writeFileSync(filePath, 'abc')
repo.getPathStatus(filePath)
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(true)
})
})
describe('.refreshStatus()', () => {
let newPath, modifiedPath, cleanPath, workingDirectory
beforeEach(() => {
workingDirectory = copyRepository()
repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config})
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
cleanPath = path.join(workingDirectory, 'other.txt')
fs.writeFileSync(cleanPath, 'Full of text')
fs.writeFileSync(newPath, '')
newPath = fs.absolute(newPath)
})
it('returns status information for all new and modified files', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
fs.writeFileSync(modifiedPath, 'making this path modified')
await repo.refreshStatus()
expect(statusHandler.callCount).toBe(1)
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath) )).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
})
it('caches the proper statuses when a subdir is open', async () => {
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
await atom.workspace.open('b.txt')
repo = atom.project.getRepositories()[0]
await repo.refreshStatus()
const status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(false)
expect(repo.isStatusNew(status)).toBe(false)
})
it('works correctly when the project has multiple folders (regression)', async () => {
atom.project.addPath(workingDirectory)
atom.project.addPath(path.join(__dirname, 'fixtures', 'dir'))
await repo.refreshStatus()
expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined()
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
})
it('caches statuses that were looked up synchronously', async () => {
const originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
await repo.refreshStatus()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
})
})
describe('buffer events', () => {
let editor
beforeEach(async () => {
atom.project.setPaths([copyRepository()])
const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve))
editor = await atom.workspace.open('other.txt')
await refreshPromise
})
it('emits a status-changed event when a buffer is saved', async () => {
editor.insertNewline()
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
await editor.save()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
})
it('emits a status-changed event when a buffer is reloaded', async () => {
fs.writeFileSync(editor.getPath(), 'changed')
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
await editor.getBuffer().reload()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
await editor.getBuffer().reload()
expect(statusHandler.callCount).toBe(1)
})
it("emits a status-changed event when a buffer's path changes", () => {
fs.writeFileSync(editor.getPath(), 'changed')
const statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepositories()[0].onDidChangeStatus(statusHandler)
editor.getBuffer().emitter.emit('did-change-path')
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256})
editor.getBuffer().emitter.emit('did-change-path')
expect(statusHandler.callCount).toBe(1)
})
it('stops listening to the buffer when the repository is destroyed (regression)', () => {
atom.project.getRepositories()[0].destroy()
expect(() => editor.save()).not.toThrow()
})
})
describe('when a project is deserialized', () => {
let buffer, project2, statusHandler
afterEach(() => {
if (project2) project2.destroy()
})
it('subscribes to all the serialized buffers in the project', async () => {
atom.project.setPaths([copyRepository()])
await atom.workspace.open('file.txt')
project2 = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars,
applicationDelegate: atom.applicationDelegate
})
await project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]
const originalContent = buffer.getText()
buffer.append('changes')
statusHandler = jasmine.createSpy('statusHandler')
project2.getRepositories()[0].onDidChangeStatus(statusHandler)
await buffer.save()
expect(statusHandler.callCount).toBe(1)
expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256})
})
})
})
function copyRepository () {
const workingDirPath = temp.mkdirSync('atom-spec-git')
fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath)
fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git'))
return workingDirPath
}

View File

@ -5,6 +5,7 @@ import dedent from 'dedent'
import electron from 'electron'
import fs from 'fs-plus'
import path from 'path'
import sinon from 'sinon'
import AtomApplication from '../../src/main-process/atom-application'
import parseCommandLine from '../../src/main-process/parse-command-line'
import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers'
@ -137,7 +138,7 @@ describe('AtomApplication', function () {
// Does not change the project paths when doing so.
const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath]))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
sendBackToMainProcess(textEditor.getPath())
@ -177,7 +178,7 @@ describe('AtomApplication', function () {
// parent directory to the project
let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
sendBackToMainProcess(textEditor.getPath())
@ -191,7 +192,7 @@ describe('AtomApplication', function () {
// the directory to the project
reusedWindow = atomApplication.launch(parseCommandLine([dirBPath, '-a']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.windows, [window1])
assert.deepEqual(atomApplication.getAllWindows(), [window1])
await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 3)
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath])
@ -276,7 +277,7 @@ describe('AtomApplication', function () {
})
assert.equal(window2EditorTitle, 'untitled')
assert.deepEqual(atomApplication.windows, [window1, window2])
assert.deepEqual(atomApplication.getAllWindows(), [window2, window1])
})
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () {
@ -461,6 +462,31 @@ describe('AtomApplication', function () {
assert.equal(reached, true);
windows[0].close();
})
it('triggers /core/open/file in the correct window', async function() {
const dirAPath = makeTempDir('a')
const dirBPath = makeTempDir('b')
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath)]))
await focusWindow(window1)
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)]))
await focusWindow(window2)
const fileA = path.join(dirAPath, 'file-a')
const uriA = `atom://core/open/file?filename=${fileA}`
const fileB = path.join(dirBPath, 'file-b')
const uriB = `atom://core/open/file?filename=${fileB}`
sinon.spy(window1, 'sendURIMessage')
sinon.spy(window2, 'sendURIMessage')
atomApplication.launch(parseCommandLine(['--uri-handler', uriA]))
await conditionPromise(() => window1.sendURIMessage.calledWith(uriA), `window1 to be focused from ${fileA}`)
atomApplication.launch(parseCommandLine(['--uri-handler', uriB]))
await conditionPromise(() => window2.sendURIMessage.calledWith(uriB), `window2 to be focused from ${fileB}`)
})
})
})
@ -514,7 +540,7 @@ describe('AtomApplication', function () {
async function focusWindow (window) {
window.focus()
await window.loadedPromise
await conditionPromise(() => window.atomApplication.lastFocusedWindow === window)
await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window)
}
function mockElectronAppQuit () {

View File

@ -385,9 +385,14 @@ describe('Project', () => {
isRoot () { return true }
existsSync () { return this.path.endsWith('does-exist') }
contains (filePath) { return filePath.startsWith(this.path) }
onDidChangeFiles (callback) {
onDidChangeFilesCallback = callback
return {dispose: () => {}}
}
}
let serviceDisposable = null
let onDidChangeFilesCallback = null
beforeEach(() => {
serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
@ -399,6 +404,7 @@ describe('Project', () => {
}
}
})
onDidChangeFilesCallback = null
waitsFor(() => atom.project.directoryProviders.length > 0)
})
@ -433,6 +439,28 @@ describe('Project', () => {
atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
expect(atom.project.getDirectories().length).toBe(0)
})
it('uses the custom onDidChangeFiles as the watcher if available', () => {
// Ensure that all preexisting watchers are stopped
waitsForPromise(() => stopAllWatchers())
const remotePath = 'ssh://another-directory:8080/does-exist'
runs(() => atom.project.setPaths([remotePath]))
waitsForPromise(() => atom.project.getWatcherPromise(remotePath))
runs(() => {
expect(onDidChangeFilesCallback).not.toBeNull()
const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles')
const disposable = atom.project.onDidChangeFiles(changeSpy)
const events = [{action: 'created', path: remotePath + '/test.txt'}]
onDidChangeFilesCallback(events)
expect(changeSpy).toHaveBeenCalledWith(events)
disposable.dispose()
})
})
})
describe('.open(path)', () => {

View File

@ -2007,13 +2007,17 @@ describe('TextEditor', () => {
describe('when the cursor is between two words', () => {
it('selects the word the cursor is on', () => {
editor.setCursorScreenPosition([0, 4])
editor.setCursorBufferPosition([0, 4])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('quicksort')
editor.setCursorScreenPosition([0, 3])
editor.setCursorBufferPosition([0, 3])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('var')
editor.setCursorBufferPosition([1, 22])
editor.selectWordsContainingCursors()
expect(editor.getSelectedText()).toBe('items')
})
})

View File

@ -1,437 +0,0 @@
path = require 'path'
fs = require 'fs-plus'
temp = require('temp').track()
describe "atom.themes", ->
beforeEach ->
spyOn(atom, 'inSpecMode').andReturn(false)
spyOn(console, 'warn')
afterEach ->
waitsForPromise ->
atom.themes.deactivateThemes()
runs ->
try
temp.cleanupSync()
describe "theme getters and setters", ->
beforeEach ->
jasmine.snapshotDeprecations()
atom.packages.loadPackages()
afterEach ->
jasmine.restoreDeprecationsSnapshot()
describe 'getLoadedThemes', ->
it 'gets all the loaded themes', ->
themes = atom.themes.getLoadedThemes()
expect(themes.length).toBeGreaterThan(2)
describe "getActiveThemes", ->
it 'gets all the active themes', ->
waitsForPromise -> atom.themes.activateThemes()
runs ->
names = atom.config.get('core.themes')
expect(names.length).toBeGreaterThan(0)
themes = atom.themes.getActiveThemes()
expect(themes).toHaveLength(names.length)
describe "when the core.themes config value contains invalid entry", ->
it "ignores theme", ->
atom.config.set 'core.themes', [
'atom-light-ui'
null
undefined
''
false
4
{}
[]
'atom-dark-ui'
]
expect(atom.themes.getEnabledThemeNames()).toEqual ['atom-dark-ui', 'atom-light-ui']
describe "::getImportPaths()", ->
it "returns the theme directories before the themes are loaded", ->
atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui'])
paths = atom.themes.getImportPaths()
# syntax theme is not a dir at this time, so only two.
expect(paths.length).toBe 2
expect(paths[0]).toContain 'atom-light-ui'
expect(paths[1]).toContain 'atom-dark-ui'
it "ignores themes that cannot be resolved to a directory", ->
atom.config.set('core.themes', ['definitely-not-a-theme'])
expect(-> atom.themes.getImportPaths()).not.toThrow()
describe "when the core.themes config value changes", ->
it "add/removes stylesheets to reflect the new config value", ->
atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()
spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake -> null
waitsForPromise ->
atom.themes.activateThemes()
runs ->
didChangeActiveThemesHandler.reset()
atom.config.set('core.themes', [])
waitsFor 'a', ->
didChangeActiveThemesHandler.callCount is 1
runs ->
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style.theme')).toHaveLength 0
atom.config.set('core.themes', ['atom-dark-ui'])
waitsFor 'b', ->
didChangeActiveThemesHandler.callCount is 1
runs ->
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2
expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch /atom-dark-ui/
atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui'])
waitsFor 'c', ->
didChangeActiveThemesHandler.callCount is 1
runs ->
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2
expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch /atom-dark-ui/
expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch /atom-light-ui/
atom.config.set('core.themes', [])
waitsFor ->
didChangeActiveThemesHandler.callCount is 1
runs ->
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2
# atom-dark-ui has an directory path, the syntax one doesn't
atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui'])
waitsFor ->
didChangeActiveThemesHandler.callCount is 1
runs ->
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2
importPaths = atom.themes.getImportPaths()
expect(importPaths.length).toBe 1
expect(importPaths[0]).toContain 'atom-dark-ui'
it 'adds theme-* classes to the workspace for each active theme', ->
atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax'])
workspaceElement = atom.workspace.getElement()
atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()
waitsForPromise ->
atom.themes.activateThemes()
runs ->
expect(workspaceElement).toHaveClass 'theme-atom-dark-ui'
atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()
atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables'])
waitsFor ->
didChangeActiveThemesHandler.callCount > 0
runs ->
# `theme-` twice as it prefixes the name with `theme-`
expect(workspaceElement).toHaveClass 'theme-theme-with-ui-variables'
expect(workspaceElement).toHaveClass 'theme-theme-with-syntax-variables'
expect(workspaceElement).not.toHaveClass 'theme-atom-dark-ui'
expect(workspaceElement).not.toHaveClass 'theme-atom-dark-syntax'
describe "when a theme fails to load", ->
it "logs a warning", ->
console.warn.reset()
atom.packages.activatePackage('a-theme-that-will-not-be-found').then((->), (->))
expect(console.warn.callCount).toBe 1
expect(console.warn.argsForCall[0][0]).toContain "Could not resolve 'a-theme-that-will-not-be-found'"
describe "::requireStylesheet(path)", ->
beforeEach ->
jasmine.snapshotDeprecations()
afterEach ->
jasmine.restoreDeprecationsSnapshot()
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler")
cssPath = atom.project.getDirectories()[0]?.resolve('css.css')
lengthBefore = document.querySelectorAll('head style').length
atom.themes.requireStylesheet(cssPath)
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
expect(styleElementAddedHandler).toHaveBeenCalled()
element = document.querySelector('head style[source-path*="css.css"]')
expect(element.getAttribute('source-path')).toEqualPath cssPath
expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8')
# doesn't append twice
styleElementAddedHandler.reset()
atom.themes.requireStylesheet(cssPath)
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
expect(styleElementAddedHandler).not.toHaveBeenCalled()
for styleElement in document.querySelectorAll('head style[id*="css.css"]')
styleElement.remove()
it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
lessPath = atom.project.getDirectories()[0]?.resolve('sample.less')
lengthBefore = document.querySelectorAll('head style').length
atom.themes.requireStylesheet(lessPath)
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
element = document.querySelector('head style[source-path*="sample.less"]')
expect(element.getAttribute('source-path')).toEqualPath lessPath
expect(element.textContent.toLowerCase()).toBe """
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
"""
# doesn't append twice
atom.themes.requireStylesheet(lessPath)
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
for styleElement in document.querySelectorAll('head style[id*="sample.less"]')
styleElement.remove()
it "supports requiring css and less stylesheets without an explicit extension", ->
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css')
expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('css.css')
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample')
expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('sample.less')
document.querySelector('head style[source-path*="css.css"]').remove()
document.querySelector('head style[source-path*="sample.less"]').remove()
it "returns a disposable allowing styles applied by the given path to be removed", ->
cssPath = require.resolve('./fixtures/css.css')
expect(getComputedStyle(document.body).fontWeight).not.toBe("bold")
disposable = atom.themes.requireStylesheet(cssPath)
expect(getComputedStyle(document.body).fontWeight).toBe("bold")
atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler")
disposable.dispose()
expect(getComputedStyle(document.body).fontWeight).not.toBe("bold")
expect(styleElementRemovedHandler).toHaveBeenCalled()
describe "base style sheet loading", ->
beforeEach ->
workspaceElement = atom.workspace.getElement()
jasmine.attachToDOM(atom.workspace.getElement())
workspaceElement.appendChild document.createElement('atom-text-editor')
waitsForPromise ->
atom.themes.activateThemes()
it "loads the correct values from the theme's ui-variables file", ->
atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()
atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables'])
waitsFor ->
didChangeActiveThemesHandler.callCount > 0
runs ->
# an override loaded in the base css
expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)"
# from within the theme itself
expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingTop).toBe "150px"
expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingRight).toBe "150px"
expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingBottom).toBe "150px"
describe "when there is a theme with incomplete variables", ->
it "loads the correct values from the fallback ui-variables", ->
atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()
atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables'])
waitsFor ->
didChangeActiveThemesHandler.callCount > 0
runs ->
# an override loaded in the base css
expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)"
# from within the theme itself
expect(getComputedStyle(document.querySelector("atom-text-editor")).backgroundColor).toBe "rgb(0, 152, 255)"
describe "user stylesheet", ->
userStylesheetPath = null
beforeEach ->
userStylesheetPath = path.join(temp.mkdirSync("atom"), 'styles.less')
fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}')
spyOn(atom.styles, 'getUserStyleSheetPath').andReturn userStylesheetPath
describe "when the user stylesheet changes", ->
beforeEach ->
jasmine.snapshotDeprecations()
afterEach ->
jasmine.restoreDeprecationsSnapshot()
it "reloads it", ->
[styleElementAddedHandler, styleElementRemovedHandler] = []
waitsForPromise ->
atom.themes.activateThemes()
runs ->
atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler")
atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler")
spyOn(atom.themes, 'loadUserStylesheet').andCallThrough()
expect(getComputedStyle(document.body).borderStyle).toBe 'dotted'
fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}')
waitsFor ->
atom.themes.loadUserStylesheet.callCount is 1
runs ->
expect(getComputedStyle(document.body).borderStyle).toBe 'dashed'
expect(styleElementRemovedHandler).toHaveBeenCalled()
expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dotted'
expect(styleElementAddedHandler).toHaveBeenCalled()
expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain 'dashed'
styleElementRemovedHandler.reset()
fs.removeSync(userStylesheetPath)
waitsFor ->
atom.themes.loadUserStylesheet.callCount is 2
runs ->
expect(styleElementRemovedHandler).toHaveBeenCalled()
expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dashed'
expect(getComputedStyle(document.body).borderStyle).toBe 'none'
describe "when there is an error reading the stylesheet", ->
addErrorHandler = null
beforeEach ->
atom.themes.loadUserStylesheet()
spyOn(atom.themes.lessCache, 'cssForFile').andCallFake ->
throw new Error('EACCES permission denied "styles.less"')
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
it "creates an error notification and does not add the stylesheet", ->
atom.themes.loadUserStylesheet()
expect(addErrorHandler).toHaveBeenCalled()
note = addErrorHandler.mostRecentCall.args[0]
expect(note.getType()).toBe 'error'
expect(note.getMessage()).toContain 'Error loading'
expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined()
describe "when there is an error watching the user stylesheet", ->
addErrorHandler = null
beforeEach ->
{File} = require 'pathwatcher'
spyOn(File::, 'on').andCallFake (event) ->
if event.indexOf('contents-changed') > -1
throw new Error('Unable to watch path')
spyOn(atom.themes, 'loadStylesheet').andReturn ''
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
it "creates an error notification", ->
atom.themes.loadUserStylesheet()
expect(addErrorHandler).toHaveBeenCalled()
note = addErrorHandler.mostRecentCall.args[0]
expect(note.getType()).toBe 'error'
expect(note.getMessage()).toContain 'Unable to watch path'
it "adds a notification when a theme's stylesheet is invalid", ->
addErrorHandler = jasmine.createSpy()
atom.notifications.onDidAddNotification(addErrorHandler)
expect(-> atom.packages.activatePackage('theme-with-invalid-styles').then((->), (->))).not.toThrow()
expect(addErrorHandler.callCount).toBe 2
expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to activate the theme-with-invalid-styles theme")
describe "when a non-existent theme is present in the config", ->
beforeEach ->
console.warn.reset()
atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax'])
waitsForPromise ->
atom.themes.activateThemes()
it 'uses the default dark UI and syntax themes and logs a warning', ->
activeThemeNames = atom.themes.getActiveThemeNames()
expect(console.warn.callCount).toBe 2
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
describe "when in safe mode", ->
describe 'when the enabled UI and syntax themes are bundled with Atom', ->
beforeEach ->
atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax'])
waitsForPromise ->
atom.themes.activateThemes()
it 'uses the enabled themes', ->
activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
describe 'when the enabled UI and syntax themes are not bundled with Atom', ->
beforeEach ->
atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax'])
waitsForPromise ->
atom.themes.activateThemes()
it 'uses the default dark UI and syntax themes', ->
activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
describe 'when the enabled UI theme is not bundled with Atom', ->
beforeEach ->
atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax'])
waitsForPromise ->
atom.themes.activateThemes()
it 'uses the default dark UI theme', ->
activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-light-syntax')
describe 'when the enabled syntax theme is not bundled with Atom', ->
beforeEach ->
atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax'])
waitsForPromise ->
atom.themes.activateThemes()
it 'uses the default dark syntax theme', ->
activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')

503
spec/theme-manager-spec.js Normal file
View File

@ -0,0 +1,503 @@
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
describe('atom.themes', function () {
beforeEach(function () {
spyOn(atom, 'inSpecMode').andReturn(false)
spyOn(console, 'warn')
})
afterEach(function () {
waitsForPromise(() => atom.themes.deactivateThemes())
runs(function () {
try {
temp.cleanupSync()
} catch (error) {}
})
})
describe('theme getters and setters', function () {
beforeEach(function () {
jasmine.snapshotDeprecations()
atom.packages.loadPackages()
})
afterEach(() => jasmine.restoreDeprecationsSnapshot())
describe('getLoadedThemes', () =>
it('gets all the loaded themes', function () {
const themes = atom.themes.getLoadedThemes()
expect(themes.length).toBeGreaterThan(2)
})
)
describe('getActiveThemes', () =>
it('gets all the active themes', function () {
waitsForPromise(() => atom.themes.activateThemes())
runs(function () {
const names = atom.config.get('core.themes')
expect(names.length).toBeGreaterThan(0)
const themes = atom.themes.getActiveThemes()
expect(themes).toHaveLength(names.length)
})
})
)
})
describe('when the core.themes config value contains invalid entry', () =>
it('ignores theme', function () {
atom.config.set('core.themes', [
'atom-light-ui',
null,
undefined,
'',
false,
4,
{},
[],
'atom-dark-ui'
])
expect(atom.themes.getEnabledThemeNames()).toEqual(['atom-dark-ui', 'atom-light-ui'])
})
)
describe('::getImportPaths()', function () {
it('returns the theme directories before the themes are loaded', function () {
atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui'])
const paths = atom.themes.getImportPaths()
// syntax theme is not a dir at this time, so only two.
expect(paths.length).toBe(2)
expect(paths[0]).toContain('atom-light-ui')
expect(paths[1]).toContain('atom-dark-ui')
})
it('ignores themes that cannot be resolved to a directory', function () {
atom.config.set('core.themes', ['definitely-not-a-theme'])
expect(() => atom.themes.getImportPaths()).not.toThrow()
})
})
describe('when the core.themes config value changes', function () {
it('add/removes stylesheets to reflect the new config value', function () {
let didChangeActiveThemesHandler
atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy())
spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake(() => null)
waitsForPromise(() => atom.themes.activateThemes())
runs(function () {
didChangeActiveThemesHandler.reset()
atom.config.set('core.themes', [])
})
waitsFor('a', () => didChangeActiveThemesHandler.callCount === 1)
runs(function () {
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style.theme')).toHaveLength(0)
atom.config.set('core.themes', ['atom-dark-ui'])
})
waitsFor('b', () => didChangeActiveThemesHandler.callCount === 1)
runs(function () {
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2)
expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch(/atom-dark-ui/)
atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui'])
})
waitsFor('c', () => didChangeActiveThemesHandler.callCount === 1)
runs(function () {
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2)
expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch(/atom-dark-ui/)
expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch(/atom-light-ui/)
atom.config.set('core.themes', [])
})
waitsFor(() => didChangeActiveThemesHandler.callCount === 1)
runs(function () {
didChangeActiveThemesHandler.reset()
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2)
// atom-dark-ui has a directory path, the syntax one doesn't
atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui'])
})
waitsFor(() => didChangeActiveThemesHandler.callCount === 1)
runs(function () {
expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2)
const importPaths = atom.themes.getImportPaths()
expect(importPaths.length).toBe(1)
expect(importPaths[0]).toContain('atom-dark-ui')
})
})
it('adds theme-* classes to the workspace for each active theme', function () {
atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax'])
let didChangeActiveThemesHandler
atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy())
waitsForPromise(() => atom.themes.activateThemes())
const workspaceElement = atom.workspace.getElement()
runs(function () {
expect(workspaceElement).toHaveClass('theme-atom-dark-ui')
atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy())
atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables'])
})
waitsFor(() => didChangeActiveThemesHandler.callCount > 0)
runs(function () {
// `theme-` twice as it prefixes the name with `theme-`
expect(workspaceElement).toHaveClass('theme-theme-with-ui-variables')
expect(workspaceElement).toHaveClass('theme-theme-with-syntax-variables')
expect(workspaceElement).not.toHaveClass('theme-atom-dark-ui')
expect(workspaceElement).not.toHaveClass('theme-atom-dark-syntax')
})
})
})
describe('when a theme fails to load', () =>
it('logs a warning', function () {
console.warn.reset()
atom.packages.activatePackage('a-theme-that-will-not-be-found').then(function () {}, function () {})
expect(console.warn.callCount).toBe(1)
expect(console.warn.argsForCall[0][0]).toContain("Could not resolve 'a-theme-that-will-not-be-found'")
})
)
describe('::requireStylesheet(path)', function () {
beforeEach(() => jasmine.snapshotDeprecations())
afterEach(() => jasmine.restoreDeprecationsSnapshot())
it('synchronously loads css at the given path and installs a style tag for it in the head', function () {
let styleElementAddedHandler
atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler'))
const cssPath = getAbsolutePath(atom.project.getDirectories()[0], 'css.css')
const lengthBefore = document.querySelectorAll('head style').length
atom.themes.requireStylesheet(cssPath)
expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1)
expect(styleElementAddedHandler).toHaveBeenCalled()
const element = document.querySelector('head style[source-path*="css.css"]')
expect(element.getAttribute('source-path')).toEqualPath(cssPath)
expect(element.textContent).toBe(fs.readFileSync(cssPath, 'utf8'))
// doesn't append twice
styleElementAddedHandler.reset()
atom.themes.requireStylesheet(cssPath)
expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1)
expect(styleElementAddedHandler).not.toHaveBeenCalled()
document.querySelectorAll('head style[id*="css.css"]').forEach((styleElement) => {
styleElement.remove()
})
})
it('synchronously loads and parses less files at the given path and installs a style tag for it in the head', function () {
const lessPath = getAbsolutePath(atom.project.getDirectories()[0], 'sample.less')
const lengthBefore = document.querySelectorAll('head style').length
atom.themes.requireStylesheet(lessPath)
expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1)
const element = document.querySelector('head style[source-path*="sample.less"]')
expect(element.getAttribute('source-path')).toEqualPath(lessPath)
expect(element.textContent.toLowerCase()).toBe(`\
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
\
`
)
// doesn't append twice
atom.themes.requireStylesheet(lessPath)
expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1)
document.querySelectorAll('head style[id*="sample.less"]').forEach((styleElement) => {
styleElement.remove()
})
})
it('supports requiring css and less stylesheets without an explicit extension', function () {
atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'css'))
expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path'))
.toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'css.css'))
atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'sample'))
expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path'))
.toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'sample.less'))
document.querySelector('head style[source-path*="css.css"]').remove()
document.querySelector('head style[source-path*="sample.less"]').remove()
})
it('returns a disposable allowing styles applied by the given path to be removed', function () {
const cssPath = require.resolve('./fixtures/css.css')
expect(getComputedStyle(document.body).fontWeight).not.toBe('bold')
const disposable = atom.themes.requireStylesheet(cssPath)
expect(getComputedStyle(document.body).fontWeight).toBe('bold')
let styleElementRemovedHandler
atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler'))
disposable.dispose()
expect(getComputedStyle(document.body).fontWeight).not.toBe('bold')
expect(styleElementRemovedHandler).toHaveBeenCalled()
})
})
describe('base style sheet loading', function () {
beforeEach(function () {
const workspaceElement = atom.workspace.getElement()
jasmine.attachToDOM(atom.workspace.getElement())
workspaceElement.appendChild(document.createElement('atom-text-editor'))
waitsForPromise(() => atom.themes.activateThemes())
})
it("loads the correct values from the theme's ui-variables file", function () {
let didChangeActiveThemesHandler
atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy())
atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables'])
waitsFor(() => didChangeActiveThemesHandler.callCount > 0)
runs(function () {
// an override loaded in the base css
expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)')
// from within the theme itself
expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingTop).toBe('150px')
expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingRight).toBe('150px')
expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingBottom).toBe('150px')
})
})
describe('when there is a theme with incomplete variables', () =>
it('loads the correct values from the fallback ui-variables', function () {
let didChangeActiveThemesHandler
atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy())
atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables'])
waitsFor(() => didChangeActiveThemesHandler.callCount > 0)
runs(function () {
// an override loaded in the base css
expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)')
// from within the theme itself
expect(getComputedStyle(document.querySelector('atom-text-editor')).backgroundColor).toBe('rgb(0, 152, 255)')
})
})
)
})
describe('user stylesheet', function () {
let userStylesheetPath
beforeEach(function () {
userStylesheetPath = path.join(temp.mkdirSync('atom'), 'styles.less')
fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}')
spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath)
})
describe('when the user stylesheet changes', function () {
beforeEach(() => jasmine.snapshotDeprecations())
afterEach(() => jasmine.restoreDeprecationsSnapshot())
it('reloads it', function () {
let styleElementAddedHandler, styleElementRemovedHandler
waitsForPromise(() => atom.themes.activateThemes())
runs(function () {
atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler'))
atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler'))
spyOn(atom.themes, 'loadUserStylesheet').andCallThrough()
expect(getComputedStyle(document.body).borderStyle).toBe('dotted')
fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}')
})
waitsFor(() => atom.themes.loadUserStylesheet.callCount === 1)
runs(function () {
expect(getComputedStyle(document.body).borderStyle).toBe('dashed')
expect(styleElementRemovedHandler).toHaveBeenCalled()
expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dotted')
expect(styleElementAddedHandler).toHaveBeenCalled()
expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain('dashed')
styleElementRemovedHandler.reset()
fs.removeSync(userStylesheetPath)
})
waitsFor(() => atom.themes.loadUserStylesheet.callCount === 2)
runs(function () {
expect(styleElementRemovedHandler).toHaveBeenCalled()
expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dashed')
expect(getComputedStyle(document.body).borderStyle).toBe('none')
})
})
})
describe('when there is an error reading the stylesheet', function () {
let addErrorHandler = null
beforeEach(function () {
atom.themes.loadUserStylesheet()
spyOn(atom.themes.lessCache, 'cssForFile').andCallFake(function () {
throw new Error('EACCES permission denied "styles.less"')
})
atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy())
})
it('creates an error notification and does not add the stylesheet', function () {
atom.themes.loadUserStylesheet()
expect(addErrorHandler).toHaveBeenCalled()
const note = addErrorHandler.mostRecentCall.args[0]
expect(note.getType()).toBe('error')
expect(note.getMessage()).toContain('Error loading')
expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined()
})
})
describe('when there is an error watching the user stylesheet', function () {
let addErrorHandler = null
beforeEach(function () {
const {File} = require('pathwatcher')
spyOn(File.prototype, 'on').andCallFake(function (event) {
if (event.indexOf('contents-changed') > -1) {
throw new Error('Unable to watch path')
}
})
spyOn(atom.themes, 'loadStylesheet').andReturn('')
atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy())
})
it('creates an error notification', function () {
atom.themes.loadUserStylesheet()
expect(addErrorHandler).toHaveBeenCalled()
const note = addErrorHandler.mostRecentCall.args[0]
expect(note.getType()).toBe('error')
expect(note.getMessage()).toContain('Unable to watch path')
})
})
it("adds a notification when a theme's stylesheet is invalid", function () {
const addErrorHandler = jasmine.createSpy()
atom.notifications.onDidAddNotification(addErrorHandler)
expect(() => atom.packages.activatePackage('theme-with-invalid-styles').then(function () {}, function () {})).not.toThrow()
expect(addErrorHandler.callCount).toBe(2)
expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to activate the theme-with-invalid-styles theme')
})
})
describe('when a non-existent theme is present in the config', function () {
beforeEach(function () {
console.warn.reset()
atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax'])
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI and syntax themes and logs a warning', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(console.warn.callCount).toBe(2)
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
})
})
describe('when in safe mode', function () {
describe('when the enabled UI and syntax themes are bundled with Atom', function () {
beforeEach(function () {
atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax'])
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the enabled themes', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
})
})
describe('when the enabled UI and syntax themes are not bundled with Atom', function () {
beforeEach(function () {
atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax'])
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI and syntax themes', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
})
})
describe('when the enabled UI theme is not bundled with Atom', function () {
beforeEach(function () {
atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax'])
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-light-syntax')
})
})
describe('when the enabled syntax theme is not bundled with Atom', function () {
beforeEach(function () {
atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax'])
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark syntax theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
})
})
})
})
function getAbsolutePath (directory, relativePath) {
if (directory) {
return directory.resolve(relativePath)
}
}

View File

@ -1,37 +0,0 @@
TextBuffer = require 'text-buffer'
TokenizedBuffer = require '../src/tokenized-buffer'
describe "TokenIterator", ->
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
end x
x
""")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
expect(tokenIterator.getBufferStart()).toBe 0
expect(tokenIterator.getScopeEnds()).toEqual []
expect(tokenIterator.getScopeStarts()).toEqual ['text.broken', 'yellow.broken']

View File

@ -0,0 +1,43 @@
const TextBuffer = require('text-buffer')
const TokenizedBuffer = require('../src/tokenized-buffer')
describe('TokenIterator', () =>
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'
}
]
})
const buffer = new TextBuffer({text: `\
start x
end x
x\
`})
const tokenizedBuffer = new TokenizedBuffer({
buffer,
config: atom.config,
grammarRegistry: atom.grammars,
packageManager: atom.packages,
assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
expect(tokenIterator.getBufferStart()).toBe(0)
expect(tokenIterator.getScopeEnds()).toEqual([])
expect(tokenIterator.getScopeStarts()).toEqual(['text.broken', 'yellow.broken'])
})
)

View File

@ -32,6 +32,7 @@ ThemeManager = require './theme-manager'
MenuManager = require './menu-manager'
ContextMenuManager = require './context-menu-manager'
CommandInstaller = require './command-installer'
CoreURIHandlers = require './core-uri-handlers'
ProtocolHandlerInstaller = require './protocol-handler-installer'
Project = require './project'
TitleBar = require './title-bar'
@ -240,6 +241,7 @@ class AtomEnvironment extends Model
@commandInstaller.initialize(@getVersion())
@protocolHandlerInstaller.initialize(@config, @notifications)
@uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
@autoUpdater.initialize()
@config.load()

View File

@ -107,6 +107,13 @@ module.exports = class CommandRegistry {
// otherwise be generated from the event name.
// * `description`: Used by consumers to display detailed information about
// the command.
// * `hiddenInCommandPalette`: If `true`, this command will not appear in
// the bundled command palette by default, but can still be shown with.
// the `Command Palette: Show Hidden Commands` command. This is a good
// option when you need to register large numbers of commands that don't
// make sense to be executed from the command palette. Please use this
// option conservatively, as it could reduce the discoverability of your
// package's commands.
//
// ## Arguments: Registering Multiple Commands
//

38
src/core-uri-handlers.js Normal file
View File

@ -0,0 +1,38 @@
function openFile (atom, {query}) {
const {filename, line, column} = query
atom.workspace.open(filename, {
initialLine: parseInt(line || 0, 10),
initialColumn: parseInt(column || 0, 10),
searchAllPanes: true
})
}
function windowShouldOpenFile ({query}) {
const {filename} = query
return (win) => win.containsPath(filename)
}
const ROUTER = {
'/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile }
}
module.exports = {
create (atomEnv) {
return function coreURIHandler (parsed) {
const config = ROUTER[parsed.pathname]
if (config) {
config.handler(atomEnv, parsed)
}
}
},
windowPredicate (parsed) {
const config = ROUTER[parsed.pathname]
if (config && config.getWindowPredicate) {
return config.getWindowPredicate(parsed)
} else {
return (win) => true
}
}
}

View File

@ -594,7 +594,7 @@ class Cursor extends Model {
getCurrentWordBufferRange (options = {}) {
const position = this.getBufferPosition()
const ranges = this.editor.buffer.findAllInRangeSync(
options.wordRegex || this.wordRegExp(),
options.wordRegex || this.wordRegExp(options),
new Range(new Point(position.row, 0), new Point(position.row, Infinity))
)
const range = ranges.find(range =>

View File

@ -1,15 +1,7 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const {join} = require('path')
const path = require('path')
const fs = require('fs-plus')
const _ = require('underscore-plus')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const fs = require('fs-plus')
const path = require('path')
const GitUtils = require('git-utils')
let nextId = 0
@ -241,15 +233,15 @@ class GitRepository {
// * `path` The {String} path to check.
//
// Returns a {Boolean}.
isSubmodule (path) {
if (!path) return false
isSubmodule (filePath) {
if (!filePath) return false
const repo = this.getRepo(path)
if (repo.isSubmodule(repo.relativize(path))) {
const repo = this.getRepo(filePath)
if (repo.isSubmodule(repo.relativize(filePath))) {
return true
} else {
// Check if the path is a working directory in a repo that isn't the root.
return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir'
// Check if the filePath is a working directory in a repo that isn't the root.
return repo !== this.getRepo() && repo.relativize(path.join(filePath, 'dir')) === 'dir'
}
}

View File

@ -128,7 +128,7 @@ class ApplicationMenu
]
focusedWindow: ->
_.find global.atomApplication.windows, (atomWindow) -> atomWindow.isFocused()
_.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Combines a menu template with the appropriate keystroke.
#

View File

@ -67,7 +67,7 @@ class AtomApplication
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
@pidsToOpenWindows = {}
@windows = []
@windowStack = new WindowStack()
@config = new Config({enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)}
@ -114,7 +114,7 @@ class AtomApplication
@launch(options)
destroy: ->
windowsClosePromises = @windows.map (window) ->
windowsClosePromises = @getAllWindows().map (window) ->
window.close()
window.closedPromise
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
@ -162,8 +162,8 @@ class AtomApplication
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windows.splice(@windows.indexOf(window), 1)
if @windows.length is 0
@windowStack.removeWindow(window)
if @getAllWindows().length is 0
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
@ -172,22 +172,28 @@ class AtomApplication
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@windows.push window
@windowStack.addWindow(window)
@applicationMenu?.addWindow(window.browserWindow)
window.once 'window:loaded', =>
@autoUpdateManager?.emitUpdateAvailableEvent(window)
unless window.isSpec
focusHandler = => @lastFocusedWindow = window
focusHandler = => @windowStack.touch(window)
blurHandler = => @saveState(false)
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
@windowStack.removeWindow(window)
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
getAllWindows: =>
@windowStack.all().slice()
getLastFocusedWindow: (predicate) =>
@windowStack.getLastFocusedWindow(predicate)
# Creates server to listen for additional atom application launches.
#
# You can run the atom command multiple times, but after the first launch
@ -276,7 +282,7 @@ class AtomApplication
else
event.preventDefault()
@quitting = true
windowUnloadPromises = @windows.map((window) -> window.prepareToUnload())
windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload())
Promise.all(windowUnloadPromises).then((windowUnloadedResults) ->
didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow)
app.quit() if didUnloadAllWindows
@ -309,7 +315,7 @@ class AtomApplication
event.sender.send('did-resolve-proxy', requestId, proxy)
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @windows
for atomWindow in @getAllWindows()
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
@ -483,7 +489,7 @@ class AtomApplication
# Returns the {AtomWindow} for the given paths.
windowForPaths: (pathsToOpen, devMode) ->
_.find @windows, (atomWindow) ->
_.find @getAllWindows(), (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipcMain event.
@ -491,11 +497,11 @@ class AtomApplication
@atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
atomWindowForBrowserWindow: (browserWindow) ->
@windows.find((atomWindow) -> atomWindow.browserWindow is browserWindow)
@getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow)
# Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow: ->
_.find @windows, (atomWindow) -> atomWindow.isFocused()
_.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform: ->
@ -507,8 +513,8 @@ class AtomApplication
# Get the dimensions for opening a new window by cascading as appropriate to
# the platform.
getDimensionsForNewWindow: ->
return if (@focusedWindow() ? @lastFocusedWindow)?.isMaximized()
dimensions = (@focusedWindow() ? @lastFocusedWindow)?.getDimensions()
return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized()
dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions()
offset = @getWindowOffsetForCurrentPlatform()
if dimensions? and offset?
dimensions.x += offset
@ -554,7 +560,7 @@ class AtomApplication
existingWindow = @windowForPaths(pathsToOpen, devMode)
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
unless existingWindow?
if currentWindow = window ? @lastFocusedWindow
if currentWindow = window ? @getLastFocusedWindow()
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
@ -583,7 +589,7 @@ class AtomApplication
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
openedWindow.focus()
@lastFocusedWindow = openedWindow
@windowStack.addWindow(openedWindow)
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@ -617,9 +623,10 @@ class AtomApplication
saveState: (allowEmpty=false) ->
return if @quitting
states = []
for window in @windows
for window in @getAllWindows()
unless window.isSpec
states.push({initialPaths: window.representedDirectoryPaths})
states.reverse()
if states.length > 0 or allowEmpty
@storageFolder.storeSync('application.json', states)
@emit('application:did-save-state')
@ -648,30 +655,39 @@ class AtomApplication
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
parsedUrl = url.parse(urlToOpen)
parsedUrl = url.parse(urlToOpen, true)
return unless parsedUrl.protocol is "atom:"
pack = @findPackageWithName(parsedUrl.host, devMode)
if pack?.urlMain
@openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env)
else
@openPackageUriHandler(urlToOpen, devMode, safeMode, env)
@openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env)
openPackageUriHandler: (url, devMode, safeMode, env) ->
resourcePath = @resourcePath
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) ->
bestWindow = null
if parsedUrl.host is 'core'
predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
bestWindow = @getLastFocusedWindow (win) ->
not win.isSpecWindow() and predicate(win)
windowInitializationScript ?= require.resolve('../initialize-application-window')
if @lastFocusedWindow?
@lastFocusedWindow.sendURIMessage url
bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow()
if bestWindow?
bestWindow.sendURIMessage url
bestWindow.focus()
else
resourcePath = @resourcePath
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
windowDimensions = @getDimensionsForNewWindow()
@lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
@lastFocusedWindow.on 'window:loaded', =>
@lastFocusedWindow.sendURIMessage url
win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
@windowStack.addWindow(win)
win.on 'window:loaded', ->
win.sendURIMessage url
findPackageWithName: (packageName, devMode) ->
_.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName
@ -867,7 +883,7 @@ class AtomApplication
disableZoomOnDisplayChange: ->
outerCallback = =>
for window in @windows
for window in @getAllWindows()
window.disableZoom()
# Set the limits every time a display is added or removed, otherwise the
@ -878,3 +894,24 @@ class AtomApplication
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
class WindowStack
constructor: (@windows = []) ->
addWindow: (window) =>
@removeWindow(window)
@windows.unshift(window)
touch: (window) =>
@addWindow(window)
removeWindow: (window) =>
currentIndex = @windows.indexOf(window)
@windows.splice(currentIndex, 1) if currentIndex > -1
getLastFocusedWindow: (predicate) =>
predicate ?= (win) -> true
@windows.find(predicate)
all: =>
@windows

View File

@ -138,4 +138,4 @@ class AutoUpdateManager
detail: message
getWindows: ->
global.atomApplication.windows
global.atomApplication.getAllWindows()

View File

@ -341,13 +341,21 @@ class Project extends Model {
}
this.rootDirectories.push(directory)
this.watcherPromisesByPath[directory.getPath()] = watchPath(directory.getPath(), {}, events => {
const didChangeCallback = events => {
// Stop event delivery immediately on removal of a rootDirectory, even if its watcher
// promise has yet to resolve at the time of removal
if (this.rootDirectories.includes(directory)) {
this.emitter.emit('did-change-files', events)
}
})
}
// We'll use the directory's custom onDidChangeFiles callback, if available.
// CustomDirectory::onDidChangeFiles should match the signature of
// Project::onDidChangeFiles below (although it may resolve asynchronously)
this.watcherPromisesByPath[directory.getPath()] =
directory.onDidChangeFiles != null
? Promise.resolve(directory.onDidChangeFiles(didChangeCallback))
: watchPath(directory.getPath(), {}, didChangeCallback)
for (let watchedPath in this.watcherPromisesByPath) {
if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) {

View File

@ -126,7 +126,6 @@ class TextEditorComponent {
this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this))
this.lineComponentsByScreenLineId = new Map()
this.overlayComponents = new Set()
this.overlayDimensionsByElement = new WeakMap()
this.shouldRenderDummyScrollbars = true
this.remeasureScrollbars = false
this.pendingAutoscroll = null
@ -803,15 +802,9 @@ class TextEditorComponent {
{
key: overlayProps.element,
overlayComponents: this.overlayComponents,
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element),
didResize: (overlayComponent) => {
this.updateOverlayToRender(overlayProps)
overlayComponent.update(Object.assign(
{
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element)
},
overlayProps
))
overlayComponent.update(overlayProps)
}
},
overlayProps
@ -1357,7 +1350,6 @@ class TextEditorComponent {
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
const clientRect = element.getBoundingClientRect()
this.overlayDimensionsByElement.set(element, clientRect)
if (avoidOverflow !== false) {
const computedStyle = window.getComputedStyle(element)
@ -4226,17 +4218,26 @@ class OverlayComponent {
this.element.style.zIndex = 4
this.element.style.top = (this.props.pixelTop || 0) + 'px'
this.element.style.left = (this.props.pixelLeft || 0) + 'px'
this.currentContentRect = null
// Synchronous DOM updates in response to resize events might trigger a
// "loop limit exceeded" error. We disconnect the observer before
// potentially mutating the DOM, and then reconnect it on the next tick.
// Note: ResizeObserver calls its callback when .observe is called
this.resizeObserver = new ResizeObserver((entries) => {
const {contentRect} = entries[0]
if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) {
if (
this.currentContentRect &&
(this.currentContentRect.width !== contentRect.width ||
this.currentContentRect.height !== contentRect.height)
) {
this.resizeObserver.disconnect()
this.props.didResize(this)
process.nextTick(() => { this.resizeObserver.observe(this.props.element) })
}
this.currentContentRect = contentRect
})
this.didAttach()
this.props.overlayComponents.add(this)

View File

@ -1,56 +0,0 @@
module.exports =
class TokenIterator
constructor: (@tokenizedBuffer) ->
reset: (@line) ->
@index = null
@startColumn = 0
@endColumn = 0
@scopes = @line.openScopes.map (id) => @tokenizedBuffer.grammar.scopeForId(id)
@scopeStarts = @scopes.slice()
@scopeEnds = []
this
next: ->
{tags} = @line
if @index?
@startColumn = @endColumn
@scopeEnds.length = 0
@scopeStarts.length = 0
@index++
else
@index = 0
while @index < tags.length
tag = tags[@index]
if tag < 0
scope = @tokenizedBuffer.grammar.scopeForId(tag)
if tag % 2 is 0
if @scopeStarts[@scopeStarts.length - 1] is scope
@scopeStarts.pop()
else
@scopeEnds.push(scope)
@scopes.pop()
else
@scopeStarts.push(scope)
@scopes.push(scope)
@index++
else
@endColumn += tag
@text = @line.text.substring(@startColumn, @endColumn)
return true
false
getScopes: -> @scopes
getScopeStarts: -> @scopeStarts
getScopeEnds: -> @scopeEnds
getText: -> @text
getBufferStart: -> @startColumn
getBufferEnd: -> @endColumn

79
src/token-iterator.js Normal file
View File

@ -0,0 +1,79 @@
module.exports =
class TokenIterator {
constructor (tokenizedBuffer) {
this.tokenizedBuffer = tokenizedBuffer
}
reset (line) {
this.line = line
this.index = null
this.startColumn = 0
this.endColumn = 0
this.scopes = this.line.openScopes.map(id => this.tokenizedBuffer.grammar.scopeForId(id))
this.scopeStarts = this.scopes.slice()
this.scopeEnds = []
return this
}
next () {
const {tags} = this.line
if (this.index != null) {
this.startColumn = this.endColumn
this.scopeEnds.length = 0
this.scopeStarts.length = 0
this.index++
} else {
this.index = 0
}
while (this.index < tags.length) {
const tag = tags[this.index]
if (tag < 0) {
const scope = this.tokenizedBuffer.grammar.scopeForId(tag)
if ((tag % 2) === 0) {
if (this.scopeStarts[this.scopeStarts.length - 1] === scope) {
this.scopeStarts.pop()
} else {
this.scopeEnds.push(scope)
}
this.scopes.pop()
} else {
this.scopeStarts.push(scope)
this.scopes.push(scope)
}
this.index++
} else {
this.endColumn += tag
this.text = this.line.text.substring(this.startColumn, this.endColumn)
return true
}
}
return false
}
getScopes () {
return this.scopes
}
getScopeStarts () {
return this.scopeStarts
}
getScopeEnds () {
return this.scopeEnds
}
getText () {
return this.text
}
getBufferStart () {
return this.startColumn
}
getBufferEnd () {
return this.endColumn
}
}