WIP: Add a LineWrapper object

Only passing specs are focused. Everything is still broken. Editor uses
the line wrapper to render lines, but the line wrapper isn't updating
on buffer change events yet. Still moving vertically and clipping
positions based on unwrapped lines as well.
This commit is contained in:
Nathan Sobo 2012-02-07 18:07:12 -07:00
parent 62470f61ab
commit b21595f037
4 changed files with 140 additions and 28 deletions

View File

@ -50,15 +50,21 @@ describe "Editor", ->
buffer.insert([1,0], "/*")
expect(editor.lines.find('.line:eq(2) span:eq(0)')).toMatchSelector '.comment'
describe "when soft-wrap is enabled", ->
fdescribe "when soft-wrap is enabled", ->
beforeEach ->
editor.attachToDom()
editor.width(editor.charWidth * 50)
editor.setSoftWrap(true)
it "wraps lines that are too long to fit within the editor's width", ->
expect(editor.lines.find('pre:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [],"
expect(editor.lines.find('pre:eq(4)').text()).toBe " right = [];"
it "wraps lines that are too long to fit within the editor's width, adjusting cursor positioning accordingly", ->
expect(editor.lines.find('pre:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], "
expect(editor.lines.find('pre:eq(4)').text()).toBe "right = [];"
editor.cursor.setPosition([3, 52])
expect(editor.cursor.position()).toEqual(editor.lines.find('pre:eq(4)').position())
editor.cursor.setPosition([4, 0])
expect(editor.cursor.position()).toEqual(editor.lines.find('pre:eq(5)').position())
describe "cursor movement", ->
describe ".setCursorPosition({row, column})", ->

View File

@ -0,0 +1,51 @@
Buffer = require 'buffer'
LineWrapper = require 'line-wrapper'
Highlighter = require 'highlighter'
_ = require 'underscore'
fdescribe "LineWrapper", ->
[wrapper, buffer] = []
beforeEach ->
buffer = new Buffer(require.resolve('fixtures/sample.js'))
wrapper = new LineWrapper(50, new Highlighter(buffer))
describe ".segmentsForRow(row)", ->
describe "when the line does not need to wrap", ->
it "returns tokens for a single segment", ->
line = buffer.getLine(0)
expect(line.length).toBeLessThan(50)
segments = wrapper.segmentsForRow(0)
expect(segments.length).toBe 1
expect(segments[0].lastIndex).toBe line.length
describe "when the line needs to wrap once", ->
it "breaks the line into 2 segments at the beginning of the first word that exceeds the max length", ->
line = buffer.getLine(6)
expect(line.length).toBeGreaterThan 50
segments = wrapper.segmentsForRow(6)
expect(segments.length).toBe 2
expect(segments[0].lastIndex).toBe 45
expect(segments[0].map((t) -> t.value).join('')).toBe ' current < pivot ? left.push(current) : '
expect(segments[1].lastIndex).toBe 65
expect(segments[1].map((t) -> t.value).join('')).toBe 'right.push(current);'
describe "when the line needs to wrap more than once", ->
it "breaks the line into multiple segments", ->
wrapper.setMaxLength(30)
segments = wrapper.segmentsForRow(6)
expect(segments.length).toBe 3
expect(segments[0].lastIndex).toBe 24
expect(_.pluck(segments[0], 'value').join('')).toBe ' current < pivot ? '
expect(segments[1].lastIndex).toBe 45
expect(_.pluck(segments[1], 'value').join('')).toBe 'left.push(current) : '
expect(segments[2].lastIndex).toBe 65
expect(_.pluck(segments[2], 'value').join('')).toBe 'right.push(current);'
describe "when the buffer changes", ->

View File

@ -4,6 +4,7 @@ Point = require 'point'
Cursor = require 'cursor'
Selection = require 'selection'
Highlighter = require 'highlighter'
LineWrapper = require 'line-wrapper'
UndoManager = require 'undo-manager'
Range = require 'range'
@ -20,12 +21,14 @@ class Editor extends View
vScrollMargin: 2
hScrollMargin: 10
softWrap: false
cursor: null
buffer: null
undoManager: null
selection: null
lineHeight: null
charWidth: null
cursor: null
selection: null
buffer: null
highlighter: null
lineWrapper: null
undoManager: null
initialize: () ->
requireStylesheet 'editor.css'
@ -111,26 +114,8 @@ class Editor extends View
@on 'mousemove', moveHandler
$(document).one 'mouseup', => @off 'mousemove', moveHandler
buildLineElement: (row) ->
maxSegmentLength =
if @softWrap
Math.floor(@width() / @charWidth)
else
Infinity
currentSegmentLength = 0
currentSegment = []
segments = [currentSegment]
for token in @highlighter.tokensForRow(row)
if (currentSegmentLength + token.value.length) <= maxSegmentLength
currentSegmentLength += token.value.length
currentSegment.push(token)
else
currentSegment = [token]
currentSegmentLength = token.value.length
segments.push(currentSegment)
segments = @lineWrapper.segmentsForRow(row)
$$ ->
for segment in segments
@pre class: 'line', =>
@ -148,6 +133,7 @@ class Editor extends View
setBuffer: (@buffer) ->
@highlighter = new Highlighter(@buffer)
@lineWrapper = new LineWrapper(Infinity, @highlighter)
@undoManager = new UndoManager(@buffer)
@renderLines()
@setCursorPosition(row: 0, column: 0)
@ -186,6 +172,13 @@ class Editor extends View
@lines.find("pre.line:eq(#{row})")
setSoftWrap: (@softWrap) ->
maxLength =
if @softWrap
Math.floor(@width() / @charWidth)
else
infinity
@lineWrapper.setMaxLength(maxLength)
@renderLines()
clipPosition: ({row, column}) ->
@ -199,7 +192,17 @@ class Editor extends View
new Point(row, column)
pixelPositionFromPoint: ({row, column}) ->
{ top: row * @lineHeight, left: column * @charWidth }
segmentsAbove = 0
segmentsAbove += @lineWrapper.segmentsForRow(i).length for i in [0...row]
for segment in @lineWrapper.segmentsForRow(row)
if column > segment.lastIndex
segmentsAbove++
column -= segment.textLength
else
break
{ top: segmentsAbove * @lineHeight, left: column * @charWidth }
pointFromPixelPosition: ({top, left}) ->
{ row: Math.floor(top / @lineHeight), column: Math.floor(left / @charWidth) }

View File

@ -0,0 +1,52 @@
getWordRegex = -> /\b[^\s]+/g
module.exports =
class LineWrapper
constructor: (@maxLength, @highlighter) ->
@buffer = @highlighter.buffer
@segmentBuffer()
setMaxLength: (@maxLength) ->
@segmentBuffer()
segmentBuffer: ->
@lines = @segmentRows(0, @buffer.lastRow())
segmentsForRow: (row) ->
@lines[row]
segmentRows: (start, end) ->
for row in [start..end]
@segmentRow(row)
segmentRow: (row) ->
wordRegex = getWordRegex()
line = @buffer.getLine(row)
breakIndices = []
lastBreakIndex = 0
while match = wordRegex.exec(line)
startIndex = match.index
endIndex = startIndex + match[0].length
if endIndex - lastBreakIndex > @maxLength
breakIndices.push(startIndex)
lastBreakIndex = startIndex
currentSegment = []
currentSegment.lastIndex = 0
segments = [currentSegment]
nextBreak = breakIndices.shift()
for token in @highlighter.tokensForRow(row)
if currentSegment.lastIndex >= nextBreak
nextBreak = breakIndices.shift()
newSegment = []
newSegment.lastIndex = currentSegment.lastIndex
newSegment.textLength = 0
segments.push(newSegment)
currentSegment = newSegment
currentSegment.push token
currentSegment.lastIndex += token.value.length
currentSegment.textLength += token.value.length
segments