mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-19 23:17:16 +03:00
Merge branch 'master' into mb-use-language-mode-api
This commit is contained in:
commit
4ec1d85aad
20
package.json
20
package.json
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
394
spec/git-repository-spec.js
Normal 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
|
||||
}
|
@ -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 () {
|
||||
|
@ -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)', () => {
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
503
spec/theme-manager-spec.js
Normal 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)
|
||||
}
|
||||
}
|
@ -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']
|
43
spec/token-iterator-spec.js
Normal file
43
spec/token-iterator-spec.js
Normal 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'])
|
||||
})
|
||||
)
|
@ -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()
|
||||
|
@ -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
38
src/core-uri-handlers.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -138,4 +138,4 @@ class AutoUpdateManager
|
||||
detail: message
|
||||
|
||||
getWindows: ->
|
||||
global.atomApplication.windows
|
||||
global.atomApplication.getAllWindows()
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
|
@ -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
79
src/token-iterator.js
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user