Editor supports move-to-next-word events

This commit is contained in:
Nathan Sobo
2012-03-28 13:50:52 -07:00
parent 777d4d9680
commit cce1218fda
8 changed files with 286 additions and 228 deletions

View File

@@ -243,12 +243,12 @@ describe 'Buffer', ->
expect(matches[2][1]).toBe 'rr'
expect(ranges[2]).toEqual [[6,34], [6,41]]
describe "when the iterator returns a replacement string", ->
describe "when the iterator calls the 'replace' control function with a replacement string", ->
it "replaces each occurrence of the regex match with the string", ->
ranges = []
buffer.traverseRegexMatchesInRange /cu(rr)ent/g, [[4,0], [6,59]], (match, range) ->
buffer.traverseRegexMatchesInRange /cu(rr)ent/g, [[4,0], [6,59]], (match, range, { replace }) ->
ranges.push(range)
"foo"
replace("foo")
expect(ranges[0]).toEqual [[5,6], [5,13]]
expect(ranges[1]).toEqual [[6,6], [6,13]]
@@ -257,6 +257,15 @@ describe 'Buffer', ->
expect(buffer.lineForRow(5)).toBe ' foo = items.shift();'
expect(buffer.lineForRow(6)).toBe ' foo < pivot ? left.push(foo) : right.push(current);'
describe "when the iterator calls the 'stop' control function", ->
it "stops the traversal", ->
ranges = []
buffer.traverseRegexMatchesInRange /cu(rr)ent/g, [[4,0], [6,59]], (match, range, { stop }) ->
ranges.push(range)
stop() if ranges.length == 2
expect(ranges.length).toBe 2
describe ".characterIndexForPosition(position)", ->
it "returns the total number of charachters that precede the given position", ->
expect(buffer.characterIndexForPosition([0, 0])).toBe 0

View File

