pulsar/spec/text-editor-registry-spec.js
David Wilson dbd4a0a4c0 Preserve TextEditor settings when language mode changes
This change fixes #13829 which reports that the `softWrapped` setting of
an untitled TextEditor is lost when the buffer is saved to a file.  This
is caused by logic that updates TextEditor settings when the buffer's
language mode changes.

The fix is to preserve any TextEditor settings that would not change when
switching between the previous and current language mode of the buffer.
2018-01-12 13:51:35 -08:00

604 lines
21 KiB
JavaScript

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
}