Files
atom/src/text-editor-registry.js
2017-11-03 00:12:33 -07:00

291 lines
9.7 KiB
JavaScript

/** @babel */
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
import TextEditor from './text-editor'
import ScopeDescriptor from './scope-descriptor'
import Grim from 'grim'
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']
]
// 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, assert, packageManager}) {
this.assert = assert
this.config = config
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()
}
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) {
scope = new ScopeDescriptor({scopes: [params.buffer.getLanguageMode().getGrammar().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)
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)
tokenizeSubscription.dispose()
grammarChangeSubscription.dispose()
this.subscriptions.remove(grammarChangeSubscription)
this.subscriptions.remove(tokenizeSubscription)
})
}
// Deprecated: 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) {
Grim.deprecate('Use atom.grammars.maintainGrammar(buffer) instead.')
atom.grammars.maintainGrammar(editor.getBuffer())
}
// Deprecated: 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) {
Grim.deprecate('Use atom.grammars.setGrammarOverrideForPath(filePath) instead.')
atom.grammars.setGrammarOverrideForPath(editor.getPath(), scopeName)
}
// Deprecated: 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) {
Grim.deprecate('Use atom.grammars.grammarOverrideForPath(filePath) instead.')
return atom.grammars.grammarOverrideForPath(editor.getPath())
}
// Deprecated: Remove any grammar override that has been set for the given {TextEditor}.
//
// * `editor` The editor.
clearGrammarOverride (editor) {
Grim.deprecate('Use atom.grammars.clearGrammarOverrideForPath(filePath) instead.')
atom.grammars.clearGrammarOverrideForPath(editor.getPath())
}
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 () {}