Make leading spaces atomic (length based on tabLength)

This commit is contained in:
Corey Johnson
2012-11-06 09:03:54 -08:00
parent 89ef3f0f20
commit b85a95eaa1
6 changed files with 69 additions and 33 deletions

View File

@@ -110,12 +110,12 @@ describe "EditSession", ->
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
editSession.setCursorScreenPosition(row: lastLineIndex, column: 1)
editSession.setCursorScreenPosition(row: lastLineIndex, column: editSession.tabLength)
editSession.moveCursorDown()
expect(editSession.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length)
editSession.moveCursorUp()
expect(editSession.getCursorScreenPosition().column).toBe 1
expect(editSession.getCursorScreenPosition().column).toBe editSession.tabLength
it "retains a goal column of 0 when moving back up", ->
lastLineIndex = buffer.getLines().length - 1
@@ -138,9 +138,9 @@ describe "EditSession", ->
describe ".moveCursorLeft()", ->
it "moves the cursor by one column to the left", ->
editSession.setCursorScreenPosition([3, 3])
editSession.setCursorScreenPosition([1, 8])
editSession.moveCursorLeft()
expect(editSession.getCursorScreenPosition()).toEqual [3, 2]
expect(editSession.getCursorScreenPosition()).toEqual [1, 7]
describe "when the cursor is in the first column", ->
describe "when there is a previous line", ->
@@ -155,6 +155,13 @@ describe "EditSession", ->
editSession.moveCursorLeft()
expect(editSession.getCursorScreenPosition()).toEqual(row: 0, column: 0)
describe "when softTabs is enabled and the cursor is preceded by leading whitespace", ->
it "skips tabLength worth of whitespace at a time", ->
editSession.setCursorBufferPosition([5, 6])
editSession.moveCursorLeft()
expect(editSession.getCursorBufferPosition()).toEqual [5, 4]
it "merges cursors when they overlap", ->
editSession.setCursorScreenPosition([0, 0])
editSession.addCursorAtScreenPosition([0, 1])
@@ -354,14 +361,14 @@ describe "EditSession", ->
describe ".selectToScreenPosition(screenPosition)", ->
it "expands the last selection to the given position", ->
editSession.setSelectedBufferRange([[3, 0], [4, 5]])
editSession.addCursorAtScreenPosition([5, 5])
editSession.selectToScreenPosition([6, 1])
editSession.addCursorAtScreenPosition([5, 6])
editSession.selectToScreenPosition([6, 2])
selections = editSession.getSelections()
expect(selections.length).toBe 2
[selection1, selection2] = selections
expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]]
expect(selection2.getScreenRange()).toEqual [[5, 5], [6, 1]]
expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]]
it "merges selections if they intersect, maintaining the directionality of the last selection", ->
editSession.setCursorScreenPosition([4, 10])
@@ -1637,18 +1644,18 @@ describe "EditSession", ->
it "merges cursors when the change causes them to overlap", ->
editSession.setCursorScreenPosition([0, 0])
editSession.addCursorAtScreenPosition([0, 1])
editSession.addCursorAtScreenPosition([1, 1])
editSession.addCursorAtScreenPosition([0, 2])
editSession.addCursorAtScreenPosition([1, 2])
[cursor1, cursor2, cursor3] = editSession.getCursors()
expect(editSession.getCursors().length).toBe 3
buffer.delete([[0, 0], [0, 1]])
buffer.delete([[0, 0], [0, 2]])
expect(editSession.getCursors().length).toBe 2
expect(editSession.getCursors()).toEqual [cursor1, cursor3]
expect(cursor1.getBufferPosition()).toEqual [0,0]
expect(cursor3.getBufferPosition()).toEqual [1,1]
expect(cursor3.getBufferPosition()).toEqual [1,2]
describe "folding", ->
describe "structural folding", ->

View File

@@ -486,13 +486,13 @@ describe "Editor", ->
rootView.setFontSize(10)
lineHeightBefore = editor.lineHeight
charWidthBefore = editor.charWidth
editor.setCursorScreenPosition [5, 5]
editor.setCursorScreenPosition [5, 6]
rootView.setFontSize(30)
expect(editor.css('font-size')).toBe '30px'
expect(editor.lineHeight).toBeGreaterThan lineHeightBefore
expect(editor.charWidth).toBeGreaterThan charWidthBefore
expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 5 * editor.charWidth }
expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth }
# ensure we clean up font size subscription
editor.trigger('core:close')

View File