@@ -223,241 +223,241 @@ describe "Editor", ->
editor.trigger keydownEvent('up')
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
describe "vertical movement", ->
describe "auto-scrolling", ->
beforeEach ->
editor.attachToDom()
editor.focus()
editor.vScrollMargin = 3
it "scrolls the buffer with the specified scroll margin when cursor approaches the end of the screen", ->
editor.height(editor.lineHeight * 10)
_.times 6, -> editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
_.times 3, -> editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
it "reduces scroll margins when there isn't enough height to maintain them and scroll smoothly", ->
editor.height(editor.lineHeight * 5)
_.times 3, -> editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
describe "goal column retention", ->
lineLengths = null
beforeEach ->
lineLengths = buffer.getLines().map (line) -> line.length
expect(lineLengths[3]).toBeGreaterThan(lineLengths[4])
expect(lineLengths[5]).toBeGreaterThan(lineLengths[4])
expect(lineLengths[6]).toBeGreaterThan(lineLengths[3])
it "retains the goal column when moving up", ->
expect(lineLengths[6]).toBeGreaterThan(32)
editor.setCursorScreenPosition(row: 6, column: 32)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[5]
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[4]
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 32
it "retains the goal column when moving down", ->
editor.setCursorScreenPosition(row: 3, column: lineLengths[3])
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[4]
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[5]
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[3]
it "clears the goal column when the cursor is set", ->
# set a goal column by moving down
editor.setCursorScreenPosition(row: 3, column: lineLengths[3])
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).not.toBe 6
# clear the goal column by explicitly setting the cursor position
editor.setCursorScreenPosition([4,6])
expect(editor.getCursorScreenPosition().column).toBe 6
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe 6
describe "when up is pressed on the first line", ->
it "moves the cursor to the beginning of the line, but retains the goal column", ->
editor.setCursorScreenPosition(row: 0, column: 4)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
editor.moveCursorDown()
expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 4)
describe "when down is pressed on the last line", ->
it "moves the cursor to the end of line, but retains the goal column", ->
lastLineIndex = buffer.getLines().length - 1
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
editor.setCursorScreenPosition(row: lastLineIndex, column: 1)
editor.moveCursorDown()
expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 1
it "retains a goal column of 0", ->
lastLineIndex = buffer.getLines().length - 1
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
editor.setCursorScreenPosition(row: lastLineIndex, column: 0)
editor.moveCursorDown()
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 0
describe "horizontal movement", ->
describe "auto-scrolling", ->
charWidth = null
beforeEach ->
editor.attachToDom()
{charWidth} = editor
editor.hScrollMargin = 5
it "scrolls horizontally to keep the cursor on screen", ->
setEditorWidthInChars(editor, 30)
# moving right
editor.setCursorScreenPosition([2, 24])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 25])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth
editor.setCursorScreenPosition([2, 28])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 4
# moving left
editor.setCursorScreenPosition([2, 9])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 4
editor.setCursorScreenPosition([2, 8])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 3
editor.setCursorScreenPosition([2, 5])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe 0
it "reduces scroll margins when there isn't enough width to maintain them and scroll smoothly", ->
editor.hScrollMargin = 6
setEditorWidthInChars(editor, 7)
editor.setCursorScreenPosition([2, 3])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(0)
editor.setCursorScreenPosition([2, 4])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(charWidth)
editor.setCursorScreenPosition([2, 3])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(0)
describe "when soft-wrap is on", ->
describe "vertical movement", ->
describe "auto-scrolling", ->
beforeEach ->
editor.setSoftWrap(true)
editor.attachToDom()
editor.focus()
editor.vScrollMargin = 3
it "does not scroll the buffer horizontally", ->
editor.width(charWidth * 30)
it "scrolls the buffer with the specified scroll margin when cursor approaches the end of the screen", ->
editor.height(editor.lineHeight * 10)
# moving right
editor.setCursorScreenPosition([2, 24])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
_.times 6, -> editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
editor.setCursorScreenPosition([2, 25])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.setCursorScreenPosition([2, 28])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
# moving left
editor.setCursorScreenPosition([2, 9])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
_.times 3, -> editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight * 2)
editor.setCursorScreenPosition([2, 8])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.setCursorScreenPosition([2, 5])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
describe "when left is pressed on the first column", ->
describe "when there is a previous line", ->
it "wraps to the end of the previous line", ->
editor.setCursorScreenPosition(row: 1, column: 0)
editor.moveCursorLeft()
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length)
it "reduces scroll margins when there isn't enough height to maintain them and scroll smoothly", ->
editor.height(editor.lineHeight * 5)
describe "when the cursor is on the first line", ->
it "remains in the same position (0,0)", ->
editor.setCursorScreenPosition(row: 0, column: 0)
editor.moveCursorLeft()
_.times 3, -> editor.moveCursorDown()
window.advanceClock()
expect(editor.scrollTop()).toBe(editor.lineHeight)
editor.moveCursorUp()
window.advanceClock()
expect(editor.scrollTop()).toBe(0)
describe "goal column retention", ->
lineLengths = null
beforeEach ->
lineLengths = buffer.getLines().map (line) -> line.length
expect(lineLengths[3]).toBeGreaterThan(lineLengths[4])
expect(lineLengths[5]).toBeGreaterThan(lineLengths[4])
expect(lineLengths[6]).toBeGreaterThan(lineLengths[3])
it "retains the goal column when moving up", ->
expect(lineLengths[6]).toBeGreaterThan(32)
editor.setCursorScreenPosition(row: 6, column: 32)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[5]
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[4]
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 32
it "retains the goal column when moving down", ->
editor.setCursorScreenPosition(row: 3, column: lineLengths[3])
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[4]
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[5]
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe lineLengths[3]
it "clears the goal column when the cursor is set", ->
# set a goal column by moving down
editor.setCursorScreenPosition(row: 3, column: lineLengths[3])
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).not.toBe 6
# clear the goal column by explicitly setting the cursor position
editor.setCursorScreenPosition([4,6])
expect(editor.getCursorScreenPosition().column).toBe 6
editor.moveCursorDown()
expect(editor.getCursorScreenPosition().column).toBe 6
describe "when up is pressed on the first line", ->
it "moves the cursor to the beginning of the line, but retains the goal column", ->
editor.setCursorScreenPosition(row: 0, column: 4)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
describe "when right is pressed on the last column", ->
describe "when there is a subsequent line", ->
it "wraps to the beginning of the next line", ->
editor.setCursorScreenPosition(row: 0, column: buffer.lineForRow(0).length)
editor.moveCursorRight()
expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 0)
editor.moveCursorDown()
expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 4)
describe "when the cursor is on the last line", ->
it "remains in the same position", ->
describe "when down is pressed on the last line", ->
it "moves the cursor to the end of line, but retains the goal column", ->
lastLineIndex = buffer.getLines().length - 1
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
lastPosition = { row: lastLineIndex, column: lastLine.length }
editor.setCursorScreenPosition(lastPosition)
editor.moveCursorRight()
editor.setCursorScreenPosition(row: lastLineIndex, column: 1)
editor.moveCursorDown()
expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length)
expect(editor.getCursorScreenPosition()).toEqual(lastPosition)
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 1
it "retains a goal column of 0", ->
lastLineIndex = buffer.getLines().length - 1
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
editor.setCursorScreenPosition(row: lastLineIndex, column: 0)
editor.moveCursorDown()
editor.moveCursorUp()
expect(editor.getCursorScreenPosition().column).toBe 0
describe "horizontal movement", ->
describe "auto-scrolling", ->
charWidth = null
beforeEach ->
editor.attachToDom()
{charWidth} = editor
editor.hScrollMargin = 5
it "scrolls horizontally to keep the cursor on screen", ->
setEditorWidthInChars(editor, 30)
# moving right
editor.setCursorScreenPosition([2, 24])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 25])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth
editor.setCursorScreenPosition([2, 28])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 4
# moving left
editor.setCursorScreenPosition([2, 9])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 4
editor.setCursorScreenPosition([2, 8])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe charWidth * 3
editor.setCursorScreenPosition([2, 5])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe 0
it "reduces scroll margins when there isn't enough width to maintain them and scroll smoothly", ->
editor.hScrollMargin = 6
setEditorWidthInChars(editor, 7)
editor.setCursorScreenPosition([2, 3])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(0)
editor.setCursorScreenPosition([2, 4])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(charWidth)
editor.setCursorScreenPosition([2, 3])
window.advanceClock()
expect(editor.horizontalScroller.scrollLeft()).toBe(0)
describe "when soft-wrap is on", ->
beforeEach ->
editor.setSoftWrap(true)
it "does not scroll the buffer horizontally", ->
editor.width(charWidth * 30)
# moving right
editor.setCursorScreenPosition([2, 24])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 25])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 28])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
# moving left
editor.setCursorScreenPosition([2, 9])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 8])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
editor.setCursorScreenPosition([2, 5])
expect(editor.horizontalScroller.scrollLeft()).toBe 0
describe "when left is pressed on the first column", ->
describe "when there is a previous line", ->
it "wraps to the end of the previous line", ->
editor.setCursorScreenPosition(row: 1, column: 0)
editor.moveCursorLeft()
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length)
describe "when the cursor is on the first line", ->
it "remains in the same position (0,0)", ->
editor.setCursorScreenPosition(row: 0, column: 0)
editor.moveCursorLeft()
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
describe "when right is pressed on the last column", ->
describe "when there is a subsequent line", ->
it "wraps to the beginning of the next line", ->
editor.setCursorScreenPosition(row: 0, column: buffer.lineForRow(0).length)
editor.moveCursorRight()
expect(editor.getCursorScreenPosition()).toEqual(row: 1, column: 0)
describe "when the cursor is on the last line", ->
it "remains in the same position", ->
lastLineIndex = buffer.getLines().length - 1
lastLine = buffer.lineForRow(lastLineIndex)
expect(lastLine.length).toBeGreaterThan(0)
lastPosition = { row: lastLineIndex, column: lastLine.length }
editor.setCursorScreenPosition(lastPosition)
editor.moveCursorRight()
expect(editor.getCursorScreenPosition()).toEqual(lastPosition)
describe "when a mousedown event occurs in the editor", ->
beforeEach ->
@@ -558,6 +558,24 @@ describe "Editor", ->
editor.lines.trigger 'mouseup'
expect(editor.getSelectedText()).toBe " if (items.length <= 1) return items;"
describe "move-to-next-word", ->
it "moves the cursor to the next word or the end of file if there is no next word", ->
editor.setCursorBufferPosition [2, 5]
editor.addCursorAtBufferPosition [3, 60]
[cursor1, cursor2] = editor.getCursors()
editor.trigger 'move-to-next-word'
expect(cursor1.getBufferPosition()).toEqual [2, 7]
expect(cursor2.getBufferPosition()).toEqual [4, 4]
buffer.insert([12, 2], ' ')
cursor1.setBufferPosition([12, 1])
expect(cursor1.getBufferPosition()).toEqual [12, 1]
editor.trigger 'move-to-next-word'
expect(cursor1.getBufferPosition()).toEqual [12, 5]
describe "auto indent/outdent", ->
beforeEach ->
editor.autoIndent = true

View File

@@ -158,9 +158,13 @@ class Buffer
startPosition = @positionForCharacterIndex(matchStartIndex + lengthDelta)
endPosition = @positionForCharacterIndex(matchEndIndex + lengthDelta)
range = new Range(startPosition, endPosition)
replacementText = iterator(match, range)
recurse = true
replacementText = null
stop = -> recurse = false
replace = (text) -> replacementText = text
iterator(match, range, { stop, replace })
if _.isString(replacementText)
if replacementText
@change(range, replacementText)
lengthDelta += replacementText.length - matchLength
@@ -168,7 +172,7 @@ class Buffer
matchStartIndex++
matchEndIndex++
if global
if global and recurse
traverseRecursively(text, matchEndIndex, endIndex, lengthDelta)
startIndex = @characterIndexForPosition(range.start)

View File

@@ -11,6 +11,6 @@ class Substitution extends Command
execute: (editor) ->
range = editor.getSelection().getBufferRange()
editor.buffer.traverseRegexMatchesInRange @regex, range, =>
@replacementText
editor.buffer.traverseRegexMatchesInRange @regex, range, (match, matchRange, { replace }) =>
replace(@replacementText)

View File

@@ -25,6 +25,10 @@ class CompositeCursor
cursor = @addCursor()
cursor.setScreenPosition(screenPosition)
addCursorAtBufferPosition: (bufferPosition) ->
cursor = @addCursor()
cursor.setBufferPosition(bufferPosition)
removeCursor: (cursor) ->
_.remove(@cursors, cursor)
@@ -53,6 +57,9 @@ class CompositeCursor
moveDown: ->
@modifyCursors (cursor) -> cursor.moveDown()
moveToNextWord: ->
@modifyCursors (cursor) -> cursor.moveToNextWord()
handleBufferChange: (e) ->
@modifyCursors (cursor) -> cursor.handleBufferChange(e)

View File

@@ -103,9 +103,22 @@ class Cursor extends View
@setScreenPosition({row: row + 1, column: column})
@goalColumn = column
moveToNextWord: ->
wordRegex = /(\w+)|([^\w\s]+)/g
bufferPosition = @getBufferPosition()
range = [bufferPosition, @editor.getEofPosition()]
nextPosition = null
@editor.traverseRegexMatchesInRange wordRegex, range, (match, matchRange, { stop }) =>
if matchRange.start.isGreaterThan(bufferPosition)
nextPosition = matchRange.start
stop()
@setBufferPosition(nextPosition or @editor.getEofPosition())
moveToLineEnd: ->
{ row } = @getScreenPosition()
@setScreenPosition({ row, column: @editor.buffer.lineForRow(row).length })
{ row } = @getBufferPosition()
@setBufferPosition({ row, column: @editor.buffer.lineForRow(row).length })
moveToLineStart: ->
{ row } = @getScreenPosition()

View File

@@ -82,6 +82,7 @@ class Editor extends View
@on 'move-left', => @moveCursorLeft()
@on 'move-down', => @moveCursorDown()
@on 'move-up', => @moveCursorUp()
@on 'move-to-next-word', => @moveCursorToNextWord()
@on 'select-right', => @selectRight()
@on 'select-left', => @selectLeft()
@on 'select-up', => @selectUp()
@@ -109,6 +110,9 @@ class Editor extends View
addCursorAtScreenPosition: (screenPosition) ->
@compositeCursor.addCursorAtScreenPosition(screenPosition)
addCursorAtBufferPosition: (bufferPosition) ->
@compositeCursor.addCursorAtBufferPosition(bufferPosition)
addSelectionForCursor: (cursor) ->
@compositeSelection.addSelectionForCursor(cursor)
@@ -347,11 +351,12 @@ class Editor extends View
@lineHeight = fragment.outerHeight()
fragment.remove()
getCursors: -> @compositeCursor.getCursors()
moveCursorUp: -> @compositeCursor.moveUp()
moveCursorDown: -> @compositeCursor.moveDown()
moveCursorRight: -> @compositeCursor.moveRight()
getCursors: -> @compositeCursor.getCursors()
moveCursorLeft: -> @compositeCursor.moveLeft()
moveCursorToNextWord: -> @compositeCursor.moveToNextWord()
setCursorScreenPosition: (position) -> @compositeCursor.setScreenPosition(position)
getCursorScreenPosition: -> @compositeCursor.getCursor().getScreenPosition()
setCursorBufferPosition: (position) -> @compositeCursor.setBufferPosition(position)
@@ -377,6 +382,7 @@ class Editor extends View
getTextInRange: (range) -> @buffer.getTextInRange(range)
getEofPosition: -> @buffer.getEofPosition()
lineForBufferRow: (row) -> @buffer.lineForRow(row)
traverseRegexMatchesInRange: (args...) -> @buffer.traverseRegexMatchesInRange(args...)
insertText: (text) ->
@compositeSelection.insertText(text)

View File

@@ -3,3 +3,4 @@ window.keymap.bindKeys '.editor',
'ctrl-b': 'move-left'
'ctrl-p': 'move-up'
'ctrl-n': 'move-down'
'alt-f': 'move-to-next-word'