diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 80f81a02b..e666d7aab 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -1,17 +1,31 @@ /** @babel */ +import {TextBuffer} from 'atom' import TextEditorRegistry from '../src/text-editor-registry' +import TextEditor from '../src/text-editor' describe('TextEditorRegistry', function () { let registry, editor beforeEach(function () { - registry = new TextEditorRegistry() + registry = new TextEditorRegistry({ + config: atom.config + }) + + editor = new TextEditor({ + buffer: new TextBuffer({filePath: 'test.js'}), + config: atom.config, + clipboard: atom.clipboard, + grammarRegistry: atom.grammars + }) + }) + + afterEach(function () { + registry.destroy() }) describe('.add', function () { it('adds an editor to the list of registered editors', function () { - editor = {} registry.add(editor) expect(editor.registered).toBe(true) expect(registry.editors.size).toBe(1) @@ -19,7 +33,6 @@ describe('TextEditorRegistry', function () { }) it('returns a Disposable that can unregister the editor', function () { - editor = {} const disposable = registry.add(editor) expect(registry.editors.size).toBe(1) disposable.dispose() @@ -46,4 +59,92 @@ describe('TextEditorRegistry', function () { expect(spy.calls.length).toBe(2) }) }) + + describe('.maintainGrammar', function () { + it('assigns a grammar to the editor based on its path', async function () { + await atom.packages.activatePackage('language-javascript') + await atom.packages.activatePackage('language-c') + + registry.maintainGrammar(editor) + expect(editor.getGrammar().name).toBe('JavaScript') + + editor.getBuffer().setPath('test.c') + expect(editor.getGrammar().name).toBe('C') + }) + + it('updates the editor\'s grammar when a more appropriate grammar is added for its path', async function () { + expect(editor.getGrammar().name).toBe('Null Grammar') + registry.maintainGrammar(editor) + await atom.packages.activatePackage('language-javascript') + expect(editor.getGrammar().name).toBe('JavaScript') + }); + }) + + describe('.maintainConfig(editor)', function () { + it('sets the encoding based on the config', function () { + editor.setEncoding('utf8') + expect(editor.getEncoding()).toBe('utf8') + + atom.config.set('core.fileEncoding', 'utf16le') + registry.maintainConfig(editor) + expect(editor.getEncoding()).toBe('utf16le') + + atom.config.set('core.fileEncoding', 'utf8') + expect(editor.getEncoding()).toBe('utf8') + }); + + it('sets the tab length based on the config', function () { + editor.setTabLength(4) + expect(editor.getTabLength()).toBe(4) + + atom.config.set('editor.tabLength', 8) + registry.maintainConfig(editor) + expect(editor.getTabLength()).toBe(8) + + atom.config.set('editor.tabLength', 4) + expect(editor.getTabLength()).toBe(4) + }); + + it('enables or disables atomic soft tabs based on the config', function () { + editor.setAtomicSoftTabs(true) + expect(editor.hasAtomicSoftTabs()).toBe(true) + + atom.config.set('editor.atomicSoftTabs', false) + registry.maintainConfig(editor) + expect(editor.hasAtomicSoftTabs()).toBe(false) + + atom.config.set('editor.atomicSoftTabs', true) + expect(editor.hasAtomicSoftTabs()).toBe(true) + }); + + it('enables or disables invisible based on the config', function () { + editor.setShowInvisibles(true) + expect(editor.doesShowInvisibles()).toBe(true) + + atom.config.set('editor.showInvisibles', false) + registry.maintainConfig(editor) + expect(editor.doesShowInvisibles()).toBe(false) + + atom.config.set('editor.showInvisibles', true) + expect(editor.doesShowInvisibles()).toBe(true) + }); + + it('sets the invisibles based on the config', function () { + editor.setShowInvisibles(true) + atom.config.set('editor.showInvisibles', true) + + const invisibles1 = {'tab': 'a', 'cr': false, eol: false, space: false} + const invisibles2 = {'tab': 'b', 'cr': false, eol: false, space: false} + + editor.setInvisibles(invisibles1) + expect(editor.getInvisibles()).toEqual(invisibles1) + + atom.config.set('editor.invisibles', invisibles2) + registry.maintainConfig(editor) + expect(editor.getInvisibles()).toEqual(invisibles2) + + atom.config.set('editor.invisibles', invisibles1) + expect(editor.getInvisibles()).toEqual(invisibles1) + }); + }) }) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 5247ceb97..14dfdc77a 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -195,7 +195,7 @@ class AtomEnvironment extends Model }) @themes.workspace = @workspace - @textEditors = new TextEditorRegistry + @textEditors = new TextEditorRegistry({@config}) @autoUpdater = new AutoUpdateManager({@applicationDelegate}) @config.load() diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index c9ce3cb35..b00743d9e 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -1,6 +1,14 @@ /** @babel */ -import {Emitter, Disposable} from "event-kit" +import {Emitter, Disposable, CompositeDisposable} from "event-kit" + +const EDITOR_SETTER_NAMES_BY_SETTING_KEY = [ + ['core.fileEncoding', 'setEncoding'], + ['editor.atomicSoftTabs', 'setAtomicSoftTabs'], + ['editor.showInvisibles', 'setShowInvisibles'], + ['editor.tabLength', 'setTabLength'], + ['editor.invisibles', 'setInvisibles'], +] // Experimental: This global registry tracks registered `TextEditors`. // @@ -14,9 +22,18 @@ import {Emitter, Disposable} from "event-kit" // done using your editor, be sure to call `dispose` on the returned disposable // to avoid leaking editors. export default class TextEditorRegistry { - constructor () { + constructor ({config}) { + this.config = config + this.subscriptions = new CompositeDisposable() this.editors = new Set() this.emitter = new Emitter() + this.scopesWithConfigSubscriptions = new Set() + this.editorsWithMaintainedConfig = new Set() + } + + destroy () { + this.subscriptions.dispose() + this.editorsWithMaintainedConfig = null } // Register a `TextEditor`. @@ -55,4 +72,40 @@ export default class TextEditorRegistry { this.editors.forEach(callback) return this.emitter.on("did-add-editor", callback) } + + maintainGrammar (editor) { + + } + + maintainConfig (editor) { + this.editorsWithMaintainedConfig.add(editor) + this.subscribeToSettingsForEditorScope(editor) + + const configOptions = {scope: editor.getRootScopeDescriptor()} + for (const [settingKey, setterName] of EDITOR_SETTER_NAMES_BY_SETTING_KEY) { + editor[setterName](atom.config.get(settingKey, configOptions)) + } + } + + subscribeToSettingsForEditorScope (editor) { + const scopeDescriptor = editor.getRootScopeDescriptor() + const scopeChain = scopeDescriptor.getScopeChain() + + if (!this.scopesWithConfigSubscriptions.has(scopeChain)) { + this.scopesWithConfigSubscriptions.add(scopeChain) + + const configOptions = {scope: scopeDescriptor} + for (const [settingKey, setterName] of EDITOR_SETTER_NAMES_BY_SETTING_KEY) { + this.subscriptions.add( + this.config.onDidChange(settingKey, configOptions, ({newValue}) => { + this.editorsWithMaintainedConfig.forEach(editor => { + if (editor.getRootScopeDescriptor().getScopeChain() === scopeChain) { + editor[setterName](newValue) + } + }) + }) + ) + } + } + } } diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 1a3f83232..0a3d18fab 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -77,6 +77,8 @@ class TextEditor extends Model height: null width: null registered: false + atomicSoftTabs: true + invisibles: null Object.defineProperty @prototype, "element", get: -> @getElement() @@ -176,8 +178,6 @@ class TextEditor extends Model @languageMode = new LanguageMode(this, @config) - @setEncoding(@config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) - @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter name: 'line-number' @@ -231,10 +231,7 @@ class TextEditor extends Model @scopedConfigSubscriptions = subscriptions = new CompositeDisposable scopeDescriptor = @getRootScopeDescriptor() - subscriptions.add @config.onDidChange 'editor.atomicSoftTabs', scope: scopeDescriptor, @resetDisplayLayer.bind(this) subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) - subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this) subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this) subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this) subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this) @@ -2704,17 +2701,24 @@ class TextEditor extends Model # * `softTabs` A {Boolean} setSoftTabs: (@softTabs) -> @softTabs + # Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. + hasAtomicSoftTabs: -> @atomicSoftTabs + + # Enable or disable atomic soft tabs for this editor. + # + # * `atomicSoftTabs` A {Boolean} + setAtomicSoftTabs: (atomicSoftTabs) -> + return if atomicSoftTabs is @atomicSoftTabs + @atomicSoftTabs = atomicSoftTabs + @resetDisplayLayer() + # Essential: Toggle soft tabs for this editor toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) # Essential: Get the on-screen length of tab characters. # # Returns a {Number}. - getTabLength: -> - if @tabLength? - @tabLength - else - @config.get('editor.tabLength', scope: @getRootScopeDescriptor()) + getTabLength: -> @tabLength # Essential: Set the on-screen length of tab characters. Setting this to a # {Number} This will override the `editor.tabLength` setting. @@ -2728,19 +2732,35 @@ class TextEditor extends Model @tokenizedBuffer.setTabLength(@tabLength) @resetDisplayLayer() - setIgnoreInvisibles: (ignoreInvisibles) -> - return if ignoreInvisibles is @ignoreInvisibles + # Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. + doesShowInvisibles: -> @showInvisibles - @ignoreInvisibles = ignoreInvisibles + # Enable or disable invisible character substitution for this editor. + # + # * `showInvisibles` A {Boolean} + setShowInvisibles: (showInvisibles) -> + return if showInvisibles is @showInvisibles + @showInvisibles = showInvisibles @resetDisplayLayer() + # Returns an {Object} representing the current invisible character + # substitutions for this editor. See {::setInvisibles}. getInvisibles: -> - scopeDescriptor = @getRootScopeDescriptor() - if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles and @showInvisibles - @config.get('editor.invisibles', scope: scopeDescriptor) + if not @mini and @showInvisibles and @invisibles? + @invisibles else {} + # Set the invisible character substitutions for this editor. + # + # * `invisibles` An {Object} whose keys are names of invisible characters + # and whose values are 1-character {Strings}s to display in place of those + # invisble characters + setInvisibles: (invisibles) -> + return if invisibles is @invisibles + @invisibles = invisibles + @resetDisplayLayer() + # Extended: Determine if the buffer uses hard or soft tabs. # # Returns `true` if the first non-comment line with leading whitespace starts