_ = require 'underscore-plus' {CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = require 'text-buffer' TokenizedBuffer = require './tokenized-buffer' RowMap = require './row-map' Fold = require './fold' Model = require './model' Token = require './token' Decoration = require './decoration' Marker = require './marker' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> super Error.captureStackTrace(this, BufferToScreenConversionError) module.exports = class DisplayBuffer extends Model verticalScrollMargin: 2 horizontalScrollMargin: 6 changeCount: 0 softWrapped: null editorWidthInChars: null lineHeightInPixels: null defaultCharWidth: null height: null width: null @deserialize: (state, atomEnvironment) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) state.config = atomEnvironment.config state.assert = atomEnvironment.assert state.grammarRegistry = atomEnvironment.grammars state.packageManager = atomEnvironment.packages new this(state) constructor: (params={}) -> super { tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager } = params @emitter = new Emitter @disposables = new CompositeDisposable @tokenizedBuffer ?= new TokenizedBuffer({ tabLength, buffer, ignoreInvisibles, @largeFileMode, @config, @grammarRegistry, @packageManager, @assert }) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @markers = {} @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} @overlayDecorationsById = {} @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated @disposables.add @buffer.onDidUpdateMarkers => @emitter.emit 'did-update-markers' @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id}) folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())) @updateAllScreenLines() @decorateFold(fold) for fold in folds subscribeToScopedConfigSettings: => @scopedConfigSubscriptions?.dispose() @scopedConfigSubscriptions = subscriptions = new CompositeDisposable scopeDescriptor = @getRootScopeDescriptor() oldConfigSettings = @configSettings @configSettings = scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor) softWrap: @config.get('editor.softWrap', scope: scopeDescriptor) softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor) preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor) subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrap = newValue @updateWrappedScreenLines() subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrapHangingIndent = newValue @updateWrappedScreenLines() subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrapAtPreferredLineLength = newValue @updateWrappedScreenLines() if @isSoftWrapped() subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) => @configSettings.preferredLineLength = newValue @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) => @configSettings.scrollPastEnd = value @updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings) serialize: -> deserializer: 'DisplayBuffer' id: @id softWrapped: @isSoftWrapped() editorWidthInChars: @editorWidthInChars tokenizedBuffer: @tokenizedBuffer.serialize() largeFileMode: @largeFileMode copy: -> newDisplayBuffer = new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, @grammarRegistry, @packageManager }) for marker in @findMarkers(displayBufferId: @id) marker.copy(displayBufferId: newDisplayBuffer.id) newDisplayBuffer updateAllScreenLines: -> @maxLineLength = 0 @screenLines = [] @rowMap = new RowMap @updateScreenLines(0, @buffer.getLineCount(), null, suppressChangeEvent: true) onDidChangeSoftWrapped: (callback) -> @emitter.on 'did-change-soft-wrapped', callback onDidChangeGrammar: (callback) -> @tokenizedBuffer.onDidChangeGrammar(callback) onDidTokenize: (callback) -> @tokenizedBuffer.onDidTokenize(callback) onDidChange: (callback) -> @emitter.on 'did-change', callback onDidChangeCharacterWidths: (callback) -> @emitter.on 'did-change-character-widths', callback onDidRequestAutoscroll: (callback) -> @emitter.on 'did-request-autoscroll', callback observeDecorations: (callback) -> callback(decoration) for decoration in @getDecorations() @onDidAddDecoration(callback) onDidAddDecoration: (callback) -> @emitter.on 'did-add-decoration', callback onDidRemoveDecoration: (callback) -> @emitter.on 'did-remove-decoration', callback onDidCreateMarker: (callback) -> @emitter.on 'did-create-marker', callback onDidUpdateMarkers: (callback) -> @emitter.on 'did-update-markers', callback emitDidChange: (eventProperties, refreshMarkers=true) -> @emitter.emit 'did-change', eventProperties if refreshMarkers @refreshMarkerScreenPositions() @emitter.emit 'did-update-markers' updateWrappedScreenLines: -> start = 0 end = @getLastRow() @updateAllScreenLines() screenDelta = @getLastRow() - end bufferDelta = 0 @emitDidChange({start, end, screenDelta, bufferDelta}) # Sets the visibility of the tokenized buffer. # # visible - A {Boolean} indicating of the tokenized buffer is shown setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) getVerticalScrollMargin: -> maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2) Math.min(@verticalScrollMargin, maxScrollMargin) setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2)) setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin getHeight: -> @height setHeight: (@height) -> @height getWidth: -> @width setWidth: (newWidth) -> oldWidth = @width @width = newWidth @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped() @width getLineHeightInPixels: -> @lineHeightInPixels setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels getKoreanCharWidth: -> @koreanCharWidth getHalfWidthCharWidth: -> @halfWidthCharWidth getDoubleWidthCharWidth: -> @doubleWidthCharWidth getDefaultCharWidth: -> @defaultCharWidth setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> doubleWidthCharWidth ?= defaultCharWidth halfWidthCharWidth ?= defaultCharWidth koreanCharWidth ?= defaultCharWidth if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth @defaultCharWidth = defaultCharWidth @doubleWidthCharWidth = doubleWidthCharWidth @halfWidthCharWidth = halfWidthCharWidth @koreanCharWidth = koreanCharWidth @updateWrappedScreenLines() if @isSoftWrapped() and @getEditorWidthInChars()? defaultCharWidth getCursorWidth: -> 1 scrollToScreenRange: (screenRange, options = {}) -> scrollEvent = {screenRange, options} @emitter.emit "did-request-autoscroll", scrollEvent scrollToScreenPosition: (screenPosition, options) -> @scrollToScreenRange(new Range(screenPosition, screenPosition), options) scrollToBufferPosition: (bufferPosition, options) -> @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) # Retrieves the current tab length. # # Returns a {Number}. getTabLength: -> @tokenizedBuffer.getTabLength() # Specifies the tab length. # # tabLength - A {Number} that defines the new tab length. setTabLength: (tabLength) -> @tokenizedBuffer.setTabLength(tabLength) setIgnoreInvisibles: (ignoreInvisibles) -> @tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles) setSoftWrapped: (softWrapped) -> if softWrapped isnt @softWrapped @softWrapped = softWrapped @updateWrappedScreenLines() softWrapped = @isSoftWrapped() @emitter.emit 'did-change-soft-wrapped', softWrapped softWrapped else @isSoftWrapped() isSoftWrapped: -> if @largeFileMode false else @softWrapped ? @configSettings.softWrap ? false # Set the number of characters that fit horizontally in the editor. # # editorWidthInChars - A {Number} of characters. setEditorWidthInChars: (editorWidthInChars) -> if editorWidthInChars > 0 previousWidthInChars = @editorWidthInChars @editorWidthInChars = editorWidthInChars if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped() @updateWrappedScreenLines() # Returns the editor width in characters for soft wrap. getEditorWidthInChars: -> width = @getWidth() if width? and @defaultCharWidth > 0 Math.max(0, Math.floor(width / @defaultCharWidth)) else @editorWidthInChars getSoftWrapColumn: -> if @configSettings.softWrapAtPreferredLineLength Math.min(@getEditorWidthInChars(), @configSettings.preferredLineLength) else @getEditorWidthInChars() getSoftWrapColumnForTokenizedLine: (tokenizedLine) -> lineMaxWidth = @getSoftWrapColumn() * @getDefaultCharWidth() return if Number.isNaN(lineMaxWidth) return 0 if lineMaxWidth is 0 iterator = tokenizedLine.getTokenIterator(false) column = 0 currentWidth = 0 while iterator.next() textIndex = 0 text = iterator.getText() while textIndex < text.length if iterator.isPairedCharacter() charLength = 2 else charLength = 1 if iterator.hasDoubleWidthCharacterAt(textIndex) charWidth = @getDoubleWidthCharWidth() else if iterator.hasHalfWidthCharacterAt(textIndex) charWidth = @getHalfWidthCharWidth() else if iterator.hasKoreanCharacterAt(textIndex) charWidth = @getKoreanCharWidth() else charWidth = @getDefaultCharWidth() return column if currentWidth + charWidth > lineMaxWidth currentWidth += charWidth column += charLength textIndex += charLength column # Gets the screen line for the given screen row. # # * `screenRow` - A {Number} indicating the screen row. # # Returns {TokenizedLine} tokenizedLineForScreenRow: (screenRow) -> if @largeFileMode if line = @tokenizedBuffer.tokenizedLineForRow(screenRow) if line.text.length > @maxLineLength @maxLineLength = line.text.length @longestScreenRow = screenRow line else @screenLines[screenRow] # Gets the screen lines for the given screen row range. # # startRow - A {Number} indicating the beginning screen row. # endRow - A {Number} indicating the ending screen row. # # Returns an {Array} of {TokenizedLine}s. tokenizedLinesForScreenRows: (startRow, endRow) -> if @largeFileMode @tokenizedBuffer.tokenizedLinesForRows(startRow, endRow) else @screenLines[startRow..endRow] # Gets all the screen lines. # # Returns an {Array} of {TokenizedLine}s. getTokenizedLines: -> if @largeFileMode @tokenizedBuffer.tokenizedLinesForRows(0, @getLastRow()) else new Array(@screenLines...) indentLevelForLine: (line) -> @tokenizedBuffer.indentLevelForLine(line) # Given starting and ending screen rows, this returns an array of the # buffer rows corresponding to every screen row in the range # # startScreenRow - The screen row {Number} to start at # endScreenRow - The screen row {Number} to end at (default: the last screen row) # # Returns an {Array} of buffer rows as {Numbers}s. bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> if @largeFileMode [startScreenRow..endScreenRow] else for screenRow in [startScreenRow..endScreenRow] @rowMap.bufferRowRangeForScreenRow(screenRow)[0] # Creates a new fold between two row numbers. # # startRow - The row {Number} to start folding at # endRow - The row {Number} to end the fold # # Returns the new {Fold}. createFold: (startRow, endRow) -> unless @largeFileMode foldMarker = @findFoldMarker({startRow, endRow}) ? @buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes()) @foldForMarker(foldMarker) isFoldedAtBufferRow: (bufferRow) -> @largestFoldContainingBufferRow(bufferRow)? isFoldedAtScreenRow: (screenRow) -> @largestFoldContainingBufferRow(@bufferRowForScreenRow(screenRow))? # Destroys the fold with the given id destroyFoldWithId: (id) -> @foldsByMarkerId[id]?.destroy() # Removes any folds found that contain the given buffer row. # # bufferRow - The buffer row {Number} to check against unfoldBufferRow: (bufferRow) -> fold.destroy() for fold in @foldsContainingBufferRow(bufferRow) return # Given a buffer row, this returns the largest fold that starts there. # # Largest is defined as the fold whose difference between its start and end points # are the greatest. # # bufferRow - A {Number} indicating the buffer row # # Returns a {Fold} or null if none exists. largestFoldStartingAtBufferRow: (bufferRow) -> @foldsStartingAtBufferRow(bufferRow)[0] # Public: Given a buffer row, this returns all folds that start there. # # bufferRow - A {Number} indicating the buffer row # # Returns an {Array} of {Fold}s. foldsStartingAtBufferRow: (bufferRow) -> for marker in @findFoldMarkers(startRow: bufferRow) @foldForMarker(marker) # Given a screen row, this returns the largest fold that starts there. # # Largest is defined as the fold whose difference between its start and end points # are the greatest. # # screenRow - A {Number} indicating the screen row # # Returns a {Fold}. largestFoldStartingAtScreenRow: (screenRow) -> @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow)) # Given a buffer row, this returns the largest fold that includes it. # # Largest is defined as the fold whose difference between its start and end rows # is the greatest. # # bufferRow - A {Number} indicating the buffer row # # Returns a {Fold}. largestFoldContainingBufferRow: (bufferRow) -> @foldsContainingBufferRow(bufferRow)[0] # Returns the folds in the given row range (exclusive of end row) that are # not contained by any other folds. outermostFoldsInBufferRowRange: (startRow, endRow) -> folds = [] lastFoldEndRow = -1 for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow]) range = marker.getRange() if range.start.row > lastFoldEndRow lastFoldEndRow = range.end.row if startRow <= range.start.row <= range.end.row < endRow folds.push(@foldForMarker(marker)) folds # Public: Given a buffer row, this returns folds that include it. # # # bufferRow - A {Number} indicating the buffer row # # Returns an {Array} of {Fold}s. foldsContainingBufferRow: (bufferRow) -> for marker in @findFoldMarkers(intersectsRow: bufferRow) @foldForMarker(marker) # Given a buffer row, this converts it into a screen row. # # bufferRow - A {Number} representing a buffer row # # Returns a {Number}. screenRowForBufferRow: (bufferRow) -> if @largeFileMode bufferRow else @rowMap.screenRowRangeForBufferRow(bufferRow)[0] lastScreenRowForBufferRow: (bufferRow) -> if @largeFileMode bufferRow else @rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1 # Given a screen row, this converts it into a buffer row. # # screenRow - A {Number} representing a screen row # # Returns a {Number}. bufferRowForScreenRow: (screenRow) -> if @largeFileMode screenRow else @rowMap.bufferRowRangeForScreenRow(screenRow)[0] # Given a buffer range, this converts it into a screen position. # # bufferRange - The {Range} to convert # # Returns a {Range}. screenRangeForBufferRange: (bufferRange, options) -> bufferRange = Range.fromObject(bufferRange) start = @screenPositionForBufferPosition(bufferRange.start, options) end = @screenPositionForBufferPosition(bufferRange.end, options) new Range(start, end) # Given a screen range, this converts it into a buffer position. # # screenRange - The {Range} to convert # # Returns a {Range}. bufferRangeForScreenRange: (screenRange) -> screenRange = Range.fromObject(screenRange) start = @bufferPositionForScreenPosition(screenRange.start) end = @bufferPositionForScreenPosition(screenRange.end) new Range(start, end) # Gets the number of screen lines. # # Returns a {Number}. getLineCount: -> if @largeFileMode @tokenizedBuffer.getLineCount() else @screenLines.length # Gets the number of the last screen line. # # Returns a {Number}. getLastRow: -> @getLineCount() - 1 # Gets the length of the longest screen line. # # Returns a {Number}. getMaxLineLength: -> @maxLineLength # Gets the row number of the longest screen line. # # Return a {} getLongestScreenRow: -> @longestScreenRow # Given a buffer position, this converts it into a screen position. # # bufferPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # options - A hash of options with the following keys: # wrapBeyondNewlines: # wrapAtSoftNewlines: # # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> throw new Error("This TextEditor has been destroyed") if @isDestroyed() {row, column} = @buffer.clipPosition(bufferPosition) [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row) for screenRow in [startScreenRow...endScreenRow] screenLine = @tokenizedLineForScreenRow(screenRow) unless screenLine? throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row", softWrapEnabled: @isSoftWrapped() foldCount: @findFoldMarkers().length lastBufferRow: @buffer.getLastRow() lastScreenRow: @getLastRow() bufferRow: row screenRow: screenRow displayBufferChangeCount: @changeCount tokenizedBufferChangeCount: @tokenizedBuffer.changeCount bufferChangeCount: @buffer.changeCount maxBufferColumn = screenLine.getMaxBufferColumn() if screenLine.isSoftWrapped() and column > maxBufferColumn continue else if column <= maxBufferColumn screenColumn = screenLine.screenColumnForBufferColumn(column) else screenColumn = Infinity break @clipScreenPosition([screenRow, screenColumn], options) # Given a buffer position, this converts it into a screen position. # # screenPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # options - A hash of options with the following keys: # wrapBeyondNewlines: # wrapAtSoftNewlines: # # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options) [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row) new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column)) # Retrieves the grammar's token scopeDescriptor for a buffer position. # # bufferPosition - A {Point} in the {TextBuffer} # # Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition: (bufferPosition) -> @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) bufferRangeForScopeAtPosition: (selector, position) -> @tokenizedBuffer.bufferRangeForScopeAtPosition(selector, position) # Retrieves the grammar's token for a buffer position. # # bufferPosition - A {Point} in the {TextBuffer}. # # Returns a {Token}. tokenForBufferPosition: (bufferPosition) -> @tokenizedBuffer.tokenForPosition(bufferPosition) # Get the grammar for this buffer. # # Returns the current {Grammar} or the {NullGrammar}. getGrammar: -> @tokenizedBuffer.grammar # Sets the grammar for the buffer. # # grammar - Sets the new grammar rules setGrammar: (grammar) -> @tokenizedBuffer.setGrammar(grammar) # Reloads the current grammar. reloadGrammar: -> @tokenizedBuffer.reloadGrammar() # Given a position, this clips it to a real position. # # For example, if `position`'s row exceeds the row count of the buffer, # or if its column goes beyond a line's length, this "sanitizes" the value # to a real position. # # position - The {Point} to clip # options - A hash with the following values: # wrapBeyondNewlines: if `true`, continues wrapping past newlines # wrapAtSoftNewlines: if `true`, continues wrapping past soft newlines # skipSoftWrapIndentation: if `true`, skips soft wrap indentation without wrapping to the previous line # screenLine: if `true`, indicates that you're using a line number, not a row number # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options {row, column} = Point.fromObject(screenPosition) if row < 0 row = 0 column = 0 else if row > @getLastRow() row = @getLastRow() column = Infinity else if column < 0 column = 0 screenLine = @tokenizedLineForScreenRow(row) unless screenLine? error = new Error("Undefined screen line when clipping screen position") Error.captureStackTrace(error) error.metadata = { screenRow: row screenColumn: column maxScreenRow: @getLastRow() screenLinesDefined: @screenLines.map (sl) -> sl? displayBufferChangeCount: @changeCount tokenizedBufferChangeCount: @tokenizedBuffer.changeCount bufferChangeCount: @buffer.changeCount } throw error maxScreenColumn = screenLine.getMaxScreenColumn() if screenLine.isSoftWrapped() and column >= maxScreenColumn if wrapAtSoftNewlines row++ column = @tokenizedLineForScreenRow(row).clipScreenColumn(0) else column = screenLine.clipScreenColumn(maxScreenColumn - 1) else if screenLine.isColumnInsideSoftWrapIndentation(column) if skipSoftWrapIndentation column = screenLine.clipScreenColumn(0) else row-- column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1 else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow() row++ column = 0 else column = screenLine.clipScreenColumn(column, options) new Point(row, column) # Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. # # * `range` The {Range} to clip. # * `options` (optional) See {::clipScreenPosition} `options`. # Returns a {Range}. clipScreenRange: (range, options) -> start = @clipScreenPosition(range.start, options) end = @clipScreenPosition(range.end, options) new Range(start, end) # Calculates a {Range} representing the start of the {TextBuffer} until the end. # # Returns a {Range}. rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) decorationForId: (id) -> @decorationsById[id] getDecorations: (propertyFilter) -> allDecorations = [] for markerId, decorations of @decorationsByMarkerId allDecorations.push(decorations...) if decorations? if propertyFilter? allDecorations = allDecorations.filter (decoration) -> for key, value of propertyFilter return false unless decoration.properties[key] is value true allDecorations getLineDecorations: (propertyFilter) -> @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line') getLineNumberDecorations: (propertyFilter) -> @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number') getHighlightDecorations: (propertyFilter) -> @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight') getOverlayDecorations: (propertyFilter) -> result = [] for id, decoration of @overlayDecorationsById result.push(decoration) if propertyFilter? result.filter (decoration) -> for key, value of propertyFilter return false unless decoration.properties[key] is value true else result decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> decorationsByMarkerId = {} for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) if decorations = @decorationsByMarkerId[marker.id] decorationsByMarkerId[marker.id] = decorations decorationsByMarkerId decorateMarker: (marker, decorationParams) -> marker = @getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) decorationDestroyedDisposable = decoration.onDidDestroy => @removeDecoration(decoration) @disposables.remove(decorationDestroyedDisposable) @disposables.add(decorationDestroyedDisposable) @decorationsByMarkerId[marker.id] ?= [] @decorationsByMarkerId[marker.id].push(decoration) @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay') @decorationsById[decoration.id] = decoration @emitter.emit 'did-add-decoration', decoration decoration removeDecoration: (decoration) -> {marker} = decoration return unless decorations = @decorationsByMarkerId[marker.id] index = decorations.indexOf(decoration) if index > -1 decorations.splice(index, 1) delete @decorationsById[decoration.id] @emitter.emit 'did-remove-decoration', decoration delete @decorationsByMarkerId[marker.id] if decorations.length is 0 delete @overlayDecorationsById[decoration.id] decorationsForMarkerId: (markerId) -> @decorationsByMarkerId[markerId] # Retrieves a {Marker} based on its id. # # id - A {Number} representing a marker id # # Returns the {Marker} (if it exists). getMarker: (id) -> unless marker = @markers[id] if bufferMarker = @buffer.getMarker(id) marker = new Marker({bufferMarker, displayBuffer: this}) @markers[id] = marker marker # Retrieves the active markers in the buffer. # # Returns an {Array} of existing {Marker}s. getMarkers: -> @buffer.getMarkers().map ({id}) => @getMarker(id) getMarkerCount: -> @buffer.getMarkerCount() # Public: Constructs a new marker at the given screen range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor # # Returns a {Number} representing the new marker's ID. markScreenRange: (args...) -> bufferRange = @bufferRangeForScreenRange(args.shift()) @markBufferRange(bufferRange, args...) # Public: Constructs a new marker at the given buffer range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor # # Returns a {Number} representing the new marker's ID. markBufferRange: (range, options) -> @getMarker(@buffer.markRange(range, options).id) # Public: Constructs a new marker at the given screen position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor # # Returns a {Number} representing the new marker's ID. markScreenPosition: (screenPosition, options) -> @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options) # Public: Constructs a new marker at the given buffer position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor # # Returns a {Number} representing the new marker's ID. markBufferPosition: (bufferPosition, options) -> @getMarker(@buffer.markPosition(bufferPosition, options).id) # Public: Removes the marker with the given id. # # id - The {Number} of the ID to remove destroyMarker: (id) -> @buffer.destroyMarker(id) delete @markers[id] # Finds the first marker satisfying the given attributes # # Refer to {DisplayBuffer::findMarkers} for details. # # Returns a {Marker} or null findMarker: (params) -> @findMarkers(params)[0] # Public: Find all markers satisfying a set of parameters. # # params - An {Object} containing parameters that all returned markers must # satisfy. Unreserved keys will be compared against the markers' custom # properties. There are also the following reserved keys with special # meaning for the query: # :startBufferRow - A {Number}. Only returns markers starting at this row in # buffer coordinates. # :endBufferRow - A {Number}. Only returns markers ending at this row in # buffer coordinates. # :containsBufferRange - A {Range} or range-compatible {Array}. Only returns # markers containing this range in buffer coordinates. # :containsBufferPosition - A {Point} or point-compatible {Array}. Only # returns markers containing this position in buffer coordinates. # :containedInBufferRange - A {Range} or range-compatible {Array}. Only # returns markers contained within this range. # # Returns an {Array} of {Marker}s findMarkers: (params) -> params = @translateToBufferMarkerParams(params) @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) translateToBufferMarkerParams: (params) -> bufferMarkerParams = {} for key, value of params switch key when 'startBufferRow' key = 'startRow' when 'endBufferRow' key = 'endRow' when 'startScreenRow' key = 'startRow' value = @bufferRowForScreenRow(value) when 'endScreenRow' key = 'endRow' value = @bufferRowForScreenRow(value) when 'intersectsBufferRowRange' key = 'intersectsRowRange' when 'intersectsScreenRowRange' key = 'intersectsRowRange' [startRow, endRow] = value value = [@bufferRowForScreenRow(startRow), @bufferRowForScreenRow(endRow)] when 'containsBufferRange' key = 'containsRange' when 'containsBufferPosition' key = 'containsPosition' when 'containedInBufferRange' key = 'containedInRange' when 'containedInScreenRange' key = 'containedInRange' value = @bufferRangeForScreenRange(value) when 'intersectsBufferRange' key = 'intersectsRange' when 'intersectsScreenRange' key = 'intersectsRange' value = @bufferRangeForScreenRange(value) bufferMarkerParams[key] = value bufferMarkerParams findFoldMarker: (attributes) -> @findFoldMarkers(attributes)[0] findFoldMarkers: (attributes) -> @buffer.findMarkers(@getFoldMarkerAttributes(attributes)) getFoldMarkerAttributes: (attributes) -> if attributes _.extend(attributes, @foldMarkerAttributes) else @foldMarkerAttributes refreshMarkerScreenPositions: -> for marker in @getMarkers() marker.notifyObservers(textChanged: false) return destroyed: -> fold.destroy() for markerId, fold of @foldsByMarkerId marker.disposables.dispose() for id, marker of @markers @scopedConfigSubscriptions.dispose() @disposables.dispose() @tokenizedBuffer.destroy() logLines: (start=0, end=@getLastRow()) -> for row in [start..end] line = @tokenizedLineForScreenRow(row).text console.log row, @bufferRowForScreenRow(row), line, line.length return getRootScopeDescriptor: -> @tokenizedBuffer.rootScopeDescriptor handleTokenizedBufferChange: (tokenizedBufferChange) => @changeCount = @tokenizedBuffer.changeCount {start, end, delta, bufferChange} = tokenizedBufferChange @updateScreenLines(start, end + 1, delta, refreshMarkers: false) updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> return if @largeFileMode return if @isDestroyed() startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0] endBufferRow = @rowMap.bufferRowRangeForBufferRow(endBufferRow - 1)[1] startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0] endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1] {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta) screenDelta = screenLines.length - (endScreenRow - startScreenRow) _.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000) @checkScreenLinesInvariant() @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions) @findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta) return if options.suppressChangeEvent changeEvent = start: startScreenRow end: endScreenRow - 1 screenDelta: screenDelta bufferDelta: bufferDelta @emitDidChange(changeEvent, options.refreshMarkers) buildScreenLines: (startBufferRow, endBufferRow) -> screenLines = [] regions = [] rectangularRegion = null foldsByStartRow = {} for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow) foldsByStartRow[fold.getStartRow()] = fold bufferRow = startBufferRow while bufferRow < endBufferRow tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow) if fold = foldsByStartRow[bufferRow] foldLine = tokenizedLine.copy() foldLine.fold = fold screenLines.push(foldLine) if rectangularRegion? regions.push(rectangularRegion) rectangularRegion = null foldedRowCount = fold.getBufferRowCount() regions.push(bufferRows: foldedRowCount, screenRows: 1) bufferRow += foldedRowCount else softWraps = 0 if @isSoftWrapped() while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine)) [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt( wrapScreenColumn, @configSettings.softWrapHangingIndent ) break if wrappedLine.hasOnlySoftWrapIndentation() screenLines.push(wrappedLine) softWraps++ screenLines.push(tokenizedLine) 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, screenDelta) -> oldMaxLineLength = @maxLineLength if startScreenRow <= @longestScreenRow < endScreenRow @longestScreenRow = 0 @maxLineLength = 0 maxLengthCandidatesStartRow = 0 maxLengthCandidates = @screenLines else @longestScreenRow += screenDelta if endScreenRow <= @longestScreenRow maxLengthCandidatesStartRow = startScreenRow maxLengthCandidates = newScreenLines for screenLine, i in maxLengthCandidates screenRow = maxLengthCandidatesStartRow + i length = screenLine.text.length if length > @maxLineLength @longestScreenRow = screenRow @maxLineLength = length handleBufferMarkerCreated: (textBufferMarker) => if textBufferMarker.matchesParams(@getFoldMarkerAttributes()) fold = new Fold(this, textBufferMarker) fold.updateDisplayBuffer() @decorateFold(fold) if marker = @getMarker(textBufferMarker.id) # The marker might have been removed in some other handler called before # this one. Only emit when the marker still exists. @emitter.emit 'did-create-marker', marker decorateFold: (fold) -> @decorateMarker(fold.marker, type: 'line-number', class: 'folded') foldForMarker: (marker) -> @foldsByMarkerId[marker.id] decorationDidChangeType: (decoration) -> if decoration.isType('overlay') @overlayDecorationsById[decoration.id] = decoration else delete @overlayDecorationsById[decoration.id] checkScreenLinesInvariant: -> return if @isSoftWrapped() return if _.size(@foldsByMarkerId) > 0 screenLinesCount = @screenLines.length tokenizedLinesCount = @tokenizedBuffer.getLineCount() bufferLinesCount = @buffer.getLineCount() @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) -> error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount} @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) -> error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}