Merge pull request #3718 from atom/bo-scoped-user-config

Add scoped settings to user config
This commit is contained in:
Ben Ogle
2014-10-08 16:37:20 -07:00
12 changed files with 511 additions and 97 deletions

View File

@@ -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",

View File

@@ -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()

View File

@@ -2,6 +2,7 @@ require '../src/window'
atom.initialize()
atom.restoreWindowDimensions()
require 'jasmine-json'
require '../vendor/jasmine-jquery'
path = require 'path'
_ = require 'underscore-plus'

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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?)

View File

@@ -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...)

View File

@@ -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)

View File

@@ -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'

View File

@@ -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)