From b21595f0379db350d36066e44267a31a529a6e71 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 7 Feb 2012 18:07:12 -0700 Subject: [PATCH] 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. --- spec/atom/editor-spec.coffee | 14 +++++--- spec/atom/line-wrapper-spec.coffee | 51 +++++++++++++++++++++++++++++ src/atom/editor.coffee | 51 +++++++++++++++-------------- src/atom/line-wrapper.coffee | 52 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 spec/atom/line-wrapper-spec.coffee create mode 100644 src/atom/line-wrapper.coffee diff --git a/spec/atom/editor-spec.coffee b/spec/atom/editor-spec.coffee index 0704eeff2..8e3551ed3 100644 --- a/spec/atom/editor-spec.coffee +++ b/spec/atom/editor-spec.coffee @@ -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})", -> diff --git a/spec/atom/line-wrapper-spec.coffee b/spec/atom/line-wrapper-spec.coffee new file mode 100644 index 000000000..eac57df53 --- /dev/null +++ b/spec/atom/line-wrapper-spec.coffee @@ -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", -> + diff --git a/src/atom/editor.coffee b/src/atom/editor.coffee index 272bf610a..07cf5c310 100644 --- a/src/atom/editor.coffee +++ b/src/atom/editor.coffee @@ -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) } diff --git a/src/atom/line-wrapper.coffee b/src/atom/line-wrapper.coffee new file mode 100644 index 000000000..d6e44325e --- /dev/null +++ b/src/atom/line-wrapper.coffee @@ -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