mirror of
https://github.com/atom/atom.git
synced 2026-02-07 13:14:55 -05:00
434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
/** @babel */
|
|
|
|
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
|
|
import {Point, Range} from 'text-buffer'
|
|
import TextEditor from './text-editor'
|
|
import ScopeDescriptor from './scope-descriptor'
|
|
|
|
const EDITOR_PARAMS_BY_SETTING_KEY = [
|
|
['core.fileEncoding', 'encoding'],
|
|
['editor.atomicSoftTabs', 'atomicSoftTabs'],
|
|
['editor.showInvisibles', 'showInvisibles'],
|
|
['editor.tabLength', 'tabLength'],
|
|
['editor.invisibles', 'invisibles'],
|
|
['editor.showCursorOnSelection', 'showCursorOnSelection'],
|
|
['editor.showIndentGuide', 'showIndentGuide'],
|
|
['editor.showLineNumbers', 'showLineNumbers'],
|
|
['editor.softWrap', 'softWrapped'],
|
|
['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'],
|
|
['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'],
|
|
['editor.preferredLineLength', 'preferredLineLength'],
|
|
['editor.maxScreenLineLength', 'maxScreenLineLength'],
|
|
['editor.autoIndent', 'autoIndent'],
|
|
['editor.autoIndentOnPaste', 'autoIndentOnPaste'],
|
|
['editor.scrollPastEnd', 'scrollPastEnd'],
|
|
['editor.undoGroupingInterval', 'undoGroupingInterval'],
|
|
['editor.nonWordCharacters', 'nonWordCharacters'],
|
|
['editor.scrollSensitivity', 'scrollSensitivity']
|
|
]
|
|
|
|
const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
|
|
|
|
// Experimental: This global registry tracks registered `TextEditors`.
|
|
//
|
|
// If you want to add functionality to a wider set of text editors than just
|
|
// those appearing within workspace panes, use `atom.textEditors.observe` to
|
|
// invoke a callback for all current and future registered text editors.
|
|
//
|
|
// If you want packages to be able to add functionality to your non-pane text
|
|
// editors (such as a search field in a custom user interface element), register
|
|
// them for observation via `atom.textEditors.add`. **Important:** When you're
|
|
// done using your editor, be sure to call `dispose` on the returned disposable
|
|
// to avoid leaking editors.
|
|
export default class TextEditorRegistry {
|
|
constructor ({config, grammarRegistry, assert, packageManager}) {
|
|
this.assert = assert
|
|
this.config = config
|
|
this.grammarRegistry = grammarRegistry
|
|
this.scopedSettingsDelegate = new ScopedSettingsDelegate(config)
|
|
this.grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
|
|
this.clear()
|
|
|
|
this.initialPackageActivationPromise = new Promise((resolve) => {
|
|
// TODO: Remove this usage of a private property of PackageManager.
|
|
// Should PackageManager just expose a promise-based API like this?
|
|
if (packageManager.deferredActivationHooks) {
|
|
packageManager.onDidActivateInitialPackages(resolve)
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
deserialize (state) {
|
|
this.editorGrammarOverrides = state.editorGrammarOverrides
|
|
}
|
|
|
|
serialize () {
|
|
return {
|
|
editorGrammarOverrides: Object.assign({}, this.editorGrammarOverrides)
|
|
}
|
|
}
|
|
|
|
clear () {
|
|
if (this.subscriptions) {
|
|
this.subscriptions.dispose()
|
|
}
|
|
|
|
this.subscriptions = new CompositeDisposable()
|
|
this.editors = new Set()
|
|
this.emitter = new Emitter()
|
|
this.scopesWithConfigSubscriptions = new Set()
|
|
this.editorsWithMaintainedConfig = new Set()
|
|
this.editorsWithMaintainedGrammar = new Set()
|
|
this.editorGrammarOverrides = {}
|
|
this.editorGrammarScores = new WeakMap()
|
|
this.subscriptions.add(
|
|
this.grammarRegistry.onDidAddGrammar(this.grammarAddedOrUpdated),
|
|
this.grammarRegistry.onDidUpdateGrammar(this.grammarAddedOrUpdated)
|
|
)
|
|
}
|
|
|
|
destroy () {
|
|
this.subscriptions.dispose()
|
|
this.editorsWithMaintainedConfig = null
|
|
}
|
|
|
|
// Register a `TextEditor`.
|
|
//
|
|
// * `editor` The editor to register.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
|
// added editor. To avoid any memory leaks this should be called when the
|
|
// editor is destroyed.
|
|
add (editor) {
|
|
this.editors.add(editor)
|
|
editor.registered = true
|
|
this.emitter.emit('did-add-editor', editor)
|
|
|
|
return new Disposable(() => this.remove(editor))
|
|
}
|
|
|
|
build (params) {
|
|
params = Object.assign({assert: this.assert}, params)
|
|
|
|
let scope = null
|
|
if (params.buffer) {
|
|
const filePath = params.buffer.getPath()
|
|
const headContent = params.buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
|
|
params.grammar = this.grammarRegistry.selectGrammar(filePath, headContent)
|
|
scope = new ScopeDescriptor({scopes: [params.grammar.scopeName]})
|
|
}
|
|
|
|
Object.assign(params, this.textEditorParamsForScope(scope))
|
|
|
|
return new TextEditor(params)
|
|
}
|
|
|
|
// Remove a `TextEditor`.
|
|
//
|
|
// * `editor` The editor to remove.
|
|
//
|
|
// Returns a {Boolean} indicating whether the editor was successfully removed.
|
|
remove (editor) {
|
|
var removed = this.editors.delete(editor)
|
|
editor.registered = false
|
|
return removed
|
|
}
|
|
|
|
// Invoke the given callback with all the current and future registered
|
|
// `TextEditors`.
|
|
//
|
|
// * `callback` {Function} to be called with current and future text editors.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observe (callback) {
|
|
this.editors.forEach(callback)
|
|
return this.emitter.on('did-add-editor', callback)
|
|
}
|
|
|
|
// Keep a {TextEditor}'s configuration in sync with Atom's settings.
|
|
//
|
|
// * `editor` The editor whose configuration will be maintained.
|
|
//
|
|
// Returns a {Disposable} that can be used to stop updating the editor's
|
|
// configuration.
|
|
maintainConfig (editor) {
|
|
if (this.editorsWithMaintainedConfig.has(editor)) {
|
|
return new Disposable(noop)
|
|
}
|
|
this.editorsWithMaintainedConfig.add(editor)
|
|
|
|
editor.setScopedSettingsDelegate(this.scopedSettingsDelegate)
|
|
|
|
this.subscribeToSettingsForEditorScope(editor)
|
|
const grammarChangeSubscription = editor.onDidChangeGrammar(() => {
|
|
this.subscribeToSettingsForEditorScope(editor)
|
|
})
|
|
this.subscriptions.add(grammarChangeSubscription)
|
|
|
|
const updateTabTypes = () => {
|
|
const configOptions = {scope: editor.getRootScopeDescriptor()}
|
|
editor.setSoftTabs(shouldEditorUseSoftTabs(
|
|
editor,
|
|
this.config.get('editor.tabType', configOptions),
|
|
this.config.get('editor.softTabs', configOptions)
|
|
))
|
|
}
|
|
|
|
updateTabTypes()
|
|
const tokenizeSubscription = editor.onDidTokenize(updateTabTypes)
|
|
this.subscriptions.add(tokenizeSubscription)
|
|
|
|
return new Disposable(() => {
|
|
this.editorsWithMaintainedConfig.delete(editor)
|
|
editor.setScopedSettingsDelegate(null)
|
|
tokenizeSubscription.dispose()
|
|
grammarChangeSubscription.dispose()
|
|
this.subscriptions.remove(grammarChangeSubscription)
|
|
this.subscriptions.remove(tokenizeSubscription)
|
|
})
|
|
}
|
|
|
|
// Set a {TextEditor}'s grammar based on its path and content, and continue
|
|
// to update its grammar as grammars are added or updated, or the editor's
|
|
// file path changes.
|
|
//
|
|
// * `editor` The editor whose grammar will be maintained.
|
|
//
|
|
// Returns a {Disposable} that can be used to stop updating the editor's
|
|
// grammar.
|
|
maintainGrammar (editor) {
|
|
if (this.editorsWithMaintainedGrammar.has(editor)) {
|
|
return new Disposable(noop)
|
|
}
|
|
|
|
this.editorsWithMaintainedGrammar.add(editor)
|
|
|
|
const buffer = editor.getBuffer()
|
|
for (let existingEditor of this.editorsWithMaintainedGrammar) {
|
|
if (existingEditor.getBuffer() === buffer) {
|
|
const existingOverride = this.editorGrammarOverrides[existingEditor.id]
|
|
if (existingOverride) {
|
|
this.editorGrammarOverrides[editor.id] = existingOverride
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
this.selectGrammarForEditor(editor)
|
|
|
|
const pathChangeSubscription = editor.onDidChangePath(() => {
|
|
this.editorGrammarScores.delete(editor)
|
|
this.selectGrammarForEditor(editor)
|
|
})
|
|
|
|
this.subscriptions.add(pathChangeSubscription)
|
|
|
|
return new Disposable(() => {
|
|
delete this.editorGrammarOverrides[editor.id]
|
|
this.editorsWithMaintainedGrammar.delete(editor)
|
|
this.subscriptions.remove(pathChangeSubscription)
|
|
pathChangeSubscription.dispose()
|
|
})
|
|
}
|
|
|
|
// Force a {TextEditor} to use a different grammar than the one that would
|
|
// otherwise be selected for it.
|
|
//
|
|
// * `editor` The editor whose gramamr will be set.
|
|
// * `scopeName` The {String} root scope name for the desired {Grammar}.
|
|
setGrammarOverride (editor, scopeName) {
|
|
this.editorGrammarOverrides[editor.id] = scopeName
|
|
this.editorGrammarScores.delete(editor)
|
|
editor.setGrammar(this.grammarRegistry.grammarForScopeName(scopeName))
|
|
}
|
|
|
|
// Retrieve the grammar scope name that has been set as a grammar override
|
|
// for the given {TextEditor}.
|
|
//
|
|
// * `editor` The editor.
|
|
//
|
|
// Returns a {String} scope name, or `null` if no override has been set
|
|
// for the given editor.
|
|
getGrammarOverride (editor) {
|
|
return this.editorGrammarOverrides[editor.id]
|
|
}
|
|
|
|
// Remove any grammar override that has been set for the given {TextEditor}.
|
|
//
|
|
// * `editor` The editor.
|
|
clearGrammarOverride (editor) {
|
|
delete this.editorGrammarOverrides[editor.id]
|
|
this.selectGrammarForEditor(editor)
|
|
}
|
|
|
|
// Private
|
|
|
|
grammarAddedOrUpdated (grammar) {
|
|
this.editorsWithMaintainedGrammar.forEach((editor) => {
|
|
if (grammar.injectionSelector) {
|
|
if (editor.tokenizedBuffer.hasTokenForSelector(grammar.injectionSelector)) {
|
|
editor.tokenizedBuffer.retokenizeLines()
|
|
}
|
|
return
|
|
}
|
|
|
|
const grammarOverride = this.editorGrammarOverrides[editor.id]
|
|
if (grammarOverride) {
|
|
if (grammar.scopeName === grammarOverride) {
|
|
editor.setGrammar(grammar)
|
|
}
|
|
} else {
|
|
const score = this.grammarRegistry.getGrammarScore(
|
|
grammar,
|
|
editor.getPath(),
|
|
editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
|
|
)
|
|
|
|
let currentScore = this.editorGrammarScores.get(editor)
|
|
if (currentScore == null || score > currentScore) {
|
|
editor.setGrammar(grammar, score)
|
|
this.editorGrammarScores.set(editor, score)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
selectGrammarForEditor (editor) {
|
|
const grammarOverride = this.editorGrammarOverrides[editor.id]
|
|
|
|
if (grammarOverride) {
|
|
const grammar = this.grammarRegistry.grammarForScopeName(grammarOverride)
|
|
editor.setGrammar(grammar)
|
|
return
|
|
}
|
|
|
|
const {grammar, score} = this.grammarRegistry.selectGrammarWithScore(
|
|
editor.getPath(),
|
|
editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE)
|
|
)
|
|
|
|
if (!grammar) {
|
|
throw new Error(`No grammar found for path: ${editor.getPath()}`)
|
|
}
|
|
|
|
const currentScore = this.editorGrammarScores.get(editor)
|
|
if (currentScore == null || score > currentScore) {
|
|
editor.setGrammar(grammar)
|
|
this.editorGrammarScores.set(editor, score)
|
|
}
|
|
}
|
|
|
|
async subscribeToSettingsForEditorScope (editor) {
|
|
await this.initialPackageActivationPromise
|
|
|
|
const scopeDescriptor = editor.getRootScopeDescriptor()
|
|
const scopeChain = scopeDescriptor.getScopeChain()
|
|
|
|
editor.update(this.textEditorParamsForScope(scopeDescriptor))
|
|
|
|
if (!this.scopesWithConfigSubscriptions.has(scopeChain)) {
|
|
this.scopesWithConfigSubscriptions.add(scopeChain)
|
|
const configOptions = {scope: scopeDescriptor}
|
|
|
|
for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) {
|
|
this.subscriptions.add(
|
|
this.config.onDidChange(settingKey, configOptions, ({newValue}) => {
|
|
this.editorsWithMaintainedConfig.forEach((editor) => {
|
|
if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) {
|
|
editor.update({[paramName]: newValue})
|
|
}
|
|
})
|
|
})
|
|
)
|
|
}
|
|
|
|
const updateTabTypes = () => {
|
|
const tabType = this.config.get('editor.tabType', configOptions)
|
|
const softTabs = this.config.get('editor.softTabs', configOptions)
|
|
this.editorsWithMaintainedConfig.forEach((editor) => {
|
|
if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) {
|
|
editor.setSoftTabs(shouldEditorUseSoftTabs(editor, tabType, softTabs))
|
|
}
|
|
})
|
|
}
|
|
|
|
this.subscriptions.add(
|
|
this.config.onDidChange('editor.tabType', configOptions, updateTabTypes),
|
|
this.config.onDidChange('editor.softTabs', configOptions, updateTabTypes)
|
|
)
|
|
}
|
|
}
|
|
|
|
textEditorParamsForScope (scopeDescriptor) {
|
|
const result = {}
|
|
const configOptions = {scope: scopeDescriptor}
|
|
for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) {
|
|
result[paramName] = this.config.get(settingKey, configOptions)
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
function shouldEditorUseSoftTabs (editor, tabType, softTabs) {
|
|
switch (tabType) {
|
|
case 'hard':
|
|
return false
|
|
case 'soft':
|
|
return true
|
|
case 'auto':
|
|
switch (editor.usesSoftTabs()) {
|
|
case true:
|
|
return true
|
|
case false:
|
|
return false
|
|
default:
|
|
return softTabs
|
|
}
|
|
}
|
|
}
|
|
|
|
function noop () {}
|
|
|
|
class ScopedSettingsDelegate {
|
|
constructor (config) {
|
|
this.config = config
|
|
}
|
|
|
|
getNonWordCharacters (scope) {
|
|
return this.config.get('editor.nonWordCharacters', {scope: scope})
|
|
}
|
|
|
|
getIncreaseIndentPattern (scope) {
|
|
return this.config.get('editor.increaseIndentPattern', {scope: scope})
|
|
}
|
|
|
|
getDecreaseIndentPattern (scope) {
|
|
return this.config.get('editor.decreaseIndentPattern', {scope: scope})
|
|
}
|
|
|
|
getDecreaseNextIndentPattern (scope) {
|
|
return this.config.get('editor.decreaseNextIndentPattern', {scope: scope})
|
|
}
|
|
|
|
getFoldEndPattern (scope) {
|
|
return this.config.get('editor.foldEndPattern', {scope: scope})
|
|
}
|
|
|
|
getCommentStrings (scope) {
|
|
const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
|
|
const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
|
|
const commentStartEntry = commentStartEntries[0]
|
|
const commentEndEntry = commentEndEntries.find((entry) => {
|
|
return entry.scopeSelector === commentStartEntry.scopeSelector
|
|
})
|
|
return {
|
|
commentStartString: commentStartEntry && commentStartEntry.value,
|
|
commentEndString: commentEndEntry && commentEndEntry.value
|
|
}
|
|
}
|
|
}
|
|
|
|
TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate
|