From 7be3c55765bb5a1ee9e928e53ae0a058fda83d82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 11:59:32 -0600 Subject: [PATCH 01/14] Only auto-indent when typing, not when pasting or programmatically inserting This paves the way for using a different strategy to normalize indentation when pasting, without it getting tangled in auto-indent. --- spec/app/edit-session-spec.coffee | 58 +++++-------------------------- src/app/edit-session.coffee | 2 +- src/app/editor.coffee | 4 +-- src/app/selection.coffee | 13 ++----- 4 files changed, 15 insertions(+), 62 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 788859373..547fd61a2 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -662,7 +662,7 @@ describe "EditSession", -> editSession.insertText('holy cow') expect(editSession.lineForScreenRow(2).fold).toBeUndefined() - describe "when auto-indent is enabled", -> + describe "when auto-indent is enabled and the `autoIndent` option is true", -> beforeEach -> editSession.setAutoIndent(true) @@ -670,79 +670,39 @@ describe "EditSession", -> describe "when the newline is inserted on a line that starts a new level of indentation", -> it "auto-indents the new line to one additional level of indentation beyond the preceding line", -> editSession.setCursorBufferPosition([1, Infinity]) - editSession.insertText('\n') + editSession.insertText('\n', autoIndent: true) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 describe "when the newline is inserted on a normal line", -> it "auto-indents the new line to the same level of indentation as the preceding line", -> editSession.setCursorBufferPosition([5, 13]) - editSession.insertText('\n') + editSession.insertText('\n', autoIndent: true) expect(buffer.indentationForRow(6)).toBe buffer.indentationForRow(5) - describe "when text with newlines is inserted", -> - describe "when the new line matches an outdent pattern", -> - describe "when the preceding line matches an auto-indent pattern", -> - it "auto-decreases the indentation of the line to match that of the preceding line", -> - editSession.setCursorBufferPosition([1, 30]) - editSession.insertText '\n}' - expect(buffer.indentationForRow(2)).toBe 2 - - describe "when the preceding does not match an outo-indent pattern", -> - it "auto-decreases the indentation of the line to be one level below that of the preceding line", -> - editSession.setCursorBufferPosition([3, Infinity]) - editSession.insertText '\n}' - expect(buffer.indentationForRow(4)).toBe 2 - - describe "when the portion of the line preceding the inserted text is blank", -> - it "auto-increases the indentation of the first line, then fully auto-indents the subsequent lines", -> - editSession.setCursorBufferPosition([5, 2]) - editSession.insertText """ - if (true) { - console.log("It's true!") - }\n - """ - expect(buffer.indentationForRow(5)).toBe buffer.indentationForRow(4) + 2 - expect(buffer.indentationForRow(6)).toBe buffer.indentationForRow(5) + 2 - expect(buffer.indentationForRow(7)).toBe buffer.indentationForRow(4) + 2 - expect(buffer.indentationForRow(8)).toBe buffer.indentationForRow(4) + 2 - - describe "when the portion of the line preceding the inserted text is non-blank", -> - it "fully auto-indents lines subsequent to the first inserted line", -> - buffer.delete([[5, 0], [5, 2]]) - editSession.setCursorBufferPosition([5, Infinity]) - editSession.insertText """ - if (true) { - console.log("It's true!") - } - """ - expect(buffer.indentationForRow(5)).toBe 4 - expect(buffer.indentationForRow(6)).toBe 6 - expect(buffer.indentationForRow(7)).toBe 4 - describe "when text without newlines is inserted", -> describe "when the current line matches an auto-outdent pattern", -> describe "when the preceding line matches an auto-indent pattern", -> it "auto-decreases the indentation of the line to match that of the preceding line", -> editSession.setCursorBufferPosition([2, 4]) - editSession.insertText '\n' + editSession.insertText('\n', autoIndent: true) editSession.setCursorBufferPosition([2, 4]) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 - editSession.insertText ' }' + editSession.insertText(' }', autoIndent: true) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) - describe "when the preceding does not match an outo-indent pattern", -> + describe "when the preceding does not match an auto-indent pattern", -> it "auto-decreases the indentation of the line to be one level below that of the preceding line", -> editSession.setCursorBufferPosition([3, Infinity]) - editSession.insertText '\n' + editSession.insertText('\n', autoIndent: true) expect(buffer.indentationForRow(4)).toBe buffer.indentationForRow(3) - editSession.insertText ' }' + editSession.insertText(' }', autoIndent: true) expect(buffer.indentationForRow(4)).toBe buffer.indentationForRow(3) - 2 describe "when the current line does not match an auto-outdent pattern", -> it "leaves the line unchanged", -> editSession.setCursorBufferPosition([2, 4]) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 - editSession.insertText 'foo' + editSession.insertText('foo', autoIndent: true) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 describe ".insertNewline()", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 10ba9e04c..654bf7da4 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -142,7 +142,7 @@ class EditSession @mutateSelectedText (selection) -> selection.insertText(text, options) insertNewline: -> - @insertText('\n') + @insertText('\n', autoIndent: true) insertNewlineBelow: -> @moveCursorToEndOfLine() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index a5cc6633b..4bec7c356 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -214,7 +214,7 @@ class Editor extends View deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() deleteLine: -> @activeEditSession.deleteLine() cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() - insertText: (text) -> @activeEditSession.insertText(text) + insertText: (text, options) -> @activeEditSession.insertText(text, options) insertNewline: -> @activeEditSession.insertNewline() insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() indent: -> @activeEditSession.indent() @@ -327,7 +327,7 @@ class Editor extends View @selectOnMousemoveUntilMouseup() @on "textInput", (e) => - @insertText(e.originalEvent.data) + @insertText(e.originalEvent.data, autoIndent: true) false @scrollView.on 'mousewheel', (e) => diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 4d050abba..4316fc21d 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -141,16 +141,9 @@ class Selection newBufferRange = @editSession.buffer.change(oldBufferRange, text) @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed - autoIndent = options.autoIndent ? true - - if @editSession.autoIndent and autoIndent - if /\n/.test(text) - firstLinePrefix = @editSession.getTextInBufferRange([[newBufferRange.start.row, 0], newBufferRange.start]) - if /^\s*$/.test(firstLinePrefix) - @editSession.autoIncreaseIndentForBufferRow(newBufferRange.start.row) - - if newBufferRange.getRowCount() > 1 - @editSession.autoIndentBufferRows(newBufferRange.start.row + 1, newBufferRange.end.row) + if @editSession.autoIndent and options.autoIndent + if text == '\n' + @editSession.autoIndentBufferRow(newBufferRange.end.row) else @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) From 0e5c76b474418bce6c49cfb8622a7ba64fa891b7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 14:07:00 -0600 Subject: [PATCH 02/14] Start adding the `normalizeIndent` option to EditSession.proto.insertText This will be used by the paste command to ensure that indentation is normalized. --- spec/app/edit-session-spec.coffee | 44 +++++++++++++++++++++++++++++++ src/app/edit-session.coffee | 3 +++ src/app/language-mode.coffee | 17 ++++++++++++ src/app/selection.coffee | 29 ++++++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 547fd61a2..6d5e6a6f2 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -705,6 +705,50 @@ describe "EditSession", -> editSession.insertText('foo', autoIndent: true) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 + fdescribe "when the `normalizeIndent` option is true", -> + describe "when the inserted text contains no newlines", -> + it "does not adjust the indentation level of the text", -> + editSession.setCursorBufferPosition([5, 2]) + editSession.insertText("foo", normalizeIndent: true) + expect(editSession.lineForBufferRow(5)).toBe " foo current = items.shift();" + + describe "when the inserted text contains newlines", -> + text = null + beforeEach -> + text = """ + while (true) { + foo(); + } + bar(); + """ + + describe "when the cursor is preceded only by whitespace", -> + beforeEach -> + editSession.setCursorBufferPosition([2, Infinity]) + + describe "when auto-indent is enabled", -> + beforeEach -> + editSession.setAutoIndent(true) + + describe "when the cursor's current column is less than the suggested indent level", -> + it "indents all lines relative to the suggested indent", -> + editSession.insertText('\n ') + editSession.insertText(text, normalizeIndent: true) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();" + + describe "when the cursor's current column is greater than the suggested indent level", -> + it "preserves the current indent level, indenting all lines relative to it", -> + + describe "if auto-indent is disabled", -> + it "always normalizes indented lines to the cursor's current indentation level", -> + + describe "when the cursor is preceded by non-whitespace characters", -> + it "normalizes the indentation level of all lines based on the level of the existing first line", -> + describe ".insertNewline()", -> describe "when there is a single cursor", -> describe "when the cursor is at the beginning of a line", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 654bf7da4..43a79914f 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -256,6 +256,9 @@ class EditSession largestFoldStartingAtScreenRow: (screenRow) -> @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + suggestedIndentForBufferRow: (bufferRow) -> + @languageMode.suggestedIndentForBufferRow(bufferRow) + autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index e2fac8b42..4a6b3bf52 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -86,6 +86,23 @@ class LanguageMode [bufferRow, foldEndRow] + suggestedIndentForBufferRow: (bufferRow) -> + currentIndentation = @buffer.indentationForRow(bufferRow) + scopes = @tokenizedBuffer.scopesForPosition([bufferRow, 0]) + return currentIndentation unless increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0]) + + currentLine = @buffer.lineForRow(bufferRow) + precedingRow = @buffer.previousNonBlankRow(bufferRow) + precedingLine = @buffer.lineForRow(precedingRow) + + desiredIndentation = @buffer.indentationForRow(precedingRow) + desiredIndentation += @editSession.tabLength if increaseIndentPattern.test(precedingLine) + + return desiredIndentation unless decreaseIndentPattern = TextMateBundle.outdentRegexForScope(scopes[0]) + desiredIndentation -= @editSession.tabLength if decreaseIndentPattern.test(currentLine) + + Math.max(desiredIndentation, currentIndentation) + autoIndentBufferRows: (startRow, endRow) -> @autoIndentBufferRow(row) for row in [startRow..endRow] diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 4316fc21d..01d924c16 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -137,6 +137,7 @@ class Selection oldBufferRange = @getBufferRange() @editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row) wasReversed = @isReversed() + text = @normalizeIndent(text) if options.normalizeIndent @clear() newBufferRange = @editSession.buffer.change(oldBufferRange, text) @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed @@ -147,6 +148,34 @@ class Selection else @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) + normalizeIndent: (text) -> + return text unless /\n/.test(text) + + lines = text.split('\n') + normalizedLines = [] + + desiredBase = @editSession.suggestedIndentForBufferRow(@cursor.getBufferRow()) + currentBase = lines[0].match(/\s*/)[0].length + delta = desiredBase - currentBase + + for line, i in lines + if i == 0 + firstLineDelta = delta - @editSession.indentationForBufferRow(@cursor.getBufferRow()) + normalizedLines.push(@adjustIndentationForLine(line, firstLineDelta)) + else + normalizedLines.push(@adjustIndentationForLine(line, delta)) + + normalizedLines.join('\n') + + adjustIndentationForLine: (line, delta) -> + indentText = new Array(Math.abs(delta + 1)).join(' ') + if delta > 0 + indentText + line + else if delta < 0 + line.replace(indentText, '') + else + line + backspace: -> if @isEmpty() and not @editSession.isFoldedAtScreenRow(@cursor.getScreenRow()) if @cursor.isAtBeginningOfLine() and @editSession.isFoldedAtScreenRow(@cursor.getScreenRow() - 1) From 7bd4e8801fd48ccfa7e214441a381ba512c4586e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 14:13:51 -0600 Subject: [PATCH 03/14] If inserting on a line that is longer than the suggested indent, preserve indent --- spec/app/edit-session-spec.coffee | 7 +++++++ src/app/selection.coffee | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 6d5e6a6f2..ff711b9e9 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -742,6 +742,13 @@ describe "EditSession", -> describe "when the cursor's current column is greater than the suggested indent level", -> it "preserves the current indent level, indenting all lines relative to it", -> + editSession.insertText('\n ') + editSession.insertText(text, normalizeIndent: true) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();" describe "if auto-indent is disabled", -> it "always normalizes indented lines to the cursor's current indentation level", -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 01d924c16..b031de7ab 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -168,11 +168,11 @@ class Selection normalizedLines.join('\n') adjustIndentationForLine: (line, delta) -> - indentText = new Array(Math.abs(delta + 1)).join(' ') + indentText = new Array(Math.abs(delta) + 1).join(' ') if delta > 0 indentText + line else if delta < 0 - line.replace(indentText, '') + line.replace(new RegExp("^#{indentText}"), '') else line From 3b18c6a42d54b24ad49e90778fde4c5baee7d481 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 14:27:26 -0600 Subject: [PATCH 04/14] With autoIndent disabled, don't use suggested first line indent when normalizing Also, when some lines are *less* indented than the first line, it might not be possible to outdent them as far as they originally were relative to the first line, so we do as much as possible. --- spec/app/edit-session-spec.coffee | 22 ++++++++++++++++------ src/app/selection.coffee | 13 +++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index ff711b9e9..42e94d5ef 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -715,12 +715,12 @@ describe "EditSession", -> describe "when the inserted text contains newlines", -> text = null beforeEach -> - text = """ - while (true) { - foo(); - } - bar(); - """ + text = [ + " while (true) {" + " foo();" + " }" + " bar();" + ].join('\n') describe "when the cursor is preceded only by whitespace", -> beforeEach -> @@ -751,7 +751,17 @@ describe "EditSession", -> expect(editSession.lineForBufferRow(6)).toBe " bar();" describe "if auto-indent is disabled", -> + beforeEach -> + expect(editSession.autoIndent).toBeFalsy() + it "always normalizes indented lines to the cursor's current indentation level", -> + editSession.insertText('\n ') + editSession.insertText(text, normalizeIndent: true) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe "bar();" describe "when the cursor is preceded by non-whitespace characters", -> it "normalizes the indentation level of all lines based on the level of the existing first line", -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index b031de7ab..18334151a 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -151,16 +151,20 @@ class Selection normalizeIndent: (text) -> return text unless /\n/.test(text) + currentBufferRow = @cursor.getBufferRow() lines = text.split('\n') normalizedLines = [] - desiredBase = @editSession.suggestedIndentForBufferRow(@cursor.getBufferRow()) + if @editSession.autoIndent + desiredBase = @editSession.suggestedIndentForBufferRow(currentBufferRow) + else + desiredBase = @editSession.indentationForBufferRow(currentBufferRow) currentBase = lines[0].match(/\s*/)[0].length delta = desiredBase - currentBase for line, i in lines if i == 0 - firstLineDelta = delta - @editSession.indentationForBufferRow(@cursor.getBufferRow()) + firstLineDelta = delta - @editSession.indentationForBufferRow(currentBufferRow) normalizedLines.push(@adjustIndentationForLine(line, firstLineDelta)) else normalizedLines.push(@adjustIndentationForLine(line, delta)) @@ -169,10 +173,11 @@ class Selection adjustIndentationForLine: (line, delta) -> indentText = new Array(Math.abs(delta) + 1).join(' ') + if delta > 0 - indentText + line + new Array(delta + 1).join(' ') + line else if delta < 0 - line.replace(new RegExp("^#{indentText}"), '') + line.replace(new RegExp("^ {0,#{Math.abs(delta)}}"), '') else line From 839d57d8191c5b38807b25f1f464d576e0303512 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 16:16:59 -0600 Subject: [PATCH 05/14] Use existing line's indentation level if inserting normalized lines within it If we're pasting multiple lines starting inside an already existing line, we never want to auto indent. We should just take its existing indentation level. Also, we strip the leading whitespace off the first line we're inserting, assuming it's already being represented by the indentation of the line we're onto which we're appending it. --- spec/app/edit-session-spec.coffee | 14 ++++++++++---- src/app/cursor.coffee | 6 ++++++ src/app/selection.coffee | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 42e94d5ef..5169c0abe 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -715,6 +715,7 @@ describe "EditSession", -> describe "when the inserted text contains newlines", -> text = null beforeEach -> + editSession.setCursorBufferPosition([2, Infinity]) text = [ " while (true) {" " foo();" @@ -723,10 +724,7 @@ describe "EditSession", -> ].join('\n') describe "when the cursor is preceded only by whitespace", -> - beforeEach -> - editSession.setCursorBufferPosition([2, Infinity]) - - describe "when auto-indent is enabled", -> + describe "when auto-indent is enabled", -> beforeEach -> editSession.setAutoIndent(true) @@ -765,6 +763,14 @@ describe "EditSession", -> describe "when the cursor is preceded by non-whitespace characters", -> it "normalizes the indentation level of all lines based on the level of the existing first line", -> + editSession.setAutoIndent(true) + editSession.buffer.delete([[2, 0], [2, 2]]) + editSession.insertText(text, normalizeIndent:true) + + expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {" + expect(editSession.lineForBufferRow(3)).toBe " foo();" + expect(editSession.lineForBufferRow(4)).toBe " }" + expect(editSession.lineForBufferRow(5)).toBe "bar();" describe ".insertNewline()", -> describe "when there is a single cursor", -> diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index cc707cc8e..70ea83f9b 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -45,9 +45,15 @@ class Cursor getScreenRow: -> @getScreenPosition().row + getScreenColumn: -> + @getScreenPosition().column + getBufferRow: -> @getBufferPosition().row + getBufferColumn: -> + @getBufferPosition().column + getCurrentBufferLine: -> @editSession.lineForBufferRow(@getBufferRow()) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 18334151a..23a2ebdb5 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -152,19 +152,27 @@ class Selection return text unless /\n/.test(text) currentBufferRow = @cursor.getBufferRow() + currentBufferColumn = @cursor.getBufferColumn() lines = text.split('\n') normalizedLines = [] - if @editSession.autoIndent - desiredBase = @editSession.suggestedIndentForBufferRow(currentBufferRow) - else + textPrecedingCursor = @editSession.buffer.getTextInRange([[currentBufferRow, 0], [currentBufferRow, currentBufferColumn]]) + insideExistingLine = textPrecedingCursor.match(/\S/) + + if insideExistingLine or not @editSession.autoIndent desiredBase = @editSession.indentationForBufferRow(currentBufferRow) + else + desiredBase = @editSession.suggestedIndentForBufferRow(currentBufferRow) + currentBase = lines[0].match(/\s*/)[0].length delta = desiredBase - currentBase for line, i in lines if i == 0 - firstLineDelta = delta - @editSession.indentationForBufferRow(currentBufferRow) + if insideExistingLine + firstLineDelta = -line.length # remove all leading whitespace + else + firstLineDelta = delta - @editSession.indentationForBufferRow(currentBufferRow) normalizedLines.push(@adjustIndentationForLine(line, firstLineDelta)) else normalizedLines.push(@adjustIndentationForLine(line, delta)) @@ -172,8 +180,6 @@ class Selection normalizedLines.join('\n') adjustIndentationForLine: (line, delta) -> - indentText = new Array(Math.abs(delta) + 1).join(' ') - if delta > 0 new Array(delta + 1).join(' ') + line else if delta < 0 From d1999b91ffe946e4742a64dc2a3514bf0144adf6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 16:30:38 -0600 Subject: [PATCH 06/14] Base first line delta on the cursor column, not the existing line's indent --- spec/app/edit-session-spec.coffee | 5 +++-- src/app/selection.coffee | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 5169c0abe..799d2ad21 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -730,13 +730,14 @@ describe "EditSession", -> describe "when the cursor's current column is less than the suggested indent level", -> it "indents all lines relative to the suggested indent", -> - editSession.insertText('\n ') + editSession.insertText('\n xx') + editSession.setCursorBufferPosition([3, 1]) editSession.insertText(text, normalizeIndent: true) expect(editSession.lineForBufferRow(3)).toBe " while (true) {" expect(editSession.lineForBufferRow(4)).toBe " foo();" expect(editSession.lineForBufferRow(5)).toBe " }" - expect(editSession.lineForBufferRow(6)).toBe " bar();" + expect(editSession.lineForBufferRow(6)).toBe " bar();xx" describe "when the cursor's current column is greater than the suggested indent level", -> it "preserves the current indent level, indenting all lines relative to it", -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 23a2ebdb5..67298b380 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -159,10 +159,12 @@ class Selection textPrecedingCursor = @editSession.buffer.getTextInRange([[currentBufferRow, 0], [currentBufferRow, currentBufferColumn]]) insideExistingLine = textPrecedingCursor.match(/\S/) - if insideExistingLine or not @editSession.autoIndent + if insideExistingLine desiredBase = @editSession.indentationForBufferRow(currentBufferRow) - else + else if @editSession.autoIndent desiredBase = @editSession.suggestedIndentForBufferRow(currentBufferRow) + else + desiredBase = currentBufferColumn currentBase = lines[0].match(/\s*/)[0].length delta = desiredBase - currentBase @@ -172,7 +174,8 @@ class Selection if insideExistingLine firstLineDelta = -line.length # remove all leading whitespace else - firstLineDelta = delta - @editSession.indentationForBufferRow(currentBufferRow) + firstLineDelta = delta - currentBufferColumn + normalizedLines.push(@adjustIndentationForLine(line, firstLineDelta)) else normalizedLines.push(@adjustIndentationForLine(line, delta)) From b5b552737da5e383c1546f451b7baf3a6d8a00d4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 16:30:49 -0600 Subject: [PATCH 07/14] Enable indent normalization on paste --- src/app/edit-session.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 43a79914f..b655f0d49 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -201,7 +201,7 @@ class EditSession maintainPasteboard = true pasteText: -> - @insertText($native.readFromPasteboard()) + @insertText($native.readFromPasteboard(), normalizeIndent: true) undo: -> @buffer.undo(this) From 4d9b77735052bb9b028672405d8d4f7d1231a489 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 16:50:00 -0600 Subject: [PATCH 08/14] Fix `LanguageMode.proto.suggestedIndentationForBufferRow` exception for row 0 --- spec/app/language-mode-spec.coffee | 8 ++++++++ src/app/language-mode.coffee | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index c851eb77f..fb1c13067 100644 --- a/spec/app/language-mode-spec.coffee +++ b/spec/app/language-mode-spec.coffee @@ -109,6 +109,14 @@ describe "LanguageMode", -> expect(languageMode.rowRangeForFoldAtBufferRow(2)).toBeNull() expect(languageMode.rowRangeForFoldAtBufferRow(4)).toEqual [4, 7] + describe "suggestedIndentForBufferRow", -> + it "returns the suggested indentation based on auto-indent/outdent rules", -> + expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 + expect(languageMode.suggestedIndentForBufferRow(1)).toBe 2 + expect(languageMode.suggestedIndentForBufferRow(2)).toBe 4 + expect(languageMode.suggestedIndentForBufferRow(9)).toBe 2 + + describe "coffeescript", -> beforeEach -> editSession = fixturesProject.buildEditSessionForPath('coffee.coffee', autoIndent: false) diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index 4a6b3bf52..0bd67e8a1 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -92,7 +92,8 @@ class LanguageMode return currentIndentation unless increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0]) currentLine = @buffer.lineForRow(bufferRow) - precedingRow = @buffer.previousNonBlankRow(bufferRow) + return currentIndentation unless precedingRow = @buffer.previousNonBlankRow(bufferRow) + precedingLine = @buffer.lineForRow(precedingRow) desiredIndentation = @buffer.indentationForRow(precedingRow) From 937caf840040e9584d5320a71f1b55146b052584 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Oct 2012 17:17:28 -0600 Subject: [PATCH 09/14] Un-F --- spec/app/edit-session-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 799d2ad21..16e4380e9 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -705,7 +705,7 @@ describe "EditSession", -> editSession.insertText('foo', autoIndent: true) expect(buffer.indentationForRow(2)).toBe buffer.indentationForRow(1) + 2 - fdescribe "when the `normalizeIndent` option is true", -> + describe "when the `normalizeIndent` option is true", -> describe "when the inserted text contains no newlines", -> it "does not adjust the indentation level of the text", -> editSession.setCursorBufferPosition([5, 2]) From fd4b6c85ce98a1d3e5bb219cfe3fe77c86f51b23 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 24 Oct 2012 17:42:24 -0600 Subject: [PATCH 10/14] Add a global `pasteboard` object which supports metadata This metadata will be used to record the indentation level of the first line when copying multiple lines of text to the pasteboard. The pasteboard takes the md5 of the pasted content when writing, then when reading it associates the last written metadata only when the signature matches the previously written value. --- spec/app/pasteboard-spec.coffee | 17 ++ src/app/pasteboard.coffee | 16 ++ src/app/window.coffee | 2 + vendor/md5.js | 391 ++++++++++++++++++++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 spec/app/pasteboard-spec.coffee create mode 100644 src/app/pasteboard.coffee create mode 100755 vendor/md5.js diff --git a/spec/app/pasteboard-spec.coffee b/spec/app/pasteboard-spec.coffee new file mode 100644 index 000000000..8dc6624ff --- /dev/null +++ b/spec/app/pasteboard-spec.coffee @@ -0,0 +1,17 @@ +describe "Pasteboard", -> + nativePasteboard = null + beforeEach -> + nativePasteboard = 'first' + spyOn($native, 'writeToPasteboard').andCallFake (text) -> nativePasteboard = text + spyOn($native, 'readFromPasteboard').andCallFake -> nativePasteboard + + describe "write(text, metadata) and read()", -> + it "writes and reads text to/from the native pasteboard", -> + expect(pasteboard.read()).toEqual ['first'] + pasteboard.write('next') + expect(nativePasteboard).toBe 'next' + + it "returns metadata if the item on the native pasteboard matches the last written item", -> + pasteboard.write('next', {meta: 'data'}) + expect(nativePasteboard).toBe 'next' + expect(pasteboard.read()).toEqual ['next', {meta: 'data'}] diff --git a/src/app/pasteboard.coffee b/src/app/pasteboard.coffee new file mode 100644 index 000000000..6349c0a10 --- /dev/null +++ b/src/app/pasteboard.coffee @@ -0,0 +1,16 @@ +{hex_md5} = require 'md5' + +module.exports = +class Pasteboard + signatureForMetadata: null + + write: (text, metadata) -> + @signatureForMetadata = hex_md5(text) + @metadata = metadata + $native.writeToPasteboard(text) + + read: -> + text = $native.readFromPasteboard() + value = [text] + value.push(@metadata) if @signatureForMetadata == hex_md5(text) + value diff --git a/src/app/window.coffee b/src/app/window.coffee index a2484e54a..408e0a16b 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -9,6 +9,7 @@ _ = require 'underscore' $ = require 'jquery' {CoffeeScript} = require 'coffee-script' RootView = require 'root-view' +Pasteboard = require 'pasteboard' require 'jquery-extensions' require 'underscore-extensions' @@ -24,6 +25,7 @@ windowAdditions = TextMateBundle.loadAll() TextMateTheme.loadAll() @setUpKeymap() + @pasteboard = new Pasteboard $(window).on 'core:close', => @close() # This method is intended only to be run when starting a normal application diff --git a/vendor/md5.js b/vendor/md5.js new file mode 100755 index 000000000..5cb2b3319 --- /dev/null +++ b/vendor/md5.js @@ -0,0 +1,391 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); } +function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); } +function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); } +function hex_hmac_md5(k, d) + { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } +function b64_hmac_md5(k, d) + { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } +function any_hmac_md5(k, d, e) + { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); } + +// Modified by Nathan Sobo to integrate into Atom's module system +module.exports = { + hex_md5: hex_md5, + b64_md5: b64_md5, + any_md5: any_md5, + hex_hmac_md5: hex_hmac_md5, + rstr2hex: rstr2hex, + b64_hmac_md5: b64_hmac_md5, + rstr2b64: rstr2b64, + any_hmac_md5: any_hmac_md5 +}; + +/* + * Perform a simple self-test to see if the VM is working + */ +function md5_vm_test() +{ + return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; +} + +/* + * Calculate the MD5 of a raw string + */ +function rstr_md5(s) +{ + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); +} + +/* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ +function rstr_hmac_md5(key, data) +{ + var bkey = rstr2binl(key); + if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); +} + +/* + * Convert a raw string to a hex string + */ +function rstr2hex(input) +{ + try { hexcase } catch(e) { hexcase=0; } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; +} + +/* + * Convert a raw string to a base-64 string + */ +function rstr2b64(input) +{ + try { b64pad } catch(e) { b64pad=''; } + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; +} + +/* + * Convert a raw string to an arbitrary string encoding + */ +function rstr2any(input, encoding) +{ + var divisor = encoding.length; + var i, j, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. All remainders are stored for later + * use. + */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))); + var remainders = Array(full_length); + for(j = 0; j < full_length; j++) + { + quotient = Array(); + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[j] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + return output; +} + +/* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ +function str2rstr_utf8(input) +{ + var output = ""; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; +} + +/* + * Encode a string as utf-16 + */ +function str2rstr_utf16le(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; +} + +function str2rstr_utf16be(input) +{ + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; +} + +/* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ +function rstr2binl(input) +{ + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); + return output; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2rstr(input) +{ + var output = ""; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); + return output; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ +function binl_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} From 9fc439b30dfd825644bcd284e57a25ea4cb8f639 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 24 Oct 2012 17:54:15 -0600 Subject: [PATCH 11/14] Use `?` to avoid a bug caused by falsy zeroes --- src/app/language-mode.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index 0bd67e8a1..1967025e8 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -92,7 +92,8 @@ class LanguageMode return currentIndentation unless increaseIndentPattern = TextMateBundle.indentRegexForScope(scopes[0]) currentLine = @buffer.lineForRow(bufferRow) - return currentIndentation unless precedingRow = @buffer.previousNonBlankRow(bufferRow) + precedingRow = @buffer.previousNonBlankRow(bufferRow) + return currentIndentation unless precedingRow? precedingLine = @buffer.lineForRow(precedingRow) From 6f353fda62b3721675c9f865936804edf2bf37f3 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 24 Oct 2012 18:29:09 -0600 Subject: [PATCH 12/14] Add `indentBasis` option to insertText We will use this to normalize indentation on paste even when we didn't copy all the leading whitespace on the first line. --- spec/app/edit-session-spec.coffee | 107 +++++++++++++++++++++--------- src/app/selection.coffee | 26 ++++---- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 735e5b79c..8a1234f49 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -723,55 +723,102 @@ describe "EditSession", -> " bar();" ].join('\n') + removeLeadingWhitespace = (text) -> text.replace(/^\s*/, '') + describe "when the cursor is preceded only by whitespace", -> describe "when auto-indent is enabled", -> beforeEach -> editSession.setAutoIndent(true) describe "when the cursor's current column is less than the suggested indent level", -> - it "indents all lines relative to the suggested indent", -> - editSession.insertText('\n xx') - editSession.setCursorBufferPosition([3, 1]) - editSession.insertText(text, normalizeIndent: true) + describe "when the indentBasis is inferred from the first line", -> + it "indents all lines relative to the suggested indent", -> + editSession.insertText('\n xx') + editSession.setCursorBufferPosition([3, 1]) + editSession.insertText(text, normalizeIndent: true) - expect(editSession.lineForBufferRow(3)).toBe " while (true) {" - expect(editSession.lineForBufferRow(4)).toBe " foo();" - expect(editSession.lineForBufferRow(5)).toBe " }" - expect(editSession.lineForBufferRow(6)).toBe " bar();xx" + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();xx" + + describe "when an indentBasis is provided", -> + it "indents all lines relative to the suggested indent", -> + editSession.insertText('\n xx') + editSession.setCursorBufferPosition([3, 1]) + editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 4) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();xx" describe "when the cursor's current column is greater than the suggested indent level", -> - it "preserves the current indent level, indenting all lines relative to it", -> - editSession.insertText('\n ') - editSession.insertText(text, normalizeIndent: true) + describe "when the indentBasis is inferred from the first line", -> + it "preserves the current indent level, indenting all lines relative to it", -> + editSession.insertText('\n ') + editSession.insertText(text, normalizeIndent: true) - expect(editSession.lineForBufferRow(3)).toBe " while (true) {" - expect(editSession.lineForBufferRow(4)).toBe " foo();" - expect(editSession.lineForBufferRow(5)).toBe " }" - expect(editSession.lineForBufferRow(6)).toBe " bar();" + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();" + + describe "when an indentBasis is provided", -> + it "preserves the current indent level, indenting all lines relative to it", -> + editSession.insertText('\n ') + editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 4) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe " bar();" describe "if auto-indent is disabled", -> beforeEach -> expect(editSession.autoIndent).toBeFalsy() - it "always normalizes indented lines to the cursor's current indentation level", -> - editSession.insertText('\n ') - editSession.insertText(text, normalizeIndent: true) + describe "when the indentBasis is inferred from the first line", -> + it "always normalizes indented lines to the cursor's current indentation level", -> + editSession.insertText('\n ') + editSession.insertText(text, normalizeIndent: true) - expect(editSession.lineForBufferRow(3)).toBe " while (true) {" - expect(editSession.lineForBufferRow(4)).toBe " foo();" - expect(editSession.lineForBufferRow(5)).toBe " }" - expect(editSession.lineForBufferRow(6)).toBe "bar();" + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" + expect(editSession.lineForBufferRow(6)).toBe "bar();" + + describe "when an indentBasis is provided", -> + it "always normalizes indented lines to the cursor's current indentation level", -> + editSession.insertText('\n ') + editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 4) + + expect(editSession.lineForBufferRow(3)).toBe " while (true) {" + expect(editSession.lineForBufferRow(4)).toBe " foo();" + expect(editSession.lineForBufferRow(5)).toBe " }" describe "when the cursor is preceded by non-whitespace characters", -> - it "normalizes the indentation level of all lines based on the level of the existing first line", -> - editSession.setAutoIndent(true) - editSession.buffer.delete([[2, 0], [2, 2]]) - editSession.insertText(text, normalizeIndent:true) + describe "when the indentBasis is inferred from the first line", -> + it "normalizes the indentation level of all lines based on the level of the existing first line", -> + editSession.setAutoIndent(true) + editSession.buffer.delete([[2, 0], [2, 2]]) + editSession.insertText(text, normalizeIndent:true) - expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {" - expect(editSession.lineForBufferRow(3)).toBe " foo();" - expect(editSession.lineForBufferRow(4)).toBe " }" - expect(editSession.lineForBufferRow(5)).toBe "bar();" + expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {" + expect(editSession.lineForBufferRow(3)).toBe " foo();" + expect(editSession.lineForBufferRow(4)).toBe " }" + expect(editSession.lineForBufferRow(5)).toBe "bar();" + + describe "when an indentBasis is provided", -> + it "normalizes the indentation level of all lines based on the level of the existing first line", -> + editSession.setAutoIndent(true) + editSession.buffer.delete([[2, 0], [2, 2]]) + editSession.insertText(removeLeadingWhitespace(text), normalizeIndent:true, indentBasis: 4) + + expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {" + expect(editSession.lineForBufferRow(3)).toBe " foo();" + expect(editSession.lineForBufferRow(4)).toBe " }" + expect(editSession.lineForBufferRow(5)).toBe "bar();" describe ".insertNewline()", -> describe "when there is a single cursor", -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index fcd815603..bacc269af 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -137,7 +137,7 @@ class Selection oldBufferRange = @getBufferRange() @editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row) wasReversed = @isReversed() - text = @normalizeIndent(text) if options.normalizeIndent + text = @normalizeIndent(text, options) if options.normalizeIndent @clear() newBufferRange = @editSession.buffer.change(oldBufferRange, text) @cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed @@ -148,37 +148,37 @@ class Selection else @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) - normalizeIndent: (text) -> + normalizeIndent: (text, options) -> return text unless /\n/.test(text) currentBufferRow = @cursor.getBufferRow() currentBufferColumn = @cursor.getBufferColumn() lines = text.split('\n') + currentBasis = options.indentBasis ? lines[0].match(/\s*/)[0].length + lines[0] = lines[0].replace(/^\s*/, '') # strip leading space from first line + normalizedLines = [] textPrecedingCursor = @editSession.buffer.getTextInRange([[currentBufferRow, 0], [currentBufferRow, currentBufferColumn]]) insideExistingLine = textPrecedingCursor.match(/\S/) if insideExistingLine - desiredBase = @editSession.indentationForBufferRow(currentBufferRow) + desiredBasis = @editSession.indentationForBufferRow(currentBufferRow) else if @editSession.autoIndent - desiredBase = @editSession.suggestedIndentForBufferRow(currentBufferRow) + desiredBasis = @editSession.suggestedIndentForBufferRow(currentBufferRow) else - desiredBase = currentBufferColumn - - currentBase = lines[0].match(/\s*/)[0].length - delta = desiredBase - currentBase + desiredBasis = currentBufferColumn for line, i in lines if i == 0 if insideExistingLine - firstLineDelta = -line.length # remove all leading whitespace + delta = 0 else - firstLineDelta = delta - currentBufferColumn - - normalizedLines.push(@adjustIndentationForLine(line, firstLineDelta)) + delta = desiredBasis - currentBufferColumn else - normalizedLines.push(@adjustIndentationForLine(line, delta)) + delta = desiredBasis - currentBasis + + normalizedLines.push(@adjustIndentationForLine(line, delta)) normalizedLines.join('\n') From d3b7fbb37b0b00f6b7863063dcc7864fa47b70ce Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 24 Oct 2012 18:35:55 -0600 Subject: [PATCH 13/14] Use `window.pasteboard` in cut/copy/paste --- src/app/edit-session.coffee | 2 +- src/app/selection.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index ce26b2e9d..b7f71dd69 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -202,7 +202,7 @@ class EditSession maintainPasteboard = true pasteText: -> - @insertText($native.readFromPasteboard(), normalizeIndent: true) + @insertText(pasteboard.read()[0], normalizeIndent: true) undo: -> @buffer.undo(this) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index bacc269af..d8d3cca43 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -269,8 +269,8 @@ class Selection copy: (maintainPasteboard=false) -> return if @isEmpty() text = @editSession.buffer.getTextInRange(@getBufferRange()) - text = $native.readFromPasteboard() + "\n" + text if maintainPasteboard - $native.writeToPasteboard text + text = pasteboard.read()[0] + "\n" + text if maintainPasteboard + pasteboard.write(text) fold: -> range = @getBufferRange() From e53410b5fd2ff642bc461ed6a1ffbeea986113bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 25 Oct 2012 10:44:54 -0600 Subject: [PATCH 14/14] Add indentBasis metadata to pasteboard when copying text This allows indent to be normalized properly even if the leading whitespace isn't copied from the first line. --- spec/app/edit-session-spec.coffee | 12 ++++++++++++ src/app/edit-session.coffee | 3 ++- src/app/selection.coffee | 9 +++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 8a1234f49..16a6050ff 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1328,6 +1328,18 @@ describe "EditSession", -> expect(editSession.buffer.lineForRow(0)).toBe "var first = function () {" expect(buffer.lineForRow(1)).toBe " var first = function(items) {" + it "preserves the indent level when copying and pasting multiple lines", -> + editSession.setAutoIndent(true) + editSession.setSelectedBufferRange([[4, 4], [7, 5]]) + editSession.copySelectedText() + editSession.setCursorBufferPosition([10, 0]) + editSession.pasteText() + + expect(editSession.lineForBufferRow(10)).toBe " while(items.length > 0) {" + expect(editSession.lineForBufferRow(11)).toBe " current = items.shift();" + expect(editSession.lineForBufferRow(12)).toBe " current < pivot ? left.push(current) : right.push(current);" + expect(editSession.lineForBufferRow(13)).toBe " }" + describe ".indentSelectedRows()", -> beforeEach -> editSession.tabLength = 2 diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index b7f71dd69..2cca596df 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -202,7 +202,8 @@ class EditSession maintainPasteboard = true pasteText: -> - @insertText(pasteboard.read()[0], normalizeIndent: true) + [text, metadata] = pasteboard.read() + @insertText(text, _.extend(metadata ? {}, normalizeIndent: true)) undo: -> @buffer.undo(this) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index d8d3cca43..ca93b4538 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -269,8 +269,13 @@ class Selection copy: (maintainPasteboard=false) -> return if @isEmpty() text = @editSession.buffer.getTextInRange(@getBufferRange()) - text = pasteboard.read()[0] + "\n" + text if maintainPasteboard - pasteboard.write(text) + if maintainPasteboard + [currentText, metadata] = pasteboard.read() + text = currentText + '\n' + text + else + metadata = { indentBasis: @editSession.indentationForBufferRow(@getBufferRange().start.row) } + + pasteboard.write(text, metadata) fold: -> range = @getBufferRange()