@@ -77,7 +77,7 @@ describe "RootView", ->
editor4 = editor2.splitDown()
editor2.edit(rootView.project.buildEditSessionForPath('dir/b'))
editor3.edit(rootView.project.buildEditSessionForPath('sample.js'))
editor3.setCursorScreenPosition([2, 3])
editor3.setCursorScreenPosition([2, 4])
editor4.edit(rootView.project.buildEditSessionForPath('sample.txt'))
editor4.setCursorScreenPosition([0, 2])
rootView.attachToDom()
@@ -98,7 +98,7 @@ describe "RootView", ->
expect(editor1.getPath()).toBe require.resolve('fixtures/dir/a')
expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b')
expect(editor3.getPath()).toBe require.resolve('fixtures/sample.js')
expect(editor3.getCursorScreenPosition()).toEqual [2, 3]
expect(editor3.getCursorScreenPosition()).toEqual [2, 4]
expect(editor4.getPath()).toBe require.resolve('fixtures/sample.txt')
expect(editor4.getCursorScreenPosition()).toEqual [0, 2]

View File

@@ -49,7 +49,7 @@ describe "TokenizedBuffer", ->
expect(event.newRange).toEqual new Range([0, 0], [2,0])
# line 2 is unchanged
expect(tokenizedBuffer.lineForScreenRow(2).tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
it "updates tokens for lines beyond the changed lines if needed", ->
buffer.insert([5, 30], '/* */')
@@ -85,9 +85,9 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.lineForScreenRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
# lines below deleted regions should be shifted upward
expect(tokenizedBuffer.lineForScreenRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.lineForScreenRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
expect(tokenizedBuffer.lineForScreenRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
expect(tokenizedBuffer.lineForScreenRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.lineForScreenRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -126,7 +126,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.lineForScreenRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
expect(tokenizedBuffer.lineForScreenRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
expect(tokenizedBuffer.lineForScreenRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -167,6 +167,7 @@ describe "TokenizedBuffer", ->
screenLine0 = tokenizedBuffer.lineForScreenRow(0)
expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}"
{ tokens } = screenLine0
expect(tokens.length).toBe 3
expect(tokens[0].value).toBe "#"
expect(tokens[1].value).toBe " Econ 101"

View File

@@ -61,8 +61,15 @@ class TextMateGrammar
))
break
tokens = _.flatten(tokens.map (token) -> token.breakOutTabCharacters(tabLength))
{ tokens, ruleStack }
{ tokens: @breakOutAtomicTokens(tokens, tabLength), ruleStack }
breakOutAtomicTokens: (inputTokens, tabLength) ->
outputTokens = []
breakOutLeadingWhitespace = true
for token in inputTokens
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
outputTokens
ruleForInclude: (name) ->
if name[0] == "#"

View File

@@ -5,9 +5,9 @@ class Token
value: null
scopes: null
isAtomic: null
isTab: null
isHardTab: null
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold, @isTab}) ->
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @fold, @isHardTab}) ->
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
@@ -22,24 +22,45 @@ class Token
value2 = @value.substring(splitIndex)
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
breakOutTabCharacters: (tabLength) ->
return [this] unless /\t/.test(@value)
breakOutAtomicTokens: (tabLength, breakOutLeadingWhitespace) ->
value = @value
outputTokens = []
for substring in @value.match(/[^\t]+|\t/g)
if breakOutLeadingWhitespace
return [this] unless /^ |\t/.test(value)
else
return [this] unless /\t/.test(value)
if breakOutLeadingWhitespace
endOfLeadingWhitespace = value.match(new RegExp("^( {#{tabLength}})*"))[0].length
whitespaceTokenCount = endOfLeadingWhitespace / tabLength
_.times whitespaceTokenCount, =>
outputTokens.push(@buildTabToken(tabLength, false))
value = @value[endOfLeadingWhitespace..]
return outputTokens unless value.length > 0
for substring in value.match(/[^\t]+|\t/g)
if substring == "\t"
@buildTabToken(tabLength)
outputTokens.push(@buildTabToken(tabLength, true))
else
new Token(value: substring, scopes: @scopes)
outputTokens.push(new Token(value: substring, scopes: @scopes))
buildTabToken: (tabLength) ->
outputTokens
buildTabToken: (tabLength, isHardTab) ->
new Token(
value: _.multiplyString(" ", tabLength)
scopes: @scopes
bufferDelta: 1
bufferDelta: if isHardTab then 1 else tabLength
isAtomic: true
isTab: true
isHardTab: isHardTab
)
isOnlyWhitespace: ->
not /\S/.test(@value)
getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace})->
html = @value
.replace(/&/g, '&amp;')
@@ -49,7 +70,7 @@ class Token
.replace(/>/g, '&gt;')
if invisibles
if @isTab and invisibles.tab
if @isHardTab and invisibles.tab
html = html.replace(/^./, "<span class='invisible'>#{invisibles.tab}</span>")
else if invisibles.space
if hasLeadingWhitespace