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