mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
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:
@@ -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
|
||||
|
||||
|
||||
36
spec/atom/screen-line-spec.coffee
Normal file
36
spec/atom/screen-line-spec.coffee
Normal 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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
28
src/atom/screen-line.coffee
Normal file
28
src/atom/screen-line.coffee
Normal 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}]
|
||||
Reference in New Issue
Block a user