From 312bb34c0b16b8bec90b06708c37fc4eb8be4a05 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Feb 2012 16:52:12 -0700 Subject: [PATCH] Use LineMap in LineFolder. All specs pass. --- spec/atom/line-folder-spec.coffee | 31 ++++---- spec/atom/line-map-spec.coffee | 82 +++++++++++++++++++--- spec/atom/screen-line-fragment-spec.coffee | 32 +++++++-- src/atom/delta.coffee | 9 +++ src/atom/highlighter.coffee | 3 + src/atom/line-folder.coffee | 64 ++++++++++++----- src/atom/line-map.coffee | 70 +++++++++++++++++- src/atom/point.coffee | 3 + src/atom/range.coffee | 10 +++ src/atom/screen-line-fragment.coffee | 12 +++- 10 files changed, 266 insertions(+), 50 deletions(-) diff --git a/spec/atom/line-folder-spec.coffee b/spec/atom/line-folder-spec.coffee index 30282443c..008139694 100644 --- a/spec/atom/line-folder-spec.coffee +++ b/spec/atom/line-folder-spec.coffee @@ -13,26 +13,26 @@ describe "LineFolder", -> describe "screen line rendering", -> describe "when there is a single fold spanning multiple lines", -> - fit "renders a placeholder on the first line of a fold, and skips subsequent lines", -> + it "renders a placeholder on the first line of a fold, and skips subsequent lines", -> folder.fold(new Range([4, 29], [7, 4])) - [line4, line5] = folder.screenLinesForRows(4, 5) + [line4, line5] = fragments = folder.linesForScreenRows(4, 5) expect(line4.text).toBe ' while(items.length > 0) {...}' expect(line5.text).toBe ' return sort(left).concat(pivot).concat(sort(right));' describe "when there is a single fold contained on a single line", -> it "renders a placeholder for the folded region, but does not skip any lines", -> - folder.createFold(new Range([2, 8], [2, 25])) - [line2, line3] = folder.screenLinesForRows(2, 3) + folder.fold(new Range([2, 8], [2, 25])) + [line2, line3] = folder.linesForScreenRows(2, 3) expect(line2.text).toBe ' if (...) return items;' expect(line3.text).toBe ' var pivot = items.shift(), current, left = [], right = [];' describe "when there is a nested fold on the last line of another fold", -> it "does not render a placeholder for the nested fold because it is inside of the other fold", -> - folder.createFold(new Range([8, 5], [8, 10])) - folder.createFold(new Range([4, 29], [8, 36])) - [line4, line5] = folder.screenLinesForRows(4, 5) + folder.fold(new Range([8, 5], [8, 10])) + folder.fold(new Range([4, 29], [8, 36])) + [line4, line5] = folder.linesForScreenRows(4, 5) expect(line4.text).toBe ' while(items.length > 0) {...concat(sort(right));' expect(line5.text).toBe ' };' @@ -40,24 +40,25 @@ describe "LineFolder", -> describe "when another fold begins on the last line of a fold", -> describe "when the second fold is created before the first fold", -> it "renders a placeholder for both folds on the first line of the first fold", -> - folder.createFold(new Range([7, 5], [8, 36])) - folder.createFold(new Range([4, 29], [7, 4])) - [line4, line5] = folder.screenLinesForRows(4, 5) + folder.fold(new Range([7, 5], [8, 36])) + folder.fold(new Range([4, 29], [7, 4])) + [line4, line5] = folder.linesForScreenRows(4, 5) expect(line4.text).toBe ' while(items.length > 0) {...}...concat(sort(right));' + expect(line5.text).toBe ' };' describe "when the second fold is created after the first fold", -> it "renders a placeholder for both folds on the first line of the first fold", -> - folder.createFold(new Range([4, 29], [7, 4])) - folder.createFold(new Range([7, 5], [8, 36])) - [line4, line5] = folder.screenLinesForRows(4, 5) - + folder.fold(new Range([4, 29], [7, 4])) + folder.fold(new Range([7, 5], [8, 36])) + [line4, line5] = folder.linesForScreenRows(4, 5) expect(line4.text).toBe ' while(items.length > 0) {...}...concat(sort(right));' + expect(line5.text).toBe ' };' describe ".screenPositionForBufferPosition(bufferPosition)", -> describe "when there is single fold spanning multiple lines", -> it "translates positions to account for folded lines and characters and the placeholder", -> - folder.createFold(new Range([4, 29], [7, 4])) + folder.fold(new Range([4, 29], [7, 4])) # preceding fold: identity expect(folder.screenPositionForBufferPosition([3, 0])).toEqual [3, 0] diff --git a/spec/atom/line-map-spec.coffee b/spec/atom/line-map-spec.coffee index a5d9911eb..be3cdeb78 100644 --- a/spec/atom/line-map-spec.coffee +++ b/spec/atom/line-map-spec.coffee @@ -89,19 +89,81 @@ describe "LineMap", -> expect(map.lineFragmentsForScreenRow(1)).toEqual [line3a, line3b] expect(map.lineFragmentsForScreenRow(2)).toEqual [line4] - describe ".lineFragmentsForScreenRows(startRow, endRow)", -> - it "returns all line fragments for the given row range", -> - [line1a, line1b] = line1.splitAt(10) - [line3a, line3b] = line3.splitAt(10) - map.insertAtBufferRow(0, [line0, line1a, line1b, line2, line3a, line3b, line4]) + describe ".spliceAtScreenRow(startRow, rowCount, lineFragemnts)", -> + describe "when called with a row count of 0", -> + it "inserts the given line fragments before the specified buffer row", -> + map.insertAtBufferRow(0, [line0, line1, line2]) + map.spliceAtScreenRow(1, 0, [line3, line4]) - expect(map.lineFragmentsForScreenRows(1, 3)).toEqual [line1a, line1b, line2, line3a, line3b] + expect(map.lineFragmentsForScreenRow(0)).toEqual [line0] + expect(map.lineFragmentsForScreenRow(1)).toEqual [line3] + expect(map.lineFragmentsForScreenRow(2)).toEqual [line4] + expect(map.lineFragmentsForScreenRow(3)).toEqual [line1] + expect(map.lineFragmentsForScreenRow(4)).toEqual [line2] + + describe "when called with a row count of 1", -> + describe "when the specified screen row is spanned by a single line fragment", -> + it "replaces the spanning line fragment with the given line fragments", -> + map.insertAtBufferRow(0, [line0, line1, line2]) + map.spliceAtScreenRow(1, 1, [line3, line4]) + + expect(map.bufferLineCount()).toBe 4 + expect(map.lineFragmentsForScreenRow(0)).toEqual [line0] + expect(map.lineFragmentsForScreenRow(1)).toEqual [line3] + expect(map.lineFragmentsForScreenRow(2)).toEqual [line4] + expect(map.lineFragmentsForScreenRow(3)).toEqual [line2] + + describe "when the specified screen row is spanned by multiple line fragments", -> + it "replaces all spanning line fragments with the given line fragments", -> + [line1a, line1b] = line1.splitAt(10) + [line3a, line3b] = line3.splitAt(10) + + map.insertAtBufferRow(0, [line0, line1a, line1b, line2]) + map.spliceAtScreenRow(1, 1, [line3a, line3b, line4]) + + expect(map.bufferLineCount()).toBe 4 + expect(map.lineFragmentsForScreenRow(0)).toEqual [line0] + expect(map.lineFragmentsForScreenRow(1)).toEqual [line3a, line3b] + expect(map.lineFragmentsForScreenRow(2)).toEqual [line4] + expect(map.lineFragmentsForScreenRow(3)).toEqual [line2] + + describe "when called with a row count greater than 1", -> + it "replaces all line fragments spanning the multiple buffer rows with the given line fragments", -> + [line1a, line1b] = line1.splitAt(10) + [line3a, line3b] = line3.splitAt(10) + + map.insertAtBufferRow(0, [line0, line1a, line1b, line2]) + map.spliceAtScreenRow(1, 2, [line3a, line3b, line4]) + + expect(map.bufferLineCount()).toBe 3 + expect(map.lineFragmentsForScreenRow(0)).toEqual [line0] + expect(map.lineFragmentsForScreenRow(1)).toEqual [line3a, line3b] + expect(map.lineFragmentsForScreenRow(2)).toEqual [line4] + + + describe ".linesForScreenRows(startRow, endRow)", -> + it "returns lines for the given row range, concatenating fragments that belong on a single screen line", -> + line1Text = line1.text + [line1a, line1b] = line1.splitAt(11) + [line3a, line3b] = line3.splitAt(16) + map.insertAtBufferRow(0, [line0, line1a, line1b, line2, line3a, line3b, line4]) + expect(map.linesForScreenRows(1, 3)).toEqual [line1, line2, line3] + # repeating assertion to cover a regression where this method mutated lines + expect(map.linesForScreenRows(1, 3)).toEqual [line1, line2, line3] describe ".screenPositionFromBufferPosition(bufferPosition)", -> - describe "", -> - - - + it "translates the given buffer position based on buffer and screen deltas of the line fragments in the map", -> + [line1a, line1b] = line1.splitAt(10) + [line3a, line3b] = line3.splitAt(20) + line1a.bufferDelta.rows = 2 + line1a.bufferDelta.columns = 20 + map.insertAtBufferRow(0, [line0, line1a, line3b, line4]) + expect(map.screenPositionForBufferPosition([0, 0])).toEqual [0, 0] + expect(map.screenPositionForBufferPosition([0, 5])).toEqual [0, 5] + expect(map.screenPositionForBufferPosition([1, 5])).toEqual [1, 5] + expect(map.screenPositionForBufferPosition([3, 20])).toEqual [1, 10] + expect(map.screenPositionForBufferPosition([3, 30])).toEqual [1, 20] + expect(map.screenPositionForBufferPosition([4, 5])).toEqual [2, 5 ] diff --git a/spec/atom/screen-line-fragment-spec.coffee b/spec/atom/screen-line-fragment-spec.coffee index 5aadf0afd..183203147 100644 --- a/spec/atom/screen-line-fragment-spec.coffee +++ b/spec/atom/screen-line-fragment-spec.coffee @@ -3,7 +3,7 @@ Buffer = require 'buffer' Highlighter = require 'highlighter' describe "screenLineFragment", -> - lineFragment = null + [lineFragment, highlighter] = [] beforeEach -> buffer = new Buffer(require.resolve 'fixtures/sample.js') @@ -48,6 +48,30 @@ describe "screenLineFragment", -> it "returns undefined for the left half", -> expect(lineFragment.splitAt(0)).toEqual [undefined, lineFragment] - describe "if splitting at a column >= the line length", -> - it "returns undefined for the right half", -> - expect(lineFragment.splitAt(lineFragment.text.length)).toEqual [lineFragment, undefined] + describe "if splitting at a column equal to the line length", -> + it "returns an empty line fragment that spans a row for the right half", -> + [left, right] = lineFragment.splitAt(lineFragment.text.length) + + expect(left.text).toBe lineFragment.text + expect(left.screenDelta).toEqual [0, lineFragment.text.length] + expect(left.bufferDelta).toEqual [0, lineFragment.text.length] + + expect(right.text).toBe '' + expect(right.screenDelta).toEqual [1, 0] + expect(right.bufferDelta).toEqual [1, 0] + + describe ".concat(otherFragment)", -> + it "returns the concatenation of the receiver and the given fragment", -> + [left, right] = lineFragment.splitAt(14) + expect(left.concat(right)).toEqual lineFragment + + concatenated = lineFragment.concat(highlighter.lineFragmentForRow(4)) + expect(concatenated.text).toBe ' var pivot = items.shift(), current, left = [], right = []; while(items.length > 0) {' + expect(tokensText concatenated.tokens).toBe concatenated.text + expect(concatenated.screenDelta).toEqual [2, 0] + expect(concatenated.bufferDelta).toEqual [2, 0] + + + + + diff --git a/src/atom/delta.coffee b/src/atom/delta.coffee index b19c5778a..867614bbb 100644 --- a/src/atom/delta.coffee +++ b/src/atom/delta.coffee @@ -1,3 +1,5 @@ +Point = require 'point' + module.exports = class Delta @fromObject: (object) -> @@ -9,6 +11,7 @@ class Delta constructor: (@rows=0, @columns=0) -> add: (other) -> + debugger unless other rows = @rows + other.rows if other.rows == 0 columns = @columns + other.columns @@ -25,6 +28,12 @@ class Delta [new Delta(0, column), new Delta(@rows, rightColumns)] + inspect: -> + "(#{@rows}, #{@columns})" + isEqual: (other) -> other = Delta.fromObject(other) @rows == other.rows and @columns == other.columns + + toPoint: -> + new Point(@rows, @columns) diff --git a/src/atom/highlighter.coffee b/src/atom/highlighter.coffee index 839f85459..ae3a6bc27 100644 --- a/src/atom/highlighter.coffee +++ b/src/atom/highlighter.coffee @@ -64,6 +64,9 @@ class Highlighter screenLineForRow: (row) -> @screenLines[row] + lineFragments: -> + @lineFragmentsForRows(0, @buffer.lastRow()) + lineFragmentsForRows: (startRow, endRow) -> for row in [startRow..endRow] @lineFragmentForRow(row) diff --git a/src/atom/line-folder.coffee b/src/atom/line-folder.coffee index cccaa78da..fc912535d 100644 --- a/src/atom/line-folder.coffee +++ b/src/atom/line-folder.coffee @@ -1,5 +1,7 @@ Point = require 'point' LineMap = require 'line-map' +ScreenLineFragment = require 'screen-line-fragment' +_ = require 'underscore' module.exports = class LineFolder @@ -9,33 +11,61 @@ class LineFolder buildLineMap: -> @lineMap = new LineMap - @lineMap.insertAtBufferRow(0, @highlighter.screenLines) + @lineMap.insertAtBufferRow(0, @highlighter.lineFragments()) - fold: (range) -> - { start, end } = range - @activeFolds[start.row] ?= [] - @activeFolds[start.row].push(new Fold(this, range)) + fold: (bufferRange) -> + @activeFolds[bufferRange.start.row] ?= [] + @activeFolds[bufferRange.start.row].push(new Fold(this, bufferRange)) + screenRange = @screenRangeForBufferRange(bufferRange) + @lineMap.replaceScreenRows(screenRange.start.row, screenRange.end.row, @renderScreenLine(screenRange.start.row)) - screenRow = @screenRowForBufferRow(start.row) - @lineMap.replaceBufferRows(start.row, end.row, @buildScreenLineForRow(screenRow)) + renderScreenLine: (screenRow) -> + @renderScreenLineForBufferRow(@bufferRowForScreenRow(screenRow)) - buildScreenLineForBufferRow: (bufferRow, startColumn) -> - screenLine = @highlighter.screenLineForRow(bufferRow).splitAt(startColumn)[1] + renderScreenLineForBufferRow: (bufferRow, startColumn=0) -> + screenLine = @highlighter.lineFragmentForRow(bufferRow).splitAt(startColumn)[1] for fold in @foldsForBufferRow(bufferRow) - if fold.start.column > startColumn - prefix = screenLine.splitAt(fold.start.column - startColumn)[0] - suffix = @buildScreenLineForBufferRow(fold.end.row, fold.end.column) - return [prefix, @foldPlaceholder(fold), suffix] + { start, end } = fold.range + if start.column > startColumn + prefix = screenLine.splitAt(start.column - startColumn)[0] + suffix = @buildScreenLineForBufferRow(end.row, end.column) + return _.flatten([prefix, @buildFoldPlaceholder(fold), suffix]) screenLine - screenRowForBufferRow: (screenRow) -> - @lineMap.screenPositionForBufferPosition([screenRow, 0]).row + buildScreenLineForBufferRow: (bufferRow, startColumn=0) -> + screenLine = @highlighter.lineFragmentForRow(bufferRow).splitAt(startColumn)[1] + for fold in @foldsForBufferRow(bufferRow) + { start, end } = fold.range + if start.column > startColumn + prefix = screenLine.splitAt(start.column - startColumn)[0] + suffix = @buildScreenLineForBufferRow(end.row, end.column) + screenLine = _.flatten([prefix, @buildFoldPlaceholder(fold), suffix]) + return screenLine + screenLine - lineFragmentsForScreenRows: (startRow, endRow) -> - @lineMap.lineFragmentsForScreenRows(startRow, endRow) + buildFoldPlaceholder: (fold) -> + new ScreenLineFragment([{value: '...', type: 'fold-placeholder'}], '...', [0, 3], fold.range.toDelta()) + + foldsForBufferRow: (bufferRow) -> + @activeFolds[bufferRow] or [] + + linesForScreenRows: (startRow, endRow) -> + @lineMap.linesForScreenRows(startRow, endRow) + + screenRowForBufferRow: (bufferRow) -> + @screenPositionForBufferPosition([bufferRow, 0]).row + + bufferRowForScreenRow: (screenRow) -> + @bufferPositionForScreenPosition([screenRow, 0]).row screenPositionForBufferPosition: (bufferPosition) -> @lineMap.screenPositionForBufferPosition(bufferPosition) + bufferPositionForScreenPosition: (screenPosition) -> + @lineMap.bufferPositionForScreenPosition(screenPosition) + + screenRangeForBufferRange: (bufferRange) -> + @lineMap.screenRangeForBufferRange(bufferRange) + class Fold constructor: (@lineFolder, @range) -> diff --git a/src/atom/line-map.coffee b/src/atom/line-map.coffee index 7e770d8c2..887d5fee7 100644 --- a/src/atom/line-map.coffee +++ b/src/atom/line-map.coffee @@ -1,5 +1,7 @@ _ = require 'underscore' Delta = require 'delta' +Point = require 'point' +Range = require 'range' module.exports = class LineMap @@ -20,20 +22,32 @@ class LineMap @lineFragments[insertIndex...insertIndex] = lineFragments spliceAtBufferRow: (startRow, rowCount, lineFragments) -> + @spliceByDelta('bufferDelta', startRow, rowCount, lineFragments) + + spliceAtScreenRow: (startRow, rowCount, lineFragments) -> + @spliceByDelta('screenDelta', startRow, rowCount, lineFragments) + + spliceByDelta: (deltaType, startRow, rowCount, lineFragments) -> stopRow = startRow + rowCount startIndex = undefined stopIndex = 0 delta = new Delta for lineFragment, i in @lineFragments - startIndex ?= i if delta.rows == startRow - nextDelta = delta.add(lineFragment.bufferDelta) + startIndex = i if delta.rows == startRow and not startIndex + nextDelta = delta.add(lineFragment[deltaType]) break if nextDelta.rows > stopRow delta = nextDelta stopIndex++ @lineFragments[startIndex...stopIndex] = lineFragments + replaceBufferRows: (start, end, lineFragments) -> + @spliceAtBufferRow(start, end - start + 1, lineFragments) + + replaceScreenRows: (start, end, lineFragments) -> + @spliceAtScreenRow(start, end - start + 1, lineFragments) + lineFragmentsForScreenRow: (screenRow) -> @lineFragmentsForScreenRows(screenRow, screenRow) @@ -48,8 +62,60 @@ class LineMap lineFragments + linesForScreenRows: (startRow, endRow) -> + lastLine = null + lines = [] + delta = new Delta + + for fragment in @lineFragments + break if delta.rows > endRow + if delta.rows >= startRow + if pendingFragment + pendingFragment = pendingFragment.concat(fragment) + else + pendingFragment = fragment + if pendingFragment.screenDelta.rows > 0 + lines.push pendingFragment + pendingFragment = null + delta = delta.add(fragment.screenDelta) + lines + bufferLineCount: -> delta = new Delta for lineFragment in @lineFragments delta = delta.add(lineFragment.bufferDelta) delta.rows + + screenPositionForBufferPosition: (bufferPosition) -> + bufferPosition = Point.fromObject(bufferPosition) + bufferDelta = new Delta + screenDelta = new Delta + + for lineFragment in @lineFragments + nextDelta = bufferDelta.add(lineFragment.bufferDelta) + break if nextDelta.toPoint().greaterThan(bufferPosition) + bufferDelta = nextDelta + screenDelta = screenDelta.add(lineFragment.screenDelta) + + columns = screenDelta.columns + (bufferPosition.column - bufferDelta.columns) + new Point(screenDelta.rows, columns) + + bufferPositionForScreenPosition: (screenPosition) -> + screenPosition = Point.fromObject(screenPosition) + bufferDelta = new Delta + screenDelta = new Delta + + for lineFragment in @lineFragments + nextDelta = screenDelta.add(lineFragment.screenDelta) + break if nextDelta.toPoint().greaterThan(screenPosition) + screenDelta = nextDelta + bufferDelta = bufferDelta.add(lineFragment.bufferDelta) + + columns = bufferDelta.columns + (screenPosition.column - screenDelta.columns) + new Point(bufferDelta.rows, columns) + + screenRangeForBufferRange: (bufferRange) -> + start = @screenPositionForBufferPosition(bufferRange.start) + end = @screenPositionForBufferPosition(bufferRange.end) + new Range(start, end) + diff --git a/src/atom/point.coffee b/src/atom/point.coffee index 1b8b050ad..85521baff 100644 --- a/src/atom/point.coffee +++ b/src/atom/point.coffee @@ -34,3 +34,6 @@ class Point -1 else 0 + + greaterThan: (other) -> + @compare(other) > 0 diff --git a/src/atom/range.coffee b/src/atom/range.coffee index 5daab0192..0faa9992b 100644 --- a/src/atom/range.coffee +++ b/src/atom/range.coffee @@ -1,6 +1,8 @@ Point = require 'point' +Delta = require 'delta' _ = require 'underscore' + module.exports = class Range constructor: (pointA = new Point(0, 0), pointB = new Point(0, 0)) -> @@ -29,3 +31,11 @@ class Range isEmpty: -> @start.isEqual(@end) + toDelta: -> + rows = @end.row - @start.row + if rows == 0 + columns = @end.column - @start.column + else + columns = @end.column + new Delta(rows, columns) + diff --git a/src/atom/screen-line-fragment.coffee b/src/atom/screen-line-fragment.coffee index dfa6e2860..4593b65e9 100644 --- a/src/atom/screen-line-fragment.coffee +++ b/src/atom/screen-line-fragment.coffee @@ -7,10 +7,8 @@ class ScreenLineFragment @screenDelta = Delta.fromObject(screenDelta) @bufferDelta = Delta.fromObject(bufferDelta) - splitAt: (column) -> return [undefined, this] if column == 0 - return [this, undefined] if column >= @text.length rightTokens = _.clone(@tokens) leftTokens = [] @@ -37,3 +35,13 @@ class ScreenLineFragment value1 = value.substring(0, splitIndex) value2 = value.substring(splitIndex) [{value: value1, type }, {value: value2, type}] + + concat: (other) -> + tokens = @tokens.concat(other.tokens) + text = @text + other.text + screenDelta = @screenDelta.add(other.screenDelta) + bufferDelta = @bufferDelta.add(other.bufferDelta) + new ScreenLineFragment(tokens, text, screenDelta, bufferDelta) + + isEqual: (other) -> + _.isEqual(@tokens, other.tokens) and @screenDelta.isEqual(other.screenDelta) and @bufferDelta.isEqual(other.bufferDelta)