/ src / text-editor-registry.js
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() {}