/** @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 () {}