text-buffer
Version:
A container for large mutable strings with annotated regions
1,249 lines (1,098 loc) • 45.2 kB
JavaScript
const {Patch} = require('superstring')
const {Emitter} = require('event-kit')
const Point = require('./point')
const Range = require('./range')
const DisplayMarkerLayer = require('./display-marker-layer')
const {traverse, traversal, compare, max, isEqual} = require('./point-helpers')
const isCharacterPair = require('./is-character-pair')
const ScreenLineBuilder = require('./screen-line-builder')
const {spliceArray} = require('./helpers')
const {MAX_BUILT_IN_SCOPE_ID} = require('./constants')
module.exports =
class DisplayLayer {
constructor (id, buffer, params = {}) {
this.id = id
this.buffer = buffer
this.emitter = new Emitter()
this.screenLineBuilder = new ScreenLineBuilder(this)
this.cachedScreenLines = []
this.builtInScopeIdsByFlags = new Map()
this.builtInClassNamesByScopeId = new Map()
this.nextBuiltInScopeId = 1
this.displayMarkerLayersById = new Map()
this.destroyed = false
this.changesSinceLastEvent = new Patch()
this.invisibles = params.invisibles != null ? params.invisibles : {}
this.tabLength = params.tabLength != null ? params.tabLength : 4
this.softWrapColumn = params.softWrapColumn != null ? Math.max(1, params.softWrapColumn) : Infinity
this.softWrapHangingIndent = params.softWrapHangingIndent != null ? params.softWrapHangingIndent : 0
this.showIndentGuides = params.showIndentGuides != null ? params.showIndentGuides : false
this.ratioForCharacter = params.ratioForCharacter != null ? params.ratioForCharacter : unitRatio
this.isWrapBoundary = params.isWrapBoundary != null ? params.isWrapBoundary : isWordStart
this.foldCharacter = params.foldCharacter != null ? params.foldCharacter : '⋯'
this.atomicSoftTabs = params.atomicSoftTabs != null ? params.atomicSoftTabs : true
this.eolInvisibles = {
'\r': this.invisibles.cr,
'\n': this.invisibles.eol,
'\r\n': this.invisibles.cr + this.invisibles.eol
}
this.foldsMarkerLayer = params.foldsMarkerLayer || buffer.addMarkerLayer({
maintainHistory: false,
persistent: true,
destroyInvalidatedMarkers: true
})
this.foldIdCounter = params.foldIdCounter || 1
if (params.spatialIndex) {
this.spatialIndex = params.spatialIndex
this.tabCounts = params.tabCounts
this.screenLineLengths = params.screenLineLengths
this.rightmostScreenPosition = params.rightmostScreenPosition
this.indexedBufferRowCount = params.indexedBufferRowCount
} else {
this.spatialIndex = new Patch({mergeAdjacentHunks: false})
this.tabCounts = []
this.screenLineLengths = []
this.rightmostScreenPosition = Point(0, 0)
this.indexedBufferRowCount = 0
}
this.bufferDidChangeLanguageMode()
}
static deserialize (buffer, params) {
const foldsMarkerLayer = buffer.getMarkerLayer(params.foldsMarkerLayerId)
return new DisplayLayer(params.id, buffer, {foldsMarkerLayer})
}
serialize () {
return {
id: this.id,
foldsMarkerLayerId: this.foldsMarkerLayer.id,
foldIdCounter: this.foldIdCounter
}
}
reset (params) {
if (!this.isDestroyed() && this.setParams(params)) {
this.clearSpatialIndex()
this.emitter.emit('did-reset')
this.notifyObserversIfMarkerScreenPositionsChanged()
}
}
copy () {
const copyId = this.buffer.nextDisplayLayerId++
const copy = new DisplayLayer(copyId, this.buffer, {
foldsMarkerLayer: this.foldsMarkerLayer.copy(),
foldIdCounter: this.foldIdCounter,
spatialIndex: this.spatialIndex.copy(),
tabCounts: this.tabCounts.slice(),
screenLineLengths: this.screenLineLengths.slice(),
rightmostScreenPosition: this.rightmostScreenPosition.copy(),
indexedBufferRowCount: this.indexedBufferRowCount,
invisibles: this.invisibles,
tabLength: this.tabLength,
softWrapColumn: this.softWrapColumn,
softWrapHangingIndent: this.softWrapHangingIndent,
showIndentGuides: this.showIndentGuides,
ratioForCharacter: this.ratioForCharacter,
isWrapBoundary: this.isWrapBoundary,
foldCharacter: this.foldCharacter,
atomicSoftTabs: this.atomicSoftTabs
})
this.buffer.displayLayers[copyId] = copy
return copy
}
destroy () {
if (this.destroyed) return
this.destroyed = true
this.clearSpatialIndex()
this.foldsMarkerLayer.destroy()
this.displayMarkerLayersById.forEach((layer) => layer.destroy())
if (this.languageModeDisposable) this.languageModeDisposable.dispose()
delete this.buffer.displayLayers[this.id]
}
isDestroyed () {
return this.destroyed
}
clearSpatialIndex () {
this.indexedBufferRowCount = 0
this.spatialIndex.spliceOld(Point.ZERO, Point.INFINITY, Point.INFINITY)
this.cachedScreenLines.length = 0
this.screenLineLengths.length = 0
this.tabCounts.length = 0
this.rightmostScreenPosition = Point(0, 0)
}
doBackgroundWork (deadline) {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), Infinity, deadline)
return this.indexedBufferRowCount < this.buffer.getLineCount()
}
bufferDidChangeLanguageMode () {
this.cachedScreenLines.length = 0
if (this.languageModeDisposable) this.languageModeDisposable.dispose()
this.languageModeDisposable = this.buffer.languageMode.onDidChangeHighlighting((bufferRange) => {
bufferRange = Range.fromObject(bufferRange)
this.populateSpatialIndexIfNeeded(bufferRange.end.row + 1, Infinity)
const startBufferRow = this.findBoundaryPrecedingBufferRow(bufferRange.start.row)
const endBufferRow = this.findBoundaryFollowingBufferRow(bufferRange.end.row + 1)
const startRow = this.translateBufferPositionWithSpatialIndex(Point(startBufferRow, 0), 'backward').row
const endRow = this.translateBufferPositionWithSpatialIndex(Point(endBufferRow, 0), 'backward').row
const extent = Point(endRow - startRow, 0)
spliceArray(this.cachedScreenLines, startRow, extent.row, new Array(extent.row))
this.didChange({
start: Point(startRow, 0),
oldExtent: extent,
newExtent: extent
})
})
}
addMarkerLayer (options) {
const markerLayer = new DisplayMarkerLayer(this, this.buffer.addMarkerLayer(options), true)
this.displayMarkerLayersById.set(markerLayer.id, markerLayer)
return markerLayer
}
getMarkerLayer (id) {
if (this.displayMarkerLayersById.has(id)) {
return this.displayMarkerLayersById.get(id)
} else {
const bufferMarkerLayer = this.buffer.getMarkerLayer(id)
if (bufferMarkerLayer) {
const displayMarkerLayer = new DisplayMarkerLayer(this, bufferMarkerLayer, false)
this.displayMarkerLayersById.set(id, displayMarkerLayer)
return displayMarkerLayer
}
}
}
didDestroyMarkerLayer (id) {
this.displayMarkerLayersById.delete(id)
}
onDidChange (callback) {
return this.emitter.on('did-change', callback)
}
onDidReset (callback) {
return this.emitter.on('did-reset', callback)
}
bufferRangeForFold (foldId) {
return this.foldsMarkerLayer.getMarkerRange(foldId)
}
foldBufferRange (bufferRange) {
bufferRange = Range.fromObject(bufferRange)
const containingFoldMarkers = this.foldsMarkerLayer.findMarkers({containsRange: bufferRange})
if (containingFoldMarkers.length === 0) {
this.populateSpatialIndexIfNeeded(bufferRange.end.row + 1, Infinity)
}
const foldId = this.foldsMarkerLayer.markRange(bufferRange, {invalidate: 'overlap', exclusive: true}).id
if (containingFoldMarkers.length === 0) {
const foldStartRow = bufferRange.start.row
const foldEndRow = bufferRange.end.row + 1
this.didChange(this.updateSpatialIndex(foldStartRow, foldEndRow, foldEndRow, Infinity))
this.notifyObserversIfMarkerScreenPositionsChanged()
}
return foldId
}
destroyFold (foldId) {
const foldMarker = this.foldsMarkerLayer.getMarker(foldId)
if (foldMarker) {
this.destroyFoldMarkers([foldMarker])
}
}
destroyAllFolds () {
return this.destroyFoldMarkers(this.foldsMarkerLayer.findMarkers({}))
}
destroyFoldsIntersectingBufferRange (bufferRange) {
return this.destroyFoldMarkers(
this.foldsMarkerLayer.findMarkers({
intersectsRange: this.buffer.clipRange(bufferRange)
})
)
}
destroyFoldsContainingBufferPositions (bufferPositions, excludeEndpoints) {
const markersContainingPositions = new Set()
for (const position of bufferPositions) {
const clippedPosition = this.buffer.clipPosition(position)
const foundMarkers = this.foldsMarkerLayer.findMarkers({
containsPosition: clippedPosition
})
for (const marker of foundMarkers) {
if (!excludeEndpoints || marker.getRange().containsPoint(clippedPosition, true)) {
markersContainingPositions.add(marker)
}
}
}
const sortedMarkers = Array.from(markersContainingPositions).sort((a, b) => a.compare(b))
return this.destroyFoldMarkers(sortedMarkers)
}
destroyFoldMarkers (foldMarkers) {
const foldedRanges = []
if (foldMarkers.length === 0) return foldedRanges
const combinedRangeStart = foldMarkers[0].getStartPosition()
let combinedRangeEnd = combinedRangeStart
for (const foldMarker of foldMarkers) {
const foldedRange = foldMarker.getRange()
foldedRanges.push(foldedRange)
combinedRangeEnd = max(combinedRangeEnd, foldedRange.end)
}
this.populateSpatialIndexIfNeeded(combinedRangeEnd.row + 1, Infinity)
for (const foldMarker of foldMarkers) {
foldMarker.destroy()
}
this.didChange(this.updateSpatialIndex(
combinedRangeStart.row,
combinedRangeEnd.row + 1,
combinedRangeEnd.row + 1,
Infinity
))
this.notifyObserversIfMarkerScreenPositionsChanged()
return foldedRanges
}
foldsIntersectingBufferRange (bufferRange) {
return this.foldsMarkerLayer.findMarkers({
intersectsRange: this.buffer.clipRange(bufferRange)
}).map((marker) => marker.id)
}
translateBufferPosition (bufferPosition, options) {
bufferPosition = this.buffer.clipPosition(bufferPosition)
this.populateSpatialIndexIfNeeded(bufferPosition.row + 1, Infinity)
const clipDirection = (options && options.clipDirection) || 'closest'
const columnDelta = this.getClipColumnDelta(bufferPosition, clipDirection)
if (columnDelta !== 0) {
bufferPosition = Point(bufferPosition.row, bufferPosition.column + columnDelta)
}
let screenPosition = this.translateBufferPositionWithSpatialIndex(bufferPosition, clipDirection)
const tabCount = this.tabCounts[screenPosition.row]
if (tabCount > 0) {
screenPosition = this.expandHardTabs(screenPosition, bufferPosition, tabCount)
}
return Point.fromObject(screenPosition)
}
translateBufferPositionWithSpatialIndex (bufferPosition, clipDirection) {
let hunk = this.spatialIndex.changeForOldPosition(bufferPosition)
if (hunk) {
if (compare(bufferPosition, hunk.oldEnd) < 0) {
if (compare(hunk.oldStart, bufferPosition) === 0) {
return hunk.newStart
} else { // hunk is a fold
if (clipDirection === 'backward') {
return hunk.newStart
} else if (clipDirection === 'forward') {
return hunk.newEnd
} else {
const distanceFromFoldStart = traversal(bufferPosition, hunk.oldStart)
const distanceToFoldEnd = traversal(hunk.oldEnd, bufferPosition)
if (compare(distanceFromFoldStart, distanceToFoldEnd) <= 0) {
return hunk.newStart
} else {
return hunk.newEnd
}
}
}
} else {
return traverse(hunk.newEnd, traversal(bufferPosition, hunk.oldEnd))
}
} else {
return bufferPosition
}
}
translateBufferRange (bufferRange, options) {
bufferRange = Range.fromObject(bufferRange)
return Range(
this.translateBufferPosition(bufferRange.start, options),
this.translateBufferPosition(bufferRange.end, options)
)
}
translateScreenPosition (screenPosition, options) {
screenPosition = Point.fromObject(screenPosition)
Point.assertValid(screenPosition)
const clipDirection = (options && options.clipDirection) || 'closest'
const skipSoftWrapIndentation = options && options.skipSoftWrapIndentation
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), screenPosition.row + 1)
screenPosition = this.constrainScreenPosition(screenPosition, clipDirection)
const tabCount = this.tabCounts[screenPosition.row]
if (tabCount > 0) {
screenPosition = this.collapseHardTabs(screenPosition, tabCount, clipDirection)
}
const bufferPosition = this.translateScreenPositionWithSpatialIndex(screenPosition, clipDirection, skipSoftWrapIndentation)
if (global.atom && bufferPosition.row >= this.buffer.getLineCount()) {
global.atom.assert(false, 'Invalid translated buffer row', {
bufferPosition, bufferLineCount: this.buffer.getLineCount()
})
return this.buffer.getEndPosition()
}
const columnDelta = this.getClipColumnDelta(bufferPosition, clipDirection)
if (columnDelta !== 0) {
return Point(bufferPosition.row, bufferPosition.column + columnDelta)
} else {
return Point.fromObject(bufferPosition)
}
}
translateScreenPositionWithSpatialIndex (screenPosition, clipDirection, skipSoftWrapIndentation) {
let hunk = this.spatialIndex.changeForNewPosition(screenPosition)
if (hunk) {
if (compare(screenPosition, hunk.newEnd) < 0) {
if (this.isSoftWrapHunk(hunk)) {
if ((clipDirection === 'backward' && !skipSoftWrapIndentation) ||
(clipDirection === 'closest' && isEqual(hunk.newStart, screenPosition))) {
return this.translateScreenPositionWithSpatialIndex(traverse(hunk.newStart, Point(0, -1)), clipDirection, skipSoftWrapIndentation)
} else {
return hunk.oldStart
}
} else { // Hunk is a fold. Since folds are 1 character on screen, we're at the start.
return hunk.oldStart
}
} else {
return traverse(hunk.oldEnd, traversal(screenPosition, hunk.newEnd))
}
} else {
return screenPosition
}
}
translateScreenRange (screenRange, options) {
screenRange = Range.fromObject(screenRange)
return Range(
this.translateScreenPosition(screenRange.start, options),
this.translateScreenPosition(screenRange.end, options)
)
}
clipScreenPosition (screenPosition, options) {
return this.translateBufferPosition(
this.translateScreenPosition(screenPosition, options),
options
)
}
constrainScreenPosition (screenPosition, clipDirection) {
let {row, column} = screenPosition
if (row < 0) {
return new Point(0, 0)
}
const maxRow = this.screenLineLengths.length - 1
if (row > maxRow) {
return new Point(maxRow, this.screenLineLengths[maxRow])
}
if (column < 0) {
return new Point(row, 0)
}
const maxColumn = this.screenLineLengths[row]
if (column > maxColumn) {
if (clipDirection === 'forward' && row < maxRow) {
return new Point(row + 1, 0)
} else {
return new Point(row, maxColumn)
}
}
return screenPosition
}
expandHardTabs (targetScreenPosition, targetBufferPosition, tabCount) {
const screenRowStart = Point(targetScreenPosition.row, 0)
const hunks = this.spatialIndex.getChangesInNewRange(screenRowStart, targetScreenPosition)
let hunkIndex = 0
let unexpandedScreenColumn = 0
let expandedScreenColumn = 0
let {row: bufferRow, column: bufferColumn} = this.translateScreenPositionWithSpatialIndex(screenRowStart)
let bufferLine = this.buffer.lineForRow(bufferRow)
while (tabCount > 0) {
if (unexpandedScreenColumn === targetScreenPosition.column) {
break
}
let nextHunk = hunks[hunkIndex]
if (nextHunk && nextHunk.oldStart.row === bufferRow && nextHunk.oldStart.column === bufferColumn) {
if (this.isSoftWrapHunk(nextHunk)) {
if (hunkIndex !== 0) throw new Error('Unexpected soft wrap hunk')
unexpandedScreenColumn = hunks[0].newEnd.column
expandedScreenColumn = unexpandedScreenColumn
} else {
({row: bufferRow, column: bufferColumn} = nextHunk.oldEnd)
bufferLine = this.buffer.lineForRow(bufferRow)
unexpandedScreenColumn++
expandedScreenColumn++
}
hunkIndex++
continue
}
if (bufferLine[bufferColumn] === '\t') {
expandedScreenColumn += (this.tabLength - (expandedScreenColumn % this.tabLength))
tabCount--
} else {
expandedScreenColumn++
}
unexpandedScreenColumn++
bufferColumn++
}
expandedScreenColumn += targetScreenPosition.column - unexpandedScreenColumn
if (expandedScreenColumn === targetScreenPosition.column) {
return targetScreenPosition
} else {
return Point(targetScreenPosition.row, expandedScreenColumn)
}
}
collapseHardTabs (targetScreenPosition, tabCount, clipDirection) {
const screenRowStart = Point(targetScreenPosition.row, 0)
const screenRowEnd = Point(targetScreenPosition.row, this.screenLineLengths[targetScreenPosition.row])
const hunks = this.spatialIndex.getChangesInNewRange(screenRowStart, screenRowEnd)
let hunkIndex = 0
let unexpandedScreenColumn = 0
let expandedScreenColumn = 0
let {row: bufferRow, column: bufferColumn} = this.translateScreenPositionWithSpatialIndex(screenRowStart)
let bufferLine = this.buffer.lineForRow(bufferRow)
while (tabCount > 0) {
if (expandedScreenColumn === targetScreenPosition.column) {
break
}
let nextHunk = hunks[hunkIndex]
if (nextHunk && nextHunk.oldStart.row === bufferRow && nextHunk.oldStart.column === bufferColumn) {
if (this.isSoftWrapHunk(nextHunk)) {
if (hunkIndex !== 0) throw new Error('Unexpected soft wrap hunk')
unexpandedScreenColumn = Math.min(targetScreenPosition.column, nextHunk.newEnd.column)
expandedScreenColumn = unexpandedScreenColumn
} else {
({row: bufferRow, column: bufferColumn} = nextHunk.oldEnd)
bufferLine = this.buffer.lineForRow(bufferRow)
unexpandedScreenColumn++
expandedScreenColumn++
}
hunkIndex++
continue
}
if (bufferLine[bufferColumn] === '\t') {
const nextTabStopColumn = expandedScreenColumn + this.tabLength - (expandedScreenColumn % this.tabLength)
if (nextTabStopColumn > targetScreenPosition.column) {
if (clipDirection === 'backward') {
return Point(targetScreenPosition.row, unexpandedScreenColumn)
} else if (clipDirection === 'forward') {
return Point(targetScreenPosition.row, unexpandedScreenColumn + 1)
} else {
if (targetScreenPosition.column > Math.ceil((nextTabStopColumn + expandedScreenColumn) / 2)) {
return Point(targetScreenPosition.row, unexpandedScreenColumn + 1)
} else {
return Point(targetScreenPosition.row, unexpandedScreenColumn)
}
}
}
expandedScreenColumn = nextTabStopColumn
tabCount--
} else {
expandedScreenColumn++
}
unexpandedScreenColumn++
bufferColumn++
}
unexpandedScreenColumn += targetScreenPosition.column - expandedScreenColumn
if (unexpandedScreenColumn === targetScreenPosition.column) {
return targetScreenPosition
} else {
return Point(targetScreenPosition.row, unexpandedScreenColumn)
}
}
getClipColumnDelta (bufferPosition, clipDirection) {
var {row, column} = bufferPosition
// Treat paired unicode characters as atomic...
var character = this.buffer.getCharacterAtPosition(bufferPosition)
var previousCharacter = this.buffer.getCharacterAtPosition([row, column - 1])
if (previousCharacter && character && isCharacterPair(previousCharacter, character)) {
if (clipDirection === 'closest' || clipDirection === 'backward') {
return -1
} else {
return 1
}
}
// Clip atomic soft tabs...
if (!this.atomicSoftTabs) return 0
if (column * this.ratioForCharacter(' ') > this.softWrapColumn) {
return 0
}
for (let position = {row, column}; position.column >= 0; position.column--) {
if (this.buffer.getCharacterAtPosition(position) !== ' ') return 0
}
var previousTabStop = column - (column % this.tabLength)
if (column === previousTabStop) return 0
var nextTabStop = previousTabStop + this.tabLength
// If there is a non-whitespace character before the next tab stop,
// don't this whitespace as a soft tab
for (let position = {row, column}; position.column < nextTabStop; position.column++) {
if (this.buffer.getCharacterAtPosition(position) !== ' ') return 0
}
var clippedColumn
if (clipDirection === 'closest') {
if (column - previousTabStop > this.tabLength / 2) {
clippedColumn = nextTabStop
} else {
clippedColumn = previousTabStop
}
} else if (clipDirection === 'backward') {
clippedColumn = previousTabStop
} else if (clipDirection === 'forward') {
clippedColumn = nextTabStop
}
return clippedColumn - column
}
getText (startRow, endRow) {
return this.getScreenLines(startRow, endRow).map((line) => line.lineText).join('\n')
}
lineLengthForScreenRow (screenRow) {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), screenRow + 1)
return this.screenLineLengths[screenRow]
}
getLastScreenRow () {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), Infinity)
return this.screenLineLengths.length - 1
}
getScreenLineCount () {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), Infinity)
return this.screenLineLengths.length
}
getApproximateScreenLineCount () {
if (this.indexedBufferRowCount > 0) {
return Math.floor(this.buffer.getLineCount() * this.screenLineLengths.length / this.indexedBufferRowCount)
} else {
return this.buffer.getLineCount()
}
}
getRightmostScreenPosition () {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), Infinity)
return this.rightmostScreenPosition
}
getApproximateRightmostScreenPosition () {
return this.rightmostScreenPosition
}
getScreenLine (screenRow) {
return this.cachedScreenLines[screenRow] || this.getScreenLines(screenRow, screenRow + 1)[0]
}
getScreenLines (screenStartRow = 0, screenEndRow = this.getScreenLineCount()) {
return this.screenLineBuilder.buildScreenLines(screenStartRow, screenEndRow)
}
bufferRowsForScreenRows (startRow, endRow) {
this.populateSpatialIndexIfNeeded(this.buffer.getLineCount(), endRow)
const startPosition = Point(startRow, 0)
const bufferRows = []
let lastScreenRow = startRow
let lastBufferRow = this.translateScreenPositionWithSpatialIndex(startPosition).row
const hunks = this.spatialIndex.getChangesInNewRange(startPosition, Point(endRow, 0))
for (let i = 0; i < hunks.length; i++) {
const hunk = hunks[i]
while (lastScreenRow <= hunk.newStart.row) {
bufferRows.push(lastBufferRow)
lastScreenRow++
lastBufferRow++
}
lastBufferRow = this.isSoftWrapHunk(hunk) ? hunk.oldEnd.row : hunk.oldEnd.row + 1
}
while (lastScreenRow < endRow) {
bufferRows.push(lastBufferRow)
lastScreenRow++
lastBufferRow++
}
return bufferRows
}
leadingWhitespaceLengthForSurroundingLines (startBufferRow) {
let length = 0
for (let bufferRow = startBufferRow - 1; bufferRow >= 0; bufferRow--) {
const line = this.buffer.lineForRow(bufferRow)
if (line.length > 0) {
length = this.leadingWhitespaceLengthForNonEmptyLine(line)
break
}
}
const lineCount = this.buffer.getLineCount()
for (let bufferRow = startBufferRow + 1; bufferRow < lineCount; bufferRow++) {
const line = this.buffer.lineForRow(bufferRow)
if (line.length > 0) {
length = Math.max(length, this.leadingWhitespaceLengthForNonEmptyLine(line))
break
}
}
return length
}
leadingWhitespaceLengthForNonEmptyLine (line) {
let length = 0
for (let i = 0; i < line.length; i++) {
const character = line[i]
if (character === ' ') {
length++
} else if (character === '\t') {
length += this.tabLength - (length % this.tabLength)
} else {
break
}
}
return length
}
findTrailingWhitespaceStartColumn (bufferRow) {
let position
for (position = {row: bufferRow, column: this.buffer.lineLengthForRow(bufferRow) - 1}; position.column >= 0; position.column--) {
const previousCharacter = this.buffer.getCharacterAtPosition(position)
if (previousCharacter !== ' ' && previousCharacter !== '\t') {
break
}
}
return position.column + 1
}
registerBuiltInScope (flags, className) {
if (this.nextBuiltInScopeId > MAX_BUILT_IN_SCOPE_ID) {
throw new Error('Built in scope ids exhausted')
}
let scopeId
if (className.length > 0) {
scopeId = this.nextBuiltInScopeId += 2
this.builtInClassNamesByScopeId.set(scopeId, className)
} else {
scopeId = 0
}
this.builtInScopeIdsByFlags.set(flags, scopeId)
return scopeId
}
getBuiltInScopeId (flags) {
if (this.builtInScopeIdsByFlags.has(flags)) {
return this.builtInScopeIdsByFlags.get(flags)
} else {
return -1
}
}
classNameForScopeId (scopeId) {
if (scopeId <= MAX_BUILT_IN_SCOPE_ID) {
return this.builtInClassNamesByScopeId.get(scopeId)
} else {
return this.buffer.languageMode.classNameForScopeId(scopeId)
}
}
scopeIdForTag (tag) {
if (this.isCloseTag(tag)) tag++
return -tag
}
classNameForTag (tag) {
return this.classNameForScopeId(this.scopeIdForTag(tag))
}
openTagForScopeId (scopeId) {
return -scopeId
}
closeTagForScopeId (scopeId) {
return -scopeId - 1
}
isOpenTag (tag) {
return tag < 0 && (tag & 1) === 1
}
isCloseTag (tag) {
return tag < 0 && (tag & 1) === 0
}
bufferWillChange (change) {
const lineCount = this.buffer.getLineCount()
let endRow = change.oldRange.end.row
while (endRow + 1 < lineCount && this.buffer.lineLengthForRow(endRow + 1) === 0) {
endRow++
}
this.populateSpatialIndexIfNeeded(endRow + 1, Infinity)
}
bufferDidChange ({oldRange, newRange}) {
let startRow = oldRange.start.row
let oldEndRow = oldRange.end.row
let newEndRow = newRange.end.row
// Indent guides on sequences of blank lines are affected by the content of
// adjacent lines.
if (this.showIndentGuides) {
while (startRow > 0) {
if (this.buffer.lineLengthForRow(startRow - 1) > 0) break
startRow--
}
while (newEndRow < this.buffer.getLastRow()) {
if (this.buffer.lineLengthForRow(newEndRow + 1) > 0) break
oldEndRow++
newEndRow++
}
}
this.indexedBufferRowCount += newEndRow - oldEndRow
this.didChange(this.updateSpatialIndex(startRow, oldEndRow + 1, newEndRow + 1, Infinity))
}
didChange ({start, oldExtent, newExtent}) {
this.changesSinceLastEvent.splice(start, oldExtent, newExtent)
if (this.buffer.transactCallDepth === 0) this.emitDeferredChangeEvents()
}
emitDeferredChangeEvents () {
if (this.changesSinceLastEvent.getChangeCount() > 0) {
this.emitter.emit('did-change', this.changesSinceLastEvent.getChanges().map((change) => {
return {
oldRange: new Range(change.oldStart, change.oldEnd),
newRange: new Range(change.newStart, change.newEnd)
}
}))
this.changesSinceLastEvent = new Patch()
}
}
notifyObserversIfMarkerScreenPositionsChanged () {
this.displayMarkerLayersById.forEach((layer) => {
layer.notifyObserversIfMarkerScreenPositionsChanged()
})
}
updateSpatialIndex (startBufferRow, oldEndBufferRow, newEndBufferRow, endScreenRow, deadline = NullDeadline) {
const originalOldEndBufferRow = oldEndBufferRow
startBufferRow = this.findBoundaryPrecedingBufferRow(startBufferRow)
oldEndBufferRow = this.findBoundaryFollowingBufferRow(oldEndBufferRow)
newEndBufferRow += (oldEndBufferRow - originalOldEndBufferRow)
const startScreenRow = this.translateBufferPositionWithSpatialIndex({row: startBufferRow, column: 0}, 'backward').row
const oldEndScreenRow = this.translateBufferPositionWithSpatialIndex({row: oldEndBufferRow, column: 0}, 'backward').row
this.spatialIndex.spliceOld(
{row: startBufferRow, column: 0},
{row: oldEndBufferRow - startBufferRow, column: 0},
{row: newEndBufferRow - startBufferRow, column: 0}
)
const folds = this.computeFoldsInBufferRowRange(startBufferRow, newEndBufferRow)
const insertedScreenLineLengths = []
const insertedTabCounts = []
const currentScreenLineTabColumns = []
let rightmostInsertedScreenPosition = Point(0, -1)
let bufferRow = startBufferRow
let screenRow = startScreenRow
let bufferColumn = 0
let unexpandedScreenColumn = 0
let expandedScreenColumn = 0
while (true) {
if (bufferRow >= newEndBufferRow) break
if (screenRow >= endScreenRow && bufferColumn === 0) break
if (deadline.timeRemaining() < 2) break
let bufferLine = this.buffer.lineForRow(bufferRow)
if (bufferLine == null) break
let bufferLineLength = bufferLine.length
currentScreenLineTabColumns.length = 0
let screenLineWidth = 0
let lastWrapBoundaryUnexpandedScreenColumn = 0
let lastWrapBoundaryExpandedScreenColumn = 0
let lastWrapBoundaryScreenLineWidth = 0
let firstNonWhitespaceScreenColumn = -1
while (bufferColumn <= bufferLineLength) {
const foldEnd = folds[bufferRow] && folds[bufferRow][bufferColumn]
const previousCharacter = bufferLine[bufferColumn - 1]
const character = foldEnd ? this.foldCharacter : bufferLine[bufferColumn]
// Are we in leading whitespace? If yes, record the *end* of the leading
// whitespace if we've reached a non whitespace character. If no, record
// the current column if it is a viable soft wrap boundary.
if (firstNonWhitespaceScreenColumn < 0) {
if (character !== ' ' && character !== '\t') {
firstNonWhitespaceScreenColumn = expandedScreenColumn
}
} else {
if (previousCharacter &&
character &&
this.isWrapBoundary(previousCharacter, character)) {
lastWrapBoundaryUnexpandedScreenColumn = unexpandedScreenColumn
lastWrapBoundaryExpandedScreenColumn = expandedScreenColumn
lastWrapBoundaryScreenLineWidth = screenLineWidth
}
}
// Determine the on-screen width of the character for soft-wrap calculations
let characterWidth
if (character === '\t') {
const distanceToNextTabStop = this.tabLength - (expandedScreenColumn % this.tabLength)
characterWidth = this.ratioForCharacter(' ') * distanceToNextTabStop
} else if (character) {
characterWidth = this.ratioForCharacter(character)
} else {
characterWidth = 0
}
const insertSoftLineBreak =
screenLineWidth > 0 && characterWidth > 0 &&
screenLineWidth + characterWidth > this.softWrapColumn &&
previousCharacter && character &&
!isCharacterPair(previousCharacter, character)
if (insertSoftLineBreak) {
let indentLength = (firstNonWhitespaceScreenColumn < this.softWrapColumn)
? Math.max(0, firstNonWhitespaceScreenColumn)
: 0
if (indentLength + this.softWrapHangingIndent < this.softWrapColumn) {
indentLength += this.softWrapHangingIndent
}
const unexpandedWrapColumn = lastWrapBoundaryUnexpandedScreenColumn || unexpandedScreenColumn
const expandedWrapColumn = lastWrapBoundaryExpandedScreenColumn || expandedScreenColumn
const wrapWidth = lastWrapBoundaryScreenLineWidth || screenLineWidth
this.spatialIndex.splice(
Point(screenRow, unexpandedWrapColumn),
Point.ZERO,
Point(1, indentLength)
)
insertedScreenLineLengths.push(expandedWrapColumn)
if (expandedWrapColumn > rightmostInsertedScreenPosition.column) {
rightmostInsertedScreenPosition.row = screenRow
rightmostInsertedScreenPosition.column = expandedWrapColumn
}
screenRow++
// To determine the expanded screen column following the wrap, we need
// to re-expand each tab following the wrap boundary, because tabs may
// take on different lengths due to starting at different screen columns.
let unexpandedScreenColumnAfterLastTab = indentLength
let expandedScreenColumnAfterLastTab = indentLength
let tabCountPrecedingWrap = 0
for (let i = 0; i < currentScreenLineTabColumns.length; i++) {
const tabColumn = currentScreenLineTabColumns[i]
if (tabColumn < unexpandedWrapColumn) {
tabCountPrecedingWrap++
} else {
const tabColumnAfterWrap = indentLength + tabColumn - unexpandedWrapColumn
expandedScreenColumnAfterLastTab += (tabColumnAfterWrap - unexpandedScreenColumnAfterLastTab)
expandedScreenColumnAfterLastTab += this.tabLength - (expandedScreenColumnAfterLastTab % this.tabLength)
unexpandedScreenColumnAfterLastTab = tabColumnAfterWrap + 1
currentScreenLineTabColumns[i - tabCountPrecedingWrap] = tabColumnAfterWrap
}
}
insertedTabCounts.push(tabCountPrecedingWrap)
currentScreenLineTabColumns.length -= tabCountPrecedingWrap
unexpandedScreenColumn = unexpandedScreenColumn - unexpandedWrapColumn + indentLength
expandedScreenColumn = expandedScreenColumnAfterLastTab + unexpandedScreenColumn - unexpandedScreenColumnAfterLastTab
screenLineWidth = (indentLength * this.ratioForCharacter(' ')) + (screenLineWidth - wrapWidth)
lastWrapBoundaryUnexpandedScreenColumn = 0
lastWrapBoundaryExpandedScreenColumn = 0
lastWrapBoundaryScreenLineWidth = 0
}
// If there is a fold at this position, splice it into the spatial index
// and jump to the end of the fold.
if (foldEnd) {
this.spatialIndex.splice(
{row: screenRow, column: unexpandedScreenColumn},
traversal(foldEnd, {row: bufferRow, column: bufferColumn}),
{row: 0, column: 1}
)
unexpandedScreenColumn++
expandedScreenColumn++
screenLineWidth += characterWidth
bufferRow = foldEnd.row
bufferColumn = foldEnd.column
bufferLine = this.buffer.lineForRow(bufferRow)
bufferLineLength = bufferLine.length
} else {
// If there is no fold at this position, check if we need to handle
// a hard tab at this position and advance by a single buffer column.
if (character === '\t') {
currentScreenLineTabColumns.push(unexpandedScreenColumn)
const distanceToNextTabStop = this.tabLength - (expandedScreenColumn % this.tabLength)
expandedScreenColumn += distanceToNextTabStop
screenLineWidth += distanceToNextTabStop * this.ratioForCharacter(' ')
} else {
expandedScreenColumn++
screenLineWidth += characterWidth
}
unexpandedScreenColumn++
bufferColumn++
}
}
expandedScreenColumn--
insertedScreenLineLengths.push(expandedScreenColumn)
insertedTabCounts.push(currentScreenLineTabColumns.length)
if (expandedScreenColumn > rightmostInsertedScreenPosition.column) {
rightmostInsertedScreenPosition.row = screenRow
rightmostInsertedScreenPosition.column = expandedScreenColumn
}
bufferRow++
bufferColumn = 0
screenRow++
unexpandedScreenColumn = 0
expandedScreenColumn = 0
}
if (bufferRow > this.indexedBufferRowCount) {
this.indexedBufferRowCount = bufferRow
if (bufferRow === this.buffer.getLineCount()) {
this.spatialIndex.rebalance()
}
}
const oldScreenRowCount = oldEndScreenRow - startScreenRow
spliceArray(
this.screenLineLengths,
startScreenRow,
oldScreenRowCount,
insertedScreenLineLengths
)
spliceArray(
this.tabCounts,
startScreenRow,
oldScreenRowCount,
insertedTabCounts
)
const lastRemovedScreenRow = startScreenRow + oldScreenRowCount
if (rightmostInsertedScreenPosition.column > this.rightmostScreenPosition.column) {
this.rightmostScreenPosition = rightmostInsertedScreenPosition
} else if (lastRemovedScreenRow < this.rightmostScreenPosition.row) {
this.rightmostScreenPosition.row += insertedScreenLineLengths.length - oldScreenRowCount
} else if (startScreenRow <= this.rightmostScreenPosition.row) {
this.rightmostScreenPosition = Point(0, 0)
for (let row = 0, rowCount = this.screenLineLengths.length; row < rowCount; row++) {
if (this.screenLineLengths[row] > this.rightmostScreenPosition.column) {
this.rightmostScreenPosition.row = row
this.rightmostScreenPosition.column = this.screenLineLengths[row]
}
}
}
spliceArray(
this.cachedScreenLines,
startScreenRow,
oldScreenRowCount,
new Array(insertedScreenLineLengths.length)
)
return {
start: Point(startScreenRow, 0),
oldExtent: Point(oldScreenRowCount, 0),
newExtent: Point(insertedScreenLineLengths.length, 0)
}
}
populateSpatialIndexIfNeeded (endBufferRow, endScreenRow, deadline = NullDeadline) {
endBufferRow = Math.min(this.buffer.getLineCount(), endBufferRow)
if (endBufferRow > this.indexedBufferRowCount && endScreenRow > this.screenLineLengths.length) {
this.updateSpatialIndex(
this.indexedBufferRowCount,
endBufferRow,
endBufferRow,
endScreenRow,
deadline
)
}
}
findBoundaryPrecedingBufferRow (bufferRow) {
while (true) {
if (bufferRow === 0) return 0
let screenPosition = this.translateBufferPositionWithSpatialIndex(Point(bufferRow, 0), 'backward')
let bufferPosition = this.translateScreenPositionWithSpatialIndex(Point(screenPosition.row, 0), 'backward', false)
if (screenPosition.column === 0 && bufferPosition.column === 0) {
return bufferPosition.row
} else {
bufferRow = bufferPosition.row
}
}
}
findBoundaryFollowingBufferRow (bufferRow) {
while (true) {
let screenPosition = this.translateBufferPositionWithSpatialIndex(Point(bufferRow, 0), 'forward')
if (screenPosition.column === 0) {
return bufferRow
} else {
const endOfScreenRow = Point(
screenPosition.row,
this.screenLineLengths[screenPosition.row]
)
bufferRow = this.translateScreenPositionWithSpatialIndex(endOfScreenRow, 'forward', false).row + 1
}
}
}
findBoundaryFollowingScreenRow (screenRow) {
while (true) {
let bufferPosition = this.translateScreenPositionWithSpatialIndex(Point(screenRow, 0), 'forward')
if (bufferPosition.column === 0) {
return [bufferPosition.row, screenRow]
} else {
const endOfBufferRow = Point(
bufferPosition.row,
this.buffer.lineLengthForRow(bufferPosition.row)
)
screenRow = this.translateBufferPositionWithSpatialIndex(endOfBufferRow, 'forward').row + 1
}
}
}
// Returns a map describing fold starts and ends, structured as
// fold start row -> fold start column -> fold end point
computeFoldsInBufferRowRange (startBufferRow, endBufferRow) {
const folds = {}
const foldMarkers = this.foldsMarkerLayer.findMarkers({
intersectsRowRange: [startBufferRow, endBufferRow - 1]
})
// If the given buffer range exceeds the indexed range, we need to ensure
// we consider any folds that intersect the combined row range of the
// initially-queried folds, since we couldn't use the index to expand the
// row range to account for these extra folds ahead of time.
if (endBufferRow >= this.indexedBufferRowCount) {
for (let i = 0; i < foldMarkers.length; i++) {
const marker = foldMarkers[i]
const nextMarker = foldMarkers[i + 1]
if (marker.getEndPosition().row >= endBufferRow &&
(!nextMarker || nextMarker.getEndPosition().row < marker.getEndPosition().row)) {
const intersectingMarkers = this.foldsMarkerLayer.findMarkers({
intersectsRow: marker.getEndPosition().row
})
endBufferRow = marker.getEndPosition().row + 1
foldMarkers.splice(i, foldMarkers.length - i, ...intersectingMarkers)
}
}
}
for (let i = 0; i < foldMarkers.length; i++) {
const foldStart = foldMarkers[i].getStartPosition()
let foldEnd = foldMarkers[i].getEndPosition()
// Merge overlapping folds
while (i < foldMarkers.length - 1) {
const nextFoldMarker = foldMarkers[i + 1]
if (compare(nextFoldMarker.getStartPosition(), foldEnd) < 0) {
if (compare(foldEnd, nextFoldMarker.getEndPosition()) < 0) {
foldEnd = nextFoldMarker.getEndPosition()
}
i++
} else {
break
}
}
// Add non-empty folds to the returned result
if (compare(foldStart, foldEnd) < 0) {
if (!folds[foldStart.row]) folds[foldStart.row] = {}
folds[foldStart.row][foldStart.column] = foldEnd
}
}
return folds
}
setParams (params) {
let paramsChanged = false
if (params.hasOwnProperty('tabLength') && params.tabLength !== this.tabLength) {
paramsChanged = true
this.tabLength = params.tabLength
}
if (params.hasOwnProperty('invisibles') && !invisiblesEqual(params.invisibles, this.invisibles)) {
paramsChanged = true
this.invisibles = params.invisibles
this.eolInvisibles = {
'\r': this.invisibles.cr,
'\n': this.invisibles.eol,
'\r\n': this.invisibles.cr + this.invisibles.eol
}
}
if (params.hasOwnProperty('showIndentGuides') && params.showIndentGuides !== this.showIndentGuides) {
paramsChanged = true
this.showIndentGuides = params.showIndentGuides
}
if (params.hasOwnProperty('softWrapColumn')) {
let softWrapColumn = params.softWrapColumn != null
? Math.max(1, params.softWrapColumn)
: Infinity
if (softWrapColumn !== this.softWrapColumn) {
paramsChanged = true
this.softWrapColumn = softWrapColumn
}
}
if (params.hasOwnProperty('softWrapHangingIndent') && params.softWrapHangingIndent !== this.softWrapHangingIndent) {
paramsChanged = true
this.softWrapHangingIndent = params.softWrapHangingIndent
}
if (params.hasOwnProperty('ratioForCharacter') && params.ratioForCharacter !== this.ratioForCharacter) {
paramsChanged = true
this.ratioForCharacter = params.ratioForCharacter
}
if (params.hasOwnProperty('isWrapBoundary') && params.isWrapBoundary !== this.isWrapBoundary) {
paramsChanged = true
this.isWrapBoundary = params.isWrapBoundary
}
if (params.hasOwnProperty('foldCharacter') && params.foldCharacter !== this.foldCharacter) {
paramsChanged = true
this.foldCharacter = params.foldCharacter
}
if (params.hasOwnProperty('atomicSoftTabs') && params.atomicSoftTabs !== this.atomicSoftTabs) {
paramsChanged = true
this.atomicSoftTabs = params.atomicSoftTabs
}
return paramsChanged
}
isSoftWrapHunk (hunk) {
return isEqual(hunk.oldStart, hunk.oldEnd)
}
}
function invisiblesEqual (left, right) {
let leftKeys = Object.keys(left)
let rightKeys = Object.keys(right)
if (leftKeys.length !== rightKeys.length) return false
for (let key of leftKeys) {
if (left[key] !== right[key]) return false
}
return true
}
function isWordStart (previousCharacter, character) {
return (previousCharacter === ' ' || previousCharacter === '\t') &&
(character !== ' ' && character !== '\t')
}
function unitRatio () {
return 1
}
const NullDeadline = {
didTimeout: false,
timeRemaining () { return Infinity }
}