Add ScreenLine & refactor LineWrapper to use it.

ScreenLine encapsulates the idea of a single line on screen. ScreenLines are first generated by the highlighter. A ScreenLine contains tokens and text, and currently has a method called splitAt which the LineWrapper, and soon the LineFolder, use to fragment the line.
This commit is contained in:
Nathan Sobo
2012-02-13 15:05:33 -07:00
parent 68d08acc1b
commit c89a8fbb37
5 changed files with 120 additions and 71 deletions

View File

@@ -2,6 +2,7 @@ Buffer = require 'buffer'
LineWrapper = require 'line-wrapper'
Highlighter = require 'highlighter'
Range = require 'range'
ScreenLine = require 'screen-line'
_ = require 'underscore'
describe "LineWrapper", ->
@@ -134,73 +135,81 @@ describe "LineWrapper", ->
# following a wrapped line
expect(wrapper.bufferPositionFromScreenPosition([5, 5])).toEqual([4, 5])
describe ".splitTokens(tokens)", ->
makeTokens = (array) ->
array.map (value) -> { value, type: 'foo' }
describe ".wrapScreenLine(screenLine)", ->
makeTokens = (tokenValues...) ->
tokenValues.map (value) -> { value, type: 'foo' }
makeScreenLine = (tokenValues...) ->
tokens = makeTokens(tokenValues...)
text = tokenValues.join('')
new ScreenLine(tokens, text)
beforeEach ->
wrapper.setMaxLength(10)
describe "when the buffer line is shorter than max length", ->
it "does not split the line", ->
screenLines = wrapper.splitTokens(makeTokens ['abc', 'def'])
expect(screenLines).toEqual [makeTokens ['abc', 'def']]
screenLines = wrapper.wrapScreenLine(makeScreenLine 'abc', 'def')
expect(screenLines.length).toBe 1
expect(screenLines[0].tokens).toEqual(makeTokens 'abc', 'def')
[line1] = screenLines
expect(line1.startColumn).toBe 0
expect(line1.endColumn).toBe 6
expect(line1.textLength).toBe 6
expect(line1.text.length).toBe 6
describe "when the buffer line is empty", ->
it "returns a single empty screen line", ->
expect(wrapper.splitTokens([])).toEqual [[]]
screenLines = wrapper.wrapScreenLine(makeScreenLine())
expect(screenLines.length).toBe 1
expect(screenLines[0].tokens).toEqual []
describe "when there is a non-whitespace character at the max-length boundary", ->
describe "when there is whitespace before the max-length boundary", ->
it "splits the line at the start of the first word before the boundary", ->
screenLines = wrapper.splitTokens(makeTokens ['12 ', '45 ', ' 89A', 'BC'])
screenLines = wrapper.wrapScreenLine(makeScreenLine '12 ', '45 ', ' 89A', 'BC')
expect(screenLines.length).toBe 2
[line1, line2] = screenLines
expect(line1).toEqual(makeTokens ['12 ', '45 ', ' '])
expect(line2).toEqual(makeTokens ['89A', 'BC'])
expect(line1.tokens).toEqual(makeTokens '12 ', '45 ', ' ')
expect(line2.tokens).toEqual(makeTokens '89A', 'BC')
expect(line1.startColumn).toBe 0
expect(line1.endColumn).toBe 7
expect(line1.textLength).toBe 7
expect(line1.text.length).toBe 7
expect(line2.startColumn).toBe 7
expect(line2.endColumn).toBe 12
expect(line2.textLength).toBe 5
expect(line2.text.length).toBe 5
describe "when there is no whitespace before the max-length boundary", ->
it "splits the line at the boundary, because there's no 'good' place to split it", ->
screenLines = wrapper.splitTokens(makeTokens ['123', '456', '789AB', 'CD'])
screenLines = wrapper.wrapScreenLine(makeScreenLine '123', '456', '789AB', 'CD')
expect(screenLines.length).toBe 2
[line1, line2] = screenLines
expect(line1).toEqual(makeTokens ['123', '456', '789A'])
expect(line2).toEqual(makeTokens ['B', 'CD'])
expect(line1.tokens).toEqual(makeTokens '123', '456', '789A')
expect(line2.tokens).toEqual(makeTokens 'B', 'CD')
expect(line1.startColumn).toBe 0
expect(line1.endColumn).toBe 10
expect(line1.textLength).toBe 10
expect(line1.text.length).toBe 10
expect(line2.startColumn).toBe 10
expect(line2.endColumn).toBe 13
expect(line2.textLength).toBe 3
expect(line2.text.length).toBe 3
describe "when there is a whitespace character at the max-length boundary", ->
it "splits the line at the start of the first word beyond the boundary", ->
screenLines = wrapper.splitTokens(makeTokens ['12 ', '45 ', ' 89 C', 'DE'])
screenLines = wrapper.wrapScreenLine(makeScreenLine '12 ', '45 ', ' 89 C', 'DE')
expect(screenLines.length).toBe 2
[line1, line2] = screenLines
expect(line1).toEqual(makeTokens ['12 ', '45 ', ' 89 '])
expect(line2).toEqual(makeTokens ['C', 'DE'])
expect(line1.tokens).toEqual(makeTokens '12 ', '45 ', ' 89 ')
expect(line2.tokens).toEqual(makeTokens 'C', 'DE')
expect(line1.startColumn).toBe 0
expect(line1.endColumn).toBe 11
expect(line1.textLength).toBe 11
expect(line1.text.length).toBe 11
expect(line2.startColumn).toBe 11
expect(line2.endColumn).toBe 14
expect(line2.textLength).toBe 3
expect(line2.text.length).toBe 3

View File

@@ -0,0 +1,36 @@
Buffer = require 'buffer'
Highlighter = require 'highlighter'
describe "ScreenLine", ->
screenLine = null
beforeEach ->
buffer = new Buffer(require.resolve 'fixtures/sample.js')
highlighter = new Highlighter(buffer)
screenLine = highlighter.screenLineForRow(3)
describe ".splitAt(splitColumn)", ->
describe "when the split column is less than the line length", ->
describe "when the split column is at the start of a token", ->
it "returns two screen lines", ->
[left, right] = screenLine.splitAt(31)
expect(left.text).toBe ' var pivot = items.shift(), '
expect(tokensText left.tokens).toBe left.text
expect(right.text).toBe 'current, left = [], right = [];'
expect(tokensText right.tokens).toBe right.text
describe "when the split column is in the middle of a token", ->
it "it returns two screen lines, with the token split in half", ->
[left, right] = screenLine.splitAt(34)
expect(left.text).toBe ' var pivot = items.shift(), cur'
expect(tokensText left.tokens).toBe left.text
expect(right.text).toBe 'rent, left = [], right = [];'
expect(tokensText right.tokens).toBe right.text
describe "when the split column is 0 or equals the line length", ->
it "returns a singleton array of the screen line (doesn't split it)", ->
expect(screenLine.splitAt(0)).toEqual([screenLine])
expect(screenLine.splitAt(screenLine.text.length)).toEqual([screenLine])

View File

@@ -1,4 +1,5 @@
_ = require 'underscore'
ScreenLine = require 'screen-line'
EventEmitter = require 'event-emitter'
module.exports =
@@ -57,6 +58,9 @@ class Highlighter
tokenizeRow: (state, row) ->
@tokenizer.getLineTokens(@buffer.getLine(row), state)
screenLineForRow: (row) ->
new ScreenLine(@tokensForRow(row), @buffer.getLine(row))
tokensForRow: (row) ->
_.clone(@lines[row].tokens)

View File

@@ -13,11 +13,11 @@ class LineWrapper
setMaxLength: (@maxLength) ->
oldRange = new Range
oldRange.end.row = @screenLineCount() - 1
oldRange.end.column = _.last(_.last(@wrappedLines).screenLines).textLength
oldRange.end.column = _.last(_.last(@wrappedLines).screenLines).text.length
@buildWrappedLines()
newRange = new Range
newRange.end.row = @screenLineCount() - 1
newRange.end.column = _.last(_.last(@wrappedLines).screenLines).textLength
newRange.end.column = _.last(_.last(@wrappedLines).screenLines).text.length
@trigger 'change', { oldRange, newRange }
buildWrappedLines: ->
@@ -29,13 +29,13 @@ class LineWrapper
bufferRow = e.oldRange.start.row
oldRange.start.row = @firstScreenRowForBufferRow(e.oldRange.start.row)
oldRange.end.row = @lastScreenRowForBufferRow(e.oldRange.end.row)
oldRange.end.column = _.last(@wrappedLines[e.oldRange.end.row].screenLines).textLength
oldRange.end.column = _.last(@wrappedLines[e.oldRange.end.row].screenLines).text.length
@wrappedLines[e.oldRange.start.row..e.oldRange.end.row] = @buildWrappedLinesForBufferRows(e.newRange.start.row, e.newRange.end.row)
newRange = oldRange.copy()
newRange.end.row = @lastScreenRowForBufferRow(e.newRange.end.row)
newRange.end.column = _.last(@wrappedLines[e.newRange.end.row].screenLines).textLength
newRange.end.column = _.last(@wrappedLines[e.newRange.end.row].screenLines).text.length
@trigger 'change', { oldRange, newRange }
@@ -51,59 +51,31 @@ class LineWrapper
@buildWrappedLineForBufferRow(row)
buildWrappedLineForBufferRow: (bufferRow) ->
{ screenLines: @splitTokens(@highlighter.tokensForRow(bufferRow)) }
{ screenLines: @wrapScreenLine(@highlighter.screenLineForRow(bufferRow)) }
splitTokens: (tokens, startColumn = 0) ->
splitColumn = @findSplitColumn(tokens)
screenLine = []
textLength = 0
while tokens.length
nextToken = tokens[0]
if textLength + nextToken.value.length > splitColumn
tokenFragments = @splitTokenAt(nextToken, splitColumn - textLength)
[token1, token2] = tokenFragments
tokens[0..0] = _.compact(tokenFragments)
break unless token1
nextToken = tokens.shift()
textLength += nextToken.value.length
screenLine.push nextToken
endColumn = startColumn + textLength
_.extend(screenLine, { textLength, startColumn, endColumn })
if tokens.length
[screenLine].concat @splitTokens(tokens, endColumn)
wrapScreenLine: (screenLine, startColumn=0) ->
[leftHalf, rightHalf] = screenLine.splitAt(@findSplitColumn(screenLine.text))
endColumn = startColumn + leftHalf.text.length
_.extend(leftHalf, {startColumn, endColumn})
if rightHalf
[leftHalf].concat @wrapScreenLine(rightHalf, endColumn)
else
[screenLine]
[leftHalf]
findSplitColumn: (tokens) ->
lineText = _.pluck(tokens, 'value').join('')
lineLength = lineText.length
return lineLength unless lineLength > @maxLength
findSplitColumn: (line) ->
return line.length unless line.length > @maxLength
if /\s/.test(lineText[@maxLength])
if /\s/.test(line[@maxLength])
# search forward for the start of a word past the boundary
for column in [@maxLength..lineLength]
return column if /\S/.test(lineText[column])
return lineLength
for column in [@maxLength..line.length]
return column if /\S/.test(line[column])
return line.length
else
# search backward for the start of the word on the boundary
for column in [@maxLength..0]
return column + 1 if /\s/.test(lineText[column])
return column + 1 if /\s/.test(line[column])
return @maxLength
splitTokenAt: (token, splitIndex) ->
{ type, value } = token
switch splitIndex
when 0
[null, token]
when value.length
[token, null]
else
value1 = value.substring(0, splitIndex)
value2 = value.substring(splitIndex)
[{value: value1, type }, {value: value2, type}]
screenRangeFromBufferRange: (bufferRange) ->
start = @screenPositionFromBufferPosition(bufferRange.start, true)
end = @screenPositionFromBufferPosition(bufferRange.end, true)
@@ -125,7 +97,7 @@ class LineWrapper
else
break if screenLine.endColumn > bufferPosition.column
column -= screenLine.textLength
column -= screenLine.text.length
row++
new Point(row, column)
@@ -145,7 +117,7 @@ class LineWrapper
currentScreenRow = 0
for wrappedLine in @wrappedLines
for screenLine in wrappedLine.screenLines
return screenLine if currentScreenRow == screenRow
return screenLine.tokens if currentScreenRow == screenRow
currentScreenRow++
screenLineCount: ->

View File

@@ -0,0 +1,28 @@
_ = require 'underscore'
module.exports =
class ScreenLine
constructor: (@tokens, @text) ->
splitAt: (column) ->
return [this] if column == 0 or column >= @text.length
rightTokens = _.clone(@tokens)
leftTokens = []
leftTextLength = 0
while leftTextLength < column
if leftTextLength + rightTokens[0].value.length > column
rightTokens[0..0] = @splitTokenAt(rightTokens[0], column - leftTextLength)
nextToken = rightTokens.shift()
leftTextLength += nextToken.value.length
leftTokens.push nextToken
leftLine = new ScreenLine(leftTokens, @text.substring(0, column))
rightLine = new ScreenLine(rightTokens, @text.substring(column))
[leftLine, rightLine]
splitTokenAt: (token, splitIndex) ->
{ type, value } = token
value1 = value.substring(0, splitIndex)
value2 = value.substring(splitIndex)
[{value: value1, type }, {value: value2, type}]