diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index aa691496e..7a697bdbe 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -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", -> diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 837363322..27479364b 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -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') diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index e93e1b1a7..7b5589188 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -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] diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 019a5570a..513074bd3 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -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" diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 99f482d9a..38549d0a7 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -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] == "#" diff --git a/src/app/token.coffee b/src/app/token.coffee index 4693208ac..28e5f4d0e 100644 --- a/src/app/token.coffee +++ b/src/app/token.coffee @@ -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, '&') @@ -49,7 +70,7 @@ class Token .replace(/>/g, '>') if invisibles - if @isTab and invisibles.tab + if @isHardTab and invisibles.tab html = html.replace(/^./, "") else if invisibles.space if hasLeadingWhitespace