diff --git a/package.json b/package.json index ac17137eb..40cb1008f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "git-utils": "^2.1.5", "grim": "0.12.0", "guid": "0.0.10", + "jasmine-json": "~0.0", "jasmine-tagged": "^1.1.2", "less-cache": "0.15.0", "mixto": "^1", @@ -50,7 +51,7 @@ "reactionary-atom-fork": "^1.0.0", "runas": "1.0.1", "scandal": "1.0.2", - "scoped-property-store": "^0.11.0", + "scoped-property-store": "^0.12.0", "scrollbar-style": "^1.0.2", "season": "^1.0.2", "semver": "1.1.4", diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index c42add20c..8f6dad245 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -200,7 +200,7 @@ describe "Config", -> expect(CSON.writeFileSync.argsForCall[0][0]).toBe(path.join(atom.config.configDirPath, "atom.config.json")) writtenConfig = CSON.writeFileSync.argsForCall[0][1] - expect(writtenConfig).toBe atom.config.settings + expect(writtenConfig).toEqual global: atom.config.settings describe "when ~/.atom/config.json doesn't exist", -> it "writes any non-default properties to ~/.atom/config.cson", -> @@ -214,9 +214,29 @@ describe "Config", -> atom.config.save() expect(CSON.writeFileSync.argsForCall[0][0]).toBe(path.join(atom.config.configDirPath, "atom.config.cson")) - CoffeeScript = require 'coffee-script' writtenConfig = CSON.writeFileSync.argsForCall[0][1] - expect(writtenConfig).toEqual atom.config.settings + expect(writtenConfig).toEqual global: atom.config.settings + + describe "when scoped settings are defined", -> + it 'writes out explicitly set config settings', -> + atom.config.set('.source.ruby', 'foo.bar', 'ruby') + atom.config.set('.source.ruby', 'foo.omg', 'wow') + atom.config.set('.source.coffee', 'foo.bar', 'coffee') + + CSON.writeFileSync.reset() + atom.config.save() + + writtenConfig = CSON.writeFileSync.argsForCall[0][1] + expect(writtenConfig).toEqualJson + global: + atom.config.settings + '.ruby.source': + foo: + bar: 'ruby' + omg: 'wow' + '.coffee.source': + foo: + bar: 'coffee' describe ".setDefaults(keyPath, defaults)", -> it "assigns any previously-unassigned keys to the object at the key path", -> @@ -356,6 +376,23 @@ describe "Config", -> afterEach -> fs.removeSync(dotAtomPath) + describe "when the config file contains scoped settings", -> + beforeEach -> + fs.writeFileSync atom.config.configFilePath, """ + global: + foo: + bar: 'baz' + + '.source.ruby': + foo: + bar: 'more-specific' + """ + atom.config.loadUserConfig() + + it "updates the config data based on the file contents", -> + expect(atom.config.get("foo.bar")).toBe 'baz' + expect(atom.config.get(['.source.ruby'], "foo.bar")).toBe 'more-specific' + describe "when the config file contains valid cson", -> beforeEach -> fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'") @@ -430,7 +467,15 @@ describe "Config", -> atom.config.configDirPath = dotAtomPath atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() - fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'") + fs.writeFileSync atom.config.configFilePath, """ + global: + foo: + bar: 'baz' + scoped: false + '.source.ruby': + foo: + scoped: true + """ atom.config.loadUserConfig() atom.config.observeUserConfig() updatedHandler = jasmine.createSpy("updatedHandler") @@ -474,6 +519,38 @@ describe "Config", -> expect(atom.config.get('foo.bar')).toEqual ['baz', 'ok'] expect(atom.config.get('foo.omg')).toBe 'another' + describe 'when scoped settings are used', -> + it "fires a change event for scoped settings that are removed", -> + atom.config.onDidChange ['.source.ruby'], 'foo.scoped', scopedSpy = jasmine.createSpy() + + fs.writeFileSync atom.config.configFilePath, """ + global: + foo: + scoped: false + """ + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> + expect(scopedSpy).toHaveBeenCalled() + expect(atom.config.get(['.source.ruby'], 'foo.scoped')).toBe false + + it "does not fire a change event for paths that did not change", -> + atom.config.onDidChange ['.source.ruby'], 'foo.scoped', noChangeSpy = jasmine.createSpy() + + fs.writeFileSync atom.config.configFilePath, """ + global: + foo: + bar: 'baz' + '.source.ruby': + foo: + scoped: true + """ + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> + expect(noChangeSpy).not.toHaveBeenCalled() + expect(atom.config.get(['.source.ruby'], 'foo.bar')).toBe 'baz' + expect(atom.config.get(['.source.ruby'], 'foo.scoped')).toBe true + + describe "when the config file changes to omit a setting with a default", -> it "resets the setting back to the default", -> fs.writeFileSync(atom.config.configFilePath, "foo: { baz: 'new'}") @@ -866,9 +943,9 @@ describe "Config", -> describe "scoped settings", -> describe ".get(scopeDescriptor, keyPath)", -> it "returns the property with the most specific scope selector", -> - atom.config.addScopedSettings(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) - atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22) - atom.config.addScopedSettings(".source", foo: bar: baz: 11) + atom.config.addScopedSettings("config", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + atom.config.addScopedSettings("config", ".source .string.quoted.double", foo: bar: baz: 22) + atom.config.addScopedSettings("config", ".source", foo: bar: baz: 11) expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 expect(atom.config.get([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBe 22 @@ -876,8 +953,8 @@ describe "Config", -> expect(atom.config.get([".text"], "foo.bar.baz")).toBeUndefined() it "favors the most recently added properties in the event of a specificity tie", -> - atom.config.addScopedSettings(".source.coffee .string.quoted.single", foo: bar: baz: 42) - atom.config.addScopedSettings(".source.coffee .string.quoted.double", foo: bar: baz: 22) + atom.config.addScopedSettings("config", ".source.coffee .string.quoted.single", foo: bar: baz: 42) + atom.config.addScopedSettings("config", ".source.coffee .string.quoted.double", foo: bar: baz: 22) expect(atom.config.get([".source.coffee", ".string.quoted.single"], "foo.bar.baz")).toBe 42 expect(atom.config.get([".source.coffee", ".string.quoted.single.double"], "foo.bar.baz")).toBe 22 @@ -889,9 +966,9 @@ describe "Config", -> describe ".set(scope, keyPath, value)", -> it "sets the value and overrides the others", -> - atom.config.addScopedSettings(".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) - atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22) - atom.config.addScopedSettings(".source", foo: bar: baz: 11) + atom.config.addScopedSettings("config", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + atom.config.addScopedSettings("config", ".source .string.quoted.double", foo: bar: baz: 22) + atom.config.addScopedSettings("config", ".source", foo: bar: baz: 11) expect(atom.config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 @@ -917,11 +994,11 @@ describe "Config", -> expect(changeSpy).toHaveBeenCalledWith(12) changeSpy.reset() - disposable1 = atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22) + disposable1 = atom.config.addScopedSettings("a", ".source .string.quoted.double", foo: bar: baz: 22) expect(changeSpy).toHaveBeenCalledWith(22) changeSpy.reset() - disposable2 = atom.config.addScopedSettings("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + disposable2 = atom.config.addScopedSettings("b", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) expect(changeSpy).toHaveBeenCalledWith(42) changeSpy.reset() @@ -946,11 +1023,11 @@ describe "Config", -> expect(changeSpy).toHaveBeenCalledWith({oldValue: undefined, newValue: 12, keyPath}) changeSpy.reset() - disposable1 = atom.config.addScopedSettings(".source .string.quoted.double", foo: bar: baz: 22) + disposable1 = atom.config.addScopedSettings("a", ".source .string.quoted.double", foo: bar: baz: 22) expect(changeSpy).toHaveBeenCalledWith({oldValue: 12, newValue: 22, keyPath}) changeSpy.reset() - disposable2 = atom.config.addScopedSettings("a", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) + disposable2 = atom.config.addScopedSettings("b", ".source.coffee .string.quoted.double.coffee", foo: bar: baz: 42) expect(changeSpy).toHaveBeenCalledWith({oldValue: 22, newValue: 42, keyPath}) changeSpy.reset() diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index e483d25a5..0396f4673 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -2,6 +2,7 @@ require '../src/window' atom.initialize() atom.restoreWindowDimensions() +require 'jasmine-json' require '../vendor/jasmine-jquery' path = require 'path' _ = require 'underscore-plus' diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index b3f268b56..68cb388e1 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -446,12 +446,6 @@ describe "TextEditorComponent", -> foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() - getLeafNodes = (node) -> - if node.children.length > 0 - flatten(toArray(node.children).map(getLeafNodes)) - else - [node] - describe "gutter rendering", -> [gutter] = [] @@ -2214,7 +2208,6 @@ describe "TextEditorComponent", -> it "does not render invisible characters", -> atom.config.set('editor.invisibles', eol: 'E') atom.config.set('editor.showInvisibles', true) - nextAnimationFrame() expect(component.lineNodeForScreenRow(0).textContent).toBe 'var quicksort = function () {' it "does not assign an explicit line-height on the editor contents", -> @@ -2268,6 +2261,140 @@ describe "TextEditorComponent", -> expect(editor.getCursorBufferPosition()).toEqual [0, 1] + describe 'scoped config settings', -> + [coffeeEditor, coffeeComponent] = [] + + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + + afterEach: -> + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + + describe 'soft wrap settings', -> + beforeEach -> + atom.config.set '.source.coffee', 'editor.softWrap', true + atom.config.set '.source.coffee', 'editor.preferredLineLength', 17 + atom.config.set '.source.coffee', 'editor.softWrapAtPreferredLineLength', true + + editor.setEditorWidthInChars(20) + coffeeEditor.setEditorWidthInChars(20) + + it "wraps lines when editor.softWrap is true for a matching scope", -> + expect(editor.lineTextForScreenRow(2)).toEqual ' if (items.length <= 1) return items;' + expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items ' + + it 'updates the wrapped lines when editor.preferredLineLength changes', -> + atom.config.set '.source.coffee', 'editor.preferredLineLength', 20 + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if ' + + it 'updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', -> + atom.config.set '.source.coffee', 'editor.softWrapAtPreferredLineLength', false + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if ' + + it 'updates the wrapped lines when editor.softWrap changes', -> + atom.config.set '.source.coffee', 'editor.softWrap', false + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if items.length <= 1' + + atom.config.set '.source.coffee', 'editor.softWrap', true + expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items ' + + it 'updates the wrapped lines when the grammar changes', -> + editor.setGrammar(coffeeEditor.getGrammar()) + expect(editor.isSoftWrapped()).toBe true + expect(editor.lineTextForScreenRow(0)).toEqual 'var quicksort = ' + + describe '::isSoftWrapped()', -> + it 'returns the correct value based on the scoped settings', -> + expect(editor.isSoftWrapped()).toBe false + expect(coffeeEditor.isSoftWrapped()).toBe true + + describe 'invisibles settings', -> + [jsInvisibles, coffeeInvisibles] = [] + beforeEach -> + jsInvisibles = + eol: 'J' + space: 'A' + tab: 'V' + cr: 'A' + + coffeeInvisibles = + eol: 'C' + space: 'O' + tab: 'F' + cr: 'E' + + atom.config.set '.source.js', 'editor.showInvisibles', true + atom.config.set '.source.js', 'editor.invisibles', jsInvisibles + + atom.config.set '.source.coffee', 'editor.showInvisibles', false + atom.config.set '.source.coffee', 'editor.invisibles', coffeeInvisibles + + editor.setText " a line with tabs\tand spaces \n" + nextAnimationFrame() + + it "renders the invisibles when editor.showInvisibles is true for a given grammar", -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" + + it "does not render the invisibles when editor.showInvisibles is false for a given grammar", -> + editor.setGrammar(coffeeEditor.getGrammar()) + nextAnimationFrame() + expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " + + it "re-renders the invisibles when the invisible settings change", -> + jsGrammar = editor.getGrammar() + editor.setGrammar(coffeeEditor.getGrammar()) + atom.config.set '.source.coffee', 'editor.showInvisibles', true + nextAnimationFrame() + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}" + + newInvisibles = + eol: 'N' + space: 'E' + tab: 'W' + cr: 'I' + atom.config.set '.source.coffee', 'editor.invisibles', newInvisibles + nextAnimationFrame() + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" + + editor.setGrammar(jsGrammar) + nextAnimationFrame() + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" + + describe 'editor.showIndentGuide', -> + beforeEach -> + atom.config.set '.source.js', 'editor.showIndentGuide', true + atom.config.set '.source.coffee', 'editor.showIndentGuide', false + + it "has an 'indent-guide' class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false", -> + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + + editor.setGrammar(coffeeEditor.getGrammar()) + + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + + it "removes the 'indent-guide' class when editor.showIndentGuide to false", -> + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + + atom.config.set '.source.js', 'editor.showIndentGuide', false + + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + buildMouseEvent = (type, properties...) -> properties = extend({bubbles: true, cancelable: true}, properties...) properties.detail ?= 1 @@ -2304,3 +2431,9 @@ describe "TextEditorComponent", -> lineHasClass = (screenRow, klass) -> component.lineNodeForScreenRow(screenRow).classList.contains(klass) + + getLeafNodes = (node) -> + if node.children.length > 0 + flatten(toArray(node.children).map(getLeafNodes)) + else + [node] diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 1f1f8dcbd..68722a90f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1259,7 +1259,6 @@ describe "TextEditor", -> editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe 'var' - describe "when the cursor is inside a region of whitespace", -> it "selects the whitespace region", -> editor.setCursorScreenPosition([5, 2]) @@ -1277,6 +1276,29 @@ describe "TextEditor", -> editor.selectWordsContainingCursors() expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] + describe 'when editor.nonWordCharacters is set scoped to a grammar', -> + coffeeEditor = null + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + + it 'selects the correct surrounding word for the given scoped setting', -> + coffeeEditor.setCursorBufferPosition [0, 9] # in the middle of quicksort + coffeeEditor.selectWordsContainingCursors() + expect(coffeeEditor.getSelectedBufferRange()).toEqual [[0, 6], [0, 15]] + + atom.config.set '.source.coffee', 'editor.nonWordCharacters', 'qusort' + + coffeeEditor.setCursorBufferPosition [0, 9] + coffeeEditor.selectWordsContainingCursors() + expect(coffeeEditor.getSelectedBufferRange()).toEqual [[0, 8], [0, 11]] + + editor.setCursorBufferPosition [0, 7] + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [0, 13]] + describe ".selectToFirstCharacterOfLine()", -> it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> editor.setCursorScreenPosition [0,5] @@ -3038,6 +3060,51 @@ describe "TextEditor", -> atom.workspace.open(null, softTabs: false).then (editor) -> expect(editor.getSoftTabs()).toBeFalsy() + describe '.getTabLength()', -> + describe 'when scoped settings are used', -> + coffeeEditor = null + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + + afterEach: -> + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + + it 'returns correct values based on the scope of the set grammars', -> + atom.config.set '.source.coffee', 'editor.tabLength', 6 + + expect(editor.getTabLength()).toBe 2 + expect(coffeeEditor.getTabLength()).toBe 6 + + it 'retokenizes when the tab length is updated via .setTabLength()', -> + expect(editor.getTabLength()).toBe 2 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + + editor.setTabLength(6) + expect(editor.getTabLength()).toBe 6 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + + it 'retokenizes when the editor.tabLength setting is updated', -> + expect(editor.getTabLength()).toBe 2 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + + atom.config.set '.source.js', 'editor.tabLength', 6 + expect(editor.getTabLength()).toBe 6 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + + it 'updates the tab length when the grammar changes', -> + atom.config.set '.source.coffee', 'editor.tabLength', 6 + + expect(editor.getTabLength()).toBe 2 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2 + + editor.setGrammar(coffeeEditor.getGrammar()) + expect(editor.getTabLength()).toBe 6 + expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6 + describe ".indentLevelForLine(line)", -> it "returns the indent level when the line has only leading whitespace", -> expect(editor.indentLevelForLine(" hello")).toBe(2) @@ -3077,14 +3144,15 @@ describe "TextEditor", -> expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBeGreaterThan 1 describe "auto-indent", -> - copyText = (text, {startColumn}={}) -> + copyText = (text, {startColumn, textEditor}={}) -> startColumn ?= 0 - editor.setCursorBufferPosition([0, 0]) - editor.insertText(text) + textEditor ?= editor + textEditor.setCursorBufferPosition([0, 0]) + textEditor.insertText(text) numberOfNewlines = text.match(/\n/g)?.length endColumn = text.match(/[^\n]*$/)[0]?.length - editor.getLastSelection().setBufferRange([[0,startColumn], [numberOfNewlines,endColumn]]) - editor.cutSelectedText() + textEditor.getLastSelection().setBufferRange([[0,startColumn], [numberOfNewlines,endColumn]]) + textEditor.cutSelectedText() describe "editor.autoIndent", -> describe "when editor.autoIndent is false (default)", -> @@ -3191,6 +3259,31 @@ describe "TextEditor", -> editor.insertText('foo') expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 + describe 'when scoped settings are used', -> + coffeeEditor = null + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + + runs -> + atom.config.set('.source.js', 'editor.autoIndent', true) + atom.config.set('.source.coffee', 'editor.autoIndent', false) + + afterEach: -> + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + + it "does not auto-indent the line for javascript files", -> + editor.setCursorBufferPosition([1, 30]) + editor.insertText("\n") + expect(editor.lineTextForBufferRow(2)).toBe " " + + coffeeEditor.setCursorBufferPosition([1, 18]) + coffeeEditor.insertText("\n") + expect(coffeeEditor.lineTextForBufferRow(2)).toBe "" + describe "editor.normalizeIndentOnPaste", -> beforeEach -> atom.config.set('editor.normalizeIndentOnPaste', true) @@ -3240,6 +3333,37 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(3)).toBe " }" expect(editor.lineTextForBufferRow(4)).toBe "" + describe 'when scoped settings are used', -> + coffeeEditor = null + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + waitsForPromise -> + atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + + runs -> + atom.config.set('.source.js', 'editor.normalizeIndentOnPaste', true) + atom.config.set('.source.coffee', 'editor.normalizeIndentOnPaste', false) + + afterEach: -> + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + + it "normalizes the indentation level based on scoped settings", -> + copyText(" while (true) {\n foo();\n }\n", {startColumn: 2, textEditor: coffeeEditor}) + coffeeEditor.setCursorBufferPosition([4, 4]) + coffeeEditor.pasteText() + expect(coffeeEditor.lineTextForBufferRow(4)).toBe " while (true) {" + expect(coffeeEditor.lineTextForBufferRow(5)).toBe " foo();" + expect(coffeeEditor.lineTextForBufferRow(6)).toBe " }" + + copyText(" while (true) {\n foo();\n }\n", {startColumn: 2}) + editor.setCursorBufferPosition([3, 4]) + editor.pasteText() + expect(editor.lineTextForBufferRow(3)).toBe " while (true) {" + expect(editor.lineTextForBufferRow(4)).toBe " foo();" + expect(editor.lineTextForBufferRow(5)).toBe " }" + it "autoIndentSelectedRows auto-indents the selection", -> editor.setCursorBufferPosition([2, 0]) editor.insertText("function() {\ninside=true\n}\n i=1\n") @@ -3252,10 +3376,18 @@ describe "TextEditor", -> expect(editor.lineTextForBufferRow(5)).toBe " i=1" describe "soft and hard tabs", -> + afterEach -> + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + it "resets the tab style when tokenization is complete", -> editor.destroy() - atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o - expect(editor.softTabs).toBe true + + waitsForPromise -> + atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o + + runs -> + expect(editor.softTabs).toBe true waitsForPromise -> atom.packages.activatePackage('language-coffee-script') @@ -3263,9 +3395,6 @@ describe "TextEditor", -> runs -> expect(editor.softTabs).toBe false - atom.packages.deactivatePackage('language-coffee-script') - atom.packages.unloadPackage('language-coffee-script') - describe ".destroy()", -> it "destroys all markers associated with the edit session", -> expect(buffer.getMarkerCount()).toBeGreaterThan 0 diff --git a/src/config.coffee b/src/config.coffee index bb45a09ec..bda7f2014 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -1,7 +1,7 @@ _ = require 'underscore-plus' fs = require 'fs-plus' EmitterMixin = require('emissary').Emitter -{Disposable, Emitter} = require 'event-kit' +{CompositeDisposable, Disposable, Emitter} = require 'event-kit' CSON = require 'season' path = require 'path' async = require 'async' @@ -312,6 +312,7 @@ class Config @defaultSettings = {} @settings = {} @scopedSettingsStore = new ScopedPropertyStore + @usersScopedSettings = new CompositeDisposable @configFileHasErrors = false @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') @@ -658,7 +659,7 @@ class Config try userConfig = CSON.readFileSync(@configFilePath) - @setAll(userConfig) + @resetUserSettings(userConfig) @configFileHasErrors = false catch error @configFileHasErrors = true @@ -678,18 +679,26 @@ class Config @watchSubscription = null save: -> - CSON.writeFileSync(@configFilePath, @settings) + allSettings = @scopedSettingsStore.propertiesForSource('user-config') + allSettings.global = @settings + CSON.writeFileSync(@configFilePath, allSettings) ### Section: Private methods managing global settings ### - setAll: (newSettings) -> + resetUserSettings: (newSettings) -> unless isPlainObject(newSettings) @settings = {} @emitter.emit 'did-change' return + if newSettings.global? + scopedSettings = newSettings + newSettings = newSettings.global + delete scopedSettings.global + @resetUserScopedSettings(scopedSettings) + unsetUnspecifiedValues = (keyPath, value) => if isPlainObject(value) keys = if keyPath? then keyPath.split('.') else [] @@ -782,12 +791,13 @@ class Config Section: Private Scoped Settings ### - addScopedSettings: (name, selector, value) -> - if arguments.length < 3 - value = selector - selector = name - name = null + resetUserScopedSettings: (newScopedSettings) -> + @usersScopedSettings?.dispose() + @usersScopedSettings = new CompositeDisposable + @usersScopedSettings.add @scopedSettingsStore.addProperties('user-config', newScopedSettings) + @emitter.emit 'did-change' + addScopedSettings: (name, selector, value) -> settingsBySelector = {} settingsBySelector[selector] = value disposable = @scopedSettingsStore.addProperties(name, settingsBySelector) @@ -801,7 +811,11 @@ class Config newValue = {} _.setValueForKeyPath(newValue, keyPath, value) value = newValue - @addScopedSettings(null, selector, value) + + settingsBySelector = {} + settingsBySelector[selector] = value + @usersScopedSettings.add @scopedSettingsStore.addProperties('user-config', settingsBySelector) + @emitter.emit 'did-change' getRawScopedValue: (scopeDescriptor, keyPath) -> scopeChain = scopeDescriptor diff --git a/src/cursor.coffee b/src/cursor.coffee index 7ad9ac5f6..2bda09219 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -198,7 +198,7 @@ class Cursor extends Model [before, after] = @editor.getTextInBufferRange(range) return false if /\s/.test(before) or /\s/.test(after) - nonWordCharacters = atom.config.get('editor.nonWordCharacters').split('') + nonWordCharacters = atom.config.get(@getScopes(), 'editor.nonWordCharacters').split('') _.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after) # Public: Returns whether this cursor is between a word's start and end. @@ -617,7 +617,7 @@ class Cursor extends Model # Returns a {RegExp}. wordRegExp: ({includeNonWordCharacters}={}) -> includeNonWordCharacters ?= true - nonWordCharacters = atom.config.get('editor.nonWordCharacters') + nonWordCharacters = atom.config.get(@getScopes(), 'editor.nonWordCharacters') segments = ["^[\t ]*$"] segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") if includeNonWordCharacters diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index fc465e5d7..16c964908 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -3,7 +3,7 @@ EmitterMixin = require('emissary').Emitter guid = require 'guid' Serializable = require 'serializable' {Model} = require 'theorist' -{Emitter} = require 'event-kit' +{CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' TokenizedBuffer = require './tokenized-buffer' RowMap = require './row-map' @@ -45,7 +45,6 @@ class DisplayBuffer extends Model @emitter = new Emitter - @softWrapped ?= atom.config.get('editor.softWrap') ? false @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles}) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @@ -55,17 +54,27 @@ class DisplayBuffer extends Model @decorationsByMarkerId = {} @updateAllScreenLines() @createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()) + @subscribe @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @subscribe @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated @subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated - @subscribe atom.config.onDidChange 'editor.preferredLineLength', => - @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength') + @updateAllScreenLines() - @subscribe atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', => + subscribeToScopedConfigSettings: => + @scopedConfigSubscriptions?.dispose() + @scopedConfigSubscriptions = subscriptions = new CompositeDisposable + + scopeDescriptor = @getRootScopeDescriptor() + + subscriptions.add atom.config.onDidChange scopeDescriptor, 'editor.softWrap', => + @updateWrappedScreenLines() + + subscriptions.add atom.config.onDidChange scopeDescriptor, 'editor.softWrapAtPreferredLineLength', => @updateWrappedScreenLines() if @isSoftWrapped() - @updateAllScreenLines() + subscriptions.add atom.config.onDidChange scopeDescriptor, 'editor.preferredLineLength', => + @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get(scopeDescriptor, 'editor.softWrapAtPreferredLineLength') serializeParams: -> id: @id @@ -319,7 +328,7 @@ class DisplayBuffer extends Model return 0 unless lineHeight > 0 scrollHeight = @getLineCount() * lineHeight - if @height? and atom.config.get('editor.scrollPastEnd') + if @height? and atom.config.get(@getRootScopeDescriptor(), 'editor.scrollPastEnd') scrollHeight = scrollHeight + @height - (lineHeight * 3) scrollHeight @@ -412,11 +421,15 @@ class DisplayBuffer extends Model if softWrapped isnt @softWrapped @softWrapped = softWrapped @updateWrappedScreenLines() - @emit 'soft-wrap-changed', @softWrapped - @emitter.emit 'did-change-soft-wrapped', @softWrapped - @softWrapped + softWrapped = @isSoftWrapped() + @emit 'soft-wrap-changed', softWrapped + @emitter.emit 'did-change-soft-wrapped', softWrapped + softWrapped + else + @isSoftWrapped() - isSoftWrapped: -> @softWrapped + isSoftWrapped: -> + @softWrapped ? atom.config.get(@getRootScopeDescriptor(), 'editor.softWrap') ? false # Set the number of characters that fit horizontally in the editor. # @@ -438,8 +451,8 @@ class DisplayBuffer extends Model @editorWidthInChars getSoftWrapColumn: -> - if atom.config.get('editor.softWrapAtPreferredLineLength') - Math.min(@getEditorWidthInChars(), atom.config.get('editor.preferredLineLength')) + if atom.config.get(@getRootScopeDescriptor(), 'editor.softWrapAtPreferredLineLength') + Math.min(@getEditorWidthInChars(), atom.config.get(@getRootScopeDescriptor(), 'editor.preferredLineLength')) else @getEditorWidthInChars() @@ -1034,6 +1047,9 @@ class DisplayBuffer extends Model line = @tokenizedLineForScreenRow(row).text console.log row, @bufferRowForScreenRow(row), line, line.length + getRootScopeDescriptor: -> + @tokenizedBuffer.grammarScopeDescriptor + handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange @updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?) diff --git a/src/syntax.coffee b/src/syntax.coffee index 84bb2e0bb..8c04a424a 100644 --- a/src/syntax.coffee +++ b/src/syntax.coffee @@ -40,6 +40,7 @@ class Syntax extends GrammarRegistry atom.config.scopedSettingsStore addProperties: (args...) -> + args.unshift(null) if args.length == 2 deprecate 'Consider using atom.config.set() instead. A direct (but private) replacement is available at atom.config.addScopedSettings().' atom.config.addScopedSettings(args...) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index c2f84e6dd..925e6fa63 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -5,6 +5,7 @@ React = require 'react-atom-fork' scrollbarStyle = require 'scrollbar-style' {Range, Point} = require 'text-buffer' grim = require 'grim' +{CompositeDisposable} = require 'event-kit' GutterComponent = require './gutter-component' InputComponent = require './input-component' @@ -352,6 +353,7 @@ TextEditorComponent = React.createClass observeEditor: -> {editor} = @props @subscribe editor.onDidChange(@onScreenLinesChanged) + @subscribe editor.observeGrammar(@onGrammarChanged) @subscribe editor.observeCursors(@onCursorAdded) @subscribe editor.observeSelections(@onSelectionAdded) @subscribe editor.observeDecorations(@onDecorationAdded) @@ -406,11 +408,20 @@ TextEditorComponent = React.createClass event.target.value = '' observeConfig: -> - @subscribe atom.config.observe 'editor.showIndentGuide', @setShowIndentGuide - @subscribe atom.config.observe 'editor.showLineNumbers', @setShowLineNumbers - @subscribe atom.config.observe 'editor.scrollSensitivity', @setScrollSensitivity @subscribe atom.config.observe 'editor.useHardwareAcceleration', @setUseHardwareAcceleration + onGrammarChanged: -> + {editor} = @props + + @scopedConfigSubscriptions?.dispose() + @scopedConfigSubscriptions = subscriptions = new CompositeDisposable + + scopeDescriptor = editor.getRootScopeDescriptor() + + subscriptions.add atom.config.observe scopeDescriptor, 'editor.showIndentGuide', @setShowIndentGuide + subscriptions.add atom.config.observe scopeDescriptor, 'editor.showLineNumbers', @setShowLineNumbers + subscriptions.add atom.config.observe scopeDescriptor, 'editor.scrollSensitivity', @setScrollSensitivity + onFocus: -> @refs.input.focus() if @isMounted() @@ -435,7 +446,6 @@ TextEditorComponent = React.createClass inputNode.value = event.data if editor.insertText(event.data) - onInputFocused: -> @setState(focused: true) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d1fca0412..000359077 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -5,7 +5,7 @@ Delegator = require 'delegato' {deprecate} = require 'grim' {Model} = require 'theorist' EmitterMixin = require('emissary').Emitter -{Emitter} = require 'event-kit' +{CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' LanguageMode = require './language-mode' DisplayBuffer = require './display-buffer' @@ -84,14 +84,12 @@ class TextEditor extends Model @cursors = [] @selections = [] - if @shouldShowInvisibles() - invisibles = atom.config.get('editor.invisibles') - - @displayBuffer?.setInvisibles(invisibles) - @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, invisibles}) + @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped}) @buffer = @displayBuffer.buffer @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true + @updateInvisibles() + for marker in @findMarkers(@getSelectionMarkerAttributes()) marker.setProperties(preserveFolds: true) @addSelection(marker) @@ -113,9 +111,6 @@ class TextEditor extends Model @emit 'scroll-left-changed', scrollLeft @emitter.emit 'did-change-scroll-left', scrollLeft - @subscribe atom.config.onDidChange 'editor.showInvisibles', => @updateInvisibles() - @subscribe atom.config.onDidChange 'editor.invisibles', => @updateInvisibles() - atom.workspace?.editorAdded(this) if registerEditor serializeParams: -> @@ -162,6 +157,20 @@ class TextEditor extends Model @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration + @subscribeToScopedConfigSettings() + + subscribeToScopedConfigSettings: -> + @scopedConfigSubscriptions?.dispose() + @scopedConfigSubscriptions = subscriptions = new CompositeDisposable + + scopeDescriptor = @getRootScopeDescriptor() + + subscriptions.add atom.config.onDidChange scopeDescriptor, 'editor.showInvisibles', => @updateInvisibles() + subscriptions.add atom.config.onDidChange scopeDescriptor, 'editor.invisibles', => @updateInvisibles() + + getViewClass: -> + require './text-editor-view' + destroyed: -> @unsubscribe() selection.destroy() for selection in @getSelections() @@ -251,10 +260,23 @@ class TextEditor extends Model onDidChangeSoftWrapped: (callback) -> @displayBuffer.onDidChangeSoftWrapped(callback) - # Extended: Calls your `callback` when the grammar that interprets and colorizes the text has - # been changed. + # Extended: Calls your `callback` when the grammar that interprets and + # colorizes the text has been changed. Immediately calls your callback with + # the current grammar. # # * `callback` {Function} + # * `grammar` {Grammar} + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGrammar: (callback) -> + callback(@getGrammar()) + @onDidChangeGrammar(callback) + + # Extended: Calls your `callback` when the grammar that interprets and + # colorizes the text has been changed. + # + # * `callback` {Function} + # * `grammar` {Grammar} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeGrammar: (callback) -> @@ -485,10 +507,9 @@ class TextEditor extends Model # Create an {TextEditor} with its initial state based on this object copy: -> - tabLength = @getTabLength() displayBuffer = @displayBuffer.copy() softTabs = @getSoftTabs() - newEditor = new TextEditor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true, registerEditor: true}) + newEditor = new TextEditor({@buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, registerEditor: true}) for marker in @findMarkers(editorId: @id) marker.copy(editorId: newEditor.id, preserveFolds: true) newEditor @@ -2176,9 +2197,11 @@ class TextEditor extends Model # Returns a {Number}. getTabLength: -> @displayBuffer.getTabLength() - # Essential: Set the on-screen length of tab characters. + # Essential: Set the on-screen length of tab characters. Setting this to a + # {Number} This will override the `editor.tabLength` setting. # - # * `tabLength` {Number} length of a single tab + # * `tabLength` {Number} length of a single tab. Setting to `null` will + # fallback to using the `editor.tabLength` config setting setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) # Extended: Determine if the buffer uses hard or soft tabs. @@ -2351,7 +2374,11 @@ class TextEditor extends Model # position. See {::scopesForBufferPosition} for more information. # # Returns an {Array} of {String}s. - scopesAtCursor: -> @getLastCursor().getScopes() + scopesAtCursor: -> + if cursor = @getLastCursor() + cursor.getScopes() + else + @getRootScopeDescriptor() getCursorScopes: -> deprecate 'Use TextEditor::scopesAtCursor() instead' @scopesAtCursor() @@ -2378,6 +2405,9 @@ class TextEditor extends Model bufferRangeForScopeAtCursor: (selector) -> @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + getRootScopeDescriptor: -> + @displayBuffer.getRootScopeDescriptor() + logCursorScope: -> console.log @scopesAtCursor() @@ -2429,7 +2459,7 @@ class TextEditor extends Model return - else if atom.config.get("editor.normalizeIndentOnPaste") and metadata?.indentBasis? + else if atom.config.get(@scopesAtCursor(), "editor.normalizeIndentOnPaste") and metadata?.indentBasis? if !@getLastCursor().hasPrecedingCharactersOnLine() or containsNewlines options.indentBasis ?= metadata.indentBasis @@ -2647,14 +2677,14 @@ class TextEditor extends Model ### shouldAutoIndent: -> - atom.config.get("editor.autoIndent") + atom.config.get(@getRootScopeDescriptor(), "editor.autoIndent") shouldShowInvisibles: -> - not @mini and atom.config.get('editor.showInvisibles') + not @mini and atom.config.get(@getRootScopeDescriptor(), 'editor.showInvisibles') updateInvisibles: -> if @shouldShowInvisibles() - @displayBuffer.setInvisibles(atom.config.get('editor.invisibles')) + @displayBuffer.setInvisibles(atom.config.get(@getRootScopeDescriptor(), 'editor.invisibles')) else @displayBuffer.setInvisibles(null) @@ -2666,6 +2696,8 @@ class TextEditor extends Model @softTabs = @usesSoftTabs() ? @softTabs handleGrammarChange: -> + @updateInvisibles() + @subscribeToScopedConfigSettings() @unfoldAll() @emit 'grammar-changed' @emitter.emit 'did-change-grammar' diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index ae2fa559a..260f018f0 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -25,18 +25,12 @@ class TokenizedBuffer extends Model constructor: ({@buffer, @tabLength, @invisibles}) -> @emitter = new Emitter - @tabLength ?= atom.config.get('editor.tabLength') - @subscribe atom.syntax.onDidAddGrammar(@grammarAddedOrUpdated) @subscribe atom.syntax.onDidUpdateGrammar(@grammarAddedOrUpdated) @subscribe @buffer.onDidChange (e) => @handleBufferChange(e) @subscribe @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() - @subscribe @$tabLength.changes, (tabLength) => @retokenizeLines() - - @subscribe atom.config.onDidChange 'editor.tabLength', ({newValue}) => @setTabLength(newValue) - @reloadGrammar() serializeParams: -> @@ -48,6 +42,10 @@ class TokenizedBuffer extends Model params.buffer = atom.project.bufferForPathSync(params.bufferPath) params + observeGrammar: (callback) -> + callback(@grammar) + @onDidChangeGrammar(callback) + onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback @@ -81,9 +79,16 @@ class TokenizedBuffer extends Model return if grammar is @grammar @unsubscribe(@grammar) if @grammar @grammar = grammar + @grammarScopeDescriptor = [@grammar.scopeName] @currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText()) @subscribe @grammar.onDidUpdate => @retokenizeLines() @retokenizeLines() + + @grammarTabLengthSubscription?.dispose() + @grammarTabLengthSubscription = atom.config.onDidChange @grammarScopeDescriptor, 'editor.tabLength', => + @retokenizeLines() + @subscribe @grammarTabLengthSubscription + @emit 'grammar-changed', grammar @emitter.emit 'did-change-grammar', grammar @@ -112,16 +117,11 @@ class TokenizedBuffer extends Model setVisible: (@visible) -> @tokenizeInBackground() if @visible - # Retrieves the current tab length. - # - # Returns a {Number}. getTabLength: -> - @tabLength + @tabLength ? atom.config.get(@grammarScopeDescriptor, 'editor.tabLength') - # Specifies the tab length. - # - # tabLength - A {Number} that defines the new tab length. setTabLength: (@tabLength) -> + @retokenizeLines() setInvisibles: (invisibles) -> unless _.isEqual(invisibles, @invisibles)