Fix lurking soft-wrap bugs

This commit adds two important things:

1. An editor spec that randomly mutates a buffer and toggles soft wrap
on and off, then compares the screen lines to a simple reference
implementation to ensure everything stays in a correct state.

2. A new and radically simpler implementation of RowMap that eliminates
failures in the randomized test.
This commit is contained in:
Nathan Sobo 2014-01-29 18:37:51 -07:00
parent b97db1914f
commit c3f995b165
6 changed files with 340 additions and 436 deletions

View File

@ -143,5 +143,8 @@
"scripts": {
"preinstall": "node -e 'process.exit(0)'",
"test": "node script/test"
},
"devDependencies": {
"random-words": "0.0.1"
}
}

View File

@ -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

View File

@ -6,256 +6,101 @@ 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]
map.spliceRegions(3, 7, [{bufferRows: 5, screenRows: 5}])
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)
console.log map.inspect()

View File

@ -578,7 +578,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
### Internal ###
@ -591,14 +591,12 @@ class DisplayBuffer extends Model
startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0]
endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1]
@rowMap.applyBufferDelta(startBufferRow, bufferDelta)
{screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta)
@screenLines[startScreenRow...endScreenRow] = screenLines
screenDelta = screenLines.length - (endScreenRow - startScreenRow)
{ 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)
@rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions)
@findMaxLineLength(startScreenRow, endScreenRow, screenLines)
return if options.suppressChangeEvent
@ -615,26 +613,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
@ -643,22 +624,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

View File

@ -419,6 +419,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)

View File

@ -1,179 +1,123 @@
# Private: Maintains the canonical map between screen and buffer positions.
{spliceWithArray} = require 'underscore-plus'
# Private: 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 and 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 and 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)
# Private:
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}
# Private:
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}
# Private:
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}"