diff --git a/package.json b/package.json index fda558fa8..5060d8d00 100644 --- a/package.json +++ b/package.json @@ -144,5 +144,8 @@ "scripts": { "preinstall": "node -e 'process.exit(0)'", "test": "node script/test" + }, + "devDependencies": { + "random-words": "0.0.1" } } diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee new file mode 100644 index 000000000..ff3b9b1f0 --- /dev/null +++ b/spec/random-editor-spec.coffee @@ -0,0 +1,112 @@ +{times, random} = require 'underscore-plus' +randomWords = require 'random-words' +Editor = require '../src/editor' +TextBuffer = require '../src/text-buffer' + +describe "Editor", -> + [editor, tokenizedBuffer, buffer, steps, previousSteps] = [] + + softWrapColumn = 80 + + beforeEach -> + atom.config.set('editor.softWrapAtPreferredLineLength', true) + atom.config.set('editor.preferredLineLength', softWrapColumn) + + it "properly renders soft-wrapped lines when randomly mutated", -> + previousSteps = JSON.parse(localStorage.steps ? '[]') + + times 10, (i) -> + buffer = new TextBuffer + editor = new Editor({buffer}) + editor.setEditorWidthInChars(80) + tokenizedBuffer = editor.displayBuffer.tokenizedBuffer + steps = [] + + times 30, -> + randomlyMutateEditor() + verifyLines() + + verifyLines = -> + {bufferRows, screenLines} = getReferenceScreenLines() + for referenceBufferRow, screenRow in bufferRows + referenceScreenLine = screenLines[screenRow] + actualBufferRow = editor.bufferRowForScreenRow(screenRow) + unless actualBufferRow is referenceBufferRow + logLines() + throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", ) + + actualScreenLine = editor.lineForScreenRow(screenRow) + unless actualScreenLine.text is referenceScreenLine.text + logLines() + throw new Error("Invalid line text at screen row #{screenRow}") + + logLines = -> + console.log "==== screen lines ====" + editor.logScreenLines() + console.log "==== reference lines ====" + {bufferRows, screenLines} = getReferenceScreenLines() + for bufferRow, screenRow in bufferRows + console.log screenRow, bufferRow, screenLines[screenRow].text + + randomlyMutateEditor = -> + if Math.random() < .2 + softWrap = not editor.getSoftWrap() + steps.push(['setSoftWrap', softWrap]) + editor.setSoftWrap(softWrap) + else + range = getRandomRange() + text = getRandomText() + steps.push(['setTextInBufferRange', range, text]) + editor.setTextInBufferRange(range, text) + + getRandomRange = -> + startRow = random(0, buffer.getLastRow()) + startColumn = random(0, buffer.lineForRow(startRow).length) + endRow = random(startRow, buffer.getLastRow()) + endColumn = random(0, buffer.lineForRow(endRow).length) + [[startRow, startColumn], [endRow, endColumn]] + + getRandomText = -> + text = [] + max = buffer.getText().split(/\s/).length * 0.75 + + times random(5, max), -> + if Math.random() < .1 + text += '\n' + else + text += " " if /\w$/.test(text) + text += randomWords(exactly: 1) + text + + getReferenceScreenLines = -> + if editor.getSoftWrap() + screenLines = [] + bufferRows = [] + for bufferRow in [0..tokenizedBuffer.getLastRow()] + for screenLine in softWrapLine(tokenizedBuffer.lineForScreenRow(bufferRow)) + screenLines.push(screenLine) + bufferRows.push(bufferRow) + else + screenLines = tokenizedBuffer.tokenizedLines.slice() + bufferRows = [0..tokenizedBuffer.getLastRow()] + {screenLines, bufferRows} + + softWrapLine = (tokenizedLine) -> + wrappedLines = [] + while tokenizedLine.text.length > softWrapColumn and wrapScreenColumn = findWrapColumn(tokenizedLine.text) + [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) + wrappedLines.push(wrappedLine) + wrappedLines.push(tokenizedLine) + wrappedLines + + findWrapColumn = (line) -> + if /\s/.test(line[softWrapColumn]) + # search forward for the start of a word past the boundary + for column in [softWrapColumn..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 [softWrapColumn..0] + return column + 1 if /\s/.test(line[column]) + return softWrapColumn diff --git a/spec/row-map-spec.coffee b/spec/row-map-spec.coffee index 7d8d8da65..e89ac1259 100644 --- a/spec/row-map-spec.coffee +++ b/spec/row-map-spec.coffee @@ -6,256 +6,99 @@ describe "RowMap", -> beforeEach -> map = new RowMap - describe "when no mappings have been recorded", -> - it "maps buffer rows to screen rows 1:1", -> + describe "::screenRowRangeForBufferRow(bufferRow)", -> + it "returns the range of screen rows corresponding to the given buffer row", -> + map.spliceRegions(0, 0, [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 1, screenRows: 5} + {bufferRows: 5, screenRows: 5} + {bufferRows: 5, screenRows: 1} + ]) + expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1] - expect(map.screenRowRangeForBufferRow(100)).toEqual [100, 101] + expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 10] + expect(map.screenRowRangeForBufferRow(6)).toEqual [10, 11] + expect(map.screenRowRangeForBufferRow(11)).toEqual [15, 16] + expect(map.screenRowRangeForBufferRow(12)).toEqual [15, 16] + expect(map.screenRowRangeForBufferRow(16)).toEqual [16, 17] - describe ".mapBufferRowRange(startBufferRow, endBufferRow, screenRows)", -> - describe "when mapping to a single screen row (like a visible fold)", -> - beforeEach -> - map.mapBufferRowRange(5, 10, 1) - map.mapBufferRowRange(15, 20, 1) - map.mapBufferRowRange(25, 30, 1) + describe "::bufferRowRangeForScreenRow(screenRow)", -> + it "returns the range of buffer rows corresponding to the given screen row", -> + map.spliceRegions(0, 0, [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 1, screenRows: 5} + {bufferRows: 5, screenRows: 5} + {bufferRows: 5, screenRows: 1} + ]) - it "accounts for the mapping when translating buffer rows to screen row ranges", -> - expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1] + expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1] + expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 6] + expect(map.bufferRowRangeForScreenRow(6)).toEqual [5, 6] + expect(map.bufferRowRangeForScreenRow(10)).toEqual [6, 7] + expect(map.bufferRowRangeForScreenRow(14)).toEqual [10, 11] + expect(map.bufferRowRangeForScreenRow(15)).toEqual [11, 16] + expect(map.bufferRowRangeForScreenRow(16)).toEqual [16, 17] - expect(map.screenRowRangeForBufferRow(4)).toEqual [4, 5] - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 6] - expect(map.screenRowRangeForBufferRow(9)).toEqual [5, 6] - expect(map.screenRowRangeForBufferRow(10)).toEqual [6, 7] + describe "::spliceRegions(startBufferRow, bufferRowCount, regions)", -> + it "can insert regions when empty", -> + regions = [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 1, screenRows: 5} + {bufferRows: 5, screenRows: 5} + {bufferRows: 5, screenRows: 1} + ] + map.spliceRegions(0, 0, regions) + expect(map.getRegions()).toEqual regions - expect(map.screenRowRangeForBufferRow(14)).toEqual [10, 11] - expect(map.screenRowRangeForBufferRow(15)).toEqual [11, 12] - expect(map.screenRowRangeForBufferRow(19)).toEqual [11, 12] - expect(map.screenRowRangeForBufferRow(20)).toEqual [12, 13] + it "can insert wrapped lines into rectangular regions", -> + map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) + map.spliceRegions(5, 0, [{bufferRows: 1, screenRows: 3}]) + expect(map.getRegions()).toEqual [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 1, screenRows: 3} + {bufferRows: 5, screenRows: 5} + ] - expect(map.screenRowRangeForBufferRow(24)).toEqual [16, 17] - expect(map.screenRowRangeForBufferRow(25)).toEqual [17, 18] - expect(map.screenRowRangeForBufferRow(29)).toEqual [17, 18] - expect(map.screenRowRangeForBufferRow(30)).toEqual [18, 19] + it "can splice wrapped lines into rectangular regions", -> + map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) + map.spliceRegions(5, 1, [{bufferRows: 1, screenRows: 3}]) + expect(map.getRegions()).toEqual [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 1, screenRows: 3} + {bufferRows: 4, screenRows: 4} + ] - it "accounts for the mapping when translating screen rows to buffer row ranges", -> - expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1] + it "can splice folded lines into rectangular regions", -> + map.spliceRegions(0, 0, [{bufferRows: 10, screenRows: 10}]) + map.spliceRegions(5, 3, [{bufferRows: 3, screenRows: 1}]) + expect(map.getRegions()).toEqual [ + {bufferRows: 5, screenRows: 5} + {bufferRows: 3, screenRows: 1} + {bufferRows: 2, screenRows: 2} + ] - expect(map.bufferRowRangeForScreenRow(4)).toEqual [4, 5] - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 10] - expect(map.bufferRowRangeForScreenRow(6)).toEqual [10, 11] + it "can replace folded regions with a folded region that surrounds them", -> + map.spliceRegions(0, 0, [ + {bufferRows: 3, screenRows: 3} + {bufferRows: 3, screenRows: 1} + {bufferRows: 1, screenRows: 1} + {bufferRows: 3, screenRows: 1} + {bufferRows: 3, screenRows: 3} + ]) + map.spliceRegions(2, 8, [{bufferRows: 8, screenRows: 1}]) + expect(map.getRegions()).toEqual [ + {bufferRows: 2, screenRows: 2} + {bufferRows: 8, screenRows: 1} + {bufferRows: 3, screenRows: 3} + ] - expect(map.bufferRowRangeForScreenRow(10)).toEqual [14, 15] - expect(map.bufferRowRangeForScreenRow(11)).toEqual [15, 20] - expect(map.bufferRowRangeForScreenRow(12)).toEqual [20, 21] + it "merges adjacent rectangular regions", -> + map.spliceRegions(0, 0, [ + {bufferRows: 3, screenRows: 3} + {bufferRows: 3, screenRows: 1} + {bufferRows: 1, screenRows: 1} + {bufferRows: 3, screenRows: 1} + {bufferRows: 3, screenRows: 3} + ]) - expect(map.bufferRowRangeForScreenRow(16)).toEqual [24, 25] - expect(map.bufferRowRangeForScreenRow(17)).toEqual [25, 30] - expect(map.bufferRowRangeForScreenRow(18)).toEqual [30, 31] - - describe "when mapping to zero screen rows (like an invisible fold)", -> - beforeEach -> - map.mapBufferRowRange(5, 10, 0) - map.mapBufferRowRange(15, 20, 0) - map.mapBufferRowRange(25, 30, 0) - - it "accounts for the mapping when translating buffer rows to screen row ranges", -> - expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1] - - expect(map.screenRowRangeForBufferRow(4)).toEqual [4, 5] - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 5] - expect(map.screenRowRangeForBufferRow(9)).toEqual [5, 5] - expect(map.screenRowRangeForBufferRow(10)).toEqual [5, 6] - - expect(map.screenRowRangeForBufferRow(14)).toEqual [9, 10] - expect(map.screenRowRangeForBufferRow(15)).toEqual [10, 10] - expect(map.screenRowRangeForBufferRow(19)).toEqual [10, 10] - expect(map.screenRowRangeForBufferRow(20)).toEqual [10, 11] - - expect(map.screenRowRangeForBufferRow(24)).toEqual [14, 15] - expect(map.screenRowRangeForBufferRow(25)).toEqual [15, 15] - expect(map.screenRowRangeForBufferRow(29)).toEqual [15, 15] - expect(map.screenRowRangeForBufferRow(30)).toEqual [15, 16] - - it "accounts for the mapping when translating screen rows to buffer row ranges", -> - expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1] - - expect(map.bufferRowRangeForScreenRow(4)).toEqual [4, 5] - expect(map.bufferRowRangeForScreenRow(5)).toEqual [10, 11] - - expect(map.bufferRowRangeForScreenRow(9)).toEqual [14, 15] - expect(map.bufferRowRangeForScreenRow(10)).toEqual [20, 21] - - expect(map.bufferRowRangeForScreenRow(14)).toEqual [24, 25] - expect(map.bufferRowRangeForScreenRow(15)).toEqual [30, 31] - - describe "when mapping a single buffer row to multiple screen rows (like a wrapped line)", -> - beforeEach -> - map.mapBufferRowRange(5, 6, 3) - map.mapBufferRowRange(10, 11, 2) - map.mapBufferRowRange(20, 21, 5) - - it "accounts for the mapping when translating buffer rows to screen row ranges", -> - expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 1] - - expect(map.screenRowRangeForBufferRow(4)).toEqual [4, 5] - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 8] - expect(map.screenRowRangeForBufferRow(6)).toEqual [8, 9] - - expect(map.screenRowRangeForBufferRow(9)).toEqual [11, 12] - expect(map.screenRowRangeForBufferRow(10)).toEqual [12, 14] - expect(map.screenRowRangeForBufferRow(11)).toEqual [14, 15] - - expect(map.screenRowRangeForBufferRow(19)).toEqual [22, 23] - expect(map.screenRowRangeForBufferRow(20)).toEqual [23, 28] - expect(map.screenRowRangeForBufferRow(21)).toEqual [28, 29] - - it "accounts for the mapping when translating screen rows to buffer row ranges", -> - expect(map.bufferRowRangeForScreenRow(0)).toEqual [0, 1] - - expect(map.bufferRowRangeForScreenRow(4)).toEqual [4, 5] - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 6] - expect(map.bufferRowRangeForScreenRow(7)).toEqual [5, 6] - expect(map.bufferRowRangeForScreenRow(8)).toEqual [6, 7] - - expect(map.bufferRowRangeForScreenRow(11)).toEqual [9, 10] - expect(map.bufferRowRangeForScreenRow(12)).toEqual [10, 11] - expect(map.bufferRowRangeForScreenRow(13)).toEqual [10, 11] - expect(map.bufferRowRangeForScreenRow(14)).toEqual [11, 12] - - expect(map.bufferRowRangeForScreenRow(22)).toEqual [19, 20] - expect(map.bufferRowRangeForScreenRow(23)).toEqual [20, 21] - expect(map.bufferRowRangeForScreenRow(27)).toEqual [20, 21] - expect(map.bufferRowRangeForScreenRow(28)).toEqual [21, 22] - - describe "after re-mapping a row range to a new number of screen rows", -> - beforeEach -> - map.applyScreenDelta(12, 2) # simulate adding 2 more soft wraps - map.mapBufferRowRange(10, 11, 4) - - it "updates translation accordingly", -> - expect(map.screenRowRangeForBufferRow(4)).toEqual [4, 5] - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 8] - expect(map.screenRowRangeForBufferRow(6)).toEqual [8, 9] - - expect(map.screenRowRangeForBufferRow(9)).toEqual [11, 12] - expect(map.screenRowRangeForBufferRow(10)).toEqual [12, 16] - expect(map.screenRowRangeForBufferRow(11)).toEqual [16, 17] - - expect(map.screenRowRangeForBufferRow(19)).toEqual [24, 25] - expect(map.screenRowRangeForBufferRow(20)).toEqual [25, 30] - expect(map.screenRowRangeForBufferRow(21)).toEqual [30, 31] - - describe "when the row range is inside an existing 1:1 region", -> - it "preserves the starting screen row of subsequent 1:N regions", -> - map.mapBufferRowRange(5, 10, 1) - map.mapBufferRowRange(25, 30, 1) - - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 10] - expect(map.bufferRowRangeForScreenRow(21)).toEqual [25, 30] - - map.mapBufferRowRange(15, 20, 1) - - expect(map.bufferRowRangeForScreenRow(11)).toEqual [15, 20] - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 10] - expect(map.bufferRowRangeForScreenRow(21)).toEqual [25, 30] - - describe "when the row range surrounds existing regions", -> - it "replaces the regions inside the given buffer row range with a single region", -> - map.mapBufferRowRange(5, 10, 1) # inner fold 1 - map.mapBufferRowRange(11, 13, 1) # inner fold 2 - map.mapBufferRowRange(15, 20, 1) # inner fold 3 - map.mapBufferRowRange(22, 27, 1) # following fold - - map.mapBufferRowRange(5, 20, 1) - - expect(map.bufferRowRangeForScreenRow(5)).toEqual [5, 20] - expect(map.bufferRowRangeForScreenRow(6)).toEqual [20, 21] - expect(map.bufferRowRangeForScreenRow(7)).toEqual [21, 22] - expect(map.bufferRowRangeForScreenRow(8)).toEqual [22, 27] - - it "replaces regions that cover 0 buffer rows at the start or end of the buffer row range", -> - map.mapBufferRowRange(0, 0, 1) - map.mapBufferRowRange(0, 1, 1) - map.mapBufferRowRange(1, 1, 1) - map.mapBufferRowRange(0, 1, 3) - expect(map.screenRowRangeForBufferRow(0)).toEqual [0, 3] - - describe "when the row range straddles existing regions", -> - it "splits the straddled regions and places the new region between them", -> - # filler region 0 - map.mapBufferRowRange(4, 7, 1) # region 1 - # filler region 2 - map.mapBufferRowRange(13, 15, 1) # region 3 - - # create region straddling region 0 and region 2 - map.mapBufferRowRange(2, 10, 1) - - expect(map.regions[0]).toEqual(bufferRows: 2, screenRows: 2) - expect(map.regions[1]).toEqual(bufferRows: 8, screenRows: 1) - expect(map.regions[2]).toEqual(bufferRows: 3, screenRows: 8) - expect(map.regions[3]).toEqual(bufferRows: 2, screenRows: 1) - - it "merges adjacent isomorphic mappings", -> - map.mapBufferRowRange(2, 4, 1) - map.mapBufferRowRange(4, 5, 2) - - map.mapBufferRowRange(1, 4, 3) - expect(map.regions).toEqual [{bufferRows: 5, screenRows: 5}] - - describe ".applyBufferDelta(startBufferRow, delta)", -> - describe "when applying a positive delta", -> - it "expands the region containing the given start row by the given delta", -> - map.mapBufferRowRange(4, 8, 1) - - map.applyBufferDelta(5, 4) - - expect(map.regions[0]).toEqual(bufferRows: 4, screenRows: 4) - expect(map.regions[1]).toEqual(bufferRows: 8, screenRows: 1) - - describe "when applying a negative delta", -> - it "shrinks regions starting at the start row until the entire delta is consumed", -> - map.mapBufferRowRange(4, 8, 1) - map.mapBufferRowRange(10, 14, 1) - - map.applyBufferDelta(3, -6) - - expect(map.regions[0]).toEqual(bufferRows: 3, screenRows: 4) - expect(map.regions[1]).toEqual(bufferRows: 0, screenRows: 1) - expect(map.regions[2]).toEqual(bufferRows: 1, screenRows: 2) - expect(map.regions[3]).toEqual(bufferRows: 4, screenRows: 1) - - describe ".applyScreenDelta(startScreenRow, delta)", -> - describe "when applying a positive delta", -> - it "can enlarge the screen side of existing regions", -> - map.mapBufferRowRange(5, 6, 3) # wrapped line - map.applyScreenDelta(5, 2) # wrap it twice more - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 10] - - describe "when applying a negative delta", -> - it "can collapse the screen side of multiple regions to 0 until the entire delta has been applied", -> - map.mapBufferRowRange(5, 10, 1) # inner fold 1 - map.mapBufferRowRange(11, 13, 1) # inner fold 2 - map.mapBufferRowRange(15, 20, 1) # inner fold 3 - map.mapBufferRowRange(22, 27, 1) # following fold - - map.applyScreenDelta(6, -5) - - expect(map.screenRowRangeForBufferRow(5)).toEqual [5, 6] - expect(map.screenRowRangeForBufferRow(9)).toEqual [5, 6] - expect(map.screenRowRangeForBufferRow(10)).toEqual [6, 6] - expect(map.screenRowRangeForBufferRow(19)).toEqual [6, 6] - expect(map.screenRowRangeForBufferRow(22)).toEqual [8, 9] - expect(map.screenRowRangeForBufferRow(26)).toEqual [8, 9] - - it "starts collapsing the first region at the start row, not before", -> - map.mapBufferRowRange(5, 6, 4) - map.mapBufferRowRange(11, 13, 1) - - map.applyScreenDelta(7, -5) - - expect(map.regions[0]).toEqual(bufferRows: 5, screenRows: 5) - expect(map.regions[1]).toEqual(bufferRows: 1, screenRows: 2) - expect(map.regions[2]).toEqual(bufferRows: 5, screenRows: 2) - - it "does not throw an exception when applying a delta beyond the last region", -> - map.mapBufferRowRange(5, 10, 1) # inner fold 1 - map.applyScreenDelta(15, 10) + map.spliceRegions(3, 7, [{bufferRows: 5, screenRows: 5}]) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 06e2d397a..b5063a280 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -570,7 +570,7 @@ class DisplayBuffer extends Model logLines: (start=0, end=@getLastRow())-> for row in [start..end] line = @lineForRow(row).text - console.log row, line, line.length + console.log row, @bufferRowForScreenRow(row), line, line.length handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange @@ -581,21 +581,17 @@ class DisplayBuffer extends Model startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0] endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1] - @rowMap.applyBufferDelta(startBufferRow, bufferDelta) - - { newScreenLines, newMappings } = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta) - @screenLines[startScreenRow...endScreenRow] = newScreenLines - screenDelta = newScreenLines.length - (endScreenRow - startScreenRow) - @rowMap.applyScreenDelta(startScreenRow, screenDelta) - @rowMap.mapBufferRowRange(mapping...) for mapping in newMappings - @findMaxLineLength(startScreenRow, endScreenRow, newScreenLines) + {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta) + @screenLines[startScreenRow...endScreenRow] = screenLines + @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions) + @findMaxLineLength(startScreenRow, endScreenRow, screenLines) return if options.suppressChangeEvent changeEvent = start: startScreenRow end: endScreenRow - 1 - screenDelta: screenDelta + screenDelta: screenLines.length - (endScreenRow - startScreenRow) bufferDelta: bufferDelta if options.delayChangeEvent @@ -605,26 +601,9 @@ class DisplayBuffer extends Model @emitChanged(changeEvent, options.refreshMarkers) buildScreenLines: (startBufferRow, endBufferRow) -> - newScreenLines = [] - newMappings = [] - pendingIsoMapping = null - - pushNewMapping = (startBufferRow, endBufferRow, screenRows) -> - if endBufferRow - startBufferRow == screenRows - if pendingIsoMapping - pendingIsoMapping[1] = endBufferRow - else - pendingIsoMapping = [startBufferRow, endBufferRow] - else - clearPendingIsoMapping() - newMappings.push([startBufferRow, endBufferRow, screenRows]) - - clearPendingIsoMapping = -> - if pendingIsoMapping - [isoStart, isoEnd] = pendingIsoMapping - pendingIsoMapping.push(isoEnd - isoStart) - newMappings.push(pendingIsoMapping) - pendingIsoMapping = null + screenLines = [] + regions = [] + rectangularRegion = null bufferRow = startBufferRow while bufferRow < endBufferRow @@ -633,22 +612,39 @@ class DisplayBuffer extends Model if fold = @largestFoldStartingAtBufferRow(bufferRow) foldLine = tokenizedLine.copy() foldLine.fold = fold - newScreenLines.push(foldLine) - pushNewMapping(bufferRow, fold.getEndRow() + 1, 1) - bufferRow = fold.getEndRow() + 1 + screenLines.push(foldLine) + + if rectangularRegion? + regions.push(rectangularRegion) + rectangularRegion = null + + foldedRowCount = fold.getBufferRowCount() + regions.push(bufferRows: foldedRowCount, screenRows: 1) + bufferRow += foldedRowCount else softWraps = 0 while wrapScreenColumn = @findWrapColumn(tokenizedLine.text) [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn) - - newScreenLines.push(wrappedLine) + screenLines.push(wrappedLine) softWraps++ - newScreenLines.push(tokenizedLine) - pushNewMapping(bufferRow, bufferRow + 1, softWraps + 1) - bufferRow++ - clearPendingIsoMapping() + screenLines.push(tokenizedLine) - { newScreenLines, newMappings } + if softWraps > 0 + if rectangularRegion? + regions.push(rectangularRegion) + rectangularRegion = null + regions.push(bufferRows: 1, screenRows: softWraps + 1) + else + rectangularRegion ?= {bufferRows: 0, screenRows: 0} + rectangularRegion.bufferRows++ + rectangularRegion.screenRows++ + + bufferRow++ + + if rectangularRegion? + regions.push(rectangularRegion) + + {screenLines, regions} findMaxLineLength: (startScreenRow, endScreenRow, newScreenLines) -> if startScreenRow <= @longestScreenRow < endScreenRow diff --git a/src/editor.coffee b/src/editor.coffee index adfcd2c65..633604e54 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -406,6 +406,8 @@ class Editor extends Model # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) + bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row) + # {Delegates to: DisplayBuffer.scopesForBufferPosition} scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) diff --git a/src/row-map.coffee b/src/row-map.coffee index efb9343af..fd5dfb2ea 100644 --- a/src/row-map.coffee +++ b/src/row-map.coffee @@ -1,179 +1,119 @@ -# Maintains the canonical map between screen and buffer positions. +{spliceWithArray} = require 'underscore-plus' + +# Used by the display buffer to map screen rows to buffer rows and vice-versa. +# This mapping may not be 1:1 due to folds and soft-wraps. This object maintains +# an array of regions, which contain `bufferRows` and `screenRows` fields. # -# Facilitates the mapping of screen rows to buffer rows and vice versa. All row -# ranges dealt with by this class are end-row exclusive. For example, a fold of -# rows 4 through 8 would be expressed as `mapBufferRowRange(4, 9, 1)`, which maps -# the region from 4 to 9 in the buffer to a single screen row. Conversely, a -# soft-wrapped screen line means there are multiple screen rows corresponding to -# a single buffer row, as follows: `mapBufferRowRange(4, 5, 3)`. That says that -# buffer row 4 maps to 3 rows on screen. +# Rectangular Regions: +# If a region has the same number of buffer rows and screen rows, it is referred +# to as "rectangular", and represents one or more non-soft-wrapped, non-folded +# lines. # -# The RowMap revolves around the `@regions` array. Each region describes a number -# of rows in both the screen and buffer coordinate spaces. So if you inserted a -# single fold from 5-10, the regions array would look like this: -# -# ``` -# [{bufferRows: 5, screenRows: 5}, {bufferRows: 5, screenRows: 1}] -# ``` -# -# The first region expresses an iso-mapping, a region in which one buffer row -# is equivalent to one screen row. The second region expresses the fold, with -# 5 buffer rows mapping to a single screen row. Position translation functions -# by traversing through these regions and summing the number of rows traversed -# in both the screen and the buffer. +# Trapezoidal Regions: +# If a region has one buffer row and more than one screen row, it represents a +# soft-wrapped line. If a region has one screen row and more than one buffer +# row, it represents folded lines module.exports = class RowMap constructor: -> @regions = [] - screenRowRangeForBufferRow: (targetBufferRow) -> - { region, screenRow, bufferRow } = @traverseToBufferRow(targetBufferRow) - if region and region.bufferRows != region.screenRows # 1:n region - [screenRow, screenRow + region.screenRows] - else # 1:1 region - screenRow += targetBufferRow - bufferRow - [screenRow, screenRow + 1] + # Public: Returns a copy of all the regions in the map + getRegions: -> + @regions.slice() - # This will return just the given buffer row if it is part of an iso region, - # but if it is part of a fold it will return the range of the entire fold. This - # helps the DisplayBuffer always start processing at the beginning of a fold - # for changes that occur inside the fold. + # Public: Returns an end-row-exclusive range of screen rows corresponding to + # the given buffer row. If the buffer row is soft-wrapped, the range may span + # multiple screen rows. Otherwise it will span a single screen row. + screenRowRangeForBufferRow: (targetBufferRow) -> + {region, bufferRows, screenRows} = @traverseToBufferRow(targetBufferRow) + + if region? and region.bufferRows isnt region.screenRows + [screenRows, screenRows + region.screenRows] + else + screenRows += targetBufferRow - bufferRows + [screenRows, screenRows + 1] + + # Public: Returns an end-row-exclusive range of buffer rows corresponding to + # the given screen row. If the screen row is the first line of a folded range + # of buffer rows, the range may span multiple buffer rows. Otherwise it will + # span a single buffer row. + bufferRowRangeForScreenRow: (targetScreenRow) -> + {region, screenRows, bufferRows} = @traverseToScreenRow(targetScreenRow) + if region? and region.bufferRows isnt region.screenRows + [bufferRows, bufferRows + region.bufferRows] + else + bufferRows += targetScreenRow - screenRows + [bufferRows, bufferRows + 1] + + # Public: If the given buffer row is part of a folded row range, returns that + # row range. Otherwise returns a range spanning only the given buffer row. bufferRowRangeForBufferRow: (targetBufferRow) -> - { region, screenRow, bufferRow } = @traverseToBufferRow(targetBufferRow) - if region and region.bufferRows != region.screenRows # 1:n region - [bufferRow, bufferRow + region.bufferRows] - else # 1:1 region + {region, bufferRows} = @traverseToBufferRow(targetBufferRow) + if region? and region.bufferRows isnt region.screenRows + [bufferRows, bufferRows + region.bufferRows] + else [targetBufferRow, targetBufferRow + 1] - bufferRowRangeForScreenRow: (targetScreenRow) -> - { region, screenRow, bufferRow } = @traverseToScreenRow(targetScreenRow) - if region and region.bufferRows != region.screenRows # 1:n region - [bufferRow, bufferRow + region.bufferRows] - else # 1:1 region - bufferRow += targetScreenRow - screenRow - [bufferRow, bufferRow + 1] + # Public: Given a starting buffer row, the number of buffer rows to replace, + # and an array of regions of shape {bufferRows: n, screenRows: m}, splices + # the regions at the appropriate location in the map. This method is used by + # display buffer to keep the map updated when the underlying buffer changes. + spliceRegions: (startBufferRow, bufferRowCount, regions) -> + endBufferRow = startBufferRow + bufferRowCount + {index, bufferRows} = @traverseToBufferRow(startBufferRow) + precedingRows = startBufferRow - bufferRows - # This method is used to create new regions, storing a mapping between a range - # of buffer rows to a certain number of screen rows. It will never add or remove - # rows in either coordinate space, meaning that it never changes the position - # of subsequent regions. It will overwrite or split existing regions that overlap - # with the region being stored however. - mapBufferRowRange: (startBufferRow, endBufferRow, screenRows) -> - { index, bufferRow, screenRow } = @traverseToBufferRow(startBufferRow) + count = 0 + while region = @regions[index + count] + count++ + bufferRows += region.bufferRows + if bufferRows >= endBufferRow + followingRows = bufferRows - endBufferRow + break - overlapStartIndex = index - overlapStartBufferRow = bufferRow - preRows = startBufferRow - overlapStartBufferRow - endScreenRow = screenRow + preRows + screenRows - overlapEndIndex = index - overlapEndBufferRow = bufferRow - overlapEndScreenRow = screenRow + if precedingRows > 0 + regions.unshift({bufferRows: precedingRows, screenRows: precedingRows}) - # determine regions that the new region overlaps. they will need replacement. - while overlapEndIndex < @regions.length - region = @regions[overlapEndIndex] - overlapEndBufferRow += region.bufferRows - overlapEndScreenRow += region.screenRows - break if overlapEndBufferRow >= endBufferRow and overlapEndScreenRow >= endScreenRow - overlapEndIndex++ + if followingRows > 0 + regions.push({bufferRows: followingRows, screenRows: followingRows}) - # we will replace overlapStartIndex..overlapEndIndex with these regions - newRegions = [] - - # if we straddle the first overlapping region, push a smaller region representing - # the portion before the new region - if preRows > 0 - newRegions.push(bufferRows: preRows, screenRows: preRows) - - # push the new region - newRegions.push(bufferRows: endBufferRow - startBufferRow, screenRows: screenRows) - - # if we straddle the last overlapping region, push a smaller region representing - # the portion after the new region - if overlapEndBufferRow > endBufferRow - newRegions.push(bufferRows: overlapEndBufferRow - endBufferRow, screenRows: overlapEndScreenRow - endScreenRow) - - @regions[overlapStartIndex..overlapEndIndex] = newRegions - @mergeIsomorphicRegions(Math.max(0, overlapStartIndex - 1), Math.min(@regions.length - 1, overlapEndIndex + 1)) - - mergeIsomorphicRegions: (startIndex, endIndex) -> - return if startIndex == endIndex - - region = @regions[startIndex] - nextRegion = @regions[startIndex + 1] - if region.bufferRows == region.screenRows and nextRegion.bufferRows == nextRegion.screenRows - @regions[startIndex..startIndex + 1] = - bufferRows: region.bufferRows + nextRegion.bufferRows - screenRows: region.screenRows + nextRegion.screenRows - @mergeIsomorphicRegions(startIndex, endIndex - 1) - else - @mergeIsomorphicRegions(startIndex + 1, endIndex) - - # This method records insertion or removal of rows in the buffer, adjusting the - # buffer dimension of regions following the start row accordingly. - applyBufferDelta: (startBufferRow, delta) -> - return if delta is 0 - { index, bufferRow } = @traverseToBufferRow(startBufferRow) - if delta > 0 and index < @regions.length - { bufferRows, screenRows } = @regions[index] - bufferRows += delta - @regions[index] = { bufferRows, screenRows } - else - delta = -delta - while delta > 0 and index < @regions.length - { bufferRows, screenRows } = @regions[index] - regionStartBufferRow = bufferRow - regionEndBufferRow = bufferRow + bufferRows - maxDelta = regionEndBufferRow - Math.max(regionStartBufferRow, startBufferRow) - regionDelta = Math.min(delta, maxDelta) - bufferRows -= regionDelta - @regions[index] = { bufferRows, screenRows } - delta -= regionDelta - bufferRow += bufferRows - index++ - - # This method records insertion or removal of rows on the screen, adjusting the - # screen dimension of regions following the start row accordingly. - applyScreenDelta: (startScreenRow, delta) -> - return if delta is 0 - { index, screenRow } = @traverseToScreenRow(startScreenRow) - if delta > 0 and index < @regions.length - { bufferRows, screenRows } = @regions[index] - screenRows += delta - @regions[index] = { bufferRows, screenRows } - else - delta = -delta - while delta > 0 and index < @regions.length - { bufferRows, screenRows } = @regions[index] - regionStartScreenRow = screenRow - regionEndScreenRow = screenRow + screenRows - maxDelta = regionEndScreenRow - Math.max(regionStartScreenRow, startScreenRow) - regionDelta = Math.min(delta, maxDelta) - screenRows -= regionDelta - @regions[index] = { bufferRows, screenRows } - delta -= regionDelta - screenRow += screenRows - index++ + spliceWithArray(@regions, index, count, regions) + @mergeAdjacentRectangularRegions(index - 1, index + regions.length) traverseToBufferRow: (targetBufferRow) -> - bufferRow = 0 - screenRow = 0 + bufferRows = 0 + screenRows = 0 for region, index in @regions - if (bufferRow + region.bufferRows) > targetBufferRow or region.bufferRows == 0 and bufferRow == targetBufferRow - return { region, index, screenRow, bufferRow } - bufferRow += region.bufferRows - screenRow += region.screenRows - { index, screenRow, bufferRow } + if (bufferRows + region.bufferRows) > targetBufferRow + return {region, index, screenRows, bufferRows} + bufferRows += region.bufferRows + screenRows += region.screenRows + {index, screenRows, bufferRows} traverseToScreenRow: (targetScreenRow) -> - bufferRow = 0 - screenRow = 0 + bufferRows = 0 + screenRows = 0 for region, index in @regions - if (screenRow + region.screenRows) > targetScreenRow - return { region, index, screenRow, bufferRow } - bufferRow += region.bufferRows - screenRow += region.screenRows - { index, screenRow, bufferRow } + if (screenRows + region.screenRows) > targetScreenRow + return {region, index, screenRows, bufferRows} + bufferRows += region.bufferRows + screenRows += region.screenRows + {index, screenRows, bufferRows} + mergeAdjacentRectangularRegions: (startIndex, endIndex) -> + for index in [endIndex..startIndex] + if 0 < index < @regions.length + leftRegion = @regions[index - 1] + rightRegion = @regions[index] + leftIsRectangular = leftRegion.bufferRows is leftRegion.screenRows + rightIsRectangular = rightRegion.bufferRows is rightRegion.screenRows + if leftIsRectangular and rightIsRectangular + @regions.splice index - 1, 2, + bufferRows: leftRegion.bufferRows + rightRegion.bufferRows + screenRows: leftRegion.screenRows + rightRegion.screenRows + + # Public: Returns an array of strings describing the map's regions. inspect: -> - @regions.map(({screenRows, bufferRows}) -> "#{screenRows}:#{bufferRows}").join(', ') + for {bufferRows, screenRows} in @regions + "#{bufferRows}:#{screenRows}"