_ = require 'underscore' TokenizedBuffer = require 'tokenized-buffer' LineMap = require 'line-map' Point = require 'point' EventEmitter = require 'event-emitter' Range = require 'range' Fold = require 'fold' ScreenLine = require 'screen-line' Token = require 'token' module.exports = class DisplayBuffer @idCounter: 1 lineMap: null tokenizedBuffer: null activeFolds: null foldsById: null lastTokenizedBufferChangeEvent: null constructor: (@buffer, options={}) -> @id = @constructor.idCounter++ @tokenizedBuffer = new TokenizedBuffer(@buffer, options.tabText ? ' ') @softWrapColumn = options.softWrapColumn ? Infinity @activeFolds = {} @foldsById = {} @buildLineMap() @tokenizedBuffer.on 'change', (e) => @lastTokenizedBufferChangeEvent = e @buffer.on "change.displayBuffer#{@id}", (e) => @handleBufferChange(e) buildLineMap: -> @lineMap = new LineMap @lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow()) setSoftWrapColumn: (@softWrapColumn) -> oldRange = @rangeForAllLines() @buildLineMap() newRange = @rangeForAllLines() @trigger 'change', { oldRange, newRange, lineNumbersChanged: true } lineForRow: (row) -> @lineMap.lineForScreenRow(row) linesForRows: (startRow, endRow) -> @lineMap.linesForScreenRows(startRow, endRow) getLines: -> @lineMap.linesForScreenRows(0, @lineMap.lastScreenRow()) bufferRowsForScreenRows: (startRow, endRow) -> @lineMap.bufferRowsForScreenRows(startRow, endRow) foldAll: -> for currentRow in [0..@buffer.getLastRow()] [startRow, endRow] = @tokenizedBuffer.rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? @createFold(startRow, endRow) toggleFoldAtBufferRow: (bufferRow) -> for currentRow in [bufferRow..0] [startRow, endRow] = @tokenizedBuffer.rowRangeForFoldAtBufferRow(currentRow) ? [] continue unless startRow? and startRow <= bufferRow <= endRow if fold = @largestFoldStartingAtBufferRow(startRow) fold.destroy() else @createFold(startRow, endRow) break isFoldContainedByActiveFold: (fold) -> for row, folds of @activeFolds for otherFold in folds return otherFold if fold != otherFold and fold.isContainedByFold(otherFold) foldFor: (startRow, endRow) -> _.find @activeFolds[startRow] ? [], (fold) -> fold.startRow == startRow and fold.endRow == endRow createFold: (startRow, endRow) -> return fold if fold = @foldFor(startRow, endRow) fold = new Fold(this, startRow, endRow) @registerFold(fold) unless @isFoldContainedByActiveFold(fold) bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)]) oldScreenRange = @screenLineRangeForBufferRange(bufferRange) lines = @buildLineForBufferRow(startRow) @lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines) newScreenRange = @screenLineRangeForBufferRange(bufferRange) @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true fold destroyFold: (fold) -> @unregisterFold(fold.startRow, fold) unless @isFoldContainedByActiveFold(fold) { startRow, endRow } = fold bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)]) oldScreenRange = @screenLineRangeForBufferRange(bufferRange) lines = @buildLinesForBufferRows(startRow, endRow) @lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines) newScreenRange = @screenLineRangeForBufferRange(bufferRange) @trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true destroyFoldsContainingBufferRow: (bufferRow) -> for row, folds of @activeFolds for fold in new Array(folds...) fold.destroy() if fold.getBufferRange().containsRow(bufferRow) registerFold: (fold) -> @activeFolds[fold.startRow] ?= [] @activeFolds[fold.startRow].push(fold) @foldsById[fold.id] = fold unregisterFold: (bufferRow, fold) -> folds = @activeFolds[bufferRow] _.remove(folds, fold) delete @foldsById[fold.id] largestFoldStartingAtBufferRow: (bufferRow) -> return unless folds = @activeFolds[bufferRow] (folds.sort (a, b) -> b.endRow - a.endRow)[0] screenLineRangeForBufferRange: (bufferRange) -> @expandScreenRangeToLineEnds( @lineMap.screenRangeForBufferRange( @expandBufferRangeToLineEnds(bufferRange))) screenRowForBufferRow: (bufferRow) -> @lineMap.screenPositionForBufferPosition([bufferRow, 0]).row bufferRowForScreenRow: (screenRow) -> @lineMap.bufferPositionForScreenPosition([screenRow, 0]).row screenRangeForBufferRange: (bufferRange) -> @lineMap.screenRangeForBufferRange(bufferRange) bufferRangeForScreenRange: (screenRange) -> @lineMap.bufferRangeForScreenRange(screenRange) lineCount: -> @lineMap.screenLineCount() getLastRow: -> @lineCount() - 1 maxLineLength: -> @lineMap.maxScreenLineLength() screenPositionForBufferPosition: (position, options) -> @lineMap.screenPositionForBufferPosition(position, options) bufferPositionForScreenPosition: (position, options) -> @lineMap.bufferPositionForScreenPosition(position, options) stateForScreenRow: (screenRow) -> @tokenizedBuffer.stateForRow(screenRow) clipScreenPosition: (position, options) -> @lineMap.clipScreenPosition(position, options) handleBufferChange: (e) -> allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds allFolds.push(folds...) for row, folds of @activeFolds fold.handleBufferChange(e) for fold in allFolds @handleTokenizedBufferChange(@lastTokenizedBufferChangeEvent) handleTokenizedBufferChange: (e) -> newRange = e.newRange.copy() newRange.start.row = @bufferRowForScreenRow(@screenRowForBufferRow(newRange.start.row)) oldScreenRange = @screenLineRangeForBufferRange(e.oldRange) newScreenLines = @buildLinesForBufferRows(newRange.start.row, newRange.end.row) @lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines newScreenRange = @screenLineRangeForBufferRange(newRange) @trigger 'change', oldRange: oldScreenRange newRange: newScreenRange bufferChanged: true lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange) buildLineForBufferRow: (bufferRow) -> @buildLinesForBufferRows(bufferRow, bufferRow) buildLinesForBufferRows: (startBufferRow, endBufferRow) -> lineFragments = [] startBufferColumn = null currentBufferRow = startBufferRow currentScreenLineLength = 0 startBufferColumn = 0 while currentBufferRow <= endBufferRow screenLine = @tokenizedBuffer.lineForScreenRow(currentBufferRow) screenLine.foldable = @tokenizedBuffer.isBufferRowFoldable(currentBufferRow) if fold = @largestFoldStartingAtBufferRow(currentBufferRow) screenLine = screenLine.copy() screenLine.fold = fold screenLine.bufferDelta = fold.getBufferDelta() lineFragments.push(screenLine) currentBufferRow = fold.endRow + 1 continue startBufferColumn ?= 0 screenLine = screenLine.splitAt(startBufferColumn)[1] if startBufferColumn > 0 wrapScreenColumn = @findWrapColumn(screenLine.text, @softWrapColumn) if wrapScreenColumn? screenLine = screenLine.splitAt(wrapScreenColumn)[0] screenLine.screenDelta = new Point(1, 0) startBufferColumn += wrapScreenColumn else currentBufferRow++ startBufferColumn = 0 lineFragments.push(screenLine) lineFragments findWrapColumn: (line, softWrapColumn) -> return unless line.length > softWrapColumn 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 expandScreenRangeToLineEnds: (screenRange) -> screenRange = Range.fromObject(screenRange) { start, end } = screenRange new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length]) expandBufferRangeToLineEnds: (bufferRange) -> bufferRange = Range.fromObject(bufferRange) { start, end } = bufferRange new Range([start.row, 0], [end.row, Infinity]) rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) destroy: -> @tokenizedBuffer.destroy() @buffer.off ".displayBuffer#{@id}" logLines: (start, end) -> @lineMap.logLines(start, end) _.extend DisplayBuffer.prototype, EventEmitter