Refactor DisplayBuffer to DecorationManager

This commit moves all the remaining concerns not related to decorations
out of `DisplayBuffer` and into `TextEditor`. This means the
`DisplayBuffer` is now free to be renamed to `DecorationManager`.
This commit is contained in:
Antonio Scandurra 2016-04-05 17:40:24 +02:00
parent 0cf0d6f587
commit bef7539e34
14 changed files with 583 additions and 2222 deletions

View File

@ -0,0 +1,85 @@
DecorationManager = require '../src/decoration-manager'
_ = require 'underscore-plus'
describe "DecorationManager", ->
[decorationManager, buffer, defaultMarkerLayer] = []
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
displayLayer = buffer.addDisplayLayer()
defaultMarkerLayer = displayLayer.addMarkerLayer()
decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
afterEach ->
decorationManager.destroy()
buffer.release()
describe "decorations", ->
[marker, decoration, decorationProperties] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
it "can add decorations associated with markers and remove them", ->
expect(decoration).toBeDefined()
expect(decoration.getProperties()).toBe decorationProperties
expect(decorationManager.decorationForId(decoration.id)).toBe decoration
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
decoration.destroy()
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "will not fail if the decoration is removed twice", ->
decoration.destroy()
decoration.destroy()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "does not allow destroyed markers to be decorated", ->
marker.destroy()
expect(->
decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
).toThrow("Cannot decorate a destroyed marker")
expect(decorationManager.getOverlayDecorations()).toEqual []
describe "when a decoration is updated via Decoration::update()", ->
it "emits an 'updated' event containing the new and old params", ->
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
decoration.setProperties type: 'line-number', class: 'two'
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->
expect(decorationManager.getDecorations()).toEqual [decoration]
expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
describe "::decorateMarker", ->
describe "when decorating gutters", ->
[marker] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('line-number')).toBe true
expect(decoration.isType('gutter')).toBe true
expect(decoration.getProperties().gutterName).toBe 'line-number'
expect(decoration.getProperties().class).toBe 'one'
it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('gutter')).toBe true
expect(decoration.isType('line-number')).toBe false
expect(decoration.getProperties().gutterName).toBe 'test-gutter'
expect(decoration.getProperties().class).toBe 'one'

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ describe "TextEditor", ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->

View File

@ -1477,7 +1477,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
let marker2 = editor.markBufferRange([[9, 0], [9, 0]])
editor.decorateMarker(marker2, {
type: ['line-number', 'line'],
'class': 'b'
@ -1889,7 +1889,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], {
marker = editor.markBufferRange([[9, 2], [9, 4]], {
invalidate: 'inside'
})
editor.decorateMarker(marker, {
@ -2084,7 +2084,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is empty', function () {
it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@ -2106,7 +2106,7 @@ describe('TextEditorComponent', function () {
})
it('renders the overlay element with the CSS class specified by the decoration', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@ -2127,7 +2127,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is not empty', function () {
it('renders at the head of the marker by default', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], {
let marker = editor.markBufferRange([[2, 5], [2, 10]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@ -2173,7 +2173,7 @@ describe('TextEditorComponent', function () {
})
it('slides horizontally left when near the right edge on #win32 and #darwin', async function () {
let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], {
let marker = editor.markBufferRange([[0, 26], [0, 26]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@ -4934,7 +4934,7 @@ describe('TextEditorComponent', function () {
function lineNumberForBufferRowHasClass (bufferRow, klass) {
let screenRow
screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow)
screenRow = editor.screenRowForBufferRow(bufferRow)
return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
}

View File

@ -5710,6 +5710,19 @@ describe "TextEditor", ->
expect(editor.getFirstVisibleScreenRow()).toEqual 89
expect(editor.getVisibleRowRange()).toEqual [89, 99]
describe "::scrollToScreenPosition(position, [options])", ->
it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
editor.onDidRequestAutoscroll(scrollSpy)
editor.scrollToScreenPosition([8, 20])
editor.scrollToScreenPosition([8, 20], center: true)
editor.scrollToScreenPosition([8, 20], center: false, reversed: true)
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
describe '.get/setPlaceholderText()', ->
it 'can be created with placeholderText', ->
newEditor = atom.workspace.buildTextEditor(

View File

@ -419,7 +419,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
@ -432,7 +432,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidTokenize tokenizedHandler
@ -450,7 +450,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
tokenizedHandler.reset()

View File

@ -428,7 +428,7 @@ describe "Workspace", ->
workspace.open('sample.js').then (e) -> editor = e
runs ->
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when the file is over 20MB", ->
it "prompts the user to make sure they want to open a file this big", ->
@ -453,7 +453,7 @@ describe "Workspace", ->
runs ->
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->

View File

@ -0,0 +1,180 @@
{Emitter} = require 'event-kit'
Model = require './model'
Decoration = require './decoration'
LayerDecoration = require './layer-decoration'
module.exports =
class DecorationManager extends Model
didUpdateDecorationsEventScheduled: false
updatedSynchronously: false
constructor: (@displayLayer, @defaultMarkerLayer) ->
super
@emitter = new Emitter
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
@layerDecorationsByMarkerLayerId = {}
@decorationCountsByLayerId = {}
@layerUpdateDisposablesByLayerId = {}
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
onDidUpdateDecorations: (callback) ->
@emitter.on 'did-update-decorations', callback
setUpdatedSynchronously: (@updatedSynchronously) ->
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 @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
if decorations = @decorationsByMarkerId[marker.id]
decorationsByMarkerId[marker.id] = decorations
decorationsByMarkerId
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsState = {}
for layerId of @decorationCountsByLayerId
layer = @displayLayer.getMarkerLayer(layerId)
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
screenRange = marker.getScreenRange()
rangeIsReversed = marker.isReversed()
if decorations = @decorationsByMarkerId[marker.id]
for decoration in decorations
decorationsState[decoration.id] = {
properties: decoration.properties
screenRange, rangeIsReversed
}
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
for layerDecoration in layerDecorations
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
screenRange, rangeIsReversed
}
decorationsState
decorateMarker: (marker, decorationParams) ->
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@observeDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
@emitter.emit 'did-add-decoration', decoration
decoration
decorateMarkerLayer: (markerLayer, decorationParams) ->
decoration = new LayerDecoration(markerLayer, this, decorationParams)
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
@observeDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
scheduleUpdateDecorationsEvent: ->
if @updatedSynchronously
@emitter.emit 'did-update-decorations'
return
unless @didUpdateDecorationsEventScheduled
@didUpdateDecorationsEventScheduled = true
process.nextTick =>
@didUpdateDecorationsEventScheduled = false
@emitter.emit 'did-update-decorations'
decorationDidChangeType: (decoration) ->
if decoration.isType('overlay')
@overlayDecorationsById[decoration.id] = decoration
else
delete @overlayDecorationsById[decoration.id]
didDestroyDecoration: (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]
@unobserveDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
didDestroyLayerDecoration: (decoration) ->
{markerLayer} = decoration
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
@unobserveDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
observeDecoratedLayer: (layer) ->
@decorationCountsByLayerId[layer.id] ?= 0
if ++@decorationCountsByLayerId[layer.id] is 1
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
unobserveDecoratedLayer: (layer) ->
if --@decorationCountsByLayerId[layer.id] is 0
@layerUpdateDisposablesByLayerId[layer.id].dispose()
delete @decorationCountsByLayerId[layer.id]
delete @layerUpdateDisposablesByLayerId[layer.id]

View File

@ -62,7 +62,7 @@ class Decoration
Section: Construction and Destruction
###
constructor: (@marker, @displayBuffer, properties) ->
constructor: (@marker, @decorationManager, properties) ->
@emitter = new Emitter
@id = nextId()
@setProperties properties
@ -78,7 +78,7 @@ class Decoration
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
@displayBuffer.didDestroyDecoration(this)
@decorationManager.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@ -149,8 +149,8 @@ class Decoration
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.decorationDidChangeType(this)
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@ -175,5 +175,5 @@ class Decoration
@properties.flashCount++
@properties.flashClass = klass
@properties.flashDuration = duration
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'

View File

@ -1,750 +0,0 @@
_ = require 'underscore-plus'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = require 'text-buffer'
TokenizedBuffer = require './tokenized-buffer'
RowMap = require './row-map'
Model = require './model'
Token = require './token'
Decoration = require './decoration'
LayerDecoration = require './layer-decoration'
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
ZERO_WIDTH_NBSP = '\ufeff'
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
didUpdateDecorationsEventScheduled: false
updatedSynchronously: false
@deserialize: (state, atomEnvironment) ->
state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
state.displayLayer = state.tokenizedBuffer.buffer.getDisplayLayer(state.displayLayerId)
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, @displayLayer
} = params
@emitter = new Emitter
@disposables = new CompositeDisposable
@tokenizedBuffer ?= new TokenizedBuffer({
tabLength, buffer, @largeFileMode, @config,
@grammarRegistry, @packageManager, @assert
})
@buffer = @tokenizedBuffer.buffer
@displayLayer ?= @buffer.addDisplayLayer()
@displayLayer.setTextDecorationLayer(@tokenizedBuffer)
@charWidthsByScope = {}
@defaultMarkerLayer = @displayLayer.addMarkerLayer()
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
@layerDecorationsByMarkerLayerId = {}
@decorationCountsByLayerId = {}
@layerUpdateDisposablesByLayerId = {}
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker
subscribeToScopedConfigSettings: =>
@scopedConfigSubscriptions?.dispose()
@scopedConfigSubscriptions = subscriptions = new CompositeDisposable
scopeDescriptor = @getRootScopeDescriptor()
subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
@resetDisplayLayer()
serialize: ->
deserializer: 'DisplayBuffer'
id: @id
softWrapped: @isSoftWrapped()
editorWidthInChars: @editorWidthInChars
tokenizedBuffer: @tokenizedBuffer.serialize()
largeFileMode: @largeFileMode
displayLayerId: @displayLayer.id
copy: ->
new DisplayBuffer({
@buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
@grammarRegistry, @packageManager, displayLayer: @buffer.copyDisplayLayer(@displayLayer.id)
})
resetDisplayLayer: ->
scopeDescriptor = @getRootScopeDescriptor()
invisibles =
if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles
@config.get('editor.invisibles', scope: scopeDescriptor)
else
{}
softWrapColumn =
if @isSoftWrapped()
if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
@config.get('editor.preferredLineLength', scope: scopeDescriptor)
else
@getEditorWidthInChars()
else
Infinity
@displayLayer.reset({
invisibles: invisibles
softWrapColumn: softWrapColumn
showIndentGuides: @config.get('editor.showIndentGuide', scope: scopeDescriptor)
tabLength: @getTabLength(),
ratioForCharacter: @ratioForCharacter.bind(this),
isWrapBoundary: isWrapBoundary,
foldCharacter: ZERO_WIDTH_NBSP
})
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
onDidUpdateDecorations: (callback) ->
@emitter.on 'did-update-decorations', callback
# Sets the visibility of the tokenized buffer.
#
# visible - A {Boolean} indicating of the tokenized buffer is shown
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
setUpdatedSynchronously: (@updatedSynchronously) ->
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
@resetDisplayLayer() if newWidth isnt oldWidth and @isSoftWrapped()
@width
getLineHeightInPixels: -> @lineHeightInPixels
setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
ratioForCharacter: (character) ->
if isKoreanCharacter(character)
@getKoreanCharWidth() / @getDefaultCharWidth()
else if isHalfWidthCharacter(character)
@getHalfWidthCharWidth() / @getDefaultCharWidth()
else if isDoubleWidthCharacter(character)
@getDoubleWidthCharWidth() / @getDefaultCharWidth()
else
1
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
@resetDisplayLayer() 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: ->
if @tabLength?
@tabLength
else
@config.get('editor.tabLength', scope: @getRootScopeDescriptor())
# Specifies the tab length.
#
# tabLength - A {Number} that defines the new tab length.
setTabLength: (tabLength) ->
return if tabLength is @tabLength
@tabLength = tabLength
@tokenizedBuffer.setTabLength(@tabLength)
@resetDisplayLayer()
setIgnoreInvisibles: (ignoreInvisibles) ->
return if ignoreInvisibles is @ignoreInvisibles
@ignoreInvisibles = ignoreInvisibles
@resetDisplayLayer()
setSoftWrapped: (softWrapped) ->
if softWrapped isnt @softWrapped
@softWrapped = softWrapped
@resetDisplayLayer()
softWrapped = @isSoftWrapped()
@emitter.emit 'did-change-soft-wrapped', softWrapped
softWrapped
else
@isSoftWrapped()
isSoftWrapped: ->
if @largeFileMode
false
else
scopeDescriptor = @getRootScopeDescriptor()
@softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? 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()
@resetDisplayLayer()
# 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
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) ->
for screenRow in [startScreenRow..endScreenRow]
@bufferRowForScreenRow(screenRow)
# 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}.
foldBufferRowRange: (startRow, endRow) ->
@displayLayer.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
isFoldedAtBufferRow: (bufferRow) ->
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
isFoldedAtScreenRow: (screenRow) ->
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
isFoldableAtBufferRow: (row) ->
@tokenizedBuffer.isFoldableAtRow(row)
# Removes any folds found that contain the given buffer row.
#
# bufferRow - The buffer row {Number} to check against
unfoldBufferRow: (bufferRow) ->
@displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity)))
# 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
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
lastScreenRowForBufferRow: (bufferRow) ->
if @largeFileMode
bufferRow
else
@displayLayer.translateBufferPosition(Point(bufferRow, 0), clip: 'forward').row
# Given a screen row, this converts it into a buffer row.
#
# screenRow - A {Number} representing a screen row
#
# Returns a {Number}.
bufferRowForScreenRow: (screenRow) ->
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
# 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: ->
@displayLayer.getScreenLineCount()
# Gets the number of the last screen line.
#
# Returns a {Number}.
getLastRow: ->
@getLineCount() - 1
# 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()
@displayLayer.translateBufferPosition(bufferPosition, 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) ->
return @displayLayer.translateScreenPosition(screenPosition, options)
# 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={}) ->
@displayLayer.clipScreenPosition(screenPosition, options)
# 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
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsState = {}
for layerId of @decorationCountsByLayerId
layer = @getMarkerLayer(layerId)
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
screenRange = marker.getScreenRange()
rangeIsReversed = marker.isReversed()
if decorations = @decorationsByMarkerId[marker.id]
for decoration in decorations
decorationsState[decoration.id] = {
properties: decoration.properties
screenRange, rangeIsReversed
}
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
for layerDecoration in layerDecorations
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
screenRange, rangeIsReversed
}
decorationsState
decorateMarker: (marker, decorationParams) ->
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@observeDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
@emitter.emit 'did-add-decoration', decoration
decoration
decorateMarkerLayer: (markerLayer, decorationParams) ->
decoration = new LayerDecoration(markerLayer, this, decorationParams)
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
@observeDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
# Retrieves a {DisplayMarker} based on its id.
#
# id - A {Number} representing a marker id
#
# Returns the {DisplayMarker} (if it exists).
getMarker: (id) ->
@defaultMarkerLayer.getMarker(id)
# Retrieves the active markers in the buffer.
#
# Returns an {Array} of existing {DisplayMarker}s.
getMarkers: ->
@defaultMarkerLayer.getMarkers()
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 {DisplayMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markScreenRange: (screenRange, options) ->
@defaultMarkerLayer.markScreenRange(screenRange, options)
# 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 {DisplayMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markBufferRange: (bufferRange, options) ->
@defaultMarkerLayer.markBufferRange(bufferRange, options)
# 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 {DisplayMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markScreenPosition: (screenPosition, options) ->
@defaultMarkerLayer.markScreenPosition(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 {DisplayMarker} constructor
#
# Returns a {Number} representing the new marker's ID.
markBufferPosition: (bufferPosition, options) ->
@defaultMarkerLayer.markBufferPosition(bufferPosition, options)
# Finds the first marker satisfying the given attributes
#
# Refer to {DisplayBuffer::findMarkers} for details.
#
# Returns a {DisplayMarker} or null
findMarker: (params) ->
@defaultMarkerLayer.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 {DisplayMarker}s
findMarkers: (params) ->
@defaultMarkerLayer.findMarkers(params)
addMarkerLayer: (options) ->
@displayLayer.addMarkerLayer(options)
getMarkerLayer: (id) ->
@displayLayer.getMarkerLayer(id)
getDefaultMarkerLayer: -> @defaultMarkerLayer
refreshMarkerScreenPositions: ->
@defaultMarkerLayer.refreshMarkerScreenPositions()
layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById
return
destroyed: ->
@displayLayer.destroy()
@defaultMarkerLayer.destroy()
@scopedConfigSubscriptions.dispose()
@disposables.dispose()
@tokenizedBuffer.destroy()
getRootScopeDescriptor: ->
@tokenizedBuffer.rootScopeDescriptor
didCreateDefaultLayerMarker: (textBufferMarker) =>
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
scheduleUpdateDecorationsEvent: ->
if @updatedSynchronously
@emitter.emit 'did-update-decorations'
return
unless @didUpdateDecorationsEventScheduled
@didUpdateDecorationsEventScheduled = true
process.nextTick =>
@didUpdateDecorationsEventScheduled = false
@emitter.emit 'did-update-decorations'
decorationDidChangeType: (decoration) ->
if decoration.isType('overlay')
@overlayDecorationsById[decoration.id] = decoration
else
delete @overlayDecorationsById[decoration.id]
didDestroyDecoration: (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]
@unobserveDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
didDestroyLayerDecoration: (decoration) ->
{markerLayer} = decoration
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
@unobserveDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
observeDecoratedLayer: (layer) ->
@decorationCountsByLayerId[layer.id] ?= 0
if ++@decorationCountsByLayerId[layer.id] is 1
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
unobserveDecoratedLayer: (layer) ->
if --@decorationCountsByLayerId[layer.id] is 0
@layerUpdateDisposablesByLayerId[layer.id].dispose()
delete @decorationCountsByLayerId[layer.id]
delete @layerUpdateDisposablesByLayerId[layer.id]

View File

@ -131,7 +131,7 @@ class LanguageMode
for currentRow in [bufferRow..0] by -1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
unless @editor.displayBuffer.isFoldedAtBufferRow(startRow)
unless @editor.isFoldedAtBufferRow(startRow)
return @editor.foldBufferRowRange(startRow, endRow)
# Find the row range for a fold at a given bufferRow. Will handle comments
@ -146,19 +146,19 @@ class LanguageMode
rowRange
rowRangeForCommentAtBufferRow: (bufferRow) ->
return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
return unless @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
startRow = bufferRow
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
@ -181,13 +181,13 @@ class LanguageMode
[bufferRow, foldEndRow]
isFoldableAtBufferRow: (bufferRow) ->
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
@editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
# Returns a {Boolean} indicating whether the line at the given buffer
# row is a comment.
isLineCommentedAtBufferRow: (bufferRow) ->
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
@editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
@editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
# is a block of text bounded by and empty line or a block of text that is not
@ -240,11 +240,11 @@ class LanguageMode
# Returns a {Number}.
suggestedIndentForBufferRow: (bufferRow, options) ->
line = @buffer.lineForRow(bufferRow)
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->

View File

@ -7,7 +7,7 @@ nextId = -> idCounter++
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
constructor: (@markerLayer, @displayBuffer, @properties) ->
constructor: (@markerLayer, @decorationManager, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@ -19,7 +19,7 @@ class LayerDecoration
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
@displayBuffer.didDestroyLayerDecoration(this)
@decorationManager.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
@ -44,7 +44,7 @@ class LayerDecoration
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
# Essential: Override the decoration properties for a specific marker.
#
@ -58,4 +58,4 @@ class LayerDecoration
@overridePropertiesByMarkerId[marker.id] = properties
else
delete @overridePropertiesByMarkerId[marker.id]
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()

View File

@ -1,9 +1,9 @@
module.exports =
class MarkerObservationWindow
constructor: (@displayBuffer, @bufferWindow) ->
constructor: (@decorationManager, @bufferWindow) ->
setScreenRange: (range) ->
@bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range))
@bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
setBufferRange: (range) ->
@bufferWindow.setRange(range)

View File

@ -4,7 +4,8 @@ Grim = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = TextBuffer = require 'text-buffer'
LanguageMode = require './language-mode'
DisplayBuffer = require './display-buffer'
DecorationManager = require './decoration-manager'
TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
Model = require './model'
Selection = require './selection'
@ -12,6 +13,9 @@ TextMateScopeSelector = require('first-mate').ScopeSelector
{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
ZERO_WIDTH_NBSP = '\ufeff'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@ -63,21 +67,30 @@ class TextEditor extends Model
selectionFlashDuration: 500
gutterContainer: null
editorElement: null
verticalScrollMargin: 2
horizontalScrollMargin: 6
softWrapped: null
editorWidthInChars: null
lineHeightInPixels: null
defaultCharWidth: null
height: null
width: null
Object.defineProperty @prototype, "element",
get: -> @getElement()
@deserialize: (state, atomEnvironment) ->
try
displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment)
state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
catch error
if error.syscall is 'read'
return # Error reading the file, don't deserialize an editor for it
else
throw error
state.displayBuffer = displayBuffer
state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
state.buffer = state.tokenizedBuffer.buffer
state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId)
state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId)
state.config = atomEnvironment.config
state.notificationManager = atomEnvironment.notifications
state.packageManager = atomEnvironment.packages
@ -97,13 +110,19 @@ class TextEditor extends Model
super
{
@softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
@softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, @tabLength,
@softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @config,
@notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
@project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd
@project, @assert, @applicationDelegate, grammar, @showInvisibles, @autoHeight, @scrollPastEnd
} = params
{
tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @ignoreInvisibles,
@largeFileMode, @config, @assert, @grammarRegistry, @packageManager, @displayLayer
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager?
throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager?
@ -124,17 +143,23 @@ class TextEditor extends Model
@scrollPastEnd ?= true
@hasTerminatedPendingState = false
showInvisibles ?= true
@showInvisibles ?= true
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({
buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode,
@config, @assert, @grammarRegistry, @packageManager
@buffer ?= new TextBuffer
@tokenizedBuffer ?= new TokenizedBuffer({
tabLength, @buffer, @largeFileMode, @config,
@grammarRegistry, @packageManager, @assert
})
{@buffer, @displayLayer} = @displayBuffer
@displayLayer ?= @buffer.addDisplayLayer()
@displayLayer.setTextDecorationLayer(@tokenizedBuffer)
@defaultMarkerLayer = @displayLayer.addMarkerLayer()
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
@decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer)
@decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
for marker in @selectionsMarkerLayer.getMarkers()
marker.setProperties(preserveFolds: true)
@ -142,7 +167,7 @@ class TextEditor extends Model
@subscribeToTabTypeConfig()
@subscribeToBuffer()
@subscribeToDisplayBuffer()
@subscribeToDisplayLayer()
if @cursors.length is 0 and not suppressCursorCreation
initialLine = Math.max(parseInt(initialLine) or 0, 0)
@ -168,8 +193,12 @@ class TextEditor extends Model
softTabs: @softTabs
firstVisibleScreenRow: @getFirstVisibleScreenRow()
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
softWrapped: @isSoftWrapped()
editorWidthInChars: @editorWidthInChars
tokenizedBuffer: @tokenizedBuffer.serialize()
largeFileMode: @largeFileMode
displayLayerId: @displayLayer.id
registered: atom.textEditors.editors.has this
subscribeToBuffer: ->
@ -194,10 +223,26 @@ class TextEditor extends Model
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
subscribeToDisplayBuffer: ->
subscribeToScopedConfigSettings: =>
@scopedConfigSubscriptions?.dispose()
@scopedConfigSubscriptions = subscriptions = new CompositeDisposable
scopeDescriptor = @getRootScopeDescriptor()
subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
@resetDisplayLayer()
subscribeToDisplayLayer: ->
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
@disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
@disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this)
@disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
@disposables.add @tokenizedBuffer.onDidTokenize @handleTokenization.bind(this)
@disposables.add @displayLayer.onDidChangeSync (e) =>
@mergeIntersectingSelections()
@emitter.emit 'did-change', e
@ -207,13 +252,27 @@ class TextEditor extends Model
@tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
resetDisplayLayer: ->
@displayLayer.reset({
invisibles: @getInvisibles(),
softWrapColumn: @getSoftWrapColumn(),
showIndentGuides: @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()),
tabLength: @getTabLength(),
ratioForCharacter: @ratioForCharacter.bind(this),
isWrapBoundary: isWrapBoundary,
foldCharacter: ZERO_WIDTH_NBSP
})
destroyed: ->
@disposables.dispose()
@displayLayer.destroy()
@scopedConfigSubscriptions.dispose()
@disposables.dispose()
@tokenizedBuffer.destroy()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
@selectionsMarkerLayer.destroy()
@buffer.release()
@displayBuffer.destroy()
@languageMode.destroy()
@gutterContainer.destroy()
@emitter.emit 'did-destroy'
@ -297,7 +356,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeSoftWrapped: (callback) ->
@displayBuffer.onDidChangeSoftWrapped(callback)
@emitter.on 'did-change-soft-wrapped', callback
# Extended: Calls your `callback` when the buffer's encoding has changed.
#
@ -451,7 +510,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeDecorations: (callback) ->
@displayBuffer.observeDecorations(callback)
@decorationManager.observeDecorations(callback)
# Extended: Calls your `callback` when a {Decoration} is added to the editor.
#
@ -460,7 +519,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddDecoration: (callback) ->
@displayBuffer.onDidAddDecoration(callback)
@decorationManager.onDidAddDecoration(callback)
# Extended: Calls your `callback` when a {Decoration} is removed from the editor.
#
@ -469,7 +528,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveDecoration: (callback) ->
@displayBuffer.onDidRemoveDecoration(callback)
@decorationManager.onDidRemoveDecoration(callback)
# Extended: Calls your `callback` when the placeholder text is changed.
#
@ -480,9 +539,6 @@ class TextEditor extends Model
onDidChangePlaceholderText: (callback) ->
@emitter.on 'did-change-placeholder-text', callback
onDidChangeCharacterWidths: (callback) ->
@displayBuffer.onDidChangeCharacterWidths(callback)
onDidChangeFirstVisibleScreenRow: (callback, fromView) ->
@emitter.on 'did-change-first-visible-screen-row', callback
@ -497,17 +553,14 @@ class TextEditor extends Model
@viewRegistry.getView(this).onDidChangeScrollLeft(callback)
onDidRequestAutoscroll: (callback) ->
@displayBuffer.onDidRequestAutoscroll(callback)
@emitter.on 'did-request-autoscroll', callback
# TODO Remove once the tabs package no longer uses .on subscriptions
onDidChangeIcon: (callback) ->
@emitter.on 'did-change-icon', callback
onDidUpdateMarkers: (callback) ->
@displayBuffer.onDidUpdateMarkers(callback)
onDidUpdateDecorations: (callback) ->
@displayBuffer.onDidUpdateDecorations(callback)
@decorationManager.onDidUpdateDecorations(callback)
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
@ -517,31 +570,31 @@ class TextEditor extends Model
# Create an {TextEditor} with its initial state based on this object
copy: ->
displayBuffer = @displayBuffer.copy()
selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
selectionsMarkerLayer = @getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
softTabs = @getSoftTabs()
newEditor = new TextEditor({
@buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
@buffer, selectionsMarkerLayer, @tabLength, softTabs,
suppressCursorCreation: true, @config, @notificationManager, @packageManager,
@firstVisibleScreenRow, @firstVisibleScreenColumn,
@clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
@clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate,
displayLayer: @buffer.copyDisplayLayer(@displayLayer.id)
})
newEditor
# Controls visibility based on the given {Boolean}.
setVisible: (visible) -> @displayBuffer.setVisible(visible)
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
setMini: (mini) ->
if mini isnt @mini
@mini = mini
@displayBuffer.setIgnoreInvisibles(@mini)
@setIgnoreInvisibles(@mini)
@emitter.emit 'did-change-mini', @mini
@mini
isMini: -> @mini
setUpdatedSynchronously: (updatedSynchronously) ->
@displayBuffer.setUpdatedSynchronously(updatedSynchronously)
@decorationManager.setUpdatedSynchronously(updatedSynchronously)
onDidChangeMini: (callback) ->
@emitter.on 'did-change-mini', callback
@ -594,12 +647,19 @@ class TextEditor extends Model
# * `editorWidthInChars` A {Number} representing the width of the
# {TextEditorElement} in characters.
setEditorWidthInChars: (editorWidthInChars) ->
@displayBuffer.setEditorWidthInChars(editorWidthInChars)
if editorWidthInChars > 0
previousWidthInChars = @editorWidthInChars
@editorWidthInChars = editorWidthInChars
if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped()
@resetDisplayLayer()
# Returns the editor width in characters.
getEditorWidthInChars: ->
@displayBuffer.getEditorWidthInChars()
width = @getWidth()
if width? and @defaultCharWidth > 0
Math.max(0, Math.floor(width / @defaultCharWidth))
else
@editorWidthInChars
###
Section: File Details
@ -788,16 +848,21 @@ class TextEditor extends Model
return if screenRow < 0 or screenRow > @getLastScreenRow()
@displayLayer.getScreenLines(screenRow, screenRow + 1)[0]
bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row
bufferRowForScreenRow: (screenRow) ->
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
# {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
for screenRow in [startScreenRow..endScreenRow]
@bufferRowForScreenRow(screenRow)
screenRowForBufferRow: (row) -> @displayLayer.translateBufferPosition(Point(row, 0)).row
screenRowForBufferRow: (row) ->
if @largeFileMode
bufferRow
else
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition()
# {Delegates to: DisplayBuffer.getMaxLineLength}
getMaxScreenLineLength: -> @getRightmostScreenPosition().column
getLongestScreenRow: -> @getRightmostScreenPosition().row
@ -1342,7 +1407,10 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
screenPositionForBufferPosition: (bufferPosition, options) -> @displayLayer.translateBufferPosition(bufferPosition, options)
screenPositionForBufferPosition: (bufferPosition, options) ->
throw new Error("This TextEditor has been destroyed") if @isDestroyed()
@displayLayer.translateBufferPosition(bufferPosition, options)
# Essential: Convert a position in screen-coordinates to buffer-coordinates.
#
@ -1352,21 +1420,30 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
bufferPositionForScreenPosition: (screenPosition, options) -> @displayLayer.translateScreenPosition(screenPosition, options)
bufferPositionForScreenPosition: (screenPosition, options) ->
@displayLayer.translateScreenPosition(screenPosition, options)
# Essential: Convert a range in buffer-coordinates to screen-coordinates.
#
# * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates.
#
# Returns a {Range}.
screenRangeForBufferRange: (bufferRange) -> @displayLayer.translateBufferRange(bufferRange)
screenRangeForBufferRange: (bufferRange, options) ->
bufferRange = Range.fromObject(bufferRange)
start = @screenPositionForBufferPosition(bufferRange.start, options)
end = @screenPositionForBufferPosition(bufferRange.end, options)
new Range(start, end)
# Essential: Convert a range in screen-coordinates to buffer-coordinates.
#
# * `screenRange` {Range} in screen coordinates to translate into buffer coordinates.
#
# Returns a {Range}.
bufferRangeForScreenRange: (screenRange) -> @displayLayer.translateScreenRange(screenRange)
bufferRangeForScreenRange: (screenRange) ->
screenRange = Range.fromObject(screenRange)
start = @bufferPositionForScreenPosition(screenRange.start)
end = @bufferPositionForScreenPosition(screenRange.end)
new Range(start, end)
# Extended: Clip the given {Point} to a valid position in the buffer.
#
@ -1510,7 +1587,7 @@ class TextEditor extends Model
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
@displayBuffer.decorateMarker(marker, decorationParams)
@decorationManager.decorateMarker(marker, decorationParams)
# Essential: *Experimental:* Add a decoration to every marker in the given
# marker layer. Can be used to decorate a large number of markers without
@ -1524,7 +1601,7 @@ class TextEditor extends Model
#
# Returns a {LayerDecoration}.
decorateMarkerLayer: (markerLayer, decorationParams) ->
@displayBuffer.decorateMarkerLayer(markerLayer, decorationParams)
@decorationManager.decorateMarkerLayer(markerLayer, decorationParams)
# Deprecated: Get all the decorations within a screen row range on the default
# layer.
@ -1538,10 +1615,10 @@ class TextEditor extends Model
# params objects attached to the marker.
# Returns an empty object when no decorations are found
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
@displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow)
@decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow)
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
@displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
@decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
# Extended: Get all decorations.
#
@ -1550,7 +1627,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getDecorations: (propertyFilter) ->
@displayBuffer.getDecorations(propertyFilter)
@decorationManager.getDecorations(propertyFilter)
# Extended: Get all decorations of type 'line'.
#
@ -1559,7 +1636,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineDecorations: (propertyFilter) ->
@displayBuffer.getLineDecorations(propertyFilter)
@decorationManager.getLineDecorations(propertyFilter)
# Extended: Get all decorations of type 'line-number'.
#
@ -1568,7 +1645,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineNumberDecorations: (propertyFilter) ->
@displayBuffer.getLineNumberDecorations(propertyFilter)
@decorationManager.getLineNumberDecorations(propertyFilter)
# Extended: Get all decorations of type 'highlight'.
#
@ -1577,7 +1654,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getHighlightDecorations: (propertyFilter) ->
@displayBuffer.getHighlightDecorations(propertyFilter)
@decorationManager.getHighlightDecorations(propertyFilter)
# Extended: Get all decorations of type 'overlay'.
#
@ -1586,13 +1663,13 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getOverlayDecorations: (propertyFilter) ->
@displayBuffer.getOverlayDecorations(propertyFilter)
@decorationManager.getOverlayDecorations(propertyFilter)
decorationForId: (id) ->
@displayBuffer.decorationForId(id)
@decorationManager.decorationForId(id)
decorationsForMarkerId: (id) ->
@displayBuffer.decorationsForMarkerId(id)
@decorationManager.decorationsForMarkerId(id)
###
Section: Markers
@ -1630,8 +1707,8 @@ class TextEditor extends Model
# start or start at the marker's end. This is the most fragile strategy.
#
# Returns a {DisplayMarker}.
markBufferRange: (args...) ->
@displayBuffer.markBufferRange(args...)
markBufferRange: (bufferRange, options) ->
@defaultMarkerLayer.markBufferRange(bufferRange, options)
# Essential: Create a marker on the default marker layer with the given range
# in screen coordinates. This marker will maintain its logical location as the
@ -1665,8 +1742,8 @@ class TextEditor extends Model
# start or start at the marker's end. This is the most fragile strategy.
#
# Returns a {DisplayMarker}.
markScreenRange: (args...) ->
@displayBuffer.markScreenRange(args...)
markScreenRange: (screenRange, options) ->
@defaultMarkerLayer.markScreenRange(screenRange, options)
# Essential: Mark the given position in buffer coordinates on the default
# marker layer.
@ -1675,8 +1752,8 @@ class TextEditor extends Model
# * `options` (optional) See {TextBuffer::markRange}.
#
# Returns a {DisplayMarker}.
markBufferPosition: (args...) ->
@displayBuffer.markBufferPosition(args...)
markBufferPosition: (bufferPosition, options) ->
@defaultMarkerLayer.markBufferPosition(bufferPosition, options)
# Essential: Mark the given position in screen coordinates on the default
# marker layer.
@ -1685,8 +1762,8 @@ class TextEditor extends Model
# * `options` (optional) See {TextBuffer::markRange}.
#
# Returns a {DisplayMarker}.
markScreenPosition: (args...) ->
@displayBuffer.markScreenPosition(args...)
markScreenPosition: (screenPosition, options) ->
@defaultMarkerLayer.markScreenPosition(screenPosition, options)
# Essential: Find all {DisplayMarker}s on the default marker layer that
# match the given properties.
@ -1708,20 +1785,22 @@ class TextEditor extends Model
# in range-compatible {Array} in buffer coordinates.
# * `containsBufferPosition` Only include markers containing this {Point}
# or {Array} of `[row, column]` in buffer coordinates.
findMarkers: (properties) ->
@displayBuffer.findMarkers(properties)
#
# Returns an {Array} of {DisplayMarker}s
findMarkers: (params) ->
@defaultMarkerLayer.findMarkers(params)
# Extended: Get the {DisplayMarker} on the default layer for the given
# marker id.
#
# * `id` {Number} id of the marker
getMarker: (id) ->
@displayBuffer.getMarker(id)
@defaultMarkerLayer.getMarker(id)
# Extended: Get all {DisplayMarker}s on the default marker layer. Consider
# using {::findMarkers}
getMarkers: ->
@displayBuffer.getMarkers()
@defaultMarkerLayer.getMarkers()
# Extended: Get the number of markers in the default marker layer.
#
@ -1742,7 +1821,7 @@ class TextEditor extends Model
#
# Returns a {DisplayMarkerLayer}.
addMarkerLayer: (options) ->
@displayBuffer.addMarkerLayer(options)
@displayLayer.addMarkerLayer(options)
# Public: *Experimental:* Get a {DisplayMarkerLayer} by id.
#
@ -1753,7 +1832,7 @@ class TextEditor extends Model
# Returns a {MarkerLayer} or `undefined` if no layer exists with the given
# id.
getMarkerLayer: (id) ->
@displayBuffer.getMarkerLayer(id)
@displayLayer.getMarkerLayer(id)
# Public: *Experimental:* Get the default {DisplayMarkerLayer}.
#
@ -1764,7 +1843,7 @@ class TextEditor extends Model
#
# Returns a {DisplayMarkerLayer}.
getDefaultMarkerLayer: ->
@displayBuffer.getDefaultMarkerLayer()
@defaultMarkerLayer
###
Section: Cursors
@ -2576,14 +2655,36 @@ class TextEditor extends Model
# Essential: Get the on-screen length of tab characters.
#
# Returns a {Number}.
getTabLength: -> @displayBuffer.getTabLength()
getTabLength: ->
if @tabLength?
@tabLength
else
@config.get('editor.tabLength', scope: @getRootScopeDescriptor())
# Essential: Set the on-screen length of tab characters. Setting this to a
# {Number} This will override the `editor.tabLength` setting.
#
# * `tabLength` {Number} length of a single tab. Setting to `null` will
# fallback to using the `editor.tabLength` config setting
setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
setTabLength: (tabLength) ->
return if tabLength is @tabLength
@tabLength = tabLength
@tokenizedBuffer.setTabLength(@tabLength)
@resetDisplayLayer()
setIgnoreInvisibles: (ignoreInvisibles) ->
return if ignoreInvisibles is @ignoreInvisibles
@ignoreInvisibles = ignoreInvisibles
@resetDisplayLayer()
getInvisibles: ->
scopeDescriptor = @getRootScopeDescriptor()
if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles and @showInvisibles
@config.get('editor.invisibles', scope: scopeDescriptor)
else
{}
# Extended: Determine if the buffer uses hard or soft tabs.
#
@ -2594,7 +2695,7 @@ class TextEditor extends Model
# whitespace.
usesSoftTabs: ->
for bufferRow in [0..@buffer.getLastRow()]
continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
continue if @tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
line = @buffer.lineForRow(bufferRow)
return true if line[0] is ' '
@ -2637,14 +2738,27 @@ class TextEditor extends Model
# Essential: Determine whether lines in this editor are soft-wrapped.
#
# Returns a {Boolean}.
isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped()
isSoftWrapped: ->
if @largeFileMode
false
else
scopeDescriptor = @getRootScopeDescriptor()
@softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false
# Essential: Enable or disable soft wrapping for this editor.
#
# * `softWrapped` A {Boolean}
#
# Returns a {Boolean}.
setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped)
setSoftWrapped: (softWrapped) ->
if softWrapped isnt @softWrapped
@softWrapped = softWrapped
@resetDisplayLayer()
softWrapped = @isSoftWrapped()
@emitter.emit 'did-change-soft-wrapped', softWrapped
softWrapped
else
@isSoftWrapped()
# Essential: Toggle soft wrapping for this editor
#
@ -2652,7 +2766,15 @@ class TextEditor extends Model
toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
# Essential: Gets the column at which column will soft wrap
getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
getSoftWrapColumn: ->
scopeDescriptor = @getRootScopeDescriptor()
if @isSoftWrapped()
if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
@config.get('editor.preferredLineLength', scope: scopeDescriptor)
else
@getEditorWidthInChars()
else
Infinity
###
Section: Indentation
@ -2710,7 +2832,7 @@ class TextEditor extends Model
#
# Returns a {Number}.
indentLevelForLine: (line) ->
@displayBuffer.indentLevelForLine(line)
@tokenizedBuffer.indentLevelForLine(line)
# Extended: Indent rows intersecting selections based on the grammar's suggested
# indent level.
@ -2738,7 +2860,7 @@ class TextEditor extends Model
# Essential: Get the current {Grammar} of this editor.
getGrammar: ->
@displayBuffer.getGrammar()
@tokenizedBuffer.grammar
# Essential: Set the current {Grammar} of this editor.
#
@ -2747,11 +2869,11 @@ class TextEditor extends Model
#
# * `grammar` {Grammar}
setGrammar: (grammar) ->
@displayBuffer.setGrammar(grammar)
@tokenizedBuffer.setGrammar(grammar)
# Reload the grammar based on the file name.
reloadGrammar: ->
@displayBuffer.reloadGrammar()
@tokenizedBuffer.reloadGrammar()
###
Section: Managing Syntax Scopes
@ -2761,7 +2883,7 @@ class TextEditor extends Model
# e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
# {Config::get} to get language specific config values.
getRootScopeDescriptor: ->
@displayBuffer.getRootScopeDescriptor()
@tokenizedBuffer.rootScopeDescriptor
# Essential: Get the syntactic scopeDescriptor for the given position in buffer
# coordinates. Useful with {Config::get}.
@ -2774,7 +2896,7 @@ class TextEditor extends Model
#
# Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition: (bufferPosition) ->
@displayBuffer.scopeDescriptorForBufferPosition(bufferPosition)
@tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
# Extended: Get the range in buffer coordinates of all tokens surrounding the
# cursor that match the given scope selector.
@ -2786,7 +2908,7 @@ class TextEditor extends Model
#
# Returns a {Range}.
bufferRangeForScopeAtCursor: (scopeSelector) ->
@displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
@tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
# Extended: Determine if the given row is entirely a comment
isBufferRowCommented: (bufferRow) ->
@ -2802,8 +2924,8 @@ class TextEditor extends Model
@notificationManager.addInfo(content, dismissable: true)
# {Delegates to: DisplayBuffer.tokenForBufferPosition}
tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
tokenForBufferPosition: (bufferPosition) ->
@tokenizedBuffer.tokenForPosition(bufferPosition)
###
Section: Clipboard Operations
@ -2935,7 +3057,7 @@ class TextEditor extends Model
#
# * `bufferRow` A {Number}
unfoldBufferRow: (bufferRow) ->
@displayBuffer.unfoldBufferRow(bufferRow)
@displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity)))
# Extended: For each selection, fold the rows it intersects.
foldSelectedLines: ->
@ -2965,7 +3087,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtBufferRow: (bufferRow) ->
@displayBuffer.isFoldableAtBufferRow(bufferRow)
@tokenizedBuffer.isFoldableAtRow(bufferRow)
# Extended: Determine whether the given row in screen coordinates is foldable.
#
@ -2997,7 +3119,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtBufferRow: (bufferRow) ->
@displayBuffer.isFoldedAtBufferRow(bufferRow)
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
# Extended: Determine whether the given row in screen coordinates is folded.
#
@ -3005,10 +3127,16 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtScreenRow: (screenRow) ->
@displayBuffer.isFoldedAtScreenRow(screenRow)
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
# 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}.
foldBufferRowRange: (startRow, endRow) ->
@displayBuffer.foldBufferRowRange(startRow, endRow)
@foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
foldBufferRange: (range) ->
@displayLayer.foldBufferRange(range)
@ -3066,7 +3194,7 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToBufferPosition: (bufferPosition, options) ->
@displayBuffer.scrollToBufferPosition(bufferPosition, options)
@scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options)
# Essential: Scrolls the editor to the given screen position.
#
@ -3075,7 +3203,7 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToScreenPosition: (screenPosition, options) ->
@displayBuffer.scrollToScreenPosition(screenPosition, options)
@scrollToScreenRange(new Range(screenPosition, screenPosition), options)
scrollToTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
@ -3087,7 +3215,9 @@ class TextEditor extends Model
@viewRegistry.getView(this).scrollToBottom()
scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options)
scrollToScreenRange: (screenRange, options = {}) ->
scrollEvent = {screenRange, options}
@emitter.emit "did-request-autoscroll", scrollEvent
getHorizontalScrollbarHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.")
@ -3173,48 +3303,69 @@ class TextEditor extends Model
getSelectionMarkerAttributes: ->
{type: 'selection', invalidate: 'never'}
getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
getVerticalScrollMargin: ->
maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
Math.min(@verticalScrollMargin, maxScrollMargin)
getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2))
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth()
getLineHeightInPixels: -> @lineHeightInPixels
setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth()
getKoreanCharWidth: -> @koreanCharWidth
getHalfWidthCharWidth: -> @halfWidthCharWidth
getDoubleWidthCharWidth: -> @doubleWidthCharWidth
getDefaultCharWidth: -> @defaultCharWidth
getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth()
ratioForCharacter: (character) ->
if isKoreanCharacter(character)
@getKoreanCharWidth() / @getDefaultCharWidth()
else if isHalfWidthCharacter(character)
@getHalfWidthCharWidth() / @getDefaultCharWidth()
else if isDoubleWidthCharacter(character)
@getDoubleWidthCharWidth() / @getDefaultCharWidth()
else
1
getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
@displayBuffer.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
@resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()?
defaultCharWidth
setHeight: (height, reentrant=false) ->
if reentrant
@displayBuffer.setHeight(height)
@height = height
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
@viewRegistry.getView(this).setHeight(height)
getHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
@displayBuffer.getHeight()
getClientHeight: -> @displayBuffer.getClientHeight()
@height
setWidth: (width, reentrant=false) ->
if reentrant
@displayBuffer.setWidth(width)
oldWidth = @width
@width = width
@resetDisplayLayer() if width isnt oldWidth and @isSoftWrapped()
@width
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
@viewRegistry.getView(this).setWidth(width)
getWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
@displayBuffer.getWidth()
@width
# Experimental: Scroll the editor such that the given screen row is at the
# top of the visible area.
@ -3222,10 +3373,8 @@ class TextEditor extends Model
unless fromView
maxScreenRow = @getScreenLineCount() - 1
unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
maxScreenRow -= Math.floor(height / lineHeightInPixels)
if @height? and @lineHeightInPixels?
maxScreenRow -= Math.floor(@height / @lineHeightInPixels)
screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0)
unless screenRow is @firstVisibleScreenRow
@ -3235,10 +3384,8 @@ class TextEditor extends Model
getFirstVisibleScreenRow: -> @firstVisibleScreenRow
getLastVisibleScreenRow: ->
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1)
if @height? and @lineHeightInPixels?
Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1)
else
null
@ -3333,8 +3480,6 @@ class TextEditor extends Model
inspect: ->
"<TextEditor #{@id}>"
logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
emitWillInsertTextEvent: (text) ->
result = true
cancel = -> result = false