const TextEditorRegistry = require('../src/text-editor-registry') const TextEditor = require('../src/text-editor') const TextBuffer = require('text-buffer') const {it, fit, ffit, fffit} = require('./async-spec-helpers') const dedent = require('dedent') describe('TextEditorRegistry', function () { let registry, editor, initialPackageActivation beforeEach(function () { initialPackageActivation = Promise.resolve() registry = new TextEditorRegistry({ assert: atom.assert, config: atom.config, grammarRegistry: atom.grammars, packageManager: {deferredActivationHooks: null} }) editor = new TextEditor({autoHeight: false}) expect(atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar')).toBe(true) }) afterEach(function () { registry.destroy() }) describe('.add', function () { it('adds an editor to the list of registered editors', function () { registry.add(editor) expect(editor.registered).toBe(true) expect(registry.editors.size).toBe(1) expect(registry.editors.has(editor)).toBe(true) }) it('returns a Disposable that can unregister the editor', function () { const disposable = registry.add(editor) expect(registry.editors.size).toBe(1) disposable.dispose() expect(registry.editors.size).toBe(0) expect(editor.registered).toBe(false) expect(retainedEditorCount(registry)).toBe(0) }) }) describe('.observe', function () { it('calls the callback for current and future editors until unsubscribed', function () { const spy = jasmine.createSpy() const [editor1, editor2, editor3] = [{}, {}, {}] registry.add(editor1) const subscription = registry.observe(spy) expect(spy.calls.length).toBe(1) registry.add(editor2) expect(spy.calls.length).toBe(2) expect(spy.argsForCall[0][0]).toBe(editor1) expect(spy.argsForCall[1][0]).toBe(editor2) subscription.dispose() registry.add(editor3) expect(spy.calls.length).toBe(2) }) }) describe('.build', function () { it('constructs a TextEditor with the right parameters based on its path and text', async function () { await atom.packages.activatePackage('language-javascript') await atom.packages.activatePackage('language-c') atom.config.set('editor.tabLength', 8, {scope: '.source.js'}) const editor = registry.build({buffer: new TextBuffer({filePath: 'test.js'})}) expect(editor.getTabLength()).toBe(8) }) }) describe('.maintainConfig(editor)', function () { it('does not update the editor when config settings change for unrelated scope selectors', async function () { await atom.packages.activatePackage('language-javascript') const editor2 = new TextEditor() atom.grammars.assignLanguageMode(editor2, 'source.js') registry.maintainConfig(editor) registry.maintainConfig(editor2) await initialPackageActivation expect(editor.getRootScopeDescriptor().getScopesArray()).toEqual(['text.plain.null-grammar']) expect(editor2.getRootScopeDescriptor().getScopesArray()).toEqual(['source.js']) expect(editor.getEncoding()).toBe('utf8') expect(editor2.getEncoding()).toBe('utf8') atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.text.plain.null-grammar'}) atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.source.js'}) expect(editor.getEncoding()).toBe('utf16le') expect(editor2.getEncoding()).toBe('utf16be') }) it('does not update the editor before the initial packages have loaded', async function () { let didActivateInitialPackagesCallback registry = new TextEditorRegistry({ assert: atom.assert, config: atom.config, grammarRegistry: atom.grammars, packageManager: { deferredActivationHooks: [], onDidActivateInitialPackages (callback) { didActivateInitialPackagesCallback = callback } } }) atom.config.set('core.fileEncoding', 'utf16le') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getEncoding()).toBe('utf8') atom.config.set('core.fileEncoding', 'utf16be') await initialPackageActivation expect(editor.getEncoding()).toBe('utf8') didActivateInitialPackagesCallback() await initialPackageActivation expect(editor.getEncoding()).toBe('utf16be') }) it('updates the editor\'s settings when its grammar changes', async function () { await atom.packages.activatePackage('language-javascript') registry.maintainConfig(editor) await initialPackageActivation atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.source.js'}) expect(editor.getEncoding()).toBe('utf8') atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.source.js'}) expect(editor.getEncoding()).toBe('utf8') atom.grammars.assignLanguageMode(editor, 'source.js') await initialPackageActivation expect(editor.getEncoding()).toBe('utf16le') atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.source.js'}) expect(editor.getEncoding()).toBe('utf16be') atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar') await initialPackageActivation expect(editor.getEncoding()).toBe('utf8') }) it('preserves editor settings that haven\'t changed between previous and current language modes', async function () { await atom.packages.activatePackage('language-javascript') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getEncoding()).toBe('utf8') editor.setEncoding('utf16le') expect(editor.getEncoding()).toBe('utf16le') expect(editor.isSoftWrapped()).toBe(false) editor.setSoftWrapped(true) expect(editor.isSoftWrapped()).toBe(true) atom.grammars.assignLanguageMode(editor, 'source.js') await initialPackageActivation expect(editor.getEncoding()).toBe('utf16le') expect(editor.isSoftWrapped()).toBe(true) }) it('updates editor settings that have changed between previous and current language modes', async function () { await atom.packages.activatePackage('language-javascript') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getEncoding()).toBe('utf8') atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.text.plain.null-grammar'}) atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.source.js'}) expect(editor.getEncoding()).toBe('utf16be') editor.setEncoding('utf8') expect(editor.getEncoding()).toBe('utf8') atom.grammars.assignLanguageMode(editor, 'source.js') await initialPackageActivation expect(editor.getEncoding()).toBe('utf16le') }) it('returns a disposable that can be used to stop the registry from updating the editor\'s config', async function () { await atom.packages.activatePackage('language-javascript') const previousSubscriptionCount = getSubscriptionCount(editor) const disposable = registry.maintainConfig(editor) await initialPackageActivation expect(getSubscriptionCount(editor)).toBeGreaterThan(previousSubscriptionCount) expect(registry.editorsWithMaintainedConfig.size).toBe(1) atom.config.set('core.fileEncoding', 'utf16be') expect(editor.getEncoding()).toBe('utf16be') atom.config.set('core.fileEncoding', 'utf8') expect(editor.getEncoding()).toBe('utf8') disposable.dispose() atom.config.set('core.fileEncoding', 'utf16be') expect(editor.getEncoding()).toBe('utf8') expect(getSubscriptionCount(editor)).toBe(previousSubscriptionCount) expect(retainedEditorCount(registry)).toBe(0) }) it('sets the encoding based on the config', async function () { editor.update({encoding: 'utf8'}) expect(editor.getEncoding()).toBe('utf8') atom.config.set('core.fileEncoding', 'utf16le') registry.maintainConfig(editor) await initialPackageActivation 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', async function () { editor.update({tabLength: 4}) expect(editor.getTabLength()).toBe(4) atom.config.set('editor.tabLength', 8) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getTabLength()).toBe(8) atom.config.set('editor.tabLength', 4) expect(editor.getTabLength()).toBe(4) }) it('enables soft tabs when the tabType config setting is "soft"', async function () { atom.config.set('editor.tabType', 'soft') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftTabs()).toBe(true) }) it('disables soft tabs when the tabType config setting is "hard"', async function () { atom.config.set('editor.tabType', 'hard') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftTabs()).toBe(false) }) describe('when the "tabType" config setting is "auto"', function () { it('enables or disables soft tabs based on the editor\'s content', async function () { await atom.packages.activatePackage('language-javascript') atom.grammars.assignLanguageMode(editor, 'source.js') atom.config.set('editor.tabType', 'auto') registry.maintainConfig(editor) await initialPackageActivation editor.setText(dedent` { hello; } `) editor.getBuffer().getLanguageMode().retokenizeLines() expect(editor.getSoftTabs()).toBe(true) editor.setText(dedent` { hello; } `) editor.getBuffer().getLanguageMode().retokenizeLines() expect(editor.getSoftTabs()).toBe(false) editor.setText(dedent` /* * Comment with a leading space. */ { ${'\t'}hello; } ` + editor.getText()) editor.getBuffer().getLanguageMode().retokenizeLines() expect(editor.getSoftTabs()).toBe(false) editor.setText(dedent` /* * Comment with a leading space. */ { hello; } `) editor.getBuffer().getLanguageMode().retokenizeLines() expect(editor.getSoftTabs()).toBe(false) editor.setText(dedent` /* * Comment with a leading space. */ { hello; } `) editor.getBuffer().getLanguageMode().retokenizeLines() expect(editor.getSoftTabs()).toBe(true) }) }) describe('when the "tabType" config setting is "auto"', function () { it('enables or disables soft tabs based on the "softTabs" config setting', async function () { registry.maintainConfig(editor) await initialPackageActivation editor.setText('abc\ndef') atom.config.set('editor.softTabs', true) atom.config.set('editor.tabType', 'auto') expect(editor.getSoftTabs()).toBe(true) atom.config.set('editor.softTabs', false) expect(editor.getSoftTabs()).toBe(false) }) }) it('enables or disables soft tabs based on the config', async function () { editor.update({softTabs: true}) expect(editor.getSoftTabs()).toBe(true) atom.config.set('editor.tabType', 'hard') registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftTabs()).toBe(false) atom.config.set('editor.tabType', 'soft') expect(editor.getSoftTabs()).toBe(true) atom.config.set('editor.tabType', 'auto') atom.config.set('editor.softTabs', true) expect(editor.getSoftTabs()).toBe(true) }) it('enables or disables atomic soft tabs based on the config', async function () { editor.update({atomicSoftTabs: true}) expect(editor.hasAtomicSoftTabs()).toBe(true) atom.config.set('editor.atomicSoftTabs', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.hasAtomicSoftTabs()).toBe(false) atom.config.set('editor.atomicSoftTabs', true) expect(editor.hasAtomicSoftTabs()).toBe(true) }) it('enables or disables cursor on selection visibility based on the config', async function () { editor.update({showCursorOnSelection: true}) expect(editor.getShowCursorOnSelection()).toBe(true) atom.config.set('editor.showCursorOnSelection', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getShowCursorOnSelection()).toBe(false) atom.config.set('editor.showCursorOnSelection', true) expect(editor.getShowCursorOnSelection()).toBe(true) }) it('enables or disables line numbers based on the config', async function () { editor.update({showLineNumbers: true}) expect(editor.showLineNumbers).toBe(true) atom.config.set('editor.showLineNumbers', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.showLineNumbers).toBe(false) atom.config.set('editor.showLineNumbers', true) expect(editor.showLineNumbers).toBe(true) }) it('sets the invisibles based on the config', async function () { const invisibles1 = {'tab': 'a', 'cr': false, eol: false, space: false} const invisibles2 = {'tab': 'b', 'cr': false, eol: false, space: false} editor.update({ showInvisibles: true, invisibles: invisibles1 }) expect(editor.getInvisibles()).toEqual(invisibles1) atom.config.set('editor.showInvisibles', true) atom.config.set('editor.invisibles', invisibles2) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getInvisibles()).toEqual(invisibles2) atom.config.set('editor.invisibles', invisibles1) expect(editor.getInvisibles()).toEqual(invisibles1) atom.config.set('editor.showInvisibles', false) expect(editor.getInvisibles()).toEqual({}) }) it('enables or disables the indent guide based on the config', async function () { editor.update({showIndentGuide: true}) expect(editor.doesShowIndentGuide()).toBe(true) atom.config.set('editor.showIndentGuide', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.doesShowIndentGuide()).toBe(false) atom.config.set('editor.showIndentGuide', true) expect(editor.doesShowIndentGuide()).toBe(true) }) it('enables or disables soft wrap based on the config', async function () { editor.update({softWrapped: true}) expect(editor.isSoftWrapped()).toBe(true) atom.config.set('editor.softWrap', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.isSoftWrapped()).toBe(false) atom.config.set('editor.softWrap', true) expect(editor.isSoftWrapped()).toBe(true) }) it('sets the soft wrap indent length based on the config', async function () { editor.update({softWrapHangingIndentLength: 4}) expect(editor.getSoftWrapHangingIndentLength()).toBe(4) atom.config.set('editor.softWrapHangingIndent', 2) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftWrapHangingIndentLength()).toBe(2) atom.config.set('editor.softWrapHangingIndent', 4) expect(editor.getSoftWrapHangingIndentLength()).toBe(4) }) it('enables or disables preferred line length-based soft wrap based on the config', async function () { editor.update({ softWrapped: true, preferredLineLength: 80, editorWidthInChars: 120, softWrapAtPreferredLineLength: true, }) expect(editor.getSoftWrapColumn()).toBe(80) atom.config.set('editor.softWrap', true) atom.config.set('editor.softWrapAtPreferredLineLength', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftWrapColumn()).toBe(120) atom.config.set('editor.softWrapAtPreferredLineLength', true) expect(editor.getSoftWrapColumn()).toBe(80) }) it('allows for custom definition of maximum soft wrap based on config', async function () { editor.update({ softWrapped: false, maxScreenLineLength: 1500, }) expect(editor.getSoftWrapColumn()).toBe(1500) atom.config.set('editor.softWrap', false) atom.config.set('editor.maxScreenLineLength', 500) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getSoftWrapColumn()).toBe(500) }) it('sets the preferred line length based on the config', async function () { editor.update({preferredLineLength: 80}) expect(editor.getPreferredLineLength()).toBe(80) atom.config.set('editor.preferredLineLength', 110) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getPreferredLineLength()).toBe(110) atom.config.set('editor.preferredLineLength', 80) expect(editor.getPreferredLineLength()).toBe(80) }) it('enables or disables auto-indent based on the config', async function () { editor.update({autoIndent: true}) expect(editor.shouldAutoIndent()).toBe(true) atom.config.set('editor.autoIndent', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.shouldAutoIndent()).toBe(false) atom.config.set('editor.autoIndent', true) expect(editor.shouldAutoIndent()).toBe(true) }) it('enables or disables auto-indent-on-paste based on the config', async function () { editor.update({autoIndentOnPaste: true}) expect(editor.shouldAutoIndentOnPaste()).toBe(true) atom.config.set('editor.autoIndentOnPaste', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.shouldAutoIndentOnPaste()).toBe(false) atom.config.set('editor.autoIndentOnPaste', true) expect(editor.shouldAutoIndentOnPaste()).toBe(true) }) it('enables or disables scrolling past the end of the buffer based on the config', async function () { editor.update({scrollPastEnd: true}) expect(editor.getScrollPastEnd()).toBe(true) atom.config.set('editor.scrollPastEnd', false) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getScrollPastEnd()).toBe(false) atom.config.set('editor.scrollPastEnd', true) expect(editor.getScrollPastEnd()).toBe(true) }) it('sets the undo grouping interval based on the config', async function () { editor.update({undoGroupingInterval: 300}) expect(editor.getUndoGroupingInterval()).toBe(300) atom.config.set('editor.undoGroupingInterval', 600) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getUndoGroupingInterval()).toBe(600) atom.config.set('editor.undoGroupingInterval', 300) expect(editor.getUndoGroupingInterval()).toBe(300) }) it('sets the scroll sensitivity based on the config', async function () { editor.update({scrollSensitivity: 50}) expect(editor.getScrollSensitivity()).toBe(50) atom.config.set('editor.scrollSensitivity', 60) registry.maintainConfig(editor) await initialPackageActivation expect(editor.getScrollSensitivity()).toBe(60) atom.config.set('editor.scrollSensitivity', 70) expect(editor.getScrollSensitivity()).toBe(70) }) describe('when called twice with a given editor', function () { it('does nothing the second time', async function () { editor.update({scrollSensitivity: 50}) const disposable1 = registry.maintainConfig(editor) const disposable2 = registry.maintainConfig(editor) await initialPackageActivation atom.config.set('editor.scrollSensitivity', 60) expect(editor.getScrollSensitivity()).toBe(60) disposable2.dispose() atom.config.set('editor.scrollSensitivity', 70) expect(editor.getScrollSensitivity()).toBe(70) disposable1.dispose() atom.config.set('editor.scrollSensitivity', 80) expect(editor.getScrollSensitivity()).toBe(70) }) }) }) }) function getSubscriptionCount (editor) { return editor.emitter.getTotalListenerCount() + editor.tokenizedBuffer.emitter.getTotalListenerCount() + editor.buffer.emitter.getTotalListenerCount() + editor.displayLayer.emitter.getTotalListenerCount() } function retainedEditorCount (registry) { const editors = new Set() registry.editors.forEach(e => editors.add(e)) registry.editorsWithMaintainedConfig.forEach(e => editors.add(e)) registry.editorsWithMaintainedGrammar.forEach(e => editors.add(e)) return editors.size }