diff --git a/.gitmodules b/.gitmodules
index caa40d8c9..6495d34a2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -43,3 +43,6 @@
[submodule "vendor/packages/property-list.tmbundle"]
path = vendor/packages/property-list.tmbundle
url = https://github.com/textmate/property-list.tmbundle.git
+[submodule "vendor/packages/python.tmbundle"]
+ path = vendor/packages/python.tmbundle
+ url = https://github.com/textmate/python.tmbundle
diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee
index f6ae76471..2528fba32 100644
--- a/spec/app/buffer-spec.coffee
+++ b/spec/app/buffer-spec.coffee
@@ -465,6 +465,14 @@ describe 'Buffer', ->
range = [[2,10], [4,10]]
expect(buffer.getTextInRange(range)).toBe "ems.length <= 1) return items;\n var pivot = items.shift(), current, left = [], right = [];\n while("
+ describe "when the range starts before the start of the buffer", ->
+ it "clips the range to the start of the buffer", ->
+ expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe buffer.lineForRow(0)
+
+ describe "when the range ends after the end of the buffer", ->
+ it "clips the range to the end of the buffer", ->
+ expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe buffer.lineForRow(12)
+
describe ".scanInRange(range, regex, fn)", ->
describe "when given a regex with a ignore case flag", ->
it "does a case-insensitive search", ->
diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee
index cd7ca484c..7444a3859 100644
--- a/spec/app/display-buffer-spec.coffee
+++ b/spec/app/display-buffer-spec.coffee
@@ -495,6 +495,20 @@ describe "DisplayBuffer", ->
expect(displayBuffer.lineForRow(8).text).toMatch /^9-+/
expect(displayBuffer.lineForRow(10).fold).toBeDefined()
+ describe "when the line being deleted preceeds a fold", ->
+ describe "when the command is undone", ->
+ it "restores the line and preserves the fold", ->
+ editSession.setCursorBufferPosition([4])
+ editSession.foldCurrentRow()
+ expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy()
+ editSession.setCursorBufferPosition([3])
+ editSession.deleteLine()
+ expect(editSession.isFoldedAtScreenRow(3)).toBeTruthy()
+ expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {'
+ editSession.undo()
+ expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy()
+ expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+
describe ".clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", ->
beforeEach ->
displayBuffer.setSoftWrapColumn(50)
diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee
index 01ab8648e..3e2167784 100644
--- a/spec/app/edit-session-spec.coffee
+++ b/spec/app/edit-session-spec.coffee
@@ -276,17 +276,22 @@ describe "EditSession", ->
editSession.moveCursorToBeginningOfWord()
expect(cursor1.getBufferPosition()).toEqual [0, 4]
- expect(cursor2.getBufferPosition()).toEqual [1, 10]
+ expect(cursor2.getBufferPosition()).toEqual [1, 11]
expect(cursor3.getBufferPosition()).toEqual [2, 39]
it "does not fail at position [0, 0]", ->
editSession.setCursorBufferPosition([0, 0])
editSession.moveCursorToBeginningOfWord()
- it "works when the preceding line is blank", ->
+ it "treats lines with only whitespace as a word", ->
+ editSession.setCursorBufferPosition([11, 0])
+ editSession.moveCursorToBeginningOfWord()
+ expect(editSession.getCursorBufferPosition()).toEqual [10, 0]
+
+ it "works when the current line is blank", ->
editSession.setCursorBufferPosition([10, 0])
editSession.moveCursorToBeginningOfWord()
- expect(editSession.getCursorBufferPosition()).toEqual [9, 0]
+ expect(editSession.getCursorBufferPosition()).toEqual [9, 2]
describe ".moveCursorToEndOfWord()", ->
it "moves the cursor to the end of the word", ->
@@ -298,8 +303,8 @@ describe "EditSession", ->
editSession.moveCursorToEndOfWord()
expect(cursor1.getBufferPosition()).toEqual [0, 13]
- expect(cursor2.getBufferPosition()).toEqual [1, 13]
- expect(cursor3.getBufferPosition()).toEqual [3, 4]
+ expect(cursor2.getBufferPosition()).toEqual [1, 12]
+ expect(cursor3.getBufferPosition()).toEqual [3, 7]
it "does not blow up when there is no next word", ->
editSession.setCursorBufferPosition [Infinity, Infinity]
@@ -307,6 +312,17 @@ describe "EditSession", ->
editSession.moveCursorToEndOfWord()
expect(editSession.getCursorBufferPosition()).toEqual endPosition
+ it "treats lines with only whitespace as a word", ->
+ editSession.setCursorBufferPosition([9, 4])
+ editSession.moveCursorToEndOfWord()
+ expect(editSession.getCursorBufferPosition()).toEqual [10, 0]
+
+ it "works when the current line is blank", ->
+ editSession.setCursorBufferPosition([10, 0])
+ editSession.moveCursorToEndOfWord()
+ expect(editSession.getCursorBufferPosition()).toEqual [11, 8]
+
+
describe ".getCurrentParagraphBufferRange()", ->
it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", ->
buffer.setText """
@@ -518,13 +534,13 @@ describe "EditSession", ->
expect(editSession.getCursors().length).toBe 2
[cursor1, cursor2] = editSession.getCursors()
expect(cursor1.getBufferPosition()).toEqual [0,4]
- expect(cursor2.getBufferPosition()).toEqual [3,44]
+ expect(cursor2.getBufferPosition()).toEqual [3,47]
expect(editSession.getSelections().length).toBe 2
[selection1, selection2] = editSession.getSelections()
expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]]
expect(selection1.isReversed()).toBeTruthy()
- expect(selection2.getBufferRange()).toEqual [[3,44], [3,49]]
+ expect(selection2.getBufferRange()).toEqual [[3,47], [3,49]]
expect(selection2.isReversed()).toBeTruthy()
describe ".selectToEndOfWord()", ->
@@ -537,40 +553,49 @@ describe "EditSession", ->
expect(editSession.getCursors().length).toBe 2
[cursor1, cursor2] = editSession.getCursors()
expect(cursor1.getBufferPosition()).toEqual [0,13]
- expect(cursor2.getBufferPosition()).toEqual [3,51]
+ expect(cursor2.getBufferPosition()).toEqual [3,50]
expect(editSession.getSelections().length).toBe 2
[selection1, selection2] = editSession.getSelections()
expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]]
expect(selection1.isReversed()).toBeFalsy()
- expect(selection2.getBufferRange()).toEqual [[3,48], [3,51]]
+ expect(selection2.getBufferRange()).toEqual [[3,48], [3,50]]
expect(selection2.isReversed()).toBeFalsy()
describe ".selectWord()", ->
- describe "when the cursor is inside a word", ->
- it "selects the entire word", ->
- editSession.setCursorScreenPosition([0, 8])
- editSession.selectWord()
- expect(editSession.getSelectedText()).toBe 'quicksort'
+ describe "when the cursor is inside a word", ->
+ it "selects the entire word", ->
+ editSession.setCursorScreenPosition([0, 8])
+ editSession.selectWord()
+ expect(editSession.getSelectedText()).toBe 'quicksort'
- describe "when the cursor is between two words", ->
- it "selects both words", ->
- editSession.setCursorScreenPosition([0, 4])
- editSession.selectWord()
- expect(editSession.getSelectedText()).toBe ' quicksort'
+ describe "when the cursor is between two words", ->
+ it "selects the word the cursor is on", ->
+ editSession.setCursorScreenPosition([0, 4])
+ editSession.selectWord()
+ expect(editSession.getSelectedText()).toBe 'quicksort'
- describe "when the cursor is inside a region of whitespace", ->
- it "selects the whitespace region", ->
- editSession.setCursorScreenPosition([5, 2])
- editSession.selectWord()
- expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]]
+ editSession.setCursorScreenPosition([0, 3])
+ editSession.selectWord()
+ expect(editSession.getSelectedText()).toBe 'var'
- describe "when the cursor is at the end of the text", ->
- it "select the previous word", ->
- editSession.buffer.append 'word'
- editSession.moveCursorToBottom()
- editSession.selectWord()
- expect(editSession.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]]
+
+ describe "when the cursor is inside a region of whitespace", ->
+ it "selects the whitespace region", ->
+ editSession.setCursorScreenPosition([5, 2])
+ editSession.selectWord()
+ expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]]
+
+ editSession.setCursorScreenPosition([5, 0])
+ editSession.selectWord()
+ expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]]
+
+ describe "when the cursor is at the end of the text", ->
+ it "select the previous word", ->
+ editSession.buffer.append 'word'
+ editSession.moveCursorToBottom()
+ editSession.selectWord()
+ expect(editSession.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]]
describe ".setSelectedBufferRanges(ranges)", ->
it "clears existing selections and creates selections for each of the given ranges", ->
@@ -1110,25 +1135,26 @@ describe "EditSession", ->
describe "when no text is selected", ->
it "deletes all text between the cursor and the beginning of the word", ->
editSession.setCursorBufferPosition([1, 24])
- editSession.addCursorAtBufferPosition([2, 5])
+ editSession.addCursorAtBufferPosition([3, 5])
[cursor1, cursor2] = editSession.getCursors()
editSession.backspaceToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {'
- expect(buffer.lineForRow(2)).toBe ' f (items.length <= 1) return items;'
+ expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 22]
- expect(cursor2.getBufferPosition()).toEqual [2, 4]
+ expect(cursor2.getBufferPosition()).toEqual [3, 4]
editSession.backspaceToBeginningOfWord()
expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {'
- expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;'
+ expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 21]
- expect(cursor2.getBufferPosition()).toEqual [2, 0]
+ expect(cursor2.getBufferPosition()).toEqual [2, 39]
editSession.backspaceToBeginningOfWord()
- expect(buffer.lineForRow(1)).toBe ' var sort = emsf (items.length <= 1) return items;'
+ expect(buffer.lineForRow(1)).toBe ' var sort = ems) {'
+ expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];'
expect(cursor1.getBufferPosition()).toEqual [1, 13]
- expect(cursor2.getBufferPosition()).toEqual [1, 16]
+ expect(cursor2.getBufferPosition()).toEqual [2, 34]
describe "when text is selected", ->
it "deletes only selected text", ->
@@ -1296,7 +1322,7 @@ describe "EditSession", ->
expect(cursor2.getBufferPosition()).toEqual [2, 5]
editSession.deleteToEndOfWord()
- expect(buffer.lineForRow(1)).toBe ' var sort = function(it'
+ expect(buffer.lineForRow(1)).toBe ' var sort = function(it {'
expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;'
expect(cursor1.getBufferPosition()).toEqual [1, 24]
expect(cursor2.getBufferPosition()).toEqual [2, 5]
diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee
index 5a453403c..bfac68462 100644
--- a/spec/app/editor-spec.coffee
+++ b/spec/app/editor-spec.coffee
@@ -1741,6 +1741,11 @@ describe "Editor", ->
fold.destroy()
expect(editor.gutter.find('.line-number').length).toBe 13
+ it "styles folded line numbers", ->
+ editor.createFold(3, 5)
+ expect(editor.gutter.find('.line-number.fold').length).toBe 1
+ expect(editor.gutter.find('.line-number.fold:eq(0)').text()).toBe '4'
+
describe "when the scrollView is scrolled to the right", ->
it "adds a drop shadow to the gutter", ->
editor.attachToDom()
@@ -1909,16 +1914,18 @@ describe "Editor", ->
editor.attachToDom()
describe "when a fold-selection event is triggered", ->
- it "folds the lines covered by the selection into a single line with a fold class", ->
+ it "folds the lines covered by the selection into a single line with a fold class and marker", ->
editor.getSelection().setBufferRange(new Range([4, 29], [7, 4]))
editor.trigger 'editor:fold-selection'
expect(editor.renderedLines.find('.line:eq(4)')).toHaveClass('fold')
+ expect(editor.renderedLines.find('.line:eq(4) > .fold-marker')).toExist()
expect(editor.renderedLines.find('.line:eq(5)').text()).toBe '8'
expect(editor.getSelection().isEmpty()).toBeTruthy()
expect(editor.getCursorScreenPosition()).toEqual [5, 0]
+
describe "when a fold placeholder line is clicked", ->
it "removes the associated fold and places the cursor at its beginning", ->
editor.setCursorBufferPosition([3,0])
@@ -1927,6 +1934,7 @@ describe "Editor", ->
editor.find('.fold.line').mousedown()
expect(editor.find('.fold')).not.toExist()
+ expect(editor.find('.fold-marker')).not.toExist()
expect(editor.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/
expect(editor.renderedLines.find('.line:eq(5)').text()).toMatch /5/
@@ -2271,3 +2279,196 @@ describe "Editor", ->
it "copies the absolute path to the editor's file to the pasteboard", ->
editor.trigger 'editor:copy-path'
expect(pasteboard.read()[0]).toBe editor.getPath()
+
+ describe "when editor:move-line-up is triggered", ->
+ describe "when there is no selection", ->
+ it "moves the line where the cursor is up", ->
+ editor.setCursorBufferPosition([1,0])
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {'
+ expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {'
+
+ it "moves the cursor to the new row and the same column", ->
+ editor.setCursorBufferPosition([1,2])
+ editor.trigger 'editor:move-line-up'
+ expect(editor.getCursorBufferPosition()).toEqual [0,2]
+
+ describe "where there is a selection", ->
+ describe "when the selection falls inside the line", ->
+ it "maintains the selection", ->
+ editor.setSelectedBufferRange([[1, 2], [1, 5]])
+ expect(editor.getSelectedText()).toBe 'var'
+ editor.trigger 'editor:move-line-up'
+ expect(editor.getSelectedBufferRange()).toEqual [[0, 2], [0, 5]]
+ expect(editor.getSelectedText()).toBe 'var'
+
+ describe "where there are multiple lines selected", ->
+ it "moves the selected lines up", ->
+ editor.setSelectedBufferRange([[2, 0], [3, Infinity]])
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {'
+ expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;'
+ expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(buffer.lineForRow(3)).toBe ' var sort = function(items) {'
+
+ it "maintains the selection", ->
+ editor.setSelectedBufferRange([[2, 0], [3, 62]])
+ editor.trigger 'editor:move-line-up'
+ expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 62]]
+
+ describe "when the last line is selected", ->
+ it "moves the selected line up", ->
+ editor.setSelectedBufferRange([[12, 0], [12, Infinity]])
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(11)).toBe '};'
+ expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));'
+
+ describe "when the last two lines are selected", ->
+ it "moves the selected lines up", ->
+ editor.setSelectedBufferRange([[11, 0], [12, Infinity]])
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));'
+ expect(buffer.lineForRow(11)).toBe '};'
+ expect(buffer.lineForRow(12)).toBe ''
+
+ describe "when the cursor is on the first line", ->
+ it "does not move the line", ->
+ editor.setCursorBufferPosition([0,0])
+ originalText = editor.getText()
+ editor.trigger 'editor:move-line-up'
+ expect(editor.getText()).toBe originalText
+
+ describe "when the cursor is on the trailing newline", ->
+ it "does not move the line", ->
+ editor.moveCursorToBottom()
+ editor.insertNewline()
+ editor.moveCursorToBottom()
+ originalText = editor.getText()
+ editor.trigger 'editor:move-line-up'
+ expect(editor.getText()).toBe originalText
+
+ describe "when the cursor is on a folded line", ->
+ it "moves all lines in the fold up and preserves the fold", ->
+ editor.setCursorBufferPosition([4, 0])
+ editor.foldCurrentRow()
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {'
+ expect(buffer.lineForRow(7)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [3, 0]]
+ expect(editor.isFoldedAtScreenRow(3)).toBeTruthy()
+
+ describe "when the selection contains a folded and unfolded line", ->
+ it "moves the selected lines up and preserves the fold", ->
+ editor.setCursorBufferPosition([4, 0])
+ editor.foldCurrentRow()
+ editor.setCursorBufferPosition([3, 4])
+ editor.selectDown()
+ expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {'
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 4], [3, 0]]
+ expect(editor.isFoldedAtScreenRow(3)).toBeTruthy()
+
+ describe "when an entire line is selected including the newline", ->
+ it "moves the selected line up", ->
+ editor.setCursorBufferPosition([1])
+ editor.selectToEndOfLine()
+ editor.selectRight()
+ editor.trigger 'editor:move-line-up'
+ expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {'
+ expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {'
+
+ describe "when editor:move-line-down is triggered", ->
+ describe "when there is no selection", ->
+ it "moves the line where the cursor is down", ->
+ editor.setCursorBufferPosition([0, 0])
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {'
+ expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {'
+
+ it "moves the cursor to the new row and the same column", ->
+ editor.setCursorBufferPosition([0, 2])
+ editor.trigger 'editor:move-line-down'
+ expect(editor.getCursorBufferPosition()).toEqual [1, 2]
+
+ describe "when the cursor is on the last line", ->
+ it "does not move the line", ->
+ editor.moveCursorToBottom()
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(12)).toBe '};'
+ expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]]
+
+ describe "when the cursor is on the second to last line", ->
+ it "moves the line down", ->
+ editor.setCursorBufferPosition([11, 0])
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(11)).toBe '};'
+ expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));'
+ expect(buffer.lineForRow(13)).toBeUndefined()
+
+ describe "when the cursor is on the second to last line and the last line is empty", ->
+ it "does not move the line", ->
+ editor.moveCursorToBottom()
+ editor.insertNewline()
+ editor.setCursorBufferPosition([12, 2])
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(12)).toBe '};'
+ expect(buffer.lineForRow(13)).toBe ''
+ expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]]
+
+ describe "where there is a selection", ->
+ describe "when the selection falls inside the line", ->
+ it "maintains the selection", ->
+ editor.setSelectedBufferRange([[1, 2], [1, 5]])
+ expect(editor.getSelectedText()).toBe 'var'
+ editor.trigger 'editor:move-line-down'
+ expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 5]]
+ expect(editor.getSelectedText()).toBe 'var'
+
+ describe "where there are multiple lines selected", ->
+ it "moves the selected lines down", ->
+ editor.setSelectedBufferRange([[2, 0], [3, Infinity]])
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(2)).toBe ' while(items.length > 0) {'
+ expect(buffer.lineForRow(3)).toBe ' if (items.length <= 1) return items;'
+ expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(buffer.lineForRow(5)).toBe ' current = items.shift();'
+
+ it "maintains the selection", ->
+ editor.setSelectedBufferRange([[2, 0], [3, 62]])
+ editor.trigger 'editor:move-line-down'
+ expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [4, 62]]
+
+ describe "when the cursor is on a folded line", ->
+ it "moves all lines in the fold down and preserves the fold", ->
+ editor.setCursorBufferPosition([4, 0])
+ editor.foldCurrentRow()
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(4)).toBe ' return sort(left).concat(pivot).concat(sort(right));'
+ expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {'
+ expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 0]]
+ expect(editor.isFoldedAtScreenRow(5)).toBeTruthy()
+
+ describe "when the selection contains a folded and unfolded line", ->
+ it "moves the selected lines down and preserves the fold", ->
+ editor.setCursorBufferPosition([4, 0])
+ editor.foldCurrentRow()
+ editor.setCursorBufferPosition([3, 4])
+ editor.selectDown()
+ expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(3)).toBe ' return sort(left).concat(pivot).concat(sort(right));'
+ expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {'
+ expect(editor.getSelectedBufferRange()).toEqual [[4, 4], [5, 0]]
+ expect(editor.isFoldedAtScreenRow(5)).toBeTruthy()
+
+ describe "when an entire line is selected including the newline", ->
+ it "moves the selected line down", ->
+ editor.setCursorBufferPosition([1])
+ editor.selectToEndOfLine()
+ editor.selectRight()
+ editor.trigger 'editor:move-line-down'
+ expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;'
+ expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {'
diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee
index 07f4abc77..a646bbf0f 100644
--- a/src/app/buffer.coffee
+++ b/src/app/buffer.coffee
@@ -121,9 +121,9 @@ class Buffer
new Range([0, 0], [@getLastRow(), @getLastLine().length])
getTextInRange: (range) ->
- range = Range.fromObject(range)
+ range = @clipRange(range)
if range.start.row == range.end.row
- return @lines[range.start.row][range.start.column...range.end.column]
+ return @lineForRow(range.start.row)[range.start.column...range.end.column]
multipleLines = []
multipleLines.push @lineForRow(range.start.row)[range.start.column..] # first line
@@ -200,7 +200,7 @@ class Buffer
startPoint = [start, 0]
endPoint = [end + 1, 0]
- @change(new Range(startPoint, endPoint), '')
+ @delete(new Range(startPoint, endPoint))
append: (text) ->
@insert(@getEofPosition(), text)
@@ -226,6 +226,10 @@ class Buffer
new Point(row, column)
+ clipRange: (range) ->
+ range = Range.fromObject(range)
+ new Range(@clipPosition(range.start), @clipPosition(range.end))
+
prefixAndSuffixForRange: (range) ->
prefix: @lines[range.start.row][0...range.start.column]
suffix: @lines[range.end.row][range.end.column..]
diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee
index 43bcbb1ea..fbdf78e8f 100644
--- a/src/app/cursor.coffee
+++ b/src/app/cursor.coffee
@@ -9,7 +9,6 @@ class Cursor
screenPosition: null
bufferPosition: null
goalColumn: null
- wordRegex: /(\w+)|([^\w\n]+)/g
visible: true
needsAutoscroll: null
@@ -58,9 +57,18 @@ class Cursor
isVisible: -> @visible
+ wordRegExp: ->
+ nonWordCharacters = config.get("editor.nonWordCharacters")
+ new RegExp("^[\t ]*$|[^\\s#{_.escapeRegExp(nonWordCharacters)}]+|[#{_.escapeRegExp(nonWordCharacters)}]+", "g")
+
isLastCursor: ->
this == @editSession.getCursor()
+ isSurroundedByWhitespace: ->
+ {row, column} = @getBufferPosition()
+ range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]]
+ /^\s+$/.test @editSession.getTextInBufferRange(range)
+
clearAutoscroll: ->
@needsAutoscroll = null
@@ -149,14 +157,16 @@ class Cursor
allowPrevious = options.allowPrevious ? true
currentBufferPosition = @getBufferPosition()
previousNonBlankRow = @editSession.buffer.previousNonBlankRow(currentBufferPosition.row)
- previousLinesRange = [[previousNonBlankRow, 0], currentBufferPosition]
+ range = [[previousNonBlankRow, 0], currentBufferPosition]
- beginningOfWordPosition = currentBufferPosition
- @editSession.backwardsScanInRange (options.wordRegex || @wordRegex), previousLinesRange, (match, matchRange, { stop }) =>
+ beginningOfWordPosition = null
+ @editSession.backwardsScanInRange (options.wordRegex ? @wordRegExp()), range, (match, matchRange, { stop }) =>
if matchRange.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
beginningOfWordPosition = matchRange.start
- stop()
- beginningOfWordPosition
+ if not beginningOfWordPosition?.isEqual(currentBufferPosition)
+ stop()
+
+ beginningOfWordPosition or currentBufferPosition
getEndOfCurrentWordBufferPosition: (options = {}) ->
allowNext = options.allowNext ? true
@@ -164,11 +174,12 @@ class Cursor
range = [currentBufferPosition, @editSession.getEofBufferPosition()]
endOfWordPosition = null
- @editSession.scanInRange (options.wordRegex || @wordRegex), range, (match, matchRange, { stop }) =>
- endOfWordPosition = matchRange.end
- if not allowNext and matchRange.start.isGreaterThan(currentBufferPosition)
- endOfWordPosition = currentBufferPosition
- stop()
+ @editSession.scanInRange (options.wordRegex ? @wordRegExp()), range, (match, matchRange, { stop }) =>
+ if matchRange.start.isLessThanOrEqual(currentBufferPosition) or allowNext
+ endOfWordPosition = matchRange.end
+ if not endOfWordPosition?.isEqual(currentBufferPosition)
+ stop()
+
endOfWordPosition or currentBufferPosition
getCurrentWordBufferRange: (options={}) ->
diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee
index 7d77ac731..7b6b85e06 100644
--- a/src/app/edit-session.coffee
+++ b/src/app/edit-session.coffee
@@ -311,6 +311,10 @@ class EditSession
fold.destroy()
@setCursorBufferPosition([fold.startRow, 0])
+ isFoldedAtBufferRow: (bufferRow) ->
+ screenRow = @screenPositionForBufferPosition([bufferRow]).row
+ @isFoldedAtScreenRow(screenRow)
+
isFoldedAtScreenRow: (screenRow) ->
@lineForScreenRow(screenRow)?.fold?
@@ -338,6 +342,81 @@ class EditSession
toggleLineCommentsForBufferRows: (start, end) ->
@languageMode.toggleLineCommentsForBufferRows(start, end)
+ moveLineUp: ->
+ selection = @getSelectedBufferRange()
+ return if selection.start.row is 0
+ lastRow = @buffer.getLastRow()
+ return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is ''
+
+ @transact =>
+ foldedRows = []
+ rows = [selection.start.row..selection.end.row]
+ if selection.start.row isnt selection.end.row and selection.end.column is 0
+ rows.pop() unless @isFoldedAtScreenRow(@screenPositionForBufferPosition(selection.end).row)
+ for row in rows
+ screenRow = @screenPositionForBufferPosition([row]).row
+ if @isFoldedAtScreenRow(screenRow)
+ bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]])
+ startRow = bufferRange.start.row
+ endRow = bufferRange.end.row - 1
+ foldedRows.push(endRow - 1)
+ else
+ startRow = row
+ endRow = row
+
+ endPosition = Point.min([endRow + 1], @buffer.getEofPosition())
+ lines = @buffer.getTextInRange([[startRow], endPosition])
+ if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row)
+ lines = "#{lines}\n"
+ @buffer.deleteRows(startRow, endRow)
+ @buffer.insert([startRow - 1], lines)
+
+ @foldBufferRow(foldedRow) for foldedRow in foldedRows
+
+ newStartPosition = [selection.start.row - 1, selection.start.column]
+ newEndPosition = [selection.end.row - 1, selection.end.column]
+ @setSelectedBufferRange([newStartPosition, newEndPosition], preserveFolds: true)
+
+ moveLineDown: ->
+ selection = @getSelectedBufferRange()
+ lastRow = @buffer.getLastRow()
+ return if selection.end.row is lastRow
+ return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is ''
+
+ @transact =>
+ foldedRows = []
+ rows = [selection.end.row..selection.start.row]
+ if selection.start.row isnt selection.end.row and selection.end.column is 0
+ rows.shift() unless @isFoldedAtScreenRow(@screenPositionForBufferPosition(selection.end).row)
+ for row in rows
+ screenRow = @screenPositionForBufferPosition([row]).row
+ if @isFoldedAtScreenRow(screenRow)
+ bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]])
+ startRow = bufferRange.start.row
+ endRow = bufferRange.end.row - 1
+ foldedRows.push(endRow + 1)
+ else
+ startRow = row
+ endRow = row
+
+ if endRow + 1 is lastRow
+ endPosition = [endRow, @buffer.lineLengthForRow(endRow)]
+ else
+ endPosition = [endRow + 1]
+ lines = @buffer.getTextInRange([[startRow], endPosition])
+ @buffer.deleteRows(startRow, endRow)
+ insertPosition = Point.min([startRow + 1], @buffer.getEofPosition())
+ if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0
+ lines = "\n#{lines}"
+ @buffer.insert(insertPosition, lines)
+
+ @foldBufferRow(foldedRow) for foldedRow in foldedRows
+
+ newStartPosition = [selection.start.row + 1, selection.start.column]
+ newEndPosition = [selection.end.row + 1, selection.end.column]
+ @setSelectedBufferRange([newStartPosition, newEndPosition], preserveFolds: true)
+
+
mutateSelectedText: (fn) ->
@transact => fn(selection) for selection in @getSelections()
diff --git a/src/app/editor.coffee b/src/app/editor.coffee
index f5fdd05b3..1c3bb4334 100644
--- a/src/app/editor.coffee
+++ b/src/app/editor.coffee
@@ -19,6 +19,7 @@ class Editor extends View
autosave: false
autoIndent: true
autoIndentOnPaste: false
+ nonWordCharacters: "./\\()\"’-_:,.;<>~!@#$%^&*|+=[]{}`~?"
@content: (params) ->
@div class: @classes(params), tabindex: -1, =>
@@ -183,6 +184,8 @@ class Editor extends View
'editor:close-all-edit-sessions': @destroyAllEditSessions
'editor:select-grammar': @selectGrammar
'editor:copy-path': @copyPathToPasteboard
+ 'editor:move-line-up': @moveLineUp
+ 'editor:move-line-down': @moveLineDown
documentation = {}
for name, method of editorBindings
@@ -204,6 +207,8 @@ class Editor extends View
moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine()
moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine()
moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine()
+ moveLineUp: -> @activeEditSession.moveLineUp()
+ moveLineDown: -> @activeEditSession.moveLineDown()
setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options)
getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition()
getCursorScreenRow: -> @activeEditSession.getCursorScreenRow()
@@ -271,6 +276,7 @@ class Editor extends View
destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId)
destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow)
isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow)
+ isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow)
lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow)
linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end)
@@ -312,7 +318,7 @@ class Editor extends View
setInvisibles: (@invisibles={}) ->
_.defaults @invisibles,
eol: '\u00ac'
- space: '\u2022'
+ space: '\u00b7'
tab: '\u00bb'
cr: '\u00a4'
@resetDisplay()
@@ -1056,8 +1062,6 @@ class Editor extends View
if fold = screenLine.fold
lineAttributes = { class: 'fold line', 'fold-id': fold.id }
- if @activeEditSession.selectionIntersectsBufferRange(fold.getBufferRange())
- lineAttributes.class += ' selected'
else
lineAttributes = { class: 'line' }
@@ -1090,6 +1094,8 @@ class Editor extends View
if invisibles.eol
line.push("#{invisibles.eol}")
+ line.push("") if fold
+
line.push('')
line.join('')
diff --git a/src/app/fold.coffee b/src/app/fold.coffee
index 57bea3cfa..50e5d34ba 100644
--- a/src/app/fold.coffee
+++ b/src/app/fold.coffee
@@ -36,8 +36,8 @@ class Fold
@displayBuffer.unregisterFold(@startRow, this)
return
- @updateStartRow(event)
- @updateEndRow(event)
+ @startRow += @getRowDelta(event, @startRow)
+ @endRow += @getRowDelta(event, @endRow)
if @startRow != oldStartRow
@displayBuffer.unregisterFold(oldStartRow, this)
@@ -49,26 +49,12 @@ class Fold
isContainedByFold: (fold) ->
@isContainedByRange(fold.getBufferRange())
- updateStartRow: (event) ->
+ getRowDelta: (event, row) ->
{ newRange, oldRange } = event
- if oldRange.end.row < @startRow
- delta = newRange.end.row - oldRange.end.row
- else if newRange.end.row < @startRow
- delta = newRange.end.row - @startRow
+ if oldRange.end.row <= row
+ newRange.end.row - oldRange.end.row
+ else if newRange.end.row < row
+ newRange.end.row - row
else
- delta = 0
-
- @startRow += delta
-
- updateEndRow: (event) ->
- { newRange, oldRange } = event
-
- if oldRange.end.row <= @endRow
- delta = newRange.end.row - oldRange.end.row
- else if newRange.end.row <= @endRow
- delta = newRange.end.row - @endRow
- else
- delta = 0
-
- @endRow += delta
+ 0
diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee
index e1e2f8a82..9f7de8f7a 100644
--- a/src/app/gutter.coffee
+++ b/src/app/gutter.coffee
@@ -58,16 +58,19 @@ class Gutter extends View
@renderLineNumbers(renderFrom, renderTo) if performUpdate
renderLineNumbers: (startScreenRow, endScreenRow) ->
- rows = @editor().bufferRowsForScreenRows(startScreenRow, endScreenRow)
+ editor = @editor()
+ rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow)
- cursorScreenRow = @editor().getCursorScreenPosition().row
+ cursorScreenRow = editor.getCursorScreenPosition().row
@lineNumbers[0].innerHTML = $$$ ->
for row in rows
if row == lastScreenRow
rowValue = '•'
else
rowValue = row + 1
- @div {class: 'line-number'}, rowValue
+ classes = ['line-number']
+ classes.push('fold') if editor.isFoldedAtBufferRow(row)
+ @div rowValue, class: classes.join(' ')
lastScreenRow = row
@calculateWidth()
diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson
index 06f0dd571..4c977b698 100644
--- a/src/app/keymaps/editor.cson
+++ b/src/app/keymaps/editor.cson
@@ -38,3 +38,5 @@
'meta-P': 'editor:close-all-edit-sessions'
'meta-L': 'editor:select-grammar'
'ctrl-C': 'editor:copy-path'
+ 'ctrl-meta-up': 'editor:move-line-up'
+ 'ctrl-meta-down': 'editor:move-line-down'
diff --git a/src/app/line-map.coffee b/src/app/line-map.coffee
index eae630887..1a5a74147 100644
--- a/src/app/line-map.coffee
+++ b/src/app/line-map.coffee
@@ -133,6 +133,7 @@ class LineMap
new Range(start, end)
bufferRangeForScreenRange: (screenRange) ->
+ screenRange = Range.fromObject(screenRange)
start = @bufferPositionForScreenPosition(screenRange.start)
end = @bufferPositionForScreenPosition(screenRange.end)
new Range(start, end)
@@ -141,4 +142,3 @@ class LineMap
for row in [start..end]
line = @lineForScreenRow(row).text
console.log row, line, line.length
-
diff --git a/src/app/point.coffee b/src/app/point.coffee
index a216cad0b..b740027f6 100644
--- a/src/app/point.coffee
+++ b/src/app/point.coffee
@@ -11,6 +11,14 @@ class Point
new Point(row, column)
+ @min: (point1, point2) ->
+ point1 = @fromObject(point1)
+ point2 = @fromObject(point2)
+ if point1.isLessThanOrEqual(point2)
+ point1
+ else
+ point2
+
constructor: (@row=0, @column=0) ->
copy: ->
diff --git a/src/app/selection.coffee b/src/app/selection.coffee
index ac8368425..de637d861 100644
--- a/src/app/selection.coffee
+++ b/src/app/selection.coffee
@@ -97,7 +97,10 @@ class Selection
@screenRangeChanged()
selectWord: ->
- @setBufferRange(@cursor.getCurrentWordBufferRange())
+ options = {}
+ options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
+
+ @setBufferRange(@cursor.getCurrentWordBufferRange(options))
@wordwise = true
@initialScreenRange = @getScreenRange()
diff --git a/themes/Atom - Dark/editor.css b/themes/Atom - Dark/editor.css
index a9a8429d7..d15b4a7e0 100644
--- a/themes/Atom - Dark/editor.css
+++ b/themes/Atom - Dark/editor.css
@@ -43,12 +43,24 @@
-webkit-animation-iteration-count: 1;
}
-.editor .fold {
- background-color: #444;
+.editor .gutter .line-number.fold {
+ color: #fba0e3;
+ opacity: .8;
}
-.editor .fold.selected {
- background-color: #244;
+.editor .gutter .line-number.fold.cursor-line {
+ opacity: 1;
+}
+
+.editor .fold-marker:before {
+ content: '\f25e';
+ font-family: 'Octicons Regular';
+ display: inline-block;
+ margin-left: .5em;
+ margin-top: .1em;
+ line-height: .8em;
+ -webkit-font-smoothing: antialiased;
+ color: #fba0e3;
}
.editor .invisible {
diff --git a/themes/Atom - Light/editor.css b/themes/Atom - Light/editor.css
index 9326721b9..14e16fbc5 100644
--- a/themes/Atom - Light/editor.css
+++ b/themes/Atom - Light/editor.css
@@ -46,12 +46,24 @@
-webkit-animation-iteration-count: 1;
}
-.editor .fold {
- background-color: #444;
+.editor .gutter .line-number.fold {
+ color: #fba0e3;
+ opacity: .8;
}
-.editor .fold.selected {
- background-color: #244;
+.editor .gutter .line-number.fold.cursor-line {
+ opacity: 1;
+}
+
+.editor .fold-marker:before {
+ content: '\f25e';
+ font-family: 'Octicons Regular';
+ display: inline-block;
+ margin-left: .5em;
+ margin-top: .1em;
+ line-height: .8em;
+ -webkit-font-smoothing: antialiased;
+ color: #fba0e3;
}
.editor .invisible {
diff --git a/vendor/packages/python.tmbundle b/vendor/packages/python.tmbundle
new file mode 160000
index 000000000..3675c22ae
--- /dev/null
+++ b/vendor/packages/python.tmbundle
@@ -0,0 +1 @@
+Subproject commit 3675c22ae891419b27a80c58001831d01e73d431