From 9f6d1f987f1a7241ba1dd6ecf4dccf2629be3520 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 19 Dec 2012 21:44:21 -0800 Subject: [PATCH 1/9] Add initial support for replacing anchors --- native/v8_extensions/onig_scanner.mm | 7 +-- spec/app/tokenized-buffer-spec.coffee | 20 ++++++++- spec/fixtures/COMMIT_EDITMSG | 2 + src/app/language-mode.coffee | 4 +- src/app/text-mate-grammar.coffee | 62 +++++++++++++++++++++++---- src/app/tokenized-buffer.coffee | 2 +- 6 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 spec/fixtures/COMMIT_EDITMSG diff --git a/native/v8_extensions/onig_scanner.mm b/native/v8_extensions/onig_scanner.mm index c2d7346cf..b357ffeca 100644 --- a/native/v8_extensions/onig_scanner.mm +++ b/native/v8_extensions/onig_scanner.mm @@ -57,10 +57,7 @@ class OnigScannerUserData : public CefBase { bool useCachedResult = false; OnigResult *result = NULL; - // In Oniguruma, \G is based on the start position of the match, so the result - // changes based on the start position. So it can't be cached. - BOOL containsBackslashG = [regExp.expression rangeOfString:@"\\G"].location != NSNotFound; - if (useCachedResults && index <= maxCachedIndex && ! containsBackslashG) { + if (useCachedResults && index <= maxCachedIndex) { result = cachedResults[index]; useCachedResult = (result == NULL || [result locationAt:0] >= startLocation); } @@ -158,4 +155,4 @@ bool OnigScanner::Execute(const CefString& name, return false; } -} // namespace v8_extensions \ No newline at end of file +} // namespace v8_extensions diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index ce5e37b76..22fcdbd95 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -14,7 +14,7 @@ describe "TokenizedBuffer", -> fullyTokenize = (tokenizedBuffer) -> advanceClock() while tokenizedBuffer.firstInvalidRow()? - changeHandler.reset() + changeHandler?.reset() describe "when the buffer contains soft-tabs", -> beforeEach -> @@ -326,3 +326,21 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand" + describe "when a Git commit message file is tokenized", -> + beforeEach -> + editSession = fixturesProject.buildEditSessionForPath('COMMIT_EDITMSG', autoIndent: false) + buffer = editSession.buffer + tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer + editSession.setVisible(true) + fullyTokenize(tokenizedBuffer) + + afterEach -> + editSession.destroy() + + it "correctly parses the number sign of the first comment line", -> + commentLine = tokenizedBuffer.lineForScreenRow(1) + expect(commentLine.text).toBe "# Please enter the commit message for your changes. Lines starting" + { tokens } = commentLine + + expect(tokens[0].value).toBe "#" + expect(tokens[0].scopes).toEqual ["text.git-commit", "meta.scope.metadata.git-commit", "comment.line.number-sign.git-commit", "punctuation.definition.comment.git-commit"] diff --git a/spec/fixtures/COMMIT_EDITMSG b/spec/fixtures/COMMIT_EDITMSG new file mode 100644 index 000000000..d369aa22e --- /dev/null +++ b/spec/fixtures/COMMIT_EDITMSG @@ -0,0 +1,2 @@ +longggggggggggggggggggggggggggggggggggggggggggggggg +# Please enter the commit message for your changes. Lines starting diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index 98b1689b5..1305f9191 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -186,5 +186,5 @@ class LanguageMode if desiredIndentLevel < currentIndentLevel @editSession.setIndentationForBufferRow(bufferRow, desiredIndentLevel) - tokenizeLine: (line, stack) -> - {tokens, stack} = @grammar.tokenizeLine(line, stack) + tokenizeLine: (line, stack, firstLine) -> + {tokens, stack} = @grammar.tokenizeLine(line, stack, firstLine) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index df72c8511..52a71d2db 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -29,8 +29,7 @@ class TextMateGrammar data = {patterns: [data], tempName: name} if data.begin? or data.match? @repository[name] = new Rule(this, data) - tokenizeLine: (line, ruleStack=[@initialRule]) -> - ruleStack ?= [@initialRule] + tokenizeLine: (line, ruleStack=[@initialRule], firstLine=false) -> ruleStack = new Array(ruleStack...) # clone ruleStack tokens = [] position = 0 @@ -44,7 +43,7 @@ class TextMateGrammar break if position == line.length - if match = _.last(ruleStack).getNextTokens(ruleStack, line, position) + if match = _.last(ruleStack).getNextTokens(ruleStack, line, position, firstLine) { nextTokens, tokensStartPosition, tokensEndPosition } = match if position < tokensStartPosition # unmatched text before next tokens tokens.push(new Token( @@ -79,6 +78,7 @@ class Rule patterns: null allPatterns: null createEndPattern: null + anchor: -1 constructor: (@grammar, {@scopeName, patterns, @endPattern}) -> patterns ?= [] @@ -95,14 +95,28 @@ class Rule @allPatterns.push(pattern.getIncludedPatterns(included)...) @allPatterns - getScanner: -> - @scanner ?= new OnigScanner(_.pluck(@getIncludedPatterns(), 'regexSource')) + getScanner: (position, firstLine) -> + return @scanner if @scanner - getNextTokens: (stack, line, position) -> + anchored = false + regexes = [] + @getIncludedPatterns().forEach (pattern) => + if pattern.anchored + anchored = true + regex = pattern.replaceAnchor(firstLine, position, @anchor) + else + regex = pattern.regexSource + regexes.push regex if regex + + regexScanner = new OnigScanner(regexes) + @scanner = regexScanner unless anchored + regexScanner + + getNextTokens: (stack, line, position, firstLine) -> patterns = @getIncludedPatterns() # Add a `\n` to appease patterns that contain '\n' explicitly - return null unless result = @getScanner().findNextMatch(line + "\n", position) + return null unless result = @getScanner(position, firstLine).findNextMatch("#{line}\n", position) { index, captureIndices } = result # Since the `\n' (added above) is not part of the line, truncate captures to the line's actual length lineLength = line.length @@ -130,6 +144,7 @@ class Pattern scopeName: null captures: null backReferences: null + anchored: false constructor: (@grammar, { name, contentName, @include, match, begin, end, captures, beginCaptures, endCaptures, patterns, @popRule, hasBackReferences}) -> @scopeName = name ? contentName # TODO: We need special treatment of contentName @@ -144,6 +159,34 @@ class Pattern @captures = beginCaptures ? captures endPattern = new Pattern(@grammar, { match: end, captures: endCaptures ? captures, popRule: true}) @pushRule = new Rule(@grammar, { @scopeName, patterns, endPattern }) + @anchored = @hasAnchor() + + hasAnchor: -> + return false unless @regexSource + escape = false + for character in @regexSource.split('') + return true if escape and (character is 'A' or character is 'G' or character is 'z') + escape = not escape and character is '\\' + false + + replaceAnchor: (firstLine, offset, anchor) -> + escaped = [] + placeholder = '\uFFFF' + escape = false + for character in @regexSource.split('') + if escape + switch character + when 'A' then escaped.push(placeholder) unless firstLine + when 'G' then escaped.push(placeholder) unless offset is anchor + when 'z' then escaped.push('$(?!\n)(? beginCaptures = [] @@ -180,7 +223,9 @@ class Pattern else tokens = [new Token(value: line[start...end], scopes: scopes)] if @pushRule - stack.push(@pushRule.getRuleToPush(line, captureIndices)) + ruleToPush = @pushRule.getRuleToPush(line, captureIndices) + ruleToPush.anchor = captureIndices[1] + stack.push(ruleToPush) else if @popRule stack.pop() @@ -226,4 +271,3 @@ shiftCapture = (captureIndices) -> scopesFromStack = (stack) -> _.compact(_.pluck(stack, "scopeName")) - diff --git a/src/app/tokenized-buffer.coffee b/src/app/tokenized-buffer.coffee index 7ab97222a..bc88d4e0e 100644 --- a/src/app/tokenized-buffer.coffee +++ b/src/app/tokenized-buffer.coffee @@ -134,7 +134,7 @@ class TokenizedBuffer buildTokenizedScreenLineForRow: (row, ruleStack) -> line = @buffer.lineForRow(row) - { tokens, ruleStack } = @languageMode.tokenizeLine(line, ruleStack) + { tokens, ruleStack } = @languageMode.tokenizeLine(line, ruleStack, row is 0) new ScreenLine({tokens, ruleStack, @tabLength}) lineForScreenRow: (row) -> From 4c1ecf61df00735b23ebd5e51ff5d57401ac8407 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 20 Dec 2012 21:08:02 -0800 Subject: [PATCH 2/9] Anchor second capture index --- src/app/text-mate-grammar.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 52a71d2db..21ca0db6b 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -165,7 +165,7 @@ class Pattern return false unless @regexSource escape = false for character in @regexSource.split('') - return true if escape and (character is 'A' or character is 'G' or character is 'z') + return true if escape and 'AGz'.indexOf(character) isnt -1 escape = not escape and character is '\\' false @@ -224,7 +224,7 @@ class Pattern tokens = [new Token(value: line[start...end], scopes: scopes)] if @pushRule ruleToPush = @pushRule.getRuleToPush(line, captureIndices) - ruleToPush.anchor = captureIndices[1] + ruleToPush.anchor = captureIndices[2] stack.push(ruleToPush) else if @popRule stack.pop() From 8aba95191dd433318d25b78e35a458b3151aed51 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 20 Dec 2012 21:08:09 -0800 Subject: [PATCH 3/9] Append anchor characters when placeholder is not added --- spec/app/tokenized-buffer-spec.coffee | 8 ++++++++ src/app/text-mate-grammar.coffee | 12 ++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 22fcdbd95..800243ce6 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -337,6 +337,14 @@ describe "TokenizedBuffer", -> afterEach -> editSession.destroy() + it "correctly parses a long line", -> + longLine = tokenizedBuffer.lineForScreenRow(0) + expect(longLine.text).toBe "longggggggggggggggggggggggggggggggggggggggggggggggg" + { tokens } = longLine + + expect(tokens[0].value).toBe "longggggggggggggggggggggggggggggggggggggggggggggggg" + expect(tokens[0].scopes).toEqual ["text.git-commit", "meta.scope.message.git-commit", "invalid.deprecated.line-too-long.git-commit"] + it "correctly parses the number sign of the first comment line", -> commentLine = tokenizedBuffer.lineForScreenRow(1) expect(commentLine.text).toBe "# Please enter the commit message for your changes. Lines starting" diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 21ca0db6b..9aa30bee6 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -176,8 +176,16 @@ class Pattern for character in @regexSource.split('') if escape switch character - when 'A' then escaped.push(placeholder) unless firstLine - when 'G' then escaped.push(placeholder) unless offset is anchor + when 'A' + if firstLine + escaped.push("\\#{character}") + else + escaped.push(placeholder) + when 'G' + if offset is anchor + escaped.push("\\#{character}") + else + escaped.push(placeholder) when 'z' then escaped.push('$(?!\n)(? Date: Fri, 21 Dec 2012 09:33:52 -0800 Subject: [PATCH 4/9] Clear anchor positions in stack after line is tokenized --- src/app/text-mate-grammar.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 9aa30bee6..d132d6738 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -61,6 +61,7 @@ class TextMateGrammar )) break + ruleStack.forEach (rule) -> rule.clearAnchorPosition() { tokens, ruleStack } ruleForInclude: (name) -> @@ -78,7 +79,7 @@ class Rule patterns: null allPatterns: null createEndPattern: null - anchor: -1 + anchorPosition: -1 constructor: (@grammar, {@scopeName, patterns, @endPattern}) -> patterns ?= [] @@ -95,6 +96,8 @@ class Rule @allPatterns.push(pattern.getIncludedPatterns(included)...) @allPatterns + clearAnchorPosition: -> @anchorPosition = -1 + getScanner: (position, firstLine) -> return @scanner if @scanner @@ -103,7 +106,7 @@ class Rule @getIncludedPatterns().forEach (pattern) => if pattern.anchored anchored = true - regex = pattern.replaceAnchor(firstLine, position, @anchor) + regex = pattern.replaceAnchor(firstLine, position, @anchorPosition) else regex = pattern.regexSource regexes.push regex if regex @@ -232,7 +235,7 @@ class Pattern tokens = [new Token(value: line[start...end], scopes: scopes)] if @pushRule ruleToPush = @pushRule.getRuleToPush(line, captureIndices) - ruleToPush.anchor = captureIndices[2] + ruleToPush.anchorPosition = captureIndices[2] stack.push(ruleToPush) else if @popRule stack.pop() From 559b9132f92b180db6322e4a1731f0e0865e739b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 21 Dec 2012 09:52:45 -0800 Subject: [PATCH 5/9] Parse until position equals line length including trailing newline --- spec/app/tokenized-buffer-spec.coffee | 31 +++++++++++++++++++++++++++ spec/fixtures/includes.cc | 2 ++ src/app/text-mate-grammar.coffee | 11 +++++----- 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 spec/fixtures/includes.cc diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 800243ce6..2eb7b74fb 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -352,3 +352,34 @@ describe "TokenizedBuffer", -> expect(tokens[0].value).toBe "#" expect(tokens[0].scopes).toEqual ["text.git-commit", "meta.scope.metadata.git-commit", "comment.line.number-sign.git-commit", "punctuation.definition.comment.git-commit"] + + describe "when a C++ source file is tokenized", -> + beforeEach -> + editSession = fixturesProject.buildEditSessionForPath('includes.cc', autoIndent: false) + buffer = editSession.buffer + tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer + editSession.setVisible(true) + fullyTokenize(tokenizedBuffer) + + afterEach -> + editSession.destroy() + + it "correctly parses the first include line", -> + longLine = tokenizedBuffer.lineForScreenRow(0) + expect(longLine.text).toBe '#include "a.h"' + { tokens } = longLine + + expect(tokens[0].value).toBe "#" + expect(tokens[0].scopes).toEqual ["source.c++", "meta.preprocessor.c.include"] + expect(tokens[1].value).toBe 'include' + expect(tokens[1].scopes).toEqual ["source.c++", "meta.preprocessor.c.include", "keyword.control.import.include.c"] + + it "correctly parses the second include line", -> + commentLine = tokenizedBuffer.lineForScreenRow(1) + expect(commentLine.text).toBe '#include "b.h"' + { tokens } = commentLine + + expect(tokens[0].value).toBe "#" + expect(tokens[0].scopes).toEqual ["source.c++", "meta.preprocessor.c.include"] + expect(tokens[1].value).toBe 'include' + expect(tokens[1].scopes).toEqual ["source.c++", "meta.preprocessor.c.include", "keyword.control.import.include.c"] diff --git a/spec/fixtures/includes.cc b/spec/fixtures/includes.cc new file mode 100644 index 000000000..600af314e --- /dev/null +++ b/spec/fixtures/includes.cc @@ -0,0 +1,2 @@ +#include "a.h" +#include "b.h" diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index d132d6738..dbba03344 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -41,7 +41,7 @@ class TextMateGrammar tokens = [new Token(value: "", scopes: scopes)] return { tokens, ruleStack } - break if position == line.length + break if position == line.length + 1 # include trailing newline position if match = _.last(ruleStack).getNextTokens(ruleStack, line, position, firstLine) { nextTokens, tokensStartPosition, tokensEndPosition } = match @@ -55,10 +55,11 @@ class TextMateGrammar position = tokensEndPosition else # push filler token for unmatched text at end of line - tokens.push(new Token( - value: line[position...line.length] - scopes: scopes - )) + if position < line.length + tokens.push(new Token( + value: line[position...line.length] + scopes: scopes + )) break ruleStack.forEach (rule) -> rule.clearAnchorPosition() From 905002cd5879ecf459c8fea7e71c03df94bb39ec Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 22 Dec 2012 22:02:29 -0800 Subject: [PATCH 6/9] Highlight foreground of selected line numbers Previously no gutter highlight was displayed unless the selection was empty. Now there is a separate CSS class for no selection that changes the background color independently from the foreground color. --- spec/app/editor-spec.coffee | 25 +++++++----- spec/stdlib/child-process-spec.coffee | 2 +- src/app/gutter.coffee | 56 +++++++++++++++++++-------- static/editor.css | 3 +- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 693e3b93e..f7c99be4f 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1728,14 +1728,14 @@ describe "Editor", -> describe "when there is no wrapping", -> it "highlights the line where the initial cursor position is", -> expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').text()).toBe "1" + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').text()).toBe "1" it "updates the highlighted line when the cursor position changes", -> editor.setCursorBufferPosition([1,0]) expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').text()).toBe "2" + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').text()).toBe "2" describe "when there is wrapping", -> beforeEach -> @@ -1745,23 +1745,28 @@ describe "Editor", -> it "highlights the line where the initial cursor position is", -> expect(editor.getCursorBufferPosition().row).toBe 0 - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').text()).toBe "1" + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').text()).toBe "1" it "updates the highlighted line when the cursor position changes", -> editor.setCursorBufferPosition([1,0]) expect(editor.getCursorBufferPosition().row).toBe 1 - expect(editor.find('.line-number.cursor-line').length).toBe 1 - expect(editor.find('.line-number.cursor-line').text()).toBe "2" + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 1 + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').text()).toBe "2" describe "when the selection spans multiple lines", -> beforeEach -> editor.attachToDom(30) - it "doesn't highlight the background or the gutter", -> + it "highlights the foreground of the gutter", -> editor.getSelection().setBufferRange(new Range([0,0],[2,0])) expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editor.find('.line-number.cursor-line').length).toBe 0 + expect(editor.find('.line-number.cursor-line').length).toBe 3 + + it "doesn't highlight the background of the gutter", -> + editor.getSelection().setBufferRange(new Range([0,0],[2,0])) + expect(editor.getSelection().isSingleScreenLine()).toBe false + expect(editor.find('.line-number.cursor-line-no-selection').length).toBe 0 it "when a newline is deleted with backspace, the line number of the new cursor position is highlighted", -> editor.setCursorScreenPosition([1,0]) diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index 985d814b4..4c389f087 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -126,4 +126,4 @@ describe 'Child Processes', -> ChildProcess.exec(cmd, options) runs -> - expect(output.length).toBeGreaterThan 1 \ No newline at end of file + expect(output.length).toBeGreaterThan 1 diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index 4910e21ba..f6a8440aa 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -2,6 +2,7 @@ $ = require 'jquery' _ = require 'underscore' +Range = require 'range' module.exports = class Gutter extends View @@ -18,9 +19,9 @@ class Gutter extends View @attached = true editor = @editor() - highlightCursorLine = => @highlightCursorLine() - editor.on 'cursor-move', highlightCursorLine - editor.on 'selection-change', highlightCursorLine + highlightLines = => @highlightLines() + editor.on 'cursor-move', highlightLines + editor.on 'selection-change', highlightLines @calculateWidth() editor: -> @@ -63,8 +64,8 @@ class Gutter extends View @calculateWidth() @firstScreenRow = startScreenRow @lastScreenRow = endScreenRow - @highlightedRow = null - @highlightCursorLine() + @highlightedRows = null + @highlightLines() calculateWidth: -> highestNumberWidth = @editor().getLineCount().toString().length * @editor().charWidth @@ -73,17 +74,38 @@ class Gutter extends View @lineNumbers.width(highestNumberWidth + @calculateLineNumberPadding()) @widthChanged?(@outerWidth()) - highlightCursorLine: -> - if @editor().getSelection().isEmpty() - rowToHighlight = @editor().getCursorScreenPosition().row - return if rowToHighlight == @highlightedRow - return if rowToHighlight < @firstScreenRow or rowToHighlight > @lastScreenRow + removeLineHighlights: -> + return unless @highlightedLineNumbers + for line in @highlightedLineNumbers + line.classList.remove('cursor-line') + line.classList.remove('cursor-line-no-selection') + @highlightedLineNumbers = null - @highlightedLineNumber?.classList.remove('cursor-line') - if @highlightedLineNumber = @lineNumbers[0].children[rowToHighlight - @firstScreenRow] - @highlightedLineNumber.classList.add('cursor-line') - @highlightedRow = rowToHighlight + addLineHighlight: (row, emptySelection) -> + return if row < @firstScreenRow or row > @lastScreenRow + @highlightedLineNumbers ?= [] + if highlightedLineNumber = @lineNumbers[0].children[row - @firstScreenRow] + highlightedLineNumber.classList.add('cursor-line') + highlightedLineNumber.classList.add('cursor-line-no-selection') if emptySelection + @highlightedLineNumbers.push(highlightedLineNumber) + + highlightLines: -> + if @editor().getSelection().isEmpty() + row = @editor().getCursorScreenPosition().row + rowRange = new Range([row, 0], [row, 0]) + return if @highlightedRows?.isEqual(rowRange) and @selectionEmpty + + @removeLineHighlights() + @addLineHighlight(row, true) + @highlightedRows = rowRange + @selectionEmpty = true else - @highlightedLineNumber?.classList.remove('cursor-line') - @highlightedLineNumber = null - @highlightedRow = null + selectedRows = @editor().getSelection().getScreenRange() + selectedRows = new Range([selectedRows.start.row, 0], [selectedRows.end.row, 0]) + return if @highlightedRows?.isEqual(selectedRows) and not @selectionEmpty + + @removeLineHighlights() + for row in [selectedRows.start.row..selectedRows.end.row] + @addLineHighlight(row, false) + @highlightedRows = selectedRows + @selectionEmpty = false diff --git a/static/editor.css b/static/editor.css index 7c6e10db7..e1892bee1 100644 --- a/static/editor.css +++ b/static/editor.css @@ -33,7 +33,8 @@ color: rgba(255, 255, 255, .6); } -.editor.focused .cursor-line { +.editor.focused .line-number.cursor-line-no-selection, +.editor.focused .line.cursor-line { background-color: rgba(255, 255, 255, .12); } From f7878a02dddab762b862274bd8c601decb36fa47 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 22 Dec 2012 22:13:51 -0800 Subject: [PATCH 7/9] Add no selection rule to spec selector --- spec/app/editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f7c99be4f..8d61188e2 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1766,7 +1766,7 @@ describe "Editor", -> it "doesn't highlight the background of the gutter", -> editor.getSelection().setBufferRange(new Range([0,0],[2,0])) expect(editor.getSelection().isSingleScreenLine()).toBe false - expect(editor.find('.line-number.cursor-line-no-selection').length).toBe 0 + expect(editor.find('.line-number.cursor-line.cursor-line-no-selection').length).toBe 0 it "when a newline is deleted with backspace, the line number of the new cursor position is highlighted", -> editor.setCursorScreenPosition([1,0]) From b59bd7a0992bb287f531fdb0c51772d855573234 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 22 Dec 2012 22:16:10 -0800 Subject: [PATCH 8/9] Check selection being empty before comparing highlighted rows --- src/app/gutter.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index f6a8440aa..93e1ae61e 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -93,7 +93,7 @@ class Gutter extends View if @editor().getSelection().isEmpty() row = @editor().getCursorScreenPosition().row rowRange = new Range([row, 0], [row, 0]) - return if @highlightedRows?.isEqual(rowRange) and @selectionEmpty + return if @selectionEmpty and @highlightedRows?.isEqual(rowRange) @removeLineHighlights() @addLineHighlight(row, true) @@ -102,7 +102,7 @@ class Gutter extends View else selectedRows = @editor().getSelection().getScreenRange() selectedRows = new Range([selectedRows.start.row, 0], [selectedRows.end.row, 0]) - return if @highlightedRows?.isEqual(selectedRows) and not @selectionEmpty + return if not @selectionEmpty and @highlightedRows?.isEqual(selectedRows) @removeLineHighlights() for row in [selectedRows.start.row..selectedRows.end.row] From ff3b04e01ec8afe5a13058437783b672ad1787b0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sat, 22 Dec 2012 23:40:23 -0800 Subject: [PATCH 9/9] Support clicking line numbers in gutter Clicking moves the cursor to the start of the row and shift-clicking selects to the start of the row --- spec/app/editor-spec.coffee | 12 ++++++++++++ src/app/gutter.coffee | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 8d61188e2..1ee20fa91 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1990,3 +1990,15 @@ describe "Editor", -> runs -> expect(editor.getText()).toBe(originalPathText) + + describe "when clicking a gutter line", -> + it "moves the cursor to the start of the selected line", -> + rootView.attachToDom() + expect(editor.getCursorScreenPosition()).toEqual [0,0] + editor.gutter.find(".line-number:eq(1)").trigger 'click' + expect(editor.getCursorScreenPosition()).toEqual [1,0] + + it "selects to the start of the selected line when shift is pressed", -> + expect(editor.getSelection().getScreenRange()).toEqual [0,0], [0,0] + editor.gutter.find(".line-number:eq(1)").trigger 'click', {shiftKey: true} + expect(editor.getSelection().getScreenRange()).toEqual [0,0], [1,0] diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index 93e1ae61e..19e63708b 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -3,6 +3,7 @@ $ = require 'jquery' _ = require 'underscore' Range = require 'range' +Point = require 'point' module.exports = class Gutter extends View @@ -22,6 +23,14 @@ class Gutter extends View highlightLines = => @highlightLines() editor.on 'cursor-move', highlightLines editor.on 'selection-change', highlightLines + @on 'click', '.line-number', (e) => + row = parseInt($(e.target).text()) - 1 + position = new Point(row, 0) + if e.shiftKey + @editor().selectToScreenPosition(position) + else + @editor().setCursorScreenPosition(position) + @calculateWidth() editor: ->