text-editor-registry.js
1 const _ = require('underscore-plus'); 2 const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); 3 const TextEditor = require('./text-editor'); 4 const ScopeDescriptor = require('./scope-descriptor'); 5 6 const EDITOR_PARAMS_BY_SETTING_KEY = [ 7 ['core.fileEncoding', 'encoding'], 8 ['editor.atomicSoftTabs', 'atomicSoftTabs'], 9 ['editor.showInvisibles', 'showInvisibles'], 10 ['editor.tabLength', 'tabLength'], 11 ['editor.invisibles', 'invisibles'], 12 ['editor.showCursorOnSelection', 'showCursorOnSelection'], 13 ['editor.showIndentGuide', 'showIndentGuide'], 14 ['editor.showLineNumbers', 'showLineNumbers'], 15 ['editor.softWrap', 'softWrapped'], 16 ['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'], 17 ['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'], 18 ['editor.preferredLineLength', 'preferredLineLength'], 19 ['editor.maxScreenLineLength', 'maxScreenLineLength'], 20 ['editor.autoIndent', 'autoIndent'], 21 ['editor.autoIndentOnPaste', 'autoIndentOnPaste'], 22 ['editor.scrollPastEnd', 'scrollPastEnd'], 23 ['editor.undoGroupingInterval', 'undoGroupingInterval'], 24 ['editor.scrollSensitivity', 'scrollSensitivity'] 25 ]; 26 27 // Experimental: This global registry tracks registered `TextEditors`. 28 // 29 // If you want to add functionality to a wider set of text editors than just 30 // those appearing within workspace panes, use `atom.textEditors.observe` to 31 // invoke a callback for all current and future registered text editors. 32 // 33 // If you want packages to be able to add functionality to your non-pane text 34 // editors (such as a search field in a custom user interface element), register 35 // them for observation via `atom.textEditors.add`. **Important:** When you're 36 // done using your editor, be sure to call `dispose` on the returned disposable 37 // to avoid leaking editors. 38 module.exports = class TextEditorRegistry { 39 constructor({ config, assert, packageManager }) { 40 this.config = config; 41 this.assert = assert; 42 this.packageManager = packageManager; 43 this.clear(); 44 } 45 46 deserialize(state) { 47 this.editorGrammarOverrides = state.editorGrammarOverrides; 48 } 49 50 serialize() { 51 return { 52 editorGrammarOverrides: Object.assign({}, this.editorGrammarOverrides) 53 }; 54 } 55 56 clear() { 57 if (this.subscriptions) { 58 this.subscriptions.dispose(); 59 } 60 61 this.subscriptions = new CompositeDisposable(); 62 this.editors = new Set(); 63 this.emitter = new Emitter(); 64 this.scopesWithConfigSubscriptions = new Set(); 65 this.editorsWithMaintainedConfig = new Set(); 66 this.editorsWithMaintainedGrammar = new Set(); 67 this.editorGrammarOverrides = {}; 68 this.editorGrammarScores = new WeakMap(); 69 } 70 71 destroy() { 72 this.subscriptions.dispose(); 73 this.editorsWithMaintainedConfig = null; 74 } 75 76 // Register a `TextEditor`. 77 // 78 // * `editor` The editor to register. 79 // 80 // Returns a {Disposable} on which `.dispose()` can be called to remove the 81 // added editor. To avoid any memory leaks this should be called when the 82 // editor is destroyed. 83 add(editor) { 84 this.editors.add(editor); 85 editor.registered = true; 86 this.emitter.emit('did-add-editor', editor); 87 88 return new Disposable(() => this.remove(editor)); 89 } 90 91 build(params) { 92 params = Object.assign({ assert: this.assert }, params); 93 94 let scope = null; 95 if (params.buffer) { 96 const { grammar } = params.buffer.getLanguageMode(); 97 if (grammar) { 98 scope = new ScopeDescriptor({ scopes: [grammar.scopeName] }); 99 } 100 } 101 102 Object.assign(params, this.textEditorParamsForScope(scope)); 103 104 return new TextEditor(params); 105 } 106 107 // Remove a `TextEditor`. 108 // 109 // * `editor` The editor to remove. 110 // 111 // Returns a {Boolean} indicating whether the editor was successfully removed. 112 remove(editor) { 113 var removed = this.editors.delete(editor); 114 editor.registered = false; 115 return removed; 116 } 117 118 // Invoke the given callback with all the current and future registered 119 // `TextEditors`. 120 // 121 // * `callback` {Function} to be called with current and future text editors. 122 // 123 // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 124 observe(callback) { 125 this.editors.forEach(callback); 126 return this.emitter.on('did-add-editor', callback); 127 } 128 129 // Keep a {TextEditor}'s configuration in sync with Atom's settings. 130 // 131 // * `editor` The editor whose configuration will be maintained. 132 // 133 // Returns a {Disposable} that can be used to stop updating the editor's 134 // configuration. 135 maintainConfig(editor) { 136 if (this.editorsWithMaintainedConfig.has(editor)) { 137 return new Disposable(noop); 138 } 139 this.editorsWithMaintainedConfig.add(editor); 140 141 this.updateAndMonitorEditorSettings(editor); 142 const languageChangeSubscription = editor.buffer.onDidChangeLanguageMode( 143 (newLanguageMode, oldLanguageMode) => { 144 this.updateAndMonitorEditorSettings(editor, oldLanguageMode); 145 } 146 ); 147 this.subscriptions.add(languageChangeSubscription); 148 149 const updateTabTypes = () => { 150 const configOptions = { scope: editor.getRootScopeDescriptor() }; 151 editor.setSoftTabs( 152 shouldEditorUseSoftTabs( 153 editor, 154 this.config.get('editor.tabType', configOptions), 155 this.config.get('editor.softTabs', configOptions) 156 ) 157 ); 158 }; 159 160 updateTabTypes(); 161 const tokenizeSubscription = editor.onDidTokenize(updateTabTypes); 162 this.subscriptions.add(tokenizeSubscription); 163 164 return new Disposable(() => { 165 this.editorsWithMaintainedConfig.delete(editor); 166 tokenizeSubscription.dispose(); 167 languageChangeSubscription.dispose(); 168 this.subscriptions.remove(languageChangeSubscription); 169 this.subscriptions.remove(tokenizeSubscription); 170 }); 171 } 172 173 // Deprecated: set a {TextEditor}'s grammar based on its path and content, 174 // and continue to update its grammar as grammars are added or updated, or 175 // the editor's file path changes. 176 // 177 // * `editor` The editor whose grammar will be maintained. 178 // 179 // Returns a {Disposable} that can be used to stop updating the editor's 180 // grammar. 181 maintainGrammar(editor) { 182 atom.grammars.maintainLanguageMode(editor.getBuffer()); 183 } 184 185 // Deprecated: Force a {TextEditor} to use a different grammar than the 186 // one that would otherwise be selected for it. 187 // 188 // * `editor` The editor whose gramamr will be set. 189 // * `languageId` The {String} language ID for the desired {Grammar}. 190 setGrammarOverride(editor, languageId) { 191 atom.grammars.assignLanguageMode(editor.getBuffer(), languageId); 192 } 193 194 // Deprecated: Retrieve the grammar scope name that has been set as a 195 // grammar override for the given {TextEditor}. 196 // 197 // * `editor` The editor. 198 // 199 // Returns a {String} scope name, or `null` if no override has been set 200 // for the given editor. 201 getGrammarOverride(editor) { 202 return atom.grammars.getAssignedLanguageId(editor.getBuffer()); 203 } 204 205 // Deprecated: Remove any grammar override that has been set for the given {TextEditor}. 206 // 207 // * `editor` The editor. 208 clearGrammarOverride(editor) { 209 atom.grammars.autoAssignLanguageMode(editor.getBuffer()); 210 } 211 212 async updateAndMonitorEditorSettings(editor, oldLanguageMode) { 213 await this.packageManager.getActivatePromise(); 214 this.updateEditorSettingsForLanguageMode(editor, oldLanguageMode); 215 this.subscribeToSettingsForEditorScope(editor); 216 } 217 218 updateEditorSettingsForLanguageMode(editor, oldLanguageMode) { 219 const newLanguageMode = editor.buffer.getLanguageMode(); 220 221 if (oldLanguageMode) { 222 const newSettings = this.textEditorParamsForScope( 223 newLanguageMode.rootScopeDescriptor 224 ); 225 const oldSettings = this.textEditorParamsForScope( 226 oldLanguageMode.rootScopeDescriptor 227 ); 228 229 const updatedSettings = {}; 230 for (const [, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { 231 // Update the setting only if it has changed between the two language 232 // modes. This prevents user-modified settings in an editor (like 233 // 'softWrapped') from being reset when the language mode changes. 234 if (!_.isEqual(newSettings[paramName], oldSettings[paramName])) { 235 updatedSettings[paramName] = newSettings[paramName]; 236 } 237 } 238 239 if (_.size(updatedSettings) > 0) { 240 editor.update(updatedSettings); 241 } 242 } else { 243 editor.update( 244 this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor) 245 ); 246 } 247 } 248 249 subscribeToSettingsForEditorScope(editor) { 250 if (!this.editorsWithMaintainedConfig) return; 251 252 const scopeDescriptor = editor.getRootScopeDescriptor(); 253 const scopeChain = scopeDescriptor.getScopeChain(); 254 255 if (!this.scopesWithConfigSubscriptions.has(scopeChain)) { 256 this.scopesWithConfigSubscriptions.add(scopeChain); 257 const configOptions = { scope: scopeDescriptor }; 258 259 for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { 260 this.subscriptions.add( 261 this.config.onDidChange(settingKey, configOptions, ({ newValue }) => { 262 this.editorsWithMaintainedConfig.forEach(editor => { 263 if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) { 264 editor.update({ [paramName]: newValue }); 265 } 266 }); 267 }) 268 ); 269 } 270 271 const updateTabTypes = () => { 272 const tabType = this.config.get('editor.tabType', configOptions); 273 const softTabs = this.config.get('editor.softTabs', configOptions); 274 this.editorsWithMaintainedConfig.forEach(editor => { 275 if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) { 276 editor.setSoftTabs( 277 shouldEditorUseSoftTabs(editor, tabType, softTabs) 278 ); 279 } 280 }); 281 }; 282 283 this.subscriptions.add( 284 this.config.onDidChange( 285 'editor.tabType', 286 configOptions, 287 updateTabTypes 288 ), 289 this.config.onDidChange( 290 'editor.softTabs', 291 configOptions, 292 updateTabTypes 293 ) 294 ); 295 } 296 } 297 298 textEditorParamsForScope(scopeDescriptor) { 299 const result = {}; 300 const configOptions = { scope: scopeDescriptor }; 301 for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { 302 result[paramName] = this.config.get(settingKey, configOptions); 303 } 304 return result; 305 } 306 }; 307 308 function shouldEditorUseSoftTabs(editor, tabType, softTabs) { 309 switch (tabType) { 310 case 'hard': 311 return false; 312 case 'soft': 313 return true; 314 case 'auto': 315 switch (editor.usesSoftTabs()) { 316 case true: 317 return true; 318 case false: 319 return false; 320 default: 321 return softTabs; 322 } 323 } 324 } 325 326 function noop() {}