mirror of
https://github.com/atom/atom.git
synced 2026-01-23 13:58:08 -05:00
4600 lines
170 KiB
JavaScript
4600 lines
170 KiB
JavaScript
const _ = require('underscore-plus')
|
|
const path = require('path')
|
|
const fs = require('fs-plus')
|
|
const Grim = require('grim')
|
|
const dedent = require('dedent')
|
|
const {CompositeDisposable, Disposable, Emitter} = require('event-kit')
|
|
const TextBuffer = require('text-buffer')
|
|
const {Point, Range} = TextBuffer
|
|
const DecorationManager = require('./decoration-manager')
|
|
const Cursor = require('./cursor')
|
|
const Selection = require('./selection')
|
|
const NullGrammar = require('./null-grammar')
|
|
|
|
const TextMateScopeSelector = require('first-mate').ScopeSelector
|
|
const GutterContainer = require('./gutter-container')
|
|
let TextEditorComponent = null
|
|
let TextEditorElement = null
|
|
const {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require('./text-utils')
|
|
|
|
const SERIALIZATION_VERSION = 1
|
|
const NON_WHITESPACE_REGEXP = /\S/
|
|
const ZERO_WIDTH_NBSP = '\ufeff'
|
|
let nextId = 0
|
|
|
|
// Essential: This class represents all essential editing state for a single
|
|
// {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
|
|
// If you're manipulating the state of an editor, use this class.
|
|
//
|
|
// A single {TextBuffer} can belong to multiple editors. For example, if the
|
|
// same file is open in two different panes, Atom creates a separate editor for
|
|
// each pane. If the buffer is manipulated the changes are reflected in both
|
|
// editors, but each maintains its own cursor position, folded lines, etc.
|
|
//
|
|
// ## Accessing TextEditor Instances
|
|
//
|
|
// The easiest way to get hold of `TextEditor` objects is by registering a callback
|
|
// with `::observeTextEditors` on the `atom.workspace` global. Your callback will
|
|
// then be called with all current editor instances and also when any editor is
|
|
// created in the future.
|
|
//
|
|
// ```coffee
|
|
// atom.workspace.observeTextEditors (editor) ->
|
|
// editor.insertText('Hello World')
|
|
// ```
|
|
//
|
|
// ## Buffer vs. Screen Coordinates
|
|
//
|
|
// Because editors support folds and soft-wrapping, the lines on screen don't
|
|
// always match the lines in the buffer. For example, a long line that soft wraps
|
|
// twice renders as three lines on screen, but only represents one line in the
|
|
// buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds
|
|
// to row 11 in the buffer.
|
|
//
|
|
// Your choice of coordinates systems will depend on what you're trying to
|
|
// achieve. For example, if you're writing a command that jumps the cursor up or
|
|
// down by 10 lines, you'll want to use screen coordinates because the user
|
|
// probably wants to skip lines *on screen*. However, if you're writing a package
|
|
// that jumps between method definitions, you'll want to work in buffer
|
|
// coordinates.
|
|
//
|
|
// **When in doubt, just default to buffer coordinates**, then experiment with
|
|
// soft wraps and folds to ensure your code interacts with them correctly.
|
|
module.exports =
|
|
class TextEditor {
|
|
static setClipboard (clipboard) {
|
|
this.clipboard = clipboard
|
|
}
|
|
|
|
static setScheduler (scheduler) {
|
|
if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') }
|
|
return TextEditorComponent.setScheduler(scheduler)
|
|
}
|
|
|
|
static didUpdateStyles () {
|
|
if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') }
|
|
return TextEditorComponent.didUpdateStyles()
|
|
}
|
|
|
|
static didUpdateScrollbarStyles () {
|
|
if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') }
|
|
return TextEditorComponent.didUpdateScrollbarStyles()
|
|
}
|
|
|
|
static viewForItem (item) { return item.element || item }
|
|
|
|
static deserialize (state, atomEnvironment) {
|
|
if (state.version !== SERIALIZATION_VERSION) return null
|
|
|
|
let bufferId = state.tokenizedBuffer
|
|
? state.tokenizedBuffer.bufferId
|
|
: state.bufferId
|
|
|
|
try {
|
|
state.buffer = atomEnvironment.project.bufferForIdSync(bufferId)
|
|
if (!state.buffer) return null
|
|
} catch (error) {
|
|
if (error.syscall === 'read') {
|
|
return // Error reading the file, don't deserialize an editor for it
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
state.assert = atomEnvironment.assert.bind(atomEnvironment)
|
|
const editor = new TextEditor(state)
|
|
if (state.registered) {
|
|
const disposable = atomEnvironment.textEditors.add(editor)
|
|
editor.onDidDestroy(() => disposable.dispose())
|
|
}
|
|
return editor
|
|
}
|
|
|
|
constructor (params = {}) {
|
|
if (this.constructor.clipboard == null) {
|
|
throw new Error('Must call TextEditor.setClipboard at least once before creating TextEditor instances')
|
|
}
|
|
|
|
this.id = params.id != null ? params.id : nextId++
|
|
this.initialScrollTopRow = params.initialScrollTopRow
|
|
this.initialScrollLeftColumn = params.initialScrollLeftColumn
|
|
this.decorationManager = params.decorationManager
|
|
this.selectionsMarkerLayer = params.selectionsMarkerLayer
|
|
this.mini = (params.mini != null) ? params.mini : false
|
|
this.placeholderText = params.placeholderText
|
|
this.showLineNumbers = params.showLineNumbers
|
|
this.largeFileMode = params.largeFileMode
|
|
this.assert = params.assert || (condition => condition)
|
|
this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true
|
|
this.autoHeight = params.autoHeight
|
|
this.autoWidth = params.autoWidth
|
|
this.scrollPastEnd = (params.scrollPastEnd != null) ? params.scrollPastEnd : false
|
|
this.scrollSensitivity = (params.scrollSensitivity != null) ? params.scrollSensitivity : 40
|
|
this.editorWidthInChars = params.editorWidthInChars
|
|
this.invisibles = params.invisibles
|
|
this.showIndentGuide = params.showIndentGuide
|
|
this.softWrapped = params.softWrapped
|
|
this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength
|
|
this.preferredLineLength = params.preferredLineLength
|
|
this.showCursorOnSelection = (params.showCursorOnSelection != null) ? params.showCursorOnSelection : true
|
|
this.maxScreenLineLength = params.maxScreenLineLength
|
|
this.softTabs = (params.softTabs != null) ? params.softTabs : true
|
|
this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true
|
|
this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true
|
|
this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300
|
|
this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
|
|
this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false
|
|
this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false
|
|
this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80
|
|
this.maxScreenLineLength = (params.maxScreenLineLength != null) ? params.maxScreenLineLength : 500
|
|
this.showLineNumbers = (params.showLineNumbers != null) ? params.showLineNumbers : true
|
|
const {tabLength = 2} = params
|
|
|
|
this.alive = true
|
|
this.doBackgroundWork = this.doBackgroundWork.bind(this)
|
|
this.serializationVersion = 1
|
|
this.suppressSelectionMerging = false
|
|
this.selectionFlashDuration = 500
|
|
this.gutterContainer = null
|
|
this.verticalScrollMargin = 2
|
|
this.horizontalScrollMargin = 6
|
|
this.lineHeightInPixels = null
|
|
this.defaultCharWidth = null
|
|
this.height = null
|
|
this.width = null
|
|
this.registered = false
|
|
this.atomicSoftTabs = true
|
|
this.emitter = new Emitter()
|
|
this.disposables = new CompositeDisposable()
|
|
this.cursors = []
|
|
this.cursorsByMarkerId = new Map()
|
|
this.selections = []
|
|
this.hasTerminatedPendingState = false
|
|
|
|
this.buffer = params.buffer || new TextBuffer({
|
|
shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') }
|
|
})
|
|
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
if (languageMode && languageMode.setTabLength) languageMode.setTabLength(tabLength)
|
|
|
|
if (params.displayLayer) {
|
|
this.displayLayer = params.displayLayer
|
|
} else {
|
|
const displayLayerParams = {
|
|
invisibles: this.getInvisibles(),
|
|
softWrapColumn: this.getSoftWrapColumn(),
|
|
showIndentGuides: this.doesShowIndentGuide(),
|
|
atomicSoftTabs: params.atomicSoftTabs != null ? params.atomicSoftTabs : true,
|
|
tabLength,
|
|
ratioForCharacter: this.ratioForCharacter.bind(this),
|
|
isWrapBoundary,
|
|
foldCharacter: ZERO_WIDTH_NBSP,
|
|
softWrapHangingIndent: params.softWrapHangingIndentLength != null ? params.softWrapHangingIndentLength : 0
|
|
}
|
|
|
|
this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId)
|
|
if (this.displayLayer) {
|
|
this.displayLayer.reset(displayLayerParams)
|
|
this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer(params.selectionsMarkerLayerId)
|
|
} else {
|
|
this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams)
|
|
}
|
|
}
|
|
|
|
this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork)
|
|
this.disposables.add(new Disposable(() => {
|
|
if (this.backgroundWorkHandle != null) return cancelIdleCallback(this.backgroundWorkHandle)
|
|
}))
|
|
|
|
this.defaultMarkerLayer = this.displayLayer.addMarkerLayer()
|
|
if (!this.selectionsMarkerLayer) {
|
|
this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true})
|
|
}
|
|
|
|
this.decorationManager = new DecorationManager(this)
|
|
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'})
|
|
if (!this.isMini()) this.decorateCursorLine()
|
|
|
|
this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
|
|
|
|
for (let marker of this.selectionsMarkerLayer.getMarkers()) {
|
|
this.addSelection(marker)
|
|
}
|
|
|
|
this.subscribeToBuffer()
|
|
this.subscribeToDisplayLayer()
|
|
|
|
if (this.cursors.length === 0 && !params.suppressCursorCreation) {
|
|
const initialLine = Math.max(parseInt(params.initialLine) || 0, 0)
|
|
const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0)
|
|
this.addCursorAtBufferPosition([initialLine, initialColumn])
|
|
}
|
|
|
|
this.gutterContainer = new GutterContainer(this)
|
|
this.lineNumberGutter = this.gutterContainer.addGutter({
|
|
name: 'line-number',
|
|
priority: 0,
|
|
visible: params.lineNumberGutterVisible
|
|
})
|
|
}
|
|
|
|
get element () {
|
|
return this.getElement()
|
|
}
|
|
|
|
get editorElement () {
|
|
Grim.deprecate(dedent`\
|
|
\`TextEditor.prototype.editorElement\` has always been private, but now
|
|
it is gone. Reading the \`editorElement\` property still returns a
|
|
reference to the editor element but this field will be removed in a
|
|
later version of Atom, so we recommend using the \`element\` property instead.\
|
|
`)
|
|
|
|
return this.getElement()
|
|
}
|
|
|
|
get displayBuffer () {
|
|
Grim.deprecate(dedent`\
|
|
\`TextEditor.prototype.displayBuffer\` has always been private, but now
|
|
it is gone. Reading the \`displayBuffer\` property now returns a reference
|
|
to the containing \`TextEditor\`, which now provides *some* of the API of
|
|
the defunct \`DisplayBuffer\` class.\
|
|
`)
|
|
return this
|
|
}
|
|
|
|
get languageMode () { return this.buffer.getLanguageMode() }
|
|
get tokenizedBuffer () { return this.buffer.getLanguageMode() }
|
|
|
|
get rowsPerPage () {
|
|
return this.getRowsPerPage()
|
|
}
|
|
|
|
decorateCursorLine () {
|
|
this.cursorLineDecorations = [
|
|
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line', class: 'cursor-line', onlyEmpty: true}),
|
|
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line'}),
|
|
this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true})
|
|
]
|
|
}
|
|
|
|
doBackgroundWork (deadline) {
|
|
const previousLongestRow = this.getApproximateLongestScreenRow()
|
|
if (this.displayLayer.doBackgroundWork(deadline)) {
|
|
this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork)
|
|
} else {
|
|
this.backgroundWorkHandle = null
|
|
}
|
|
|
|
if (this.component && this.getApproximateLongestScreenRow() !== previousLongestRow) {
|
|
this.component.scheduleUpdate()
|
|
}
|
|
}
|
|
|
|
update (params) {
|
|
const displayLayerParams = {}
|
|
|
|
for (let param of Object.keys(params)) {
|
|
const value = params[param]
|
|
|
|
switch (param) {
|
|
case 'autoIndent':
|
|
this.autoIndent = value
|
|
break
|
|
|
|
case 'autoIndentOnPaste':
|
|
this.autoIndentOnPaste = value
|
|
break
|
|
|
|
case 'undoGroupingInterval':
|
|
this.undoGroupingInterval = value
|
|
break
|
|
|
|
case 'nonWordCharacters':
|
|
this.nonWordCharacters = value
|
|
break
|
|
|
|
case 'scrollSensitivity':
|
|
this.scrollSensitivity = value
|
|
break
|
|
|
|
case 'encoding':
|
|
this.buffer.setEncoding(value)
|
|
break
|
|
|
|
case 'softTabs':
|
|
if (value !== this.softTabs) {
|
|
this.softTabs = value
|
|
}
|
|
break
|
|
|
|
case 'atomicSoftTabs':
|
|
if (value !== this.displayLayer.atomicSoftTabs) {
|
|
displayLayerParams.atomicSoftTabs = value
|
|
}
|
|
break
|
|
|
|
case 'tabLength':
|
|
if (value > 0 && value !== this.displayLayer.tabLength) {
|
|
this.buffer.getLanguageMode().setTabLength(value)
|
|
displayLayerParams.tabLength = value
|
|
}
|
|
break
|
|
|
|
case 'softWrapped':
|
|
if (value !== this.softWrapped) {
|
|
this.softWrapped = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped())
|
|
}
|
|
break
|
|
|
|
case 'softWrapHangingIndentLength':
|
|
if (value !== this.displayLayer.softWrapHangingIndent) {
|
|
displayLayerParams.softWrapHangingIndent = value
|
|
}
|
|
break
|
|
|
|
case 'softWrapAtPreferredLineLength':
|
|
if (value !== this.softWrapAtPreferredLineLength) {
|
|
this.softWrapAtPreferredLineLength = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
}
|
|
break
|
|
|
|
case 'preferredLineLength':
|
|
if (value !== this.preferredLineLength) {
|
|
this.preferredLineLength = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
}
|
|
break
|
|
|
|
case 'maxScreenLineLength':
|
|
if (value !== this.maxScreenLineLength) {
|
|
this.maxScreenLineLength = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
}
|
|
break
|
|
|
|
case 'mini':
|
|
if (value !== this.mini) {
|
|
this.mini = value
|
|
this.emitter.emit('did-change-mini', value)
|
|
displayLayerParams.invisibles = this.getInvisibles()
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
displayLayerParams.showIndentGuides = this.doesShowIndentGuide()
|
|
if (this.mini) {
|
|
for (let decoration of this.cursorLineDecorations) { decoration.destroy() }
|
|
this.cursorLineDecorations = null
|
|
} else {
|
|
this.decorateCursorLine()
|
|
}
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate()
|
|
}
|
|
}
|
|
break
|
|
|
|
case 'placeholderText':
|
|
if (value !== this.placeholderText) {
|
|
this.placeholderText = value
|
|
this.emitter.emit('did-change-placeholder-text', value)
|
|
}
|
|
break
|
|
|
|
case 'lineNumberGutterVisible':
|
|
if (value !== this.lineNumberGutterVisible) {
|
|
if (value) {
|
|
this.lineNumberGutter.show()
|
|
} else {
|
|
this.lineNumberGutter.hide()
|
|
}
|
|
this.emitter.emit('did-change-line-number-gutter-visible', this.lineNumberGutter.isVisible())
|
|
}
|
|
break
|
|
|
|
case 'showIndentGuide':
|
|
if (value !== this.showIndentGuide) {
|
|
this.showIndentGuide = value
|
|
displayLayerParams.showIndentGuides = this.doesShowIndentGuide()
|
|
}
|
|
break
|
|
|
|
case 'showLineNumbers':
|
|
if (value !== this.showLineNumbers) {
|
|
this.showLineNumbers = value
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate()
|
|
}
|
|
}
|
|
break
|
|
|
|
case 'showInvisibles':
|
|
if (value !== this.showInvisibles) {
|
|
this.showInvisibles = value
|
|
displayLayerParams.invisibles = this.getInvisibles()
|
|
}
|
|
break
|
|
|
|
case 'invisibles':
|
|
if (!_.isEqual(value, this.invisibles)) {
|
|
this.invisibles = value
|
|
displayLayerParams.invisibles = this.getInvisibles()
|
|
}
|
|
break
|
|
|
|
case 'editorWidthInChars':
|
|
if (value > 0 && value !== this.editorWidthInChars) {
|
|
this.editorWidthInChars = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
}
|
|
break
|
|
|
|
case 'width':
|
|
if (value !== this.width) {
|
|
this.width = value
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn()
|
|
}
|
|
break
|
|
|
|
case 'scrollPastEnd':
|
|
if (value !== this.scrollPastEnd) {
|
|
this.scrollPastEnd = value
|
|
if (this.component) this.component.scheduleUpdate()
|
|
}
|
|
break
|
|
|
|
case 'autoHeight':
|
|
if (value !== this.autoHeight) {
|
|
this.autoHeight = value
|
|
}
|
|
break
|
|
|
|
case 'autoWidth':
|
|
if (value !== this.autoWidth) {
|
|
this.autoWidth = value
|
|
}
|
|
break
|
|
|
|
case 'showCursorOnSelection':
|
|
if (value !== this.showCursorOnSelection) {
|
|
this.showCursorOnSelection = value
|
|
if (this.component) this.component.scheduleUpdate()
|
|
}
|
|
break
|
|
|
|
default:
|
|
if (param !== 'ref' && param !== 'key') {
|
|
throw new TypeError(`Invalid TextEditor parameter: '${param}'`)
|
|
}
|
|
}
|
|
}
|
|
|
|
this.displayLayer.reset(displayLayerParams)
|
|
|
|
if (this.component) {
|
|
return this.component.getNextUpdatePromise()
|
|
} else {
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
|
|
scheduleComponentUpdate () {
|
|
if (this.component) this.component.scheduleUpdate()
|
|
}
|
|
|
|
serialize () {
|
|
return {
|
|
deserializer: 'TextEditor',
|
|
version: SERIALIZATION_VERSION,
|
|
|
|
displayLayerId: this.displayLayer.id,
|
|
selectionsMarkerLayerId: this.selectionsMarkerLayer.id,
|
|
|
|
initialScrollTopRow: this.getScrollTopRow(),
|
|
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
|
|
|
tabLength: this.displayLayer.tabLength,
|
|
atomicSoftTabs: this.displayLayer.atomicSoftTabs,
|
|
softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent,
|
|
|
|
id: this.id,
|
|
bufferId: this.buffer.id,
|
|
softTabs: this.softTabs,
|
|
softWrapped: this.softWrapped,
|
|
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
|
|
preferredLineLength: this.preferredLineLength,
|
|
mini: this.mini,
|
|
editorWidthInChars: this.editorWidthInChars,
|
|
width: this.width,
|
|
largeFileMode: this.largeFileMode,
|
|
maxScreenLineLength: this.maxScreenLineLength,
|
|
registered: this.registered,
|
|
invisibles: this.invisibles,
|
|
showInvisibles: this.showInvisibles,
|
|
showIndentGuide: this.showIndentGuide,
|
|
autoHeight: this.autoHeight,
|
|
autoWidth: this.autoWidth
|
|
}
|
|
}
|
|
|
|
subscribeToBuffer () {
|
|
this.buffer.retain()
|
|
this.disposables.add(this.buffer.onDidChangeLanguageMode(this.handleLanguageModeChange.bind(this)))
|
|
this.disposables.add(this.buffer.onDidChangePath(() => {
|
|
this.emitter.emit('did-change-title', this.getTitle())
|
|
this.emitter.emit('did-change-path', this.getPath())
|
|
}))
|
|
this.disposables.add(this.buffer.onDidChangeEncoding(() => {
|
|
this.emitter.emit('did-change-encoding', this.getEncoding())
|
|
}))
|
|
this.disposables.add(this.buffer.onDidDestroy(() => this.destroy()))
|
|
this.disposables.add(this.buffer.onDidChangeModified(() => {
|
|
if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState()
|
|
}))
|
|
}
|
|
|
|
terminatePendingState () {
|
|
if (!this.hasTerminatedPendingState) this.emitter.emit('did-terminate-pending-state')
|
|
this.hasTerminatedPendingState = true
|
|
}
|
|
|
|
onDidTerminatePendingState (callback) {
|
|
return this.emitter.on('did-terminate-pending-state', callback)
|
|
}
|
|
|
|
subscribeToDisplayLayer () {
|
|
this.disposables.add(this.displayLayer.onDidChange(changes => {
|
|
this.mergeIntersectingSelections()
|
|
if (this.component) this.component.didChangeDisplayLayer(changes)
|
|
this.emitter.emit('did-change', changes.map(change => new ChangeEvent(change)))
|
|
}))
|
|
this.disposables.add(this.displayLayer.onDidReset(() => {
|
|
this.mergeIntersectingSelections()
|
|
if (this.component) this.component.didResetDisplayLayer()
|
|
this.emitter.emit('did-change', {})
|
|
}))
|
|
this.disposables.add(this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this)))
|
|
return this.disposables.add(this.selectionsMarkerLayer.onDidUpdate(() => (this.component != null ? this.component.didUpdateSelections() : undefined)))
|
|
}
|
|
|
|
destroy () {
|
|
if (!this.alive) return
|
|
this.alive = false
|
|
this.disposables.dispose()
|
|
this.displayLayer.destroy()
|
|
for (let selection of this.selections.slice()) {
|
|
selection.destroy()
|
|
}
|
|
this.buffer.release()
|
|
this.gutterContainer.destroy()
|
|
this.emitter.emit('did-destroy')
|
|
this.emitter.clear()
|
|
if (this.component) this.component.element.component = null
|
|
this.component = null
|
|
this.lineNumberGutter.element = null
|
|
}
|
|
|
|
isAlive () { return this.alive }
|
|
|
|
isDestroyed () { return !this.alive }
|
|
|
|
/*
|
|
Section: Event Subscription
|
|
*/
|
|
|
|
// Essential: Calls your `callback` when the buffer's title has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeTitle (callback) {
|
|
return this.emitter.on('did-change-title', callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when the buffer's path, and therefore title, has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePath (callback) {
|
|
return this.emitter.on('did-change-path', callback)
|
|
}
|
|
|
|
// Essential: Invoke the given callback synchronously when the content of the
|
|
// buffer changes.
|
|
//
|
|
// Because observers are invoked synchronously, it's important not to perform
|
|
// any expensive operations via this method. Consider {::onDidStopChanging} to
|
|
// delay expensive operations until after changes stop occurring.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChange (callback) {
|
|
return this.emitter.on('did-change', callback)
|
|
}
|
|
|
|
// Essential: Invoke `callback` when the buffer's contents change. It is
|
|
// emit asynchronously 300ms after the last buffer change. This is a good place
|
|
// to handle changes to the buffer without compromising typing performance.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidStopChanging (callback) {
|
|
return this.getBuffer().onDidStopChanging(callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a {Cursor} is moved. If there are
|
|
// multiple cursors, your callback will be called for each cursor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` {Object}
|
|
// * `oldBufferPosition` {Point}
|
|
// * `oldScreenPosition` {Point}
|
|
// * `newBufferPosition` {Point}
|
|
// * `newScreenPosition` {Point}
|
|
// * `textChanged` {Boolean}
|
|
// * `cursor` {Cursor} that triggered the event
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeCursorPosition (callback) {
|
|
return this.emitter.on('did-change-cursor-position', callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a selection's screen range changes.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` {Object}
|
|
// * `oldBufferRange` {Range}
|
|
// * `oldScreenRange` {Range}
|
|
// * `newBufferRange` {Range}
|
|
// * `newScreenRange` {Range}
|
|
// * `selection` {Selection} that triggered the event
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeSelectionRange (callback) {
|
|
return this.emitter.on('did-change-selection-range', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when soft wrap was enabled or disabled.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeSoftWrapped (callback) {
|
|
return this.emitter.on('did-change-soft-wrapped', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the buffer's encoding has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeEncoding (callback) {
|
|
return this.emitter.on('did-change-encoding', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the grammar that interprets and
|
|
// colorizes the text has been changed. Immediately calls your callback with
|
|
// the current grammar.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `grammar` {Grammar}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeGrammar (callback) {
|
|
callback(this.getGrammar())
|
|
return this.onDidChangeGrammar(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the grammar that interprets and
|
|
// colorizes the text has been changed.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `grammar` {Grammar}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeGrammar (callback) {
|
|
return this.buffer.onDidChangeLanguageMode(() => {
|
|
callback(this.buffer.getLanguageMode().getGrammar())
|
|
})
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the result of {::isModified} changes.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeModified (callback) {
|
|
return this.getBuffer().onDidChangeModified(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the buffer's underlying file changes on
|
|
// disk at a moment when the result of {::isModified} is true.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidConflict (callback) {
|
|
return this.getBuffer().onDidConflict(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` before text has been inserted.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` event {Object}
|
|
// * `text` {String} text to be inserted
|
|
// * `cancel` {Function} Call to prevent the text from being inserted
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onWillInsertText (callback) {
|
|
return this.emitter.on('will-insert-text', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` after text has been inserted.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` event {Object}
|
|
// * `text` {String} text to be inserted
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidInsertText (callback) {
|
|
return this.emitter.on('did-insert-text', callback)
|
|
}
|
|
|
|
// Essential: Invoke the given callback after the buffer is saved to disk.
|
|
//
|
|
// * `callback` {Function} to be called after the buffer is saved.
|
|
// * `event` {Object} with the following keys:
|
|
// * `path` The path to which the buffer was saved.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidSave (callback) {
|
|
return this.getBuffer().onDidSave(callback)
|
|
}
|
|
|
|
// Essential: Invoke the given callback when the editor is destroyed.
|
|
//
|
|
// * `callback` {Function} to be called when the editor is destroyed.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidDestroy (callback) {
|
|
return this.emitter.once('did-destroy', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is added to the editor.
|
|
// Immediately calls your callback for each existing cursor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeCursors (callback) {
|
|
this.getCursors().forEach(callback)
|
|
return this.onDidAddCursor(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddCursor (callback) {
|
|
return this.emitter.on('did-add-cursor', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveCursor (callback) {
|
|
return this.emitter.on('did-remove-cursor', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is added to the editor.
|
|
// Immediately calls your callback for each existing selection.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeSelections (callback) {
|
|
this.getSelections().forEach(callback)
|
|
return this.onDidAddSelection(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddSelection (callback) {
|
|
return this.emitter.on('did-add-selection', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveSelection (callback) {
|
|
return this.emitter.on('did-remove-selection', callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` with each {Decoration} added to the editor.
|
|
// Calls your `callback` immediately for any existing decorations.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeDecorations (callback) {
|
|
return this.decorationManager.observeDecorations(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Decoration} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddDecoration (callback) {
|
|
return this.decorationManager.onDidAddDecoration(callback)
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Decoration} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveDecoration (callback) {
|
|
return this.decorationManager.onDidRemoveDecoration(callback)
|
|
}
|
|
|
|
// Called by DecorationManager when a decoration is added.
|
|
didAddDecoration (decoration) {
|
|
if (this.component && decoration.isType('block')) {
|
|
this.component.addBlockDecoration(decoration)
|
|
}
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the placeholder text is changed.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `placeholderText` {String} new text
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePlaceholderText (callback) {
|
|
return this.emitter.on('did-change-placeholder-text', callback)
|
|
}
|
|
|
|
onDidChangeScrollTop (callback) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.')
|
|
return this.getElement().onDidChangeScrollTop(callback)
|
|
}
|
|
|
|
onDidChangeScrollLeft (callback) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.')
|
|
return this.getElement().onDidChangeScrollLeft(callback)
|
|
}
|
|
|
|
onDidRequestAutoscroll (callback) {
|
|
return this.emitter.on('did-request-autoscroll', callback)
|
|
}
|
|
|
|
// TODO Remove once the tabs package no longer uses .on subscriptions
|
|
onDidChangeIcon (callback) {
|
|
return this.emitter.on('did-change-icon', callback)
|
|
}
|
|
|
|
onDidUpdateDecorations (callback) {
|
|
return this.decorationManager.onDidUpdateDecorations(callback)
|
|
}
|
|
|
|
// Essential: Retrieves the current {TextBuffer}.
|
|
getBuffer () { return this.buffer }
|
|
|
|
// Retrieves the current buffer's URI.
|
|
getURI () { return this.buffer.getUri() }
|
|
|
|
// Create an {TextEditor} with its initial state based on this object
|
|
copy () {
|
|
const displayLayer = this.displayLayer.copy()
|
|
const selectionsMarkerLayer = displayLayer.getMarkerLayer(this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id)
|
|
const softTabs = this.getSoftTabs()
|
|
return new TextEditor({
|
|
buffer: this.buffer,
|
|
selectionsMarkerLayer,
|
|
softTabs,
|
|
suppressCursorCreation: true,
|
|
tabLength: this.getTabLength(),
|
|
initialScrollTopRow: this.getScrollTopRow(),
|
|
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
|
assert: this.assert,
|
|
displayLayer,
|
|
grammar: this.getGrammar(),
|
|
autoWidth: this.autoWidth,
|
|
autoHeight: this.autoHeight,
|
|
showCursorOnSelection: this.showCursorOnSelection
|
|
})
|
|
}
|
|
|
|
// Controls visibility based on the given {Boolean}.
|
|
setVisible (visible) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
languageMode.setVisible && languageMode.setVisible(visible)
|
|
}
|
|
|
|
setMini (mini) {
|
|
this.update({mini})
|
|
}
|
|
|
|
isMini () { return this.mini }
|
|
|
|
onDidChangeMini (callback) {
|
|
return this.emitter.on('did-change-mini', callback)
|
|
}
|
|
|
|
setLineNumberGutterVisible (lineNumberGutterVisible) { this.update({lineNumberGutterVisible}) }
|
|
|
|
isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() }
|
|
|
|
onDidChangeLineNumberGutterVisible (callback) {
|
|
return this.emitter.on('did-change-line-number-gutter-visible', callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a {Gutter} is added to the editor.
|
|
// Immediately calls your callback for each existing gutter.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `gutter` {Gutter} that currently exists/was added.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeGutters (callback) {
|
|
return this.gutterContainer.observeGutters(callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a {Gutter} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `gutter` {Gutter} that was added.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddGutter (callback) {
|
|
return this.gutterContainer.onDidAddGutter(callback)
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a {Gutter} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `name` The name of the {Gutter} that was removed.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveGutter (callback) {
|
|
return this.gutterContainer.onDidRemoveGutter(callback)
|
|
}
|
|
|
|
// Set the number of characters that can be displayed horizontally in the
|
|
// editor.
|
|
//
|
|
// * `editorWidthInChars` A {Number} representing the width of the
|
|
// {TextEditorElement} in characters.
|
|
setEditorWidthInChars (editorWidthInChars) { this.update({editorWidthInChars}) }
|
|
|
|
// Returns the editor width in characters.
|
|
getEditorWidthInChars () {
|
|
if (this.width != null && this.defaultCharWidth > 0) {
|
|
return Math.max(0, Math.floor(this.width / this.defaultCharWidth))
|
|
} else {
|
|
return this.editorWidthInChars
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: File Details
|
|
*/
|
|
|
|
// Essential: Get the editor's title for display in other parts of the
|
|
// UI such as the tabs.
|
|
//
|
|
// If the editor's buffer is saved, its title is the file name. If it is
|
|
// unsaved, its title is "untitled".
|
|
//
|
|
// Returns a {String}.
|
|
getTitle () {
|
|
return this.getFileName() || 'untitled'
|
|
}
|
|
|
|
// Essential: Get unique title for display in other parts of the UI, such as
|
|
// the window title.
|
|
//
|
|
// If the editor's buffer is unsaved, its title is "untitled"
|
|
// If the editor's buffer is saved, its unique title is formatted as one
|
|
// of the following,
|
|
// * "<filename>" when it is the only editing buffer with this file name.
|
|
// * "<filename> — <unique-dir-prefix>" when other buffers have this file name.
|
|
//
|
|
// Returns a {String}
|
|
getLongTitle () {
|
|
if (this.getPath()) {
|
|
const fileName = this.getFileName()
|
|
|
|
let myPathSegments
|
|
const openEditorPathSegmentsWithSameFilename = []
|
|
for (const textEditor of atom.workspace.getTextEditors()) {
|
|
if (textEditor.getFileName() === fileName) {
|
|
const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep)
|
|
openEditorPathSegmentsWithSameFilename.push(pathSegments)
|
|
if (textEditor === this) myPathSegments = pathSegments
|
|
}
|
|
}
|
|
|
|
if (!myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1) return fileName
|
|
|
|
let commonPathSegmentCount
|
|
for (let i = 0, {length} = myPathSegments; i < length; i++) {
|
|
const myPathSegment = myPathSegments[i]
|
|
if (openEditorPathSegmentsWithSameFilename.some(segments => (segments.length === i + 1) || (segments[i] !== myPathSegment))) {
|
|
commonPathSegmentCount = i
|
|
break
|
|
}
|
|
}
|
|
|
|
return `${fileName} \u2014 ${path.join(...myPathSegments.slice(commonPathSegmentCount))}`
|
|
} else {
|
|
return 'untitled'
|
|
}
|
|
}
|
|
|
|
// Essential: Returns the {String} path of this editor's text buffer.
|
|
getPath () {
|
|
return this.buffer.getPath()
|
|
}
|
|
|
|
getFileName () {
|
|
const fullPath = this.getPath()
|
|
if (fullPath) return path.basename(fullPath)
|
|
}
|
|
|
|
getDirectoryPath () {
|
|
const fullPath = this.getPath()
|
|
if (fullPath) return path.dirname(fullPath)
|
|
}
|
|
|
|
// Extended: Returns the {String} character set encoding of this editor's text
|
|
// buffer.
|
|
getEncoding () { return this.buffer.getEncoding() }
|
|
|
|
// Extended: Set the character set encoding to use in this editor's text
|
|
// buffer.
|
|
//
|
|
// * `encoding` The {String} character set encoding name such as 'utf8'
|
|
setEncoding (encoding) { this.buffer.setEncoding(encoding) }
|
|
|
|
// Essential: Returns {Boolean} `true` if this editor has been modified.
|
|
isModified () { return this.buffer.isModified() }
|
|
|
|
// Essential: Returns {Boolean} `true` if this editor has no content.
|
|
isEmpty () { return this.buffer.isEmpty() }
|
|
|
|
/*
|
|
Section: File Operations
|
|
*/
|
|
|
|
// Essential: Saves the editor's text buffer.
|
|
//
|
|
// See {TextBuffer::save} for more details.
|
|
save () { return this.buffer.save() }
|
|
|
|
// Essential: Saves the editor's text buffer as the given path.
|
|
//
|
|
// See {TextBuffer::saveAs} for more details.
|
|
//
|
|
// * `filePath` A {String} path.
|
|
saveAs (filePath) { return this.buffer.saveAs(filePath) }
|
|
|
|
// Determine whether the user should be prompted to save before closing
|
|
// this editor.
|
|
shouldPromptToSave ({windowCloseRequested, projectHasPaths} = {}) {
|
|
if (windowCloseRequested && projectHasPaths && atom.stateStore.isConnected()) {
|
|
return this.buffer.isInConflict()
|
|
} else {
|
|
return this.isModified() && !this.buffer.hasMultipleEditors()
|
|
}
|
|
}
|
|
|
|
// Returns an {Object} to configure dialog shown when this editor is saved
|
|
// via {Pane::saveItemAs}.
|
|
getSaveDialogOptions () { return {} }
|
|
|
|
/*
|
|
Section: Reading Text
|
|
*/
|
|
|
|
// Essential: Returns a {String} representing the entire contents of the editor.
|
|
getText () { return this.buffer.getText() }
|
|
|
|
// Essential: Get the text in the given {Range} in buffer coordinates.
|
|
//
|
|
// * `range` A {Range} or range-compatible {Array}.
|
|
//
|
|
// Returns a {String}.
|
|
getTextInBufferRange (range) {
|
|
return this.buffer.getTextInRange(range)
|
|
}
|
|
|
|
// Essential: Returns a {Number} representing the number of lines in the buffer.
|
|
getLineCount () { return this.buffer.getLineCount() }
|
|
|
|
// Essential: Returns a {Number} representing the number of screen lines in the
|
|
// editor. This accounts for folds.
|
|
getScreenLineCount () { return this.displayLayer.getScreenLineCount() }
|
|
|
|
getApproximateScreenLineCount () { return this.displayLayer.getApproximateScreenLineCount() }
|
|
|
|
// Essential: Returns a {Number} representing the last zero-indexed buffer row
|
|
// number of the editor.
|
|
getLastBufferRow () { return this.buffer.getLastRow() }
|
|
|
|
// Essential: Returns a {Number} representing the last zero-indexed screen row
|
|
// number of the editor.
|
|
getLastScreenRow () { return this.getScreenLineCount() - 1 }
|
|
|
|
// Essential: Returns a {String} representing the contents of the line at the
|
|
// given buffer row.
|
|
//
|
|
// * `bufferRow` A {Number} representing a zero-indexed buffer row.
|
|
lineTextForBufferRow (bufferRow) { return this.buffer.lineForRow(bufferRow) }
|
|
|
|
// Essential: Returns a {String} representing the contents of the line at the
|
|
// given screen row.
|
|
//
|
|
// * `screenRow` A {Number} representing a zero-indexed screen row.
|
|
lineTextForScreenRow (screenRow) {
|
|
const screenLine = this.screenLineForScreenRow(screenRow)
|
|
if (screenLine) return screenLine.lineText
|
|
}
|
|
|
|
logScreenLines (start = 0, end = this.getLastScreenRow()) {
|
|
for (let row = start; row <= end; row++) {
|
|
const line = this.lineTextForScreenRow(row)
|
|
console.log(row, this.bufferRowForScreenRow(row), line, line.length)
|
|
}
|
|
}
|
|
|
|
tokensForScreenRow (screenRow) {
|
|
const tokens = []
|
|
let lineTextIndex = 0
|
|
const currentTokenScopes = []
|
|
const {lineText, tags} = this.screenLineForScreenRow(screenRow)
|
|
for (const tag of tags) {
|
|
if (this.displayLayer.isOpenTag(tag)) {
|
|
currentTokenScopes.push(this.displayLayer.classNameForTag(tag))
|
|
} else if (this.displayLayer.isCloseTag(tag)) {
|
|
currentTokenScopes.pop()
|
|
} else {
|
|
tokens.push({
|
|
text: lineText.substr(lineTextIndex, tag),
|
|
scopes: currentTokenScopes.slice()
|
|
})
|
|
lineTextIndex += tag
|
|
}
|
|
}
|
|
return tokens
|
|
}
|
|
|
|
screenLineForScreenRow (screenRow) {
|
|
return this.displayLayer.getScreenLine(screenRow)
|
|
}
|
|
|
|
bufferRowForScreenRow (screenRow) {
|
|
return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row
|
|
}
|
|
|
|
bufferRowsForScreenRows (startScreenRow, endScreenRow) {
|
|
return this.displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1)
|
|
}
|
|
|
|
screenRowForBufferRow (row) {
|
|
return this.displayLayer.translateBufferPosition(Point(row, 0)).row
|
|
}
|
|
|
|
getRightmostScreenPosition () { return this.displayLayer.getRightmostScreenPosition() }
|
|
|
|
getApproximateRightmostScreenPosition () { return this.displayLayer.getApproximateRightmostScreenPosition() }
|
|
|
|
getMaxScreenLineLength () { return this.getRightmostScreenPosition().column }
|
|
|
|
getLongestScreenRow () { return this.getRightmostScreenPosition().row }
|
|
|
|
getApproximateLongestScreenRow () { return this.getApproximateRightmostScreenPosition().row }
|
|
|
|
lineLengthForScreenRow (screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow) }
|
|
|
|
// Returns the range for the given buffer row.
|
|
//
|
|
// * `row` A row {Number}.
|
|
// * `options` (optional) An options hash with an `includeNewline` key.
|
|
//
|
|
// Returns a {Range}.
|
|
bufferRangeForBufferRow (row, options) {
|
|
return this.buffer.rangeForRow(row, options && options.includeNewline)
|
|
}
|
|
|
|
// Get the text in the given {Range}.
|
|
//
|
|
// Returns a {String}.
|
|
getTextInRange (range) { return this.buffer.getTextInRange(range) }
|
|
|
|
// {Delegates to: TextBuffer.isRowBlank}
|
|
isBufferRowBlank (bufferRow) { return this.buffer.isRowBlank(bufferRow) }
|
|
|
|
// {Delegates to: TextBuffer.nextNonBlankRow}
|
|
nextNonBlankBufferRow (bufferRow) { return this.buffer.nextNonBlankRow(bufferRow) }
|
|
|
|
// {Delegates to: TextBuffer.getEndPosition}
|
|
getEofBufferPosition () { return this.buffer.getEndPosition() }
|
|
|
|
// Essential: Get the {Range} of the paragraph surrounding the most recently added
|
|
// cursor.
|
|
//
|
|
// Returns a {Range}.
|
|
getCurrentParagraphBufferRange () {
|
|
return this.getLastCursor().getCurrentParagraphBufferRange()
|
|
}
|
|
|
|
/*
|
|
Section: Mutating Text
|
|
*/
|
|
|
|
// Essential: Replaces the entire contents of the buffer with the given {String}.
|
|
//
|
|
// * `text` A {String} to replace with
|
|
setText (text) { return this.buffer.setText(text) }
|
|
|
|
// Essential: Set the text in the given {Range} in buffer coordinates.
|
|
//
|
|
// * `range` A {Range} or range-compatible {Array}.
|
|
// * `text` A {String}
|
|
// * `options` (optional) {Object}
|
|
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
|
|
// * `undo` (optional) {String} 'skip' will skip the undo system
|
|
//
|
|
// Returns the {Range} of the newly-inserted text.
|
|
setTextInBufferRange (range, text, options) {
|
|
return this.getBuffer().setTextInRange(range, text, options)
|
|
}
|
|
|
|
// Essential: For each selection, replace the selected text with the given text.
|
|
//
|
|
// * `text` A {String} representing the text to insert.
|
|
// * `options` (optional) See {Selection::insertText}.
|
|
//
|
|
// Returns a {Range} when the text has been inserted
|
|
// Returns a {Boolean} false when the text has not been inserted
|
|
insertText (text, options = {}) {
|
|
if (!this.emitWillInsertTextEvent(text)) return false
|
|
|
|
const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0
|
|
if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent()
|
|
if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent()
|
|
return this.mutateSelectedText(selection => {
|
|
const range = selection.insertText(text, options)
|
|
const didInsertEvent = {text, range}
|
|
this.emitter.emit('did-insert-text', didInsertEvent)
|
|
return range
|
|
}, groupingInterval)
|
|
}
|
|
|
|
// Essential: For each selection, replace the selected text with a newline.
|
|
insertNewline (options) {
|
|
return this.insertText('\n', options)
|
|
}
|
|
|
|
// Essential: For each selection, if the selection is empty, delete the character
|
|
// following the cursor. Otherwise delete the selected text.
|
|
delete () {
|
|
return this.mutateSelectedText(selection => selection.delete())
|
|
}
|
|
|
|
// Essential: For each selection, if the selection is empty, delete the character
|
|
// preceding the cursor. Otherwise delete the selected text.
|
|
backspace () {
|
|
return this.mutateSelectedText(selection => selection.backspace())
|
|
}
|
|
|
|
// Extended: Mutate the text of all the selections in a single transaction.
|
|
//
|
|
// All the changes made inside the given {Function} can be reverted with a
|
|
// single call to {::undo}.
|
|
//
|
|
// * `fn` A {Function} that will be called once for each {Selection}. The first
|
|
// argument will be a {Selection} and the second argument will be the
|
|
// {Number} index of that selection.
|
|
mutateSelectedText (fn, groupingInterval = 0) {
|
|
return this.mergeIntersectingSelections(() => {
|
|
return this.transact(groupingInterval, () => {
|
|
return this.getSelectionsOrderedByBufferPosition().map((selection, index) => fn(selection, index))
|
|
})
|
|
})
|
|
}
|
|
|
|
// Move lines intersecting the most recent selection or multiple selections
|
|
// up by one row in screen coordinates.
|
|
moveLineUp () {
|
|
const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b))
|
|
|
|
if (selections[0].start.row === 0) return
|
|
if (selections[selections.length - 1].start.row === this.getLastBufferRow() && this.buffer.getLastLine() === '') return
|
|
|
|
this.transact(() => {
|
|
const newSelectionRanges = []
|
|
|
|
while (selections.length > 0) {
|
|
// Find selections spanning a contiguous set of lines
|
|
const selection = selections.shift()
|
|
const selectionsToMove = [selection]
|
|
|
|
while (selection.end.row === (selections[0] != null ? selections[0].start.row : undefined)) {
|
|
selectionsToMove.push(selections[0])
|
|
selection.end.row = selections[0].end.row
|
|
selections.shift()
|
|
}
|
|
|
|
// Compute the buffer range spanned by all these selections, expanding it
|
|
// so that it includes any folded region that intersects them.
|
|
let startRow = selection.start.row
|
|
let endRow = selection.end.row
|
|
if (selection.end.row > selection.start.row && selection.end.column === 0) {
|
|
// Don't move the last line of a multi-line selection if the selection ends at column 0
|
|
endRow--
|
|
}
|
|
|
|
startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow)
|
|
endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)
|
|
const linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
|
|
|
|
// If selected line range is preceded by a fold, one line above on screen
|
|
// could be multiple lines in the buffer.
|
|
const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow - 1)
|
|
const insertDelta = linesRange.start.row - precedingRow
|
|
|
|
// Any folds in the text that is moved will need to be re-created.
|
|
// It includes the folds that were intersecting with the selection.
|
|
const rangesToRefold = this.displayLayer
|
|
.destroyFoldsIntersectingBufferRange(linesRange)
|
|
.map(range => range.translate([-insertDelta, 0]))
|
|
|
|
// Delete lines spanned by selection and insert them on the preceding buffer row
|
|
let lines = this.buffer.getTextInRange(linesRange)
|
|
if (lines[lines.length - 1] !== '\n') { lines += this.buffer.lineEndingForRow(linesRange.end.row - 2) }
|
|
this.buffer.delete(linesRange)
|
|
this.buffer.insert([precedingRow, 0], lines)
|
|
|
|
// Restore folds that existed before the lines were moved
|
|
for (let rangeToRefold of rangesToRefold) {
|
|
this.displayLayer.foldBufferRange(rangeToRefold)
|
|
}
|
|
|
|
for (const selectionToMove of selectionsToMove) {
|
|
newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0]))
|
|
}
|
|
}
|
|
|
|
this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true})
|
|
if (this.shouldAutoIndent()) this.autoIndentSelectedRows()
|
|
this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0])
|
|
})
|
|
}
|
|
|
|
// Move lines intersecting the most recent selection or multiple selections
|
|
// down by one row in screen coordinates.
|
|
moveLineDown () {
|
|
const selections = this.getSelectedBufferRanges()
|
|
selections.sort((a, b) => b.compare(a))
|
|
|
|
this.transact(() => {
|
|
this.consolidateSelections()
|
|
const newSelectionRanges = []
|
|
|
|
while (selections.length > 0) {
|
|
// Find selections spanning a contiguous set of lines
|
|
const selection = selections.shift()
|
|
const selectionsToMove = [selection]
|
|
|
|
// if the current selection start row matches the next selections' end row - make them one selection
|
|
while (selection.start.row === (selections[0] != null ? selections[0].end.row : undefined)) {
|
|
selectionsToMove.push(selections[0])
|
|
selection.start.row = selections[0].start.row
|
|
selections.shift()
|
|
}
|
|
|
|
// Compute the buffer range spanned by all these selections, expanding it
|
|
// so that it includes any folded region that intersects them.
|
|
let startRow = selection.start.row
|
|
let endRow = selection.end.row
|
|
if (selection.end.row > selection.start.row && selection.end.column === 0) {
|
|
// Don't move the last line of a multi-line selection if the selection ends at column 0
|
|
endRow--
|
|
}
|
|
|
|
startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow)
|
|
endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)
|
|
const linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
|
|
|
|
// If selected line range is followed by a fold, one line below on screen
|
|
// could be multiple lines in the buffer. But at the same time, if the
|
|
// next buffer row is wrapped, one line in the buffer can represent many
|
|
// screen rows.
|
|
const followingRow = Math.min(this.buffer.getLineCount(), this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1))
|
|
const insertDelta = followingRow - linesRange.end.row
|
|
|
|
// Any folds in the text that is moved will need to be re-created.
|
|
// It includes the folds that were intersecting with the selection.
|
|
const rangesToRefold = this.displayLayer
|
|
.destroyFoldsIntersectingBufferRange(linesRange)
|
|
.map(range => range.translate([insertDelta, 0]))
|
|
|
|
// Delete lines spanned by selection and insert them on the following correct buffer row
|
|
let lines = this.buffer.getTextInRange(linesRange)
|
|
if (followingRow - 1 === this.buffer.getLastRow()) {
|
|
lines = `\n${lines}`
|
|
}
|
|
|
|
this.buffer.insert([followingRow, 0], lines)
|
|
this.buffer.delete(linesRange)
|
|
|
|
// Restore folds that existed before the lines were moved
|
|
for (let rangeToRefold of rangesToRefold) {
|
|
this.displayLayer.foldBufferRange(rangeToRefold)
|
|
}
|
|
|
|
for (const selectionToMove of selectionsToMove) {
|
|
newSelectionRanges.push(selectionToMove.translate([insertDelta, 0]))
|
|
}
|
|
}
|
|
|
|
this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true})
|
|
if (this.shouldAutoIndent()) this.autoIndentSelectedRows()
|
|
this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0])
|
|
})
|
|
}
|
|
|
|
// Move any active selections one column to the left.
|
|
moveSelectionLeft () {
|
|
const selections = this.getSelectedBufferRanges()
|
|
const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0)
|
|
|
|
const translationDelta = [0, -1]
|
|
const translatedRanges = []
|
|
|
|
if (noSelectionAtStartOfLine) {
|
|
this.transact(() => {
|
|
for (let selection of selections) {
|
|
const charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start)
|
|
const charTextToLeftOfSelection = this.buffer.getTextInRange(charToLeftOfSelection)
|
|
|
|
this.buffer.insert(selection.end, charTextToLeftOfSelection)
|
|
this.buffer.delete(charToLeftOfSelection)
|
|
translatedRanges.push(selection.translate(translationDelta))
|
|
}
|
|
|
|
this.setSelectedBufferRanges(translatedRanges)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Move any active selections one column to the right.
|
|
moveSelectionRight () {
|
|
const selections = this.getSelectedBufferRanges()
|
|
const noSelectionAtEndOfLine = selections.every(selection => {
|
|
return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row)
|
|
})
|
|
|
|
const translationDelta = [0, 1]
|
|
const translatedRanges = []
|
|
|
|
if (noSelectionAtEndOfLine) {
|
|
this.transact(() => {
|
|
for (let selection of selections) {
|
|
const charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta))
|
|
const charTextToRightOfSelection = this.buffer.getTextInRange(charToRightOfSelection)
|
|
|
|
this.buffer.delete(charToRightOfSelection)
|
|
this.buffer.insert(selection.start, charTextToRightOfSelection)
|
|
translatedRanges.push(selection.translate(translationDelta))
|
|
}
|
|
|
|
this.setSelectedBufferRanges(translatedRanges)
|
|
})
|
|
}
|
|
}
|
|
|
|
duplicateLines () {
|
|
this.transact(() => {
|
|
const selections = this.getSelectionsOrderedByBufferPosition()
|
|
const previousSelectionRanges = []
|
|
|
|
let i = selections.length - 1
|
|
while (i >= 0) {
|
|
const j = i
|
|
previousSelectionRanges[i] = selections[i].getBufferRange()
|
|
if (selections[i].isEmpty()) {
|
|
const {start} = selections[i].getScreenRange()
|
|
selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], {preserveFolds: true})
|
|
}
|
|
let [startRow, endRow] = selections[i].getBufferRowRange()
|
|
endRow++
|
|
while (i > 0) {
|
|
const [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange()
|
|
if (previousSelectionEndRow === startRow) {
|
|
startRow = previousSelectionStartRow
|
|
previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange()
|
|
i--
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]])
|
|
let textToDuplicate = this.getTextInBufferRange([[startRow, 0], [endRow, 0]])
|
|
if (endRow > this.getLastBufferRow()) textToDuplicate = `\n${textToDuplicate}`
|
|
this.buffer.insert([endRow, 0], textToDuplicate)
|
|
|
|
const insertedRowCount = endRow - startRow
|
|
|
|
for (let k = i; k <= j; k++) {
|
|
selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0]))
|
|
}
|
|
|
|
for (const fold of intersectingFolds) {
|
|
const foldRange = this.displayLayer.bufferRangeForFold(fold)
|
|
this.displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0]))
|
|
}
|
|
|
|
i--
|
|
}
|
|
})
|
|
}
|
|
|
|
replaceSelectedText (options, fn) {
|
|
this.mutateSelectedText((selection) => {
|
|
selection.getBufferRange()
|
|
if (options && options.selectWordIfEmpty && selection.isEmpty()) {
|
|
selection.selectWord()
|
|
}
|
|
const text = selection.getText()
|
|
selection.deleteSelectedText()
|
|
const range = selection.insertText(fn(text))
|
|
selection.setBufferRange(range)
|
|
})
|
|
}
|
|
|
|
// Split multi-line selections into one selection per line.
|
|
//
|
|
// Operates on all selections. This method breaks apart all multi-line
|
|
// selections to create multiple single-line selections that cumulatively cover
|
|
// the same original area.
|
|
splitSelectionsIntoLines () {
|
|
this.mergeIntersectingSelections(() => {
|
|
for (const selection of this.getSelections()) {
|
|
const range = selection.getBufferRange()
|
|
if (range.isSingleLine()) continue
|
|
|
|
const {start, end} = range
|
|
this.addSelectionForBufferRange([start, [start.row, Infinity]])
|
|
let {row} = start
|
|
while (++row < end.row) {
|
|
this.addSelectionForBufferRange([[row, 0], [row, Infinity]])
|
|
}
|
|
if (end.column !== 0) this.addSelectionForBufferRange([[end.row, 0], [end.row, end.column]])
|
|
selection.destroy()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Extended: For each selection, transpose the selected text.
|
|
//
|
|
// If the selection is empty, the characters preceding and following the cursor
|
|
// are swapped. Otherwise, the selected characters are reversed.
|
|
transpose () {
|
|
this.mutateSelectedText(selection => {
|
|
if (selection.isEmpty()) {
|
|
selection.selectRight()
|
|
const text = selection.getText()
|
|
selection.delete()
|
|
selection.cursor.moveLeft()
|
|
selection.insertText(text)
|
|
} else {
|
|
selection.insertText(selection.getText().split('').reverse().join(''))
|
|
}
|
|
})
|
|
}
|
|
|
|
// Extended: Convert the selected text to upper case.
|
|
//
|
|
// For each selection, if the selection is empty, converts the containing word
|
|
// to upper case. Otherwise convert the selected text to upper case.
|
|
upperCase () {
|
|
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase())
|
|
}
|
|
|
|
// Extended: Convert the selected text to lower case.
|
|
//
|
|
// For each selection, if the selection is empty, converts the containing word
|
|
// to upper case. Otherwise convert the selected text to upper case.
|
|
lowerCase () {
|
|
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase())
|
|
}
|
|
|
|
// Extended: Toggle line comments for rows intersecting selections.
|
|
//
|
|
// If the current grammar doesn't support comments, does nothing.
|
|
toggleLineCommentsInSelection () {
|
|
this.mutateSelectedText(selection => selection.toggleLineComments())
|
|
}
|
|
|
|
// Convert multiple lines to a single line.
|
|
//
|
|
// Operates on all selections. If the selection is empty, joins the current
|
|
// line with the next line. Otherwise it joins all lines that intersect the
|
|
// selection.
|
|
//
|
|
// Joining a line means that multiple lines are converted to a single line with
|
|
// the contents of each of the original non-empty lines separated by a space.
|
|
joinLines () {
|
|
this.mutateSelectedText(selection => selection.joinLines())
|
|
}
|
|
|
|
// Extended: For each cursor, insert a newline at beginning the following line.
|
|
insertNewlineBelow () {
|
|
this.transact(() => {
|
|
this.moveToEndOfLine()
|
|
this.insertNewline()
|
|
})
|
|
}
|
|
|
|
// Extended: For each cursor, insert a newline at the end of the preceding line.
|
|
insertNewlineAbove () {
|
|
this.transact(() => {
|
|
const bufferRow = this.getCursorBufferPosition().row
|
|
const indentLevel = this.indentationForBufferRow(bufferRow)
|
|
const onFirstLine = bufferRow === 0
|
|
|
|
this.moveToBeginningOfLine()
|
|
this.moveLeft()
|
|
this.insertNewline()
|
|
|
|
if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) {
|
|
this.setIndentationForBufferRow(bufferRow, indentLevel)
|
|
}
|
|
|
|
if (onFirstLine) {
|
|
this.moveUp()
|
|
this.moveToEndOfLine()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is empty, delete all characters
|
|
// of the containing word that precede the cursor. Otherwise delete the
|
|
// selected text.
|
|
deleteToBeginningOfWord () {
|
|
this.mutateSelectedText(selection => selection.deleteToBeginningOfWord())
|
|
}
|
|
|
|
// Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
|
|
// previous word boundary.
|
|
deleteToPreviousWordBoundary () {
|
|
this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary())
|
|
}
|
|
|
|
// Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
|
|
// next word boundary.
|
|
deleteToNextWordBoundary () {
|
|
this.mutateSelectedText(selection => selection.deleteToNextWordBoundary())
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is empty, delete all characters
|
|
// of the containing subword following the cursor. Otherwise delete the selected
|
|
// text.
|
|
deleteToBeginningOfSubword () {
|
|
this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword())
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is empty, delete all characters
|
|
// of the containing subword following the cursor. Otherwise delete the selected
|
|
// text.
|
|
deleteToEndOfSubword () {
|
|
this.mutateSelectedText(selection => selection.deleteToEndOfSubword())
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is empty, delete all characters
|
|
// of the containing line that precede the cursor. Otherwise delete the
|
|
// selected text.
|
|
deleteToBeginningOfLine () {
|
|
this.mutateSelectedText(selection => selection.deleteToBeginningOfLine())
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is not empty, deletes the
|
|
// selection; otherwise, deletes all characters of the containing line
|
|
// following the cursor. If the cursor is already at the end of the line,
|
|
// deletes the following newline.
|
|
deleteToEndOfLine () {
|
|
this.mutateSelectedText(selection => selection.deleteToEndOfLine())
|
|
}
|
|
|
|
// Extended: For each selection, if the selection is empty, delete all characters
|
|
// of the containing word following the cursor. Otherwise delete the selected
|
|
// text.
|
|
deleteToEndOfWord () {
|
|
this.mutateSelectedText(selection => selection.deleteToEndOfWord())
|
|
}
|
|
|
|
// Extended: Delete all lines intersecting selections.
|
|
deleteLine () {
|
|
this.mergeSelectionsOnSameRows()
|
|
this.mutateSelectedText(selection => selection.deleteLine())
|
|
}
|
|
|
|
/*
|
|
Section: History
|
|
*/
|
|
|
|
// Essential: Undo the last change.
|
|
undo () {
|
|
this.avoidMergingSelections(() => this.buffer.undo())
|
|
this.getLastSelection().autoscroll()
|
|
}
|
|
|
|
// Essential: Redo the last change.
|
|
redo () {
|
|
this.avoidMergingSelections(() => this.buffer.redo())
|
|
this.getLastSelection().autoscroll()
|
|
}
|
|
|
|
// Extended: Batch multiple operations as a single undo/redo step.
|
|
//
|
|
// Any group of operations that are logically grouped from the perspective of
|
|
// undoing and redoing should be performed in a transaction. If you want to
|
|
// abort the transaction, call {::abortTransaction} to terminate the function's
|
|
// execution and revert any changes performed up to the abortion.
|
|
//
|
|
// * `groupingInterval` (optional) The {Number} of milliseconds for which this
|
|
// transaction should be considered 'groupable' after it begins. If a transaction
|
|
// with a positive `groupingInterval` is committed while the previous transaction is
|
|
// still 'groupable', the two transactions are merged with respect to undo and redo.
|
|
// * `fn` A {Function} to call inside the transaction.
|
|
transact (groupingInterval, fn) {
|
|
return this.buffer.transact(groupingInterval, fn)
|
|
}
|
|
|
|
// Extended: Abort an open transaction, undoing any operations performed so far
|
|
// within the transaction.
|
|
abortTransaction () { return this.buffer.abortTransaction() }
|
|
|
|
// Extended: Create a pointer to the current state of the buffer for use
|
|
// with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
|
|
//
|
|
// Returns a checkpoint value.
|
|
createCheckpoint () { return this.buffer.createCheckpoint() }
|
|
|
|
// Extended: Revert the buffer to the state it was in when the given
|
|
// checkpoint was created.
|
|
//
|
|
// The redo stack will be empty following this operation, so changes since the
|
|
// checkpoint will be lost. If the given checkpoint is no longer present in the
|
|
// undo history, no changes will be made to the buffer and this method will
|
|
// return `false`.
|
|
//
|
|
// * `checkpoint` The checkpoint to revert to.
|
|
//
|
|
// Returns a {Boolean} indicating whether the operation succeeded.
|
|
revertToCheckpoint (checkpoint) { return this.buffer.revertToCheckpoint(checkpoint) }
|
|
|
|
// Extended: Group all changes since the given checkpoint into a single
|
|
// transaction for purposes of undo/redo.
|
|
//
|
|
// If the given checkpoint is no longer present in the undo history, no
|
|
// grouping will be performed and this method will return `false`.
|
|
//
|
|
// * `checkpoint` The checkpoint from which to group changes.
|
|
//
|
|
// Returns a {Boolean} indicating whether the operation succeeded.
|
|
groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) }
|
|
|
|
/*
|
|
Section: TextEditor Coordinates
|
|
*/
|
|
|
|
// Essential: Convert a position in buffer-coordinates to screen-coordinates.
|
|
//
|
|
// The position is clipped via {::clipBufferPosition} prior to the conversion.
|
|
// The position is also clipped via {::clipScreenPosition} following the
|
|
// conversion, which only makes a difference when `options` are supplied.
|
|
//
|
|
// * `bufferPosition` A {Point} or {Array} of [row, column].
|
|
// * `options` (optional) An options hash for {::clipScreenPosition}.
|
|
//
|
|
// Returns a {Point}.
|
|
screenPositionForBufferPosition (bufferPosition, options) {
|
|
if (options && options.clip) {
|
|
Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.')
|
|
if (options.clipDirection) options.clipDirection = options.clip
|
|
}
|
|
if (options && options.wrapAtSoftNewlines != null) {
|
|
Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'
|
|
}
|
|
if (options && options.wrapBeyondNewlines != null) {
|
|
Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'
|
|
}
|
|
|
|
return this.displayLayer.translateBufferPosition(bufferPosition, options)
|
|
}
|
|
|
|
// Essential: Convert a position in screen-coordinates to buffer-coordinates.
|
|
//
|
|
// The position is clipped via {::clipScreenPosition} prior to the conversion.
|
|
//
|
|
// * `bufferPosition` A {Point} or {Array} of [row, column].
|
|
// * `options` (optional) An options hash for {::clipScreenPosition}.
|
|
//
|
|
// Returns a {Point}.
|
|
bufferPositionForScreenPosition (screenPosition, options) {
|
|
if (options && options.clip) {
|
|
Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.')
|
|
if (options.clipDirection) options.clipDirection = options.clip
|
|
}
|
|
if (options && options.wrapAtSoftNewlines != null) {
|
|
Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'
|
|
}
|
|
if (options && options.wrapBeyondNewlines != null) {
|
|
Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'
|
|
}
|
|
|
|
return this.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, options) {
|
|
bufferRange = Range.fromObject(bufferRange)
|
|
const start = this.screenPositionForBufferPosition(bufferRange.start, options)
|
|
const end = this.screenPositionForBufferPosition(bufferRange.end, options)
|
|
return 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) {
|
|
screenRange = Range.fromObject(screenRange)
|
|
const start = this.bufferPositionForScreenPosition(screenRange.start)
|
|
const end = this.bufferPositionForScreenPosition(screenRange.end)
|
|
return new Range(start, end)
|
|
}
|
|
|
|
// Extended: Clip the given {Point} to a valid position in the buffer.
|
|
//
|
|
// If the given {Point} describes a position that is actually reachable by the
|
|
// cursor based on the current contents of the buffer, it is returned
|
|
// unchanged. If the {Point} does not describe a valid position, the closest
|
|
// valid position is returned instead.
|
|
//
|
|
// ## Examples
|
|
//
|
|
// ```coffee
|
|
// editor.clipBufferPosition([-1, -1]) # -> `[0, 0]`
|
|
//
|
|
// # When the line at buffer row 2 is 10 characters long
|
|
// editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]`
|
|
// ```
|
|
//
|
|
// * `bufferPosition` The {Point} representing the position to clip.
|
|
//
|
|
// Returns a {Point}.
|
|
clipBufferPosition (bufferPosition) { return this.buffer.clipPosition(bufferPosition) }
|
|
|
|
// Extended: Clip the start and end of the given range to valid positions in the
|
|
// buffer. See {::clipBufferPosition} for more information.
|
|
//
|
|
// * `range` The {Range} to clip.
|
|
//
|
|
// Returns a {Range}.
|
|
clipBufferRange (range) { return this.buffer.clipRange(range) }
|
|
|
|
// Extended: Clip the given {Point} to a valid position on screen.
|
|
//
|
|
// If the given {Point} describes a position that is actually reachable by the
|
|
// cursor based on the current contents of the screen, it is returned
|
|
// unchanged. If the {Point} does not describe a valid position, the closest
|
|
// valid position is returned instead.
|
|
//
|
|
// ## Examples
|
|
//
|
|
// ```coffee
|
|
// editor.clipScreenPosition([-1, -1]) # -> `[0, 0]`
|
|
//
|
|
// # When the line at screen row 2 is 10 characters long
|
|
// editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]`
|
|
// ```
|
|
//
|
|
// * `screenPosition` The {Point} representing the position to clip.
|
|
// * `options` (optional) {Object}
|
|
// * `clipDirection` {String} If `'backward'`, returns the first valid
|
|
// position preceding an invalid position. If `'forward'`, returns the
|
|
// first valid position following an invalid position. If `'closest'`,
|
|
// returns the first valid position closest to an invalid position.
|
|
// Defaults to `'closest'`.
|
|
//
|
|
// Returns a {Point}.
|
|
clipScreenPosition (screenPosition, options) {
|
|
if (options && options.clip) {
|
|
Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.')
|
|
if (options.clipDirection) options.clipDirection = options.clip
|
|
}
|
|
if (options && options.wrapAtSoftNewlines != null) {
|
|
Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'
|
|
}
|
|
if (options && options.wrapBeyondNewlines != null) {
|
|
Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'
|
|
}
|
|
|
|
return this.displayLayer.clipScreenPosition(screenPosition, options)
|
|
}
|
|
|
|
// Extended: 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 (screenRange, options) {
|
|
screenRange = Range.fromObject(screenRange)
|
|
const start = this.displayLayer.clipScreenPosition(screenRange.start, options)
|
|
const end = this.displayLayer.clipScreenPosition(screenRange.end, options)
|
|
return Range(start, end)
|
|
}
|
|
|
|
/*
|
|
Section: Decorations
|
|
*/
|
|
|
|
// Essential: Add a decoration that tracks a {DisplayMarker}. When the
|
|
// marker moves, is invalidated, or is destroyed, the decoration will be
|
|
// updated to reflect the marker's state.
|
|
//
|
|
// The following are the supported decorations types:
|
|
//
|
|
// * __line__: Adds your CSS `class` to the line nodes within the range
|
|
// marked by the marker
|
|
// * __line-number__: Adds your CSS `class` to the line number nodes within the
|
|
// range marked by the marker
|
|
// * __highlight__: Adds a new highlight div to the editor surrounding the
|
|
// range marked by the marker. When the user selects text, the selection is
|
|
// visualized with a highlight decoration internally. The structure of this
|
|
// highlight will be
|
|
// ```html
|
|
// <div class="highlight <your-class>">
|
|
// <!-- Will be one region for each row in the range. Spans 2 lines? There will be 2 regions. -->
|
|
// <div class="region"></div>
|
|
// </div>
|
|
// ```
|
|
// * __overlay__: Positions the view associated with the given item at the head
|
|
// or tail of the given `DisplayMarker`.
|
|
// * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter
|
|
// decorations are created by calling {Gutter::decorateMarker} on the
|
|
// desired `Gutter` instance.
|
|
// * __block__: Positions the view associated with the given item before or
|
|
// after the row of the given `TextEditorMarker`.
|
|
//
|
|
// ## Arguments
|
|
//
|
|
// * `marker` A {DisplayMarker} you want this decoration to follow.
|
|
// * `decorationParams` An {Object} representing the decoration e.g.
|
|
// `{type: 'line-number', class: 'linter-error'}`
|
|
// * `type` There are several supported decoration types. The behavior of the
|
|
// types are as follows:
|
|
// * `line` Adds the given `class` to the lines overlapping the rows
|
|
// spanned by the `DisplayMarker`.
|
|
// * `line-number` Adds the given `class` to the line numbers overlapping
|
|
// the rows spanned by the `DisplayMarker`.
|
|
// * `text` Injects spans into all text overlapping the marked range,
|
|
// then adds the given `class` or `style` properties to these spans.
|
|
// Use this to manipulate the foreground color or styling of text in
|
|
// a given range.
|
|
// * `highlight` Creates an absolutely-positioned `.highlight` div
|
|
// containing nested divs to cover the marked region. For example, this
|
|
// is used to implement selections.
|
|
// * `overlay` Positions the view associated with the given item at the
|
|
// head or tail of the given `DisplayMarker`, depending on the `position`
|
|
// property.
|
|
// * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling
|
|
// {Gutter::decorateMarker} on the desired `Gutter` instance.
|
|
// * `block` Positions the view associated with the given item before or
|
|
// after the row of the given `TextEditorMarker`, depending on the `position`
|
|
// property.
|
|
// * `cursor` Renders a cursor at the head of the given marker. If multiple
|
|
// decorations are created for the same marker, their class strings and
|
|
// style objects are combined into a single cursor. You can use this
|
|
// decoration type to style existing cursors by passing in their markers
|
|
// or render artificial cursors that don't actually exist in the model
|
|
// by passing a marker that isn't actually associated with a cursor.
|
|
// * `class` This CSS class will be applied to the decorated line number,
|
|
// line, text spans, highlight regions, cursors, or overlay.
|
|
// * `style` An {Object} containing CSS style properties to apply to the
|
|
// relevant DOM node. Currently this only works with a `type` of `cursor`
|
|
// or `text`.
|
|
// * `item` (optional) An {HTMLElement} or a model {Object} with a
|
|
// corresponding view registered. Only applicable to the `gutter`,
|
|
// `overlay` and `block` decoration types.
|
|
// * `onlyHead` (optional) If `true`, the decoration will only be applied to
|
|
// the head of the `DisplayMarker`. Only applicable to the `line` and
|
|
// `line-number` decoration types.
|
|
// * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
|
|
// the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
|
|
// `line`, and `line-number` decoration types.
|
|
// * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
|
|
// if the associated `DisplayMarker` is non-empty. Only applicable to the
|
|
// `gutter`, `line`, and `line-number` decoration types.
|
|
// * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied
|
|
// to the last row of a non-empty range, even if it ends at column 0.
|
|
// Defaults to `true`. Only applicable to the `gutter`, `line`, and
|
|
// `line-number` decoration types.
|
|
// * `position` (optional) Only applicable to decorations of type `overlay` and `block`.
|
|
// Controls where the view is positioned relative to the `TextEditorMarker`.
|
|
// Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
|
|
// `'before'` (the default) or `'after'` for block decorations.
|
|
// * `avoidOverflow` (optional) Only applicable to decorations of type
|
|
// `overlay`. Determines whether the decoration adjusts its horizontal or
|
|
// vertical position to remain fully visible when it would otherwise
|
|
// overflow the editor. Defaults to `true`.
|
|
//
|
|
// Returns a {Decoration} object
|
|
decorateMarker (marker, decorationParams) {
|
|
return this.decorationManager.decorateMarker(marker, decorationParams)
|
|
}
|
|
|
|
// Essential: Add a decoration to every marker in the given marker layer. Can
|
|
// be used to decorate a large number of markers without having to create and
|
|
// manage many individual decorations.
|
|
//
|
|
// * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate.
|
|
// * `decorationParams` The same parameters that are passed to
|
|
// {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
|
|
//
|
|
// Returns a {LayerDecoration}.
|
|
decorateMarkerLayer (markerLayer, decorationParams) {
|
|
return this.decorationManager.decorateMarkerLayer(markerLayer, decorationParams)
|
|
}
|
|
|
|
// Deprecated: Get all the decorations within a screen row range on the default
|
|
// layer.
|
|
//
|
|
// * `startScreenRow` the {Number} beginning screen row
|
|
// * `endScreenRow` the {Number} end screen row (inclusive)
|
|
//
|
|
// Returns an {Object} of decorations in the form
|
|
// `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
|
|
// where the keys are {DisplayMarker} IDs, and the values are an array of decoration
|
|
// params objects attached to the marker.
|
|
// Returns an empty object when no decorations are found
|
|
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
|
|
return this.decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow)
|
|
}
|
|
|
|
decorationsStateForScreenRowRange (startScreenRow, endScreenRow) {
|
|
return this.decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
|
|
}
|
|
|
|
// Extended: Get all decorations.
|
|
//
|
|
// * `propertyFilter` (optional) An {Object} containing key value pairs that
|
|
// the returned decorations' properties must match.
|
|
//
|
|
// Returns an {Array} of {Decoration}s.
|
|
getDecorations (propertyFilter) {
|
|
return this.decorationManager.getDecorations(propertyFilter)
|
|
}
|
|
|
|
// Extended: Get all decorations of type 'line'.
|
|
//
|
|
// * `propertyFilter` (optional) An {Object} containing key value pairs that
|
|
// the returned decorations' properties must match.
|
|
//
|
|
// Returns an {Array} of {Decoration}s.
|
|
getLineDecorations (propertyFilter) {
|
|
return this.decorationManager.getLineDecorations(propertyFilter)
|
|
}
|
|
|
|
// Extended: Get all decorations of type 'line-number'.
|
|
//
|
|
// * `propertyFilter` (optional) An {Object} containing key value pairs that
|
|
// the returned decorations' properties must match.
|
|
//
|
|
// Returns an {Array} of {Decoration}s.
|
|
getLineNumberDecorations (propertyFilter) {
|
|
return this.decorationManager.getLineNumberDecorations(propertyFilter)
|
|
}
|
|
|
|
// Extended: Get all decorations of type 'highlight'.
|
|
//
|
|
// * `propertyFilter` (optional) An {Object} containing key value pairs that
|
|
// the returned decorations' properties must match.
|
|
//
|
|
// Returns an {Array} of {Decoration}s.
|
|
getHighlightDecorations (propertyFilter) {
|
|
return this.decorationManager.getHighlightDecorations(propertyFilter)
|
|
}
|
|
|
|
// Extended: Get all decorations of type 'overlay'.
|
|
//
|
|
// * `propertyFilter` (optional) An {Object} containing key value pairs that
|
|
// the returned decorations' properties must match.
|
|
//
|
|
// Returns an {Array} of {Decoration}s.
|
|
getOverlayDecorations (propertyFilter) {
|
|
return this.decorationManager.getOverlayDecorations(propertyFilter)
|
|
}
|
|
|
|
/*
|
|
Section: Markers
|
|
*/
|
|
|
|
// Essential: Create a marker on the default marker layer with the given range
|
|
// in buffer coordinates. This marker will maintain its logical location as the
|
|
// buffer is changed, so if you mark a particular word, the marker will remain
|
|
// over that word even if the word's location in the buffer changes.
|
|
//
|
|
// * `range` A {Range} or range-compatible {Array}
|
|
// * `properties` A hash of key-value pairs to associate with the marker. There
|
|
// are also reserved property names that have marker-specific meaning.
|
|
// * `maintainHistory` (optional) {Boolean} Whether to store this marker's
|
|
// range before and after each change in the undo history. This allows the
|
|
// marker's position to be restored more accurately for certain undo/redo
|
|
// operations, but uses more time and memory. (default: false)
|
|
// * `reversed` (optional) {Boolean} Creates the marker in a reversed
|
|
// orientation. (default: false)
|
|
// * `invalidate` (optional) {String} Determines the rules by which changes
|
|
// to the buffer *invalidate* the marker. (default: 'overlap') It can be
|
|
// any of the following strategies, in order of fragility:
|
|
// * __never__: The marker is never marked as invalid. This is a good choice for
|
|
// markers representing selections in an editor.
|
|
// * __surround__: The marker is invalidated by changes that completely surround it.
|
|
// * __overlap__: The marker is invalidated by changes that surround the
|
|
// start or end of the marker. This is the default.
|
|
// * __inside__: The marker is invalidated by changes that extend into the
|
|
// inside of the marker. Changes that end at the marker's start or
|
|
// start at the marker's end do not invalidate the marker.
|
|
// * __touch__: The marker is invalidated by a change that touches the marked
|
|
// region in any way, including changes that end at the marker's
|
|
// start or start at the marker's end. This is the most fragile strategy.
|
|
//
|
|
// Returns a {DisplayMarker}.
|
|
markBufferRange (bufferRange, options) {
|
|
return this.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
|
|
// buffer is changed, so if you mark a particular word, the marker will remain
|
|
// over that word even if the word's location in the buffer changes.
|
|
//
|
|
// * `range` A {Range} or range-compatible {Array}
|
|
// * `properties` A hash of key-value pairs to associate with the marker. There
|
|
// are also reserved property names that have marker-specific meaning.
|
|
// * `maintainHistory` (optional) {Boolean} Whether to store this marker's
|
|
// range before and after each change in the undo history. This allows the
|
|
// marker's position to be restored more accurately for certain undo/redo
|
|
// operations, but uses more time and memory. (default: false)
|
|
// * `reversed` (optional) {Boolean} Creates the marker in a reversed
|
|
// orientation. (default: false)
|
|
// * `invalidate` (optional) {String} Determines the rules by which changes
|
|
// to the buffer *invalidate* the marker. (default: 'overlap') It can be
|
|
// any of the following strategies, in order of fragility:
|
|
// * __never__: The marker is never marked as invalid. This is a good choice for
|
|
// markers representing selections in an editor.
|
|
// * __surround__: The marker is invalidated by changes that completely surround it.
|
|
// * __overlap__: The marker is invalidated by changes that surround the
|
|
// start or end of the marker. This is the default.
|
|
// * __inside__: The marker is invalidated by changes that extend into the
|
|
// inside of the marker. Changes that end at the marker's start or
|
|
// start at the marker's end do not invalidate the marker.
|
|
// * __touch__: The marker is invalidated by a change that touches the marked
|
|
// region in any way, including changes that end at the marker's
|
|
// start or start at the marker's end. This is the most fragile strategy.
|
|
//
|
|
// Returns a {DisplayMarker}.
|
|
markScreenRange (screenRange, options) {
|
|
return this.defaultMarkerLayer.markScreenRange(screenRange, options)
|
|
}
|
|
|
|
// Essential: Create a marker on the default marker layer with the given buffer
|
|
// position and no tail. To group multiple markers together in their own
|
|
// private layer, see {::addMarkerLayer}.
|
|
//
|
|
// * `bufferPosition` A {Point} or point-compatible {Array}
|
|
// * `options` (optional) An {Object} with the following keys:
|
|
// * `invalidate` (optional) {String} Determines the rules by which changes
|
|
// to the buffer *invalidate* the marker. (default: 'overlap') It can be
|
|
// any of the following strategies, in order of fragility:
|
|
// * __never__: The marker is never marked as invalid. This is a good choice for
|
|
// markers representing selections in an editor.
|
|
// * __surround__: The marker is invalidated by changes that completely surround it.
|
|
// * __overlap__: The marker is invalidated by changes that surround the
|
|
// start or end of the marker. This is the default.
|
|
// * __inside__: The marker is invalidated by changes that extend into the
|
|
// inside of the marker. Changes that end at the marker's start or
|
|
// start at the marker's end do not invalidate the marker.
|
|
// * __touch__: The marker is invalidated by a change that touches the marked
|
|
// region in any way, including changes that end at the marker's
|
|
// start or start at the marker's end. This is the most fragile strategy.
|
|
//
|
|
// Returns a {DisplayMarker}.
|
|
markBufferPosition (bufferPosition, options) {
|
|
return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options)
|
|
}
|
|
|
|
// Essential: Create a marker on the default marker layer with the given screen
|
|
// position and no tail. To group multiple markers together in their own
|
|
// private layer, see {::addMarkerLayer}.
|
|
//
|
|
// * `screenPosition` A {Point} or point-compatible {Array}
|
|
// * `options` (optional) An {Object} with the following keys:
|
|
// * `invalidate` (optional) {String} Determines the rules by which changes
|
|
// to the buffer *invalidate* the marker. (default: 'overlap') It can be
|
|
// any of the following strategies, in order of fragility:
|
|
// * __never__: The marker is never marked as invalid. This is a good choice for
|
|
// markers representing selections in an editor.
|
|
// * __surround__: The marker is invalidated by changes that completely surround it.
|
|
// * __overlap__: The marker is invalidated by changes that surround the
|
|
// start or end of the marker. This is the default.
|
|
// * __inside__: The marker is invalidated by changes that extend into the
|
|
// inside of the marker. Changes that end at the marker's start or
|
|
// start at the marker's end do not invalidate the marker.
|
|
// * __touch__: The marker is invalidated by a change that touches the marked
|
|
// region in any way, including changes that end at the marker's
|
|
// start or start at the marker's end. This is the most fragile strategy.
|
|
// * `clipDirection` {String} If `'backward'`, returns the first valid
|
|
// position preceding an invalid position. If `'forward'`, returns the
|
|
// first valid position following an invalid position. If `'closest'`,
|
|
// returns the first valid position closest to an invalid position.
|
|
// Defaults to `'closest'`.
|
|
//
|
|
// Returns a {DisplayMarker}.
|
|
markScreenPosition (screenPosition, options) {
|
|
return this.defaultMarkerLayer.markScreenPosition(screenPosition, options)
|
|
}
|
|
|
|
// Essential: Find all {DisplayMarker}s on the default marker layer that
|
|
// match the given properties.
|
|
//
|
|
// This method finds markers based on the given properties. Markers can be
|
|
// associated with custom properties that will be compared with basic equality.
|
|
// In addition, there are several special properties that will be compared
|
|
// with the range of the markers rather than their properties.
|
|
//
|
|
// * `properties` An {Object} containing properties that each returned marker
|
|
// must satisfy. Markers can be associated with custom properties, which are
|
|
// compared with basic equality. In addition, several reserved properties
|
|
// can be used to filter markers based on their current range:
|
|
// * `startBufferRow` Only include markers starting at this row in buffer
|
|
// coordinates.
|
|
// * `endBufferRow` Only include markers ending at this row in buffer
|
|
// coordinates.
|
|
// * `containsBufferRange` Only include markers containing this {Range} or
|
|
// in range-compatible {Array} in buffer coordinates.
|
|
// * `containsBufferPosition` Only include markers containing this {Point}
|
|
// or {Array} of `[row, column]` in buffer coordinates.
|
|
//
|
|
// Returns an {Array} of {DisplayMarker}s
|
|
findMarkers (params) {
|
|
return this.defaultMarkerLayer.findMarkers(params)
|
|
}
|
|
|
|
// Extended: Get the {DisplayMarker} on the default layer for the given
|
|
// marker id.
|
|
//
|
|
// * `id` {Number} id of the marker
|
|
getMarker (id) {
|
|
return this.defaultMarkerLayer.getMarker(id)
|
|
}
|
|
|
|
// Extended: Get all {DisplayMarker}s on the default marker layer. Consider
|
|
// using {::findMarkers}
|
|
getMarkers () {
|
|
return this.defaultMarkerLayer.getMarkers()
|
|
}
|
|
|
|
// Extended: Get the number of markers in the default marker layer.
|
|
//
|
|
// Returns a {Number}.
|
|
getMarkerCount () {
|
|
return this.defaultMarkerLayer.getMarkerCount()
|
|
}
|
|
|
|
destroyMarker (id) {
|
|
const marker = this.getMarker(id)
|
|
if (marker) marker.destroy()
|
|
}
|
|
|
|
// Essential: Create a marker layer to group related markers.
|
|
//
|
|
// * `options` An {Object} containing the following keys:
|
|
// * `maintainHistory` A {Boolean} indicating whether marker state should be
|
|
// restored on undo/redo. Defaults to `false`.
|
|
// * `persistent` A {Boolean} indicating whether or not this marker layer
|
|
// should be serialized and deserialized along with the rest of the
|
|
// buffer. Defaults to `false`. If `true`, the marker layer's id will be
|
|
// maintained across the serialization boundary, allowing you to retrieve
|
|
// it via {::getMarkerLayer}.
|
|
//
|
|
// Returns a {DisplayMarkerLayer}.
|
|
addMarkerLayer (options) {
|
|
return this.displayLayer.addMarkerLayer(options)
|
|
}
|
|
|
|
// Essential: Get a {DisplayMarkerLayer} by id.
|
|
//
|
|
// * `id` The id of the marker layer to retrieve.
|
|
//
|
|
// Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the
|
|
// given id.
|
|
getMarkerLayer (id) {
|
|
return this.displayLayer.getMarkerLayer(id)
|
|
}
|
|
|
|
// Essential: Get the default {DisplayMarkerLayer}.
|
|
//
|
|
// All marker APIs not tied to an explicit layer interact with this default
|
|
// layer.
|
|
//
|
|
// Returns a {DisplayMarkerLayer}.
|
|
getDefaultMarkerLayer () {
|
|
return this.defaultMarkerLayer
|
|
}
|
|
|
|
/*
|
|
Section: Cursors
|
|
*/
|
|
|
|
// Essential: Get the position of the most recently added cursor in buffer
|
|
// coordinates.
|
|
//
|
|
// Returns a {Point}
|
|
getCursorBufferPosition () {
|
|
return this.getLastCursor().getBufferPosition()
|
|
}
|
|
|
|
// Essential: Get the position of all the cursor positions in buffer coordinates.
|
|
//
|
|
// Returns {Array} of {Point}s in the order they were added
|
|
getCursorBufferPositions () {
|
|
return this.getCursors().map((cursor) => cursor.getBufferPosition())
|
|
}
|
|
|
|
// Essential: Move the cursor to the given position in buffer coordinates.
|
|
//
|
|
// If there are multiple cursors, they will be consolidated to a single cursor.
|
|
//
|
|
// * `position` A {Point} or {Array} of `[row, column]`
|
|
// * `options` (optional) An {Object} containing the following keys:
|
|
// * `autoscroll` Determines whether the editor scrolls to the new cursor's
|
|
// position. Defaults to true.
|
|
setCursorBufferPosition (position, options) {
|
|
return this.moveCursors(cursor => cursor.setBufferPosition(position, options))
|
|
}
|
|
|
|
// Essential: Get a {Cursor} at given screen coordinates {Point}
|
|
//
|
|
// * `position` A {Point} or {Array} of `[row, column]`
|
|
//
|
|
// Returns the first matched {Cursor} or undefined
|
|
getCursorAtScreenPosition (position) {
|
|
const selection = this.getSelectionAtScreenPosition(position)
|
|
if (selection && selection.getHeadScreenPosition().isEqual(position)) {
|
|
return selection.cursor
|
|
}
|
|
}
|
|
|
|
// Essential: Get the position of the most recently added cursor in screen
|
|
// coordinates.
|
|
//
|
|
// Returns a {Point}.
|
|
getCursorScreenPosition () {
|
|
return this.getLastCursor().getScreenPosition()
|
|
}
|
|
|
|
// Essential: Get the position of all the cursor positions in screen coordinates.
|
|
//
|
|
// Returns {Array} of {Point}s in the order the cursors were added
|
|
getCursorScreenPositions () {
|
|
return this.getCursors().map((cursor) => cursor.getScreenPosition())
|
|
}
|
|
|
|
// Essential: Move the cursor to the given position in screen coordinates.
|
|
//
|
|
// If there are multiple cursors, they will be consolidated to a single cursor.
|
|
//
|
|
// * `position` A {Point} or {Array} of `[row, column]`
|
|
// * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
|
|
// * `autoscroll` Determines whether the editor scrolls to the new cursor's
|
|
// position. Defaults to true.
|
|
setCursorScreenPosition (position, options) {
|
|
if (options && options.clip) {
|
|
Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.')
|
|
if (options.clipDirection) options.clipDirection = options.clip
|
|
}
|
|
if (options && options.wrapAtSoftNewlines != null) {
|
|
Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'
|
|
}
|
|
if (options && options.wrapBeyondNewlines != null) {
|
|
Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
|
|
if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'
|
|
}
|
|
|
|
return this.moveCursors(cursor => cursor.setScreenPosition(position, options))
|
|
}
|
|
|
|
// Essential: Add a cursor at the given position in buffer coordinates.
|
|
//
|
|
// * `bufferPosition` A {Point} or {Array} of `[row, column]`
|
|
//
|
|
// Returns a {Cursor}.
|
|
addCursorAtBufferPosition (bufferPosition, options) {
|
|
this.selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
|
|
if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll()
|
|
return this.getLastSelection().cursor
|
|
}
|
|
|
|
// Essential: Add a cursor at the position in screen coordinates.
|
|
//
|
|
// * `screenPosition` A {Point} or {Array} of `[row, column]`
|
|
//
|
|
// Returns a {Cursor}.
|
|
addCursorAtScreenPosition (screenPosition, options) {
|
|
this.selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'})
|
|
if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll()
|
|
return this.getLastSelection().cursor
|
|
}
|
|
|
|
// Essential: Returns {Boolean} indicating whether or not there are multiple cursors.
|
|
hasMultipleCursors () {
|
|
return this.getCursors().length > 1
|
|
}
|
|
|
|
// Essential: Move every cursor up one row in screen coordinates.
|
|
//
|
|
// * `lineCount` (optional) {Number} number of lines to move
|
|
moveUp (lineCount) {
|
|
return this.moveCursors(cursor => cursor.moveUp(lineCount, {moveToEndOfSelection: true}))
|
|
}
|
|
|
|
// Essential: Move every cursor down one row in screen coordinates.
|
|
//
|
|
// * `lineCount` (optional) {Number} number of lines to move
|
|
moveDown (lineCount) {
|
|
return this.moveCursors(cursor => cursor.moveDown(lineCount, {moveToEndOfSelection: true}))
|
|
}
|
|
|
|
// Essential: Move every cursor left one column.
|
|
//
|
|
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
|
moveLeft (columnCount) {
|
|
return this.moveCursors(cursor => cursor.moveLeft(columnCount, {moveToEndOfSelection: true}))
|
|
}
|
|
|
|
// Essential: Move every cursor right one column.
|
|
//
|
|
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
|
moveRight (columnCount) {
|
|
return this.moveCursors(cursor => cursor.moveRight(columnCount, {moveToEndOfSelection: true}))
|
|
}
|
|
|
|
// Essential: Move every cursor to the beginning of its line in buffer coordinates.
|
|
moveToBeginningOfLine () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfLine())
|
|
}
|
|
|
|
// Essential: Move every cursor to the beginning of its line in screen coordinates.
|
|
moveToBeginningOfScreenLine () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine())
|
|
}
|
|
|
|
// Essential: Move every cursor to the first non-whitespace character of its line.
|
|
moveToFirstCharacterOfLine () {
|
|
return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine())
|
|
}
|
|
|
|
// Essential: Move every cursor to the end of its line in buffer coordinates.
|
|
moveToEndOfLine () {
|
|
return this.moveCursors(cursor => cursor.moveToEndOfLine())
|
|
}
|
|
|
|
// Essential: Move every cursor to the end of its line in screen coordinates.
|
|
moveToEndOfScreenLine () {
|
|
return this.moveCursors(cursor => cursor.moveToEndOfScreenLine())
|
|
}
|
|
|
|
// Essential: Move every cursor to the beginning of its surrounding word.
|
|
moveToBeginningOfWord () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfWord())
|
|
}
|
|
|
|
// Essential: Move every cursor to the end of its surrounding word.
|
|
moveToEndOfWord () {
|
|
return this.moveCursors(cursor => cursor.moveToEndOfWord())
|
|
}
|
|
|
|
// Cursor Extended
|
|
|
|
// Extended: Move every cursor to the top of the buffer.
|
|
//
|
|
// If there are multiple cursors, they will be merged into a single cursor.
|
|
moveToTop () {
|
|
return this.moveCursors(cursor => cursor.moveToTop())
|
|
}
|
|
|
|
// Extended: Move every cursor to the bottom of the buffer.
|
|
//
|
|
// If there are multiple cursors, they will be merged into a single cursor.
|
|
moveToBottom () {
|
|
return this.moveCursors(cursor => cursor.moveToBottom())
|
|
}
|
|
|
|
// Extended: Move every cursor to the beginning of the next word.
|
|
moveToBeginningOfNextWord () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord())
|
|
}
|
|
|
|
// Extended: Move every cursor to the previous word boundary.
|
|
moveToPreviousWordBoundary () {
|
|
return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary())
|
|
}
|
|
|
|
// Extended: Move every cursor to the next word boundary.
|
|
moveToNextWordBoundary () {
|
|
return this.moveCursors(cursor => cursor.moveToNextWordBoundary())
|
|
}
|
|
|
|
// Extended: Move every cursor to the previous subword boundary.
|
|
moveToPreviousSubwordBoundary () {
|
|
return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary())
|
|
}
|
|
|
|
// Extended: Move every cursor to the next subword boundary.
|
|
moveToNextSubwordBoundary () {
|
|
return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary())
|
|
}
|
|
|
|
// Extended: Move every cursor to the beginning of the next paragraph.
|
|
moveToBeginningOfNextParagraph () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph())
|
|
}
|
|
|
|
// Extended: Move every cursor to the beginning of the previous paragraph.
|
|
moveToBeginningOfPreviousParagraph () {
|
|
return this.moveCursors(cursor => cursor.moveToBeginningOfPreviousParagraph())
|
|
}
|
|
|
|
// Extended: Returns the most recently added {Cursor}
|
|
getLastCursor () {
|
|
this.createLastSelectionIfNeeded()
|
|
return _.last(this.cursors)
|
|
}
|
|
|
|
// Extended: Returns the word surrounding the most recently added cursor.
|
|
//
|
|
// * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}.
|
|
getWordUnderCursor (options) {
|
|
return this.getTextInBufferRange(this.getLastCursor().getCurrentWordBufferRange(options))
|
|
}
|
|
|
|
// Extended: Get an Array of all {Cursor}s.
|
|
getCursors () {
|
|
this.createLastSelectionIfNeeded()
|
|
return this.cursors.slice()
|
|
}
|
|
|
|
// Extended: Get all {Cursors}s, ordered by their position in the buffer
|
|
// instead of the order in which they were added.
|
|
//
|
|
// Returns an {Array} of {Selection}s.
|
|
getCursorsOrderedByBufferPosition () {
|
|
return this.getCursors().sort((a, b) => a.compare(b))
|
|
}
|
|
|
|
cursorsForScreenRowRange (startScreenRow, endScreenRow) {
|
|
const cursors = []
|
|
for (let marker of this.selectionsMarkerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
|
const cursor = this.cursorsByMarkerId.get(marker.id)
|
|
if (cursor) cursors.push(cursor)
|
|
}
|
|
return cursors
|
|
}
|
|
|
|
// Add a cursor based on the given {DisplayMarker}.
|
|
addCursor (marker) {
|
|
const cursor = new Cursor({editor: this, marker, showCursorOnSelection: this.showCursorOnSelection})
|
|
this.cursors.push(cursor)
|
|
this.cursorsByMarkerId.set(marker.id, cursor)
|
|
return cursor
|
|
}
|
|
|
|
moveCursors (fn) {
|
|
return this.transact(() => {
|
|
this.getCursors().forEach(fn)
|
|
return this.mergeCursors()
|
|
})
|
|
}
|
|
|
|
cursorMoved (event) {
|
|
return this.emitter.emit('did-change-cursor-position', event)
|
|
}
|
|
|
|
// Merge cursors that have the same screen position
|
|
mergeCursors () {
|
|
const positions = {}
|
|
for (let cursor of this.getCursors()) {
|
|
const position = cursor.getBufferPosition().toString()
|
|
if (positions.hasOwnProperty(position)) {
|
|
cursor.destroy()
|
|
} else {
|
|
positions[position] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Selections
|
|
*/
|
|
|
|
// Essential: Get the selected text of the most recently added selection.
|
|
//
|
|
// Returns a {String}.
|
|
getSelectedText () {
|
|
return this.getLastSelection().getText()
|
|
}
|
|
|
|
// Essential: Get the {Range} of the most recently added selection in buffer
|
|
// coordinates.
|
|
//
|
|
// Returns a {Range}.
|
|
getSelectedBufferRange () {
|
|
return this.getLastSelection().getBufferRange()
|
|
}
|
|
|
|
// Essential: Get the {Range}s of all selections in buffer coordinates.
|
|
//
|
|
// The ranges are sorted by when the selections were added. Most recent at the end.
|
|
//
|
|
// Returns an {Array} of {Range}s.
|
|
getSelectedBufferRanges () {
|
|
return this.getSelections().map((selection) => selection.getBufferRange())
|
|
}
|
|
|
|
// Essential: Set the selected range in buffer coordinates. If there are multiple
|
|
// selections, they are reduced to a single selection with the given range.
|
|
//
|
|
// * `bufferRange` A {Range} or range-compatible {Array}.
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
// * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
|
// selection is set.
|
|
setSelectedBufferRange (bufferRange, options) {
|
|
return this.setSelectedBufferRanges([bufferRange], options)
|
|
}
|
|
|
|
// Essential: Set the selected ranges in buffer coordinates. If there are multiple
|
|
// selections, they are replaced by new selections with the given ranges.
|
|
//
|
|
// * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s.
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
// * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
|
// selection is set.
|
|
setSelectedBufferRanges (bufferRanges, options = {}) {
|
|
if (!bufferRanges.length) throw new Error('Passed an empty array to setSelectedBufferRanges')
|
|
|
|
const selections = this.getSelections()
|
|
for (let selection of selections.slice(bufferRanges.length)) {
|
|
selection.destroy()
|
|
}
|
|
|
|
this.mergeIntersectingSelections(options, () => {
|
|
for (let i = 0; i < bufferRanges.length; i++) {
|
|
let bufferRange = bufferRanges[i]
|
|
bufferRange = Range.fromObject(bufferRange)
|
|
if (selections[i]) {
|
|
selections[i].setBufferRange(bufferRange, options)
|
|
} else {
|
|
this.addSelectionForBufferRange(bufferRange, options)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Essential: Get the {Range} of the most recently added selection in screen
|
|
// coordinates.
|
|
//
|
|
// Returns a {Range}.
|
|
getSelectedScreenRange () {
|
|
return this.getLastSelection().getScreenRange()
|
|
}
|
|
|
|
// Essential: Get the {Range}s of all selections in screen coordinates.
|
|
//
|
|
// The ranges are sorted by when the selections were added. Most recent at the end.
|
|
//
|
|
// Returns an {Array} of {Range}s.
|
|
getSelectedScreenRanges () {
|
|
return this.getSelections().map((selection) => selection.getScreenRange())
|
|
}
|
|
|
|
// Essential: Set the selected range in screen coordinates. If there are multiple
|
|
// selections, they are reduced to a single selection with the given range.
|
|
//
|
|
// * `screenRange` A {Range} or range-compatible {Array}.
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
setSelectedScreenRange (screenRange, options) {
|
|
return this.setSelectedBufferRange(this.bufferRangeForScreenRange(screenRange, options), options)
|
|
}
|
|
|
|
// Essential: Set the selected ranges in screen coordinates. If there are multiple
|
|
// selections, they are replaced by new selections with the given ranges.
|
|
//
|
|
// * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s.
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
setSelectedScreenRanges (screenRanges, options = {}) {
|
|
if (!screenRanges.length) throw new Error('Passed an empty array to setSelectedScreenRanges')
|
|
|
|
const selections = this.getSelections()
|
|
for (let selection of selections.slice(screenRanges.length)) {
|
|
selection.destroy()
|
|
}
|
|
|
|
this.mergeIntersectingSelections(options, () => {
|
|
for (let i = 0; i < screenRanges.length; i++) {
|
|
let screenRange = screenRanges[i]
|
|
screenRange = Range.fromObject(screenRange)
|
|
if (selections[i]) {
|
|
selections[i].setScreenRange(screenRange, options)
|
|
} else {
|
|
this.addSelectionForScreenRange(screenRange, options)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Essential: Add a selection for the given range in buffer coordinates.
|
|
//
|
|
// * `bufferRange` A {Range}
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
// * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
|
// selection is set.
|
|
//
|
|
// Returns the added {Selection}.
|
|
addSelectionForBufferRange (bufferRange, options = {}) {
|
|
bufferRange = Range.fromObject(bufferRange)
|
|
if (!options.preserveFolds) {
|
|
this.displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true)
|
|
}
|
|
this.selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed != null ? options.reversed : false})
|
|
if (options.autoscroll !== false) this.getLastSelection().autoscroll()
|
|
return this.getLastSelection()
|
|
}
|
|
|
|
// Essential: Add a selection for the given range in screen coordinates.
|
|
//
|
|
// * `screenRange` A {Range}
|
|
// * `options` (optional) An options {Object}:
|
|
// * `reversed` A {Boolean} indicating whether to create the selection in a
|
|
// reversed orientation.
|
|
// * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
|
// selection is set.
|
|
// Returns the added {Selection}.
|
|
addSelectionForScreenRange (screenRange, options = {}) {
|
|
return this.addSelectionForBufferRange(this.bufferRangeForScreenRange(screenRange), options)
|
|
}
|
|
|
|
// Essential: Select from the current cursor position to the given position in
|
|
// buffer coordinates.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
//
|
|
// * `position` An instance of {Point}, with a given `row` and `column`.
|
|
selectToBufferPosition (position) {
|
|
const lastSelection = this.getLastSelection()
|
|
lastSelection.selectToBufferPosition(position)
|
|
return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()})
|
|
}
|
|
|
|
// Essential: Select from the current cursor position to the given position in
|
|
// screen coordinates.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
//
|
|
// * `position` An instance of {Point}, with a given `row` and `column`.
|
|
selectToScreenPosition (position, options) {
|
|
const lastSelection = this.getLastSelection()
|
|
lastSelection.selectToScreenPosition(position, options)
|
|
if (!options || !options.suppressSelectionMerge) {
|
|
return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()})
|
|
}
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection one character upward while
|
|
// preserving the selection's tail position.
|
|
//
|
|
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectUp (rowCount) {
|
|
return this.expandSelectionsBackward(selection => selection.selectUp(rowCount))
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection one character downward while
|
|
// preserving the selection's tail position.
|
|
//
|
|
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectDown (rowCount) {
|
|
return this.expandSelectionsForward(selection => selection.selectDown(rowCount))
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection one character leftward while
|
|
// preserving the selection's tail position.
|
|
//
|
|
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectLeft (columnCount) {
|
|
return this.expandSelectionsBackward(selection => selection.selectLeft(columnCount))
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection one character rightward while
|
|
// preserving the selection's tail position.
|
|
//
|
|
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectRight (columnCount) {
|
|
return this.expandSelectionsForward(selection => selection.selectRight(columnCount))
|
|
}
|
|
|
|
// Essential: Select from the top of the buffer to the end of the last selection
|
|
// in the buffer.
|
|
//
|
|
// This method merges multiple selections into a single selection.
|
|
selectToTop () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToTop())
|
|
}
|
|
|
|
// Essential: Selects from the top of the first selection in the buffer to the end
|
|
// of the buffer.
|
|
//
|
|
// This method merges multiple selections into a single selection.
|
|
selectToBottom () {
|
|
return this.expandSelectionsForward(selection => selection.selectToBottom())
|
|
}
|
|
|
|
// Essential: Select all text in the buffer.
|
|
//
|
|
// This method merges multiple selections into a single selection.
|
|
selectAll () {
|
|
return this.expandSelectionsForward(selection => selection.selectAll())
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection to the beginning of its line
|
|
// while preserving the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToBeginningOfLine () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToBeginningOfLine())
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection to the first non-whitespace
|
|
// character of its line while preserving the selection's tail position. If the
|
|
// cursor is already on the first character of the line, move it to the
|
|
// beginning of the line.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToFirstCharacterOfLine () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToFirstCharacterOfLine())
|
|
}
|
|
|
|
// Essential: Move the cursor of each selection to the end of its line while
|
|
// preserving the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToEndOfLine () {
|
|
return this.expandSelectionsForward(selection => selection.selectToEndOfLine())
|
|
}
|
|
|
|
// Essential: Expand selections to the beginning of their containing word.
|
|
//
|
|
// Operates on all selections. Moves the cursor to the beginning of the
|
|
// containing word while preserving the selection's tail position.
|
|
selectToBeginningOfWord () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToBeginningOfWord())
|
|
}
|
|
|
|
// Essential: Expand selections to the end of their containing word.
|
|
//
|
|
// Operates on all selections. Moves the cursor to the end of the containing
|
|
// word while preserving the selection's tail position.
|
|
selectToEndOfWord () {
|
|
return this.expandSelectionsForward(selection => selection.selectToEndOfWord())
|
|
}
|
|
|
|
// Extended: For each selection, move its cursor to the preceding subword
|
|
// boundary while maintaining the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToPreviousSubwordBoundary () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToPreviousSubwordBoundary())
|
|
}
|
|
|
|
// Extended: For each selection, move its cursor to the next subword boundary
|
|
// while maintaining the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToNextSubwordBoundary () {
|
|
return this.expandSelectionsForward(selection => selection.selectToNextSubwordBoundary())
|
|
}
|
|
|
|
// Essential: For each cursor, select the containing line.
|
|
//
|
|
// This method merges selections on successive lines.
|
|
selectLinesContainingCursors () {
|
|
return this.expandSelectionsForward(selection => selection.selectLine())
|
|
}
|
|
|
|
// Essential: Select the word surrounding each cursor.
|
|
selectWordsContainingCursors () {
|
|
return this.expandSelectionsForward(selection => selection.selectWord())
|
|
}
|
|
|
|
// Selection Extended
|
|
|
|
// Extended: For each selection, move its cursor to the preceding word boundary
|
|
// while maintaining the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToPreviousWordBoundary () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToPreviousWordBoundary())
|
|
}
|
|
|
|
// Extended: For each selection, move its cursor to the next word boundary while
|
|
// maintaining the selection's tail position.
|
|
//
|
|
// This method may merge selections that end up intersecting.
|
|
selectToNextWordBoundary () {
|
|
return this.expandSelectionsForward(selection => selection.selectToNextWordBoundary())
|
|
}
|
|
|
|
// Extended: Expand selections to the beginning of the next word.
|
|
//
|
|
// Operates on all selections. Moves the cursor to the beginning of the next
|
|
// word while preserving the selection's tail position.
|
|
selectToBeginningOfNextWord () {
|
|
return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextWord())
|
|
}
|
|
|
|
// Extended: Expand selections to the beginning of the next paragraph.
|
|
//
|
|
// Operates on all selections. Moves the cursor to the beginning of the next
|
|
// paragraph while preserving the selection's tail position.
|
|
selectToBeginningOfNextParagraph () {
|
|
return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextParagraph())
|
|
}
|
|
|
|
// Extended: Expand selections to the beginning of the next paragraph.
|
|
//
|
|
// Operates on all selections. Moves the cursor to the beginning of the next
|
|
// paragraph while preserving the selection's tail position.
|
|
selectToBeginningOfPreviousParagraph () {
|
|
return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph())
|
|
}
|
|
|
|
// Extended: Select the range of the given marker if it is valid.
|
|
//
|
|
// * `marker` A {DisplayMarker}
|
|
//
|
|
// Returns the selected {Range} or `undefined` if the marker is invalid.
|
|
selectMarker (marker) {
|
|
if (marker.isValid()) {
|
|
const range = marker.getBufferRange()
|
|
this.setSelectedBufferRange(range)
|
|
return range
|
|
}
|
|
}
|
|
|
|
// Extended: Get the most recently added {Selection}.
|
|
//
|
|
// Returns a {Selection}.
|
|
getLastSelection () {
|
|
this.createLastSelectionIfNeeded()
|
|
return _.last(this.selections)
|
|
}
|
|
|
|
getSelectionAtScreenPosition (position) {
|
|
const markers = this.selectionsMarkerLayer.findMarkers({containsScreenPosition: position})
|
|
if (markers.length > 0) return this.cursorsByMarkerId.get(markers[0].id).selection
|
|
}
|
|
|
|
// Extended: Get current {Selection}s.
|
|
//
|
|
// Returns: An {Array} of {Selection}s.
|
|
getSelections () {
|
|
this.createLastSelectionIfNeeded()
|
|
return this.selections.slice()
|
|
}
|
|
|
|
// Extended: Get all {Selection}s, ordered by their position in the buffer
|
|
// instead of the order in which they were added.
|
|
//
|
|
// Returns an {Array} of {Selection}s.
|
|
getSelectionsOrderedByBufferPosition () {
|
|
return this.getSelections().sort((a, b) => a.compare(b))
|
|
}
|
|
|
|
// Extended: Determine if a given range in buffer coordinates intersects a
|
|
// selection.
|
|
//
|
|
// * `bufferRange` A {Range} or range-compatible {Array}.
|
|
//
|
|
// Returns a {Boolean}.
|
|
selectionIntersectsBufferRange (bufferRange) {
|
|
return this.getSelections().some(selection => selection.intersectsBufferRange(bufferRange))
|
|
}
|
|
|
|
// Selections Private
|
|
|
|
// Add a similarly-shaped selection to the next eligible line below
|
|
// each selection.
|
|
//
|
|
// Operates on all selections. If the selection is empty, adds an empty
|
|
// selection to the next following non-empty line as close to the current
|
|
// selection's column as possible. If the selection is non-empty, adds a
|
|
// selection to the next line that is long enough for a non-empty selection
|
|
// starting at the same column as the current selection to be added to it.
|
|
addSelectionBelow () {
|
|
return this.expandSelectionsForward(selection => selection.addSelectionBelow())
|
|
}
|
|
|
|
// Add a similarly-shaped selection to the next eligible line above
|
|
// each selection.
|
|
//
|
|
// Operates on all selections. If the selection is empty, adds an empty
|
|
// selection to the next preceding non-empty line as close to the current
|
|
// selection's column as possible. If the selection is non-empty, adds a
|
|
// selection to the next line that is long enough for a non-empty selection
|
|
// starting at the same column as the current selection to be added to it.
|
|
addSelectionAbove () {
|
|
return this.expandSelectionsBackward(selection => selection.addSelectionAbove())
|
|
}
|
|
|
|
// Calls the given function with each selection, then merges selections
|
|
expandSelectionsForward (fn) {
|
|
this.mergeIntersectingSelections(() => this.getSelections().forEach(fn))
|
|
}
|
|
|
|
// Calls the given function with each selection, then merges selections in the
|
|
// reversed orientation
|
|
expandSelectionsBackward (fn) {
|
|
this.mergeIntersectingSelections({reversed: true}, () => this.getSelections().forEach(fn))
|
|
}
|
|
|
|
finalizeSelections () {
|
|
for (let selection of this.getSelections()) { selection.finalize() }
|
|
}
|
|
|
|
selectionsForScreenRows (startRow, endRow) {
|
|
return this.getSelections().filter(selection => selection.intersectsScreenRowRange(startRow, endRow))
|
|
}
|
|
|
|
// Merges intersecting selections. If passed a function, it executes
|
|
// the function with merging suppressed, then merges intersecting selections
|
|
// afterward.
|
|
mergeIntersectingSelections (...args) {
|
|
return this.mergeSelections(...args, (previousSelection, currentSelection) => {
|
|
const exclusive = !currentSelection.isEmpty() && !previousSelection.isEmpty()
|
|
return previousSelection.intersectsWith(currentSelection, exclusive)
|
|
})
|
|
}
|
|
|
|
mergeSelectionsOnSameRows (...args) {
|
|
return this.mergeSelections(...args, (previousSelection, currentSelection) => {
|
|
const screenRange = currentSelection.getScreenRange()
|
|
return previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row)
|
|
})
|
|
}
|
|
|
|
avoidMergingSelections (...args) {
|
|
return this.mergeSelections(...args, () => false)
|
|
}
|
|
|
|
mergeSelections (...args) {
|
|
const mergePredicate = args.pop()
|
|
let fn = args.pop()
|
|
let options = args.pop()
|
|
if (typeof fn !== 'function') {
|
|
options = fn
|
|
fn = () => {}
|
|
}
|
|
|
|
if (this.suppressSelectionMerging) return fn()
|
|
|
|
this.suppressSelectionMerging = true
|
|
const result = fn()
|
|
this.suppressSelectionMerging = false
|
|
|
|
const selections = this.getSelectionsOrderedByBufferPosition()
|
|
let lastSelection = selections.shift()
|
|
for (const selection of selections) {
|
|
if (mergePredicate(lastSelection, selection)) {
|
|
lastSelection.merge(selection, options)
|
|
} else {
|
|
lastSelection = selection
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Add a {Selection} based on the given {DisplayMarker}.
|
|
//
|
|
// * `marker` The {DisplayMarker} to highlight
|
|
// * `options` (optional) An {Object} that pertains to the {Selection} constructor.
|
|
//
|
|
// Returns the new {Selection}.
|
|
addSelection (marker, options = {}) {
|
|
const cursor = this.addCursor(marker)
|
|
let selection = new Selection(Object.assign({editor: this, marker, cursor}, options))
|
|
this.selections.push(selection)
|
|
const selectionBufferRange = selection.getBufferRange()
|
|
this.mergeIntersectingSelections({preserveFolds: options.preserveFolds})
|
|
|
|
if (selection.destroyed) {
|
|
for (selection of this.getSelections()) {
|
|
if (selection.intersectsBufferRange(selectionBufferRange)) return selection
|
|
}
|
|
} else {
|
|
this.emitter.emit('did-add-cursor', cursor)
|
|
this.emitter.emit('did-add-selection', selection)
|
|
return selection
|
|
}
|
|
}
|
|
|
|
// Remove the given selection.
|
|
removeSelection (selection) {
|
|
_.remove(this.cursors, selection.cursor)
|
|
_.remove(this.selections, selection)
|
|
this.cursorsByMarkerId.delete(selection.cursor.marker.id)
|
|
this.emitter.emit('did-remove-cursor', selection.cursor)
|
|
return this.emitter.emit('did-remove-selection', selection)
|
|
}
|
|
|
|
// Reduce one or more selections to a single empty selection based on the most
|
|
// recently added cursor.
|
|
clearSelections (options) {
|
|
this.consolidateSelections()
|
|
this.getLastSelection().clear(options)
|
|
}
|
|
|
|
// Reduce multiple selections to the least recently added selection.
|
|
consolidateSelections () {
|
|
const selections = this.getSelections()
|
|
if (selections.length > 1) {
|
|
for (let selection of selections.slice(1, (selections.length))) { selection.destroy() }
|
|
selections[0].autoscroll({center: true})
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Called by the selection
|
|
selectionRangeChanged (event) {
|
|
if (this.component) this.component.didChangeSelectionRange()
|
|
this.emitter.emit('did-change-selection-range', event)
|
|
}
|
|
|
|
createLastSelectionIfNeeded () {
|
|
if (this.selections.length === 0) {
|
|
this.addSelectionForBufferRange([[0, 0], [0, 0]], {autoscroll: false, preserveFolds: true})
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Searching and Replacing
|
|
*/
|
|
|
|
// Essential: Scan regular expression matches in the entire buffer, calling the
|
|
// given iterator function on each match.
|
|
//
|
|
// `::scan` functions as the replace method as well via the `replace`
|
|
//
|
|
// If you're programmatically modifying the results, you may want to try
|
|
// {::backwardsScanInBufferRange} to avoid tripping over your own changes.
|
|
//
|
|
// * `regex` A {RegExp} to search for.
|
|
// * `options` (optional) {Object}
|
|
// * `leadingContextLineCount` {Number} default `0`; The number of lines
|
|
// before the matched line to include in the results object.
|
|
// * `trailingContextLineCount` {Number} default `0`; The number of lines
|
|
// after the matched line to include in the results object.
|
|
// * `iterator` A {Function} that's called on each match
|
|
// * `object` {Object}
|
|
// * `match` The current regular expression match.
|
|
// * `matchText` A {String} with the text of the match.
|
|
// * `range` The {Range} of the match.
|
|
// * `stop` Call this {Function} to terminate the scan.
|
|
// * `replace` Call this {Function} with a {String} to replace the match.
|
|
scan (regex, options = {}, iterator) {
|
|
if (_.isFunction(options)) {
|
|
iterator = options
|
|
options = {}
|
|
}
|
|
|
|
return this.buffer.scan(regex, options, iterator)
|
|
}
|
|
|
|
// Essential: Scan regular expression matches in a given range, calling the given
|
|
// iterator function on each match.
|
|
//
|
|
// * `regex` A {RegExp} to search for.
|
|
// * `range` A {Range} in which to search.
|
|
// * `iterator` A {Function} that's called on each match with an {Object}
|
|
// containing the following keys:
|
|
// * `match` The current regular expression match.
|
|
// * `matchText` A {String} with the text of the match.
|
|
// * `range` The {Range} of the match.
|
|
// * `stop` Call this {Function} to terminate the scan.
|
|
// * `replace` Call this {Function} with a {String} to replace the match.
|
|
scanInBufferRange (regex, range, iterator) { return this.buffer.scanInRange(regex, range, iterator) }
|
|
|
|
// Essential: Scan regular expression matches in a given range in reverse order,
|
|
// calling the given iterator function on each match.
|
|
//
|
|
// * `regex` A {RegExp} to search for.
|
|
// * `range` A {Range} in which to search.
|
|
// * `iterator` A {Function} that's called on each match with an {Object}
|
|
// containing the following keys:
|
|
// * `match` The current regular expression match.
|
|
// * `matchText` A {String} with the text of the match.
|
|
// * `range` The {Range} of the match.
|
|
// * `stop` Call this {Function} to terminate the scan.
|
|
// * `replace` Call this {Function} with a {String} to replace the match.
|
|
backwardsScanInBufferRange (regex, range, iterator) { return this.buffer.backwardsScanInRange(regex, range, iterator) }
|
|
|
|
/*
|
|
Section: Tab Behavior
|
|
*/
|
|
|
|
// Essential: Returns a {Boolean} indicating whether softTabs are enabled for this
|
|
// editor.
|
|
getSoftTabs () { return this.softTabs }
|
|
|
|
// Essential: Enable or disable soft tabs for this editor.
|
|
//
|
|
// * `softTabs` A {Boolean}
|
|
setSoftTabs (softTabs) {
|
|
this.softTabs = softTabs
|
|
this.update({softTabs: this.softTabs})
|
|
}
|
|
|
|
// Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor.
|
|
hasAtomicSoftTabs () { return this.displayLayer.atomicSoftTabs }
|
|
|
|
// Essential: Toggle soft tabs for this editor
|
|
toggleSoftTabs () { this.setSoftTabs(!this.getSoftTabs()) }
|
|
|
|
// Essential: Get the on-screen length of tab characters.
|
|
//
|
|
// Returns a {Number}.
|
|
getTabLength () { return this.displayLayer.tabLength }
|
|
|
|
// 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) { this.update({tabLength}) }
|
|
|
|
// Returns an {Object} representing the current invisible character
|
|
// substitutions for this editor. See {::setInvisibles}.
|
|
getInvisibles () {
|
|
if (!this.mini && this.showInvisibles && (this.invisibles != null)) {
|
|
return this.invisibles
|
|
} else {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
doesShowIndentGuide () { return this.showIndentGuide && !this.mini }
|
|
|
|
getSoftWrapHangingIndentLength () { return this.displayLayer.softWrapHangingIndent }
|
|
|
|
// Extended: Determine if the buffer uses hard or soft tabs.
|
|
//
|
|
// Returns `true` if the first non-comment line with leading whitespace starts
|
|
// with a space character. Returns `false` if it starts with a hard tab (`\t`).
|
|
//
|
|
// Returns a {Boolean} or undefined if no non-comment lines had leading
|
|
// whitespace.
|
|
usesSoftTabs () {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) {
|
|
if (languageMode.isRowCommented(bufferRow)) continue
|
|
const line = this.buffer.lineForRow(bufferRow)
|
|
if (line[0] === ' ') return true
|
|
if (line[0] === '\t') return false
|
|
}
|
|
}
|
|
|
|
// Extended: Get the text representing a single level of indent.
|
|
//
|
|
// If soft tabs are enabled, the text is composed of N spaces, where N is the
|
|
// tab length. Otherwise the text is a tab character (`\t`).
|
|
//
|
|
// Returns a {String}.
|
|
getTabText () { return this.buildIndentString(1) }
|
|
|
|
// If soft tabs are enabled, convert all hard tabs to soft tabs in the given
|
|
// {Range}.
|
|
normalizeTabsInBufferRange (bufferRange) {
|
|
if (!this.getSoftTabs()) { return }
|
|
return this.scanInBufferRange(/\t/g, bufferRange, ({replace}) => replace(this.getTabText()))
|
|
}
|
|
|
|
/*
|
|
Section: Soft Wrap Behavior
|
|
*/
|
|
|
|
// Essential: Determine whether lines in this editor are soft-wrapped.
|
|
//
|
|
// Returns a {Boolean}.
|
|
isSoftWrapped () { return this.softWrapped }
|
|
|
|
// Essential: Enable or disable soft wrapping for this editor.
|
|
//
|
|
// * `softWrapped` A {Boolean}
|
|
//
|
|
// Returns a {Boolean}.
|
|
setSoftWrapped (softWrapped) {
|
|
this.update({softWrapped})
|
|
return this.isSoftWrapped()
|
|
}
|
|
|
|
getPreferredLineLength () { return this.preferredLineLength }
|
|
|
|
// Essential: Toggle soft wrapping for this editor
|
|
//
|
|
// Returns a {Boolean}.
|
|
toggleSoftWrapped () { return this.setSoftWrapped(!this.isSoftWrapped()) }
|
|
|
|
// Essential: Gets the column at which column will soft wrap
|
|
getSoftWrapColumn () {
|
|
if (this.isSoftWrapped() && !this.mini) {
|
|
if (this.softWrapAtPreferredLineLength) {
|
|
return Math.min(this.getEditorWidthInChars(), this.preferredLineLength)
|
|
} else {
|
|
return this.getEditorWidthInChars()
|
|
}
|
|
} else {
|
|
return this.maxScreenLineLength
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Indentation
|
|
*/
|
|
|
|
// Essential: Get the indentation level of the given buffer row.
|
|
//
|
|
// Determines how deeply the given row is indented based on the soft tabs and
|
|
// tab length settings of this editor. Note that if soft tabs are enabled and
|
|
// the tab length is 2, a row with 4 leading spaces would have an indentation
|
|
// level of 2.
|
|
//
|
|
// * `bufferRow` A {Number} indicating the buffer row.
|
|
//
|
|
// Returns a {Number}.
|
|
indentationForBufferRow (bufferRow) {
|
|
return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow))
|
|
}
|
|
|
|
// Essential: Set the indentation level for the given buffer row.
|
|
//
|
|
// Inserts or removes hard tabs or spaces based on the soft tabs and tab length
|
|
// settings of this editor in order to bring it to the given indentation level.
|
|
// Note that if soft tabs are enabled and the tab length is 2, a row with 4
|
|
// leading spaces would have an indentation level of 2.
|
|
//
|
|
// * `bufferRow` A {Number} indicating the buffer row.
|
|
// * `newLevel` A {Number} indicating the new indentation level.
|
|
// * `options` (optional) An {Object} with the following keys:
|
|
// * `preserveLeadingWhitespace` `true` to preserve any whitespace already at
|
|
// the beginning of the line (default: false).
|
|
setIndentationForBufferRow (bufferRow, newLevel, {preserveLeadingWhitespace} = {}) {
|
|
let endColumn
|
|
if (preserveLeadingWhitespace) {
|
|
endColumn = 0
|
|
} else {
|
|
endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length
|
|
}
|
|
const newIndentString = this.buildIndentString(newLevel)
|
|
return this.buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString)
|
|
}
|
|
|
|
// Extended: Indent rows intersecting selections by one level.
|
|
indentSelectedRows () {
|
|
return this.mutateSelectedText(selection => selection.indentSelectedRows())
|
|
}
|
|
|
|
// Extended: Outdent rows intersecting selections by one level.
|
|
outdentSelectedRows () {
|
|
return this.mutateSelectedText(selection => selection.outdentSelectedRows())
|
|
}
|
|
|
|
// Extended: Get the indentation level of the given line of text.
|
|
//
|
|
// Determines how deeply the given line is indented based on the soft tabs and
|
|
// tab length settings of this editor. Note that if soft tabs are enabled and
|
|
// the tab length is 2, a row with 4 leading spaces would have an indentation
|
|
// level of 2.
|
|
//
|
|
// * `line` A {String} representing a line of text.
|
|
//
|
|
// Returns a {Number}.
|
|
indentLevelForLine (line) {
|
|
const tabLength = this.getTabLength()
|
|
let indentLength = 0
|
|
for (let i = 0, {length} = line; i < length; i++) {
|
|
const char = line[i]
|
|
if (char === '\t') {
|
|
indentLength += tabLength - (indentLength % tabLength)
|
|
} else if (char === ' ') {
|
|
indentLength++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return indentLength / tabLength
|
|
}
|
|
|
|
// Extended: Indent rows intersecting selections based on the grammar's suggested
|
|
// indent level.
|
|
autoIndentSelectedRows () {
|
|
return this.mutateSelectedText(selection => selection.autoIndentSelectedRows())
|
|
}
|
|
|
|
// Indent all lines intersecting selections. See {Selection::indent} for more
|
|
// information.
|
|
indent (options = {}) {
|
|
if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent()
|
|
this.mutateSelectedText(selection => selection.indent(options))
|
|
}
|
|
|
|
// Constructs the string used for indents.
|
|
buildIndentString (level, column = 0) {
|
|
if (this.getSoftTabs()) {
|
|
const tabStopViolation = column % this.getTabLength()
|
|
return _.multiplyString(' ', Math.floor(level * this.getTabLength()) - tabStopViolation)
|
|
} else {
|
|
const excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * this.getTabLength()))
|
|
return _.multiplyString('\t', Math.floor(level)) + excessWhitespace
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Grammars
|
|
*/
|
|
|
|
// Essential: Get the current {Grammar} of this editor.
|
|
getGrammar () {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
return languageMode.getGrammar && languageMode.getGrammar() || NullGrammar
|
|
}
|
|
|
|
// Essential: Set the current {Grammar} of this editor.
|
|
//
|
|
// Assigning a grammar will cause the editor to re-tokenize based on the new
|
|
// grammar.
|
|
//
|
|
// * `grammar` {Grammar}
|
|
setGrammar (grammar) {
|
|
Grim.deprecate('Use atom.grammars.assignLanguageMode(buffer, languageName) instead')
|
|
atom.grammars.assignLanguageMode(this.getBuffer(), grammar.name)
|
|
}
|
|
|
|
// Experimental: Get a notification when async tokenization is completed.
|
|
onDidTokenize (callback) {
|
|
return this.buffer.getLanguageMode().onDidTokenize(callback)
|
|
}
|
|
|
|
/*
|
|
Section: Managing Syntax Scopes
|
|
*/
|
|
|
|
// Essential: Returns a {ScopeDescriptor} that includes this editor's language.
|
|
// e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
|
|
// {Config::get} to get language specific config values.
|
|
getRootScopeDescriptor () {
|
|
return this.buffer.getLanguageMode().rootScopeDescriptor
|
|
}
|
|
|
|
// Essential: Get the syntactic scopeDescriptor for the given position in buffer
|
|
// coordinates. Useful with {Config::get}.
|
|
//
|
|
// For example, if called with a position inside the parameter list of an
|
|
// anonymous CoffeeScript function, the method returns the following array:
|
|
// `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]`
|
|
//
|
|
// * `bufferPosition` A {Point} or {Array} of [row, column].
|
|
//
|
|
// Returns a {ScopeDescriptor}.
|
|
scopeDescriptorForBufferPosition (bufferPosition) {
|
|
return this.buffer.getLanguageMode().scopeDescriptorForPosition(bufferPosition)
|
|
}
|
|
|
|
// Extended: Get the range in buffer coordinates of all tokens surrounding the
|
|
// cursor that match the given scope selector.
|
|
//
|
|
// For example, if you wanted to find the string surrounding the cursor, you
|
|
// could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`.
|
|
//
|
|
// * `scopeSelector` {String} selector. e.g. `'.source.ruby'`
|
|
//
|
|
// Returns a {Range}.
|
|
bufferRangeForScopeAtCursor (scopeSelector) {
|
|
return this.bufferRangeForScopeAtPosition(scopeSelector, this.getCursorBufferPosition())
|
|
}
|
|
|
|
bufferRangeForScopeAtPosition (scopeSelector, position) {
|
|
return this.buffer.getLanguageMode().bufferRangeForScopeAtPosition(scopeSelector, position)
|
|
}
|
|
|
|
// Extended: Determine if the given row is entirely a comment
|
|
isBufferRowCommented (bufferRow) {
|
|
const match = this.lineTextForBufferRow(bufferRow).match(/\S/)
|
|
if (match) {
|
|
if (!this.commentScopeSelector) this.commentScopeSelector = new TextMateScopeSelector('comment.*')
|
|
return this.commentScopeSelector.matches(this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes)
|
|
}
|
|
}
|
|
|
|
// Get the scope descriptor at the cursor.
|
|
getCursorScope () {
|
|
return this.getLastCursor().getScopeDescriptor()
|
|
}
|
|
|
|
tokenForBufferPosition (bufferPosition) {
|
|
return this.buffer.getLanguageMode().tokenForPosition(bufferPosition)
|
|
}
|
|
|
|
/*
|
|
Section: Clipboard Operations
|
|
*/
|
|
|
|
// Essential: For each selection, copy the selected text.
|
|
copySelectedText () {
|
|
let maintainClipboard = false
|
|
for (let selection of this.getSelectionsOrderedByBufferPosition()) {
|
|
if (selection.isEmpty()) {
|
|
const previousRange = selection.getBufferRange()
|
|
selection.selectLine()
|
|
selection.copy(maintainClipboard, true)
|
|
selection.setBufferRange(previousRange)
|
|
} else {
|
|
selection.copy(maintainClipboard, false)
|
|
}
|
|
maintainClipboard = true
|
|
}
|
|
}
|
|
|
|
// Private: For each selection, only copy highlighted text.
|
|
copyOnlySelectedText () {
|
|
let maintainClipboard = false
|
|
for (let selection of this.getSelectionsOrderedByBufferPosition()) {
|
|
if (!selection.isEmpty()) {
|
|
selection.copy(maintainClipboard, false)
|
|
maintainClipboard = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Essential: For each selection, cut the selected text.
|
|
cutSelectedText () {
|
|
let maintainClipboard = false
|
|
this.mutateSelectedText(selection => {
|
|
if (selection.isEmpty()) {
|
|
selection.selectLine()
|
|
selection.cut(maintainClipboard, true)
|
|
} else {
|
|
selection.cut(maintainClipboard, false)
|
|
}
|
|
maintainClipboard = true
|
|
})
|
|
}
|
|
|
|
// Essential: For each selection, replace the selected text with the contents of
|
|
// the clipboard.
|
|
//
|
|
// If the clipboard contains the same number of selections as the current
|
|
// editor, each selection will be replaced with the content of the
|
|
// corresponding clipboard selection text.
|
|
//
|
|
// * `options` (optional) See {Selection::insertText}.
|
|
pasteText (options) {
|
|
options = Object.assign({}, options)
|
|
let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata()
|
|
if (!this.emitWillInsertTextEvent(clipboardText)) return false
|
|
|
|
if (!metadata) metadata = {}
|
|
if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndentOnPaste()
|
|
|
|
this.mutateSelectedText((selection, index) => {
|
|
let fullLine, indentBasis, text
|
|
if (metadata.selections && metadata.selections.length === this.getSelections().length) {
|
|
({text, indentBasis, fullLine} = metadata.selections[index])
|
|
} else {
|
|
({indentBasis, fullLine} = metadata)
|
|
text = clipboardText
|
|
}
|
|
|
|
if (indentBasis != null && (text.includes('\n') || !selection.cursor.hasPrecedingCharactersOnLine())) {
|
|
options.indentBasis = indentBasis
|
|
} else {
|
|
options.indentBasis = null
|
|
}
|
|
|
|
let range
|
|
if (fullLine && selection.isEmpty()) {
|
|
const oldPosition = selection.getBufferRange().start
|
|
selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]])
|
|
range = selection.insertText(text, options)
|
|
const newPosition = oldPosition.translate([1, 0])
|
|
selection.setBufferRange([newPosition, newPosition])
|
|
} else {
|
|
range = selection.insertText(text, options)
|
|
}
|
|
|
|
this.emitter.emit('did-insert-text', {text, range})
|
|
})
|
|
}
|
|
|
|
// Essential: For each selection, if the selection is empty, cut all characters
|
|
// of the containing screen line following the cursor. Otherwise cut the selected
|
|
// text.
|
|
cutToEndOfLine () {
|
|
let maintainClipboard = false
|
|
this.mutateSelectedText(selection => {
|
|
selection.cutToEndOfLine(maintainClipboard)
|
|
maintainClipboard = true
|
|
})
|
|
}
|
|
|
|
// Essential: For each selection, if the selection is empty, cut all characters
|
|
// of the containing buffer line following the cursor. Otherwise cut the
|
|
// selected text.
|
|
cutToEndOfBufferLine () {
|
|
let maintainClipboard = false
|
|
this.mutateSelectedText(selection => {
|
|
selection.cutToEndOfBufferLine(maintainClipboard)
|
|
maintainClipboard = true
|
|
})
|
|
}
|
|
|
|
/*
|
|
Section: Folds
|
|
*/
|
|
|
|
// Essential: Fold the most recent cursor's row based on its indentation level.
|
|
//
|
|
// The fold will extend from the nearest preceding line with a lower
|
|
// indentation level up to the nearest following row with a lower indentation
|
|
// level.
|
|
foldCurrentRow () {
|
|
const {row} = this.getCursorBufferPosition()
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
const range = (
|
|
languageMode.getFoldableRangeContainingPoint &&
|
|
languageMode.getFoldableRangeContainingPoint(Point(row, Infinity))
|
|
)
|
|
if (range) return this.displayLayer.foldBufferRange(range)
|
|
}
|
|
|
|
// Essential: Unfold the most recent cursor's row by one level.
|
|
unfoldCurrentRow () {
|
|
const {row} = this.getCursorBufferPosition()
|
|
return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false)
|
|
}
|
|
|
|
// Essential: Fold the given row in buffer coordinates based on its indentation
|
|
// level.
|
|
//
|
|
// If the given row is foldable, the fold will begin there. Otherwise, it will
|
|
// begin at the first foldable row preceding the given row.
|
|
//
|
|
// * `bufferRow` A {Number}.
|
|
foldBufferRow (bufferRow) {
|
|
let position = Point(bufferRow, Infinity)
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
while (true) {
|
|
const foldableRange = (
|
|
languageMode.getFoldableRangeContainingPoint &&
|
|
languageMode.getFoldableRangeContainingPoint(position, this.getTabLength())
|
|
)
|
|
if (foldableRange) {
|
|
const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start))
|
|
if (existingFolds.length === 0) {
|
|
this.displayLayer.foldBufferRange(foldableRange)
|
|
} else {
|
|
const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0])
|
|
if (firstExistingFoldRange.start.isLessThan(position)) {
|
|
position = Point(firstExistingFoldRange.start.row, 0)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Essential: Unfold all folds containing the given row in buffer coordinates.
|
|
//
|
|
// * `bufferRow` A {Number}
|
|
unfoldBufferRow (bufferRow) {
|
|
const position = Point(bufferRow, Infinity)
|
|
return this.displayLayer.destroyFoldsContainingBufferPositions([position])
|
|
}
|
|
|
|
// Extended: For each selection, fold the rows it intersects.
|
|
foldSelectedLines () {
|
|
for (let selection of this.selections) {
|
|
selection.fold()
|
|
}
|
|
}
|
|
|
|
// Extended: Fold all foldable lines.
|
|
foldAll () {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
const foldableRanges = (
|
|
languageMode.getFoldableRanges &&
|
|
languageMode.getFoldableRanges(this.getTabLength())
|
|
)
|
|
this.displayLayer.destroyAllFolds()
|
|
for (let range of foldableRanges || []) {
|
|
this.displayLayer.foldBufferRange(range)
|
|
}
|
|
}
|
|
|
|
// Extended: Unfold all existing folds.
|
|
unfoldAll () {
|
|
const result = this.displayLayer.destroyAllFolds()
|
|
this.scrollToCursorPosition()
|
|
return result
|
|
}
|
|
|
|
// Extended: Fold all foldable lines at the given indent level.
|
|
//
|
|
// * `level` A {Number}.
|
|
foldAllAtIndentLevel (level) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
const foldableRanges = (
|
|
languageMode.getFoldableRangesAtIndentLevel &&
|
|
languageMode.getFoldableRangesAtIndentLevel(level, this.getTabLength())
|
|
)
|
|
this.displayLayer.destroyAllFolds()
|
|
for (let range of foldableRanges || []) {
|
|
this.displayLayer.foldBufferRange(range)
|
|
}
|
|
}
|
|
|
|
// Extended: Determine whether the given row in buffer coordinates is foldable.
|
|
//
|
|
// A *foldable* row is a row that *starts* a row range that can be folded.
|
|
//
|
|
// * `bufferRow` A {Number}
|
|
//
|
|
// Returns a {Boolean}.
|
|
isFoldableAtBufferRow (bufferRow) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
return languageMode.isFoldableAtRow && languageMode.isFoldableAtRow(bufferRow)
|
|
}
|
|
|
|
// Extended: Determine whether the given row in screen coordinates is foldable.
|
|
//
|
|
// A *foldable* row is a row that *starts* a row range that can be folded.
|
|
//
|
|
// * `bufferRow` A {Number}
|
|
//
|
|
// Returns a {Boolean}.
|
|
isFoldableAtScreenRow (screenRow) {
|
|
return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow))
|
|
}
|
|
|
|
// Extended: Fold the given buffer row if it isn't currently folded, and unfold
|
|
// it otherwise.
|
|
toggleFoldAtBufferRow (bufferRow) {
|
|
if (this.isFoldedAtBufferRow(bufferRow)) {
|
|
return this.unfoldBufferRow(bufferRow)
|
|
} else {
|
|
return this.foldBufferRow(bufferRow)
|
|
}
|
|
}
|
|
|
|
// Extended: Determine whether the most recently added cursor's row is folded.
|
|
//
|
|
// Returns a {Boolean}.
|
|
isFoldedAtCursorRow () {
|
|
return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row)
|
|
}
|
|
|
|
// Extended: Determine whether the given row in buffer coordinates is folded.
|
|
//
|
|
// * `bufferRow` A {Number}
|
|
//
|
|
// Returns a {Boolean}.
|
|
isFoldedAtBufferRow (bufferRow) {
|
|
const range = Range(
|
|
Point(bufferRow, 0),
|
|
Point(bufferRow, this.buffer.lineLengthForRow(bufferRow))
|
|
)
|
|
return this.displayLayer.foldsIntersectingBufferRange(range).length > 0
|
|
}
|
|
|
|
// Extended: Determine whether the given row in screen coordinates is folded.
|
|
//
|
|
// * `screenRow` A {Number}
|
|
//
|
|
// Returns a {Boolean}.
|
|
isFoldedAtScreenRow (screenRow) {
|
|
return this.isFoldedAtBufferRow(this.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) {
|
|
return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
|
|
}
|
|
|
|
foldBufferRange (range) {
|
|
return this.displayLayer.foldBufferRange(range)
|
|
}
|
|
|
|
// Remove any {Fold}s found that intersect the given buffer range.
|
|
destroyFoldsIntersectingBufferRange (bufferRange) {
|
|
return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange)
|
|
}
|
|
|
|
// Remove any {Fold}s found that contain the given array of buffer positions.
|
|
destroyFoldsContainingBufferPositions (bufferPositions, excludeEndpoints) {
|
|
return this.displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints)
|
|
}
|
|
|
|
/*
|
|
Section: Gutters
|
|
*/
|
|
|
|
// Essential: Add a custom {Gutter}.
|
|
//
|
|
// * `options` An {Object} with the following fields:
|
|
// * `name` (required) A unique {String} to identify this gutter.
|
|
// * `priority` (optional) A {Number} that determines stacking order between
|
|
// gutters. Lower priority items are forced closer to the edges of the
|
|
// window. (default: -100)
|
|
// * `visible` (optional) {Boolean} specifying whether the gutter is visible
|
|
// initially after being created. (default: true)
|
|
//
|
|
// Returns the newly-created {Gutter}.
|
|
addGutter (options) {
|
|
return this.gutterContainer.addGutter(options)
|
|
}
|
|
|
|
// Essential: Get this editor's gutters.
|
|
//
|
|
// Returns an {Array} of {Gutter}s.
|
|
getGutters () {
|
|
return this.gutterContainer.getGutters()
|
|
}
|
|
|
|
getLineNumberGutter () {
|
|
return this.lineNumberGutter
|
|
}
|
|
|
|
// Essential: Get the gutter with the given name.
|
|
//
|
|
// Returns a {Gutter}, or `null` if no gutter exists for the given name.
|
|
gutterWithName (name) {
|
|
return this.gutterContainer.gutterWithName(name)
|
|
}
|
|
|
|
/*
|
|
Section: Scrolling the TextEditor
|
|
*/
|
|
|
|
// Essential: Scroll the editor to reveal the most recently added cursor if it is
|
|
// off-screen.
|
|
//
|
|
// * `options` (optional) {Object}
|
|
// * `center` Center the editor around the cursor if possible. (default: true)
|
|
scrollToCursorPosition (options) {
|
|
this.getLastCursor().autoscroll({center: options && options.center !== false})
|
|
}
|
|
|
|
// Essential: Scrolls the editor to the given buffer position.
|
|
//
|
|
// * `bufferPosition` An object that represents a buffer position. It can be either
|
|
// an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
|
// * `options` (optional) {Object}
|
|
// * `center` Center the editor around the position if possible. (default: false)
|
|
scrollToBufferPosition (bufferPosition, options) {
|
|
return this.scrollToScreenPosition(this.screenPositionForBufferPosition(bufferPosition), options)
|
|
}
|
|
|
|
// Essential: Scrolls the editor to the given screen position.
|
|
//
|
|
// * `screenPosition` An object that represents a screen position. It can be either
|
|
// an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
|
// * `options` (optional) {Object}
|
|
// * `center` Center the editor around the position if possible. (default: false)
|
|
scrollToScreenPosition (screenPosition, options) {
|
|
this.scrollToScreenRange(new Range(screenPosition, screenPosition), options)
|
|
}
|
|
|
|
scrollToTop () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.')
|
|
this.getElement().scrollToTop()
|
|
}
|
|
|
|
scrollToBottom () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.')
|
|
this.getElement().scrollToBottom()
|
|
}
|
|
|
|
scrollToScreenRange (screenRange, options = {}) {
|
|
if (options.clip !== false) screenRange = this.clipScreenRange(screenRange)
|
|
const scrollEvent = {screenRange, options}
|
|
if (this.component) this.component.didRequestAutoscroll(scrollEvent)
|
|
this.emitter.emit('did-request-autoscroll', scrollEvent)
|
|
}
|
|
|
|
getHorizontalScrollbarHeight () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.')
|
|
return this.getElement().getHorizontalScrollbarHeight()
|
|
}
|
|
|
|
getVerticalScrollbarWidth () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.')
|
|
return this.getElement().getVerticalScrollbarWidth()
|
|
}
|
|
|
|
pageUp () {
|
|
this.moveUp(this.getRowsPerPage())
|
|
}
|
|
|
|
pageDown () {
|
|
this.moveDown(this.getRowsPerPage())
|
|
}
|
|
|
|
selectPageUp () {
|
|
this.selectUp(this.getRowsPerPage())
|
|
}
|
|
|
|
selectPageDown () {
|
|
this.selectDown(this.getRowsPerPage())
|
|
}
|
|
|
|
// Returns the number of rows per page
|
|
getRowsPerPage () {
|
|
if (this.component) {
|
|
const clientHeight = this.component.getScrollContainerClientHeight()
|
|
const lineHeight = this.component.getLineHeight()
|
|
return Math.max(1, Math.ceil(clientHeight / lineHeight))
|
|
} else {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Config
|
|
*/
|
|
|
|
// Experimental: Is auto-indentation enabled for this editor?
|
|
//
|
|
// Returns a {Boolean}.
|
|
shouldAutoIndent () { return this.autoIndent }
|
|
|
|
// Experimental: Is auto-indentation on paste enabled for this editor?
|
|
//
|
|
// Returns a {Boolean}.
|
|
shouldAutoIndentOnPaste () { return this.autoIndentOnPaste }
|
|
|
|
// Experimental: Does this editor allow scrolling past the last line?
|
|
//
|
|
// Returns a {Boolean}.
|
|
getScrollPastEnd () {
|
|
if (this.getAutoHeight()) {
|
|
return false
|
|
} else {
|
|
return this.scrollPastEnd
|
|
}
|
|
}
|
|
|
|
// Experimental: How fast does the editor scroll in response to mouse wheel
|
|
// movements?
|
|
//
|
|
// Returns a positive {Number}.
|
|
getScrollSensitivity () { return this.scrollSensitivity }
|
|
|
|
// Experimental: Does this editor show cursors while there is a selection?
|
|
//
|
|
// Returns a positive {Boolean}.
|
|
getShowCursorOnSelection () { return this.showCursorOnSelection }
|
|
|
|
// Experimental: Are line numbers enabled for this editor?
|
|
//
|
|
// Returns a {Boolean}
|
|
doesShowLineNumbers () { return this.showLineNumbers }
|
|
|
|
// Experimental: Get the time interval within which text editing operations
|
|
// are grouped together in the editor's undo history.
|
|
//
|
|
// Returns the time interval {Number} in milliseconds.
|
|
getUndoGroupingInterval () { return this.undoGroupingInterval }
|
|
|
|
// Experimental: Get the characters that are *not* considered part of words,
|
|
// for the purpose of word-based cursor movements.
|
|
//
|
|
// Returns a {String} containing the non-word characters.
|
|
getNonWordCharacters (position) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
return (languageMode.getNonWordCharacters && languageMode.getNonWordCharacters(position)) ||
|
|
this.nonWordCharacters
|
|
}
|
|
|
|
/*
|
|
Section: Event Handlers
|
|
*/
|
|
|
|
handleLanguageModeChange () {
|
|
this.unfoldAll()
|
|
this.emitter.emit('did-change-grammar', this.buffer.getLanguageMode().getGrammar())
|
|
}
|
|
|
|
/*
|
|
Section: TextEditor Rendering
|
|
*/
|
|
|
|
// Get the Element for the editor.
|
|
getElement () {
|
|
if (!this.component) {
|
|
if (!TextEditorComponent) TextEditorComponent = require('./text-editor-component')
|
|
if (!TextEditorElement) TextEditorElement = require('./text-editor-element')
|
|
this.component = new TextEditorComponent({
|
|
model: this,
|
|
updatedSynchronously: TextEditorElement.prototype.updatedSynchronously,
|
|
initialScrollTopRow: this.initialScrollTopRow,
|
|
initialScrollLeftColumn: this.initialScrollLeftColumn
|
|
})
|
|
}
|
|
return this.component.element
|
|
}
|
|
|
|
getAllowedLocations () {
|
|
return ['center']
|
|
}
|
|
|
|
// Essential: Retrieves the greyed out placeholder of a mini editor.
|
|
//
|
|
// Returns a {String}.
|
|
getPlaceholderText () { return this.placeholderText }
|
|
|
|
// Essential: Set the greyed out placeholder of a mini editor. Placeholder text
|
|
// will be displayed when the editor has no content.
|
|
//
|
|
// * `placeholderText` {String} text that is displayed when the editor has no content.
|
|
setPlaceholderText (placeholderText) { this.update({placeholderText}) }
|
|
|
|
pixelPositionForBufferPosition (bufferPosition) {
|
|
Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead')
|
|
return this.getElement().pixelPositionForBufferPosition(bufferPosition)
|
|
}
|
|
|
|
pixelPositionForScreenPosition (screenPosition) {
|
|
Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead')
|
|
return this.getElement().pixelPositionForScreenPosition(screenPosition)
|
|
}
|
|
|
|
getVerticalScrollMargin () {
|
|
const maxScrollMargin = Math.floor(((this.height / this.getLineHeightInPixels()) - 1) / 2)
|
|
return Math.min(this.verticalScrollMargin, maxScrollMargin)
|
|
}
|
|
|
|
setVerticalScrollMargin (verticalScrollMargin) {
|
|
this.verticalScrollMargin = verticalScrollMargin
|
|
return this.verticalScrollMargin
|
|
}
|
|
|
|
getHorizontalScrollMargin () {
|
|
return Math.min(this.horizontalScrollMargin, Math.floor(((this.width / this.getDefaultCharWidth()) - 1) / 2))
|
|
}
|
|
setHorizontalScrollMargin (horizontalScrollMargin) {
|
|
this.horizontalScrollMargin = horizontalScrollMargin
|
|
return this.horizontalScrollMargin
|
|
}
|
|
|
|
getLineHeightInPixels () { return this.lineHeightInPixels }
|
|
setLineHeightInPixels (lineHeightInPixels) {
|
|
this.lineHeightInPixels = lineHeightInPixels
|
|
return this.lineHeightInPixels
|
|
}
|
|
|
|
getKoreanCharWidth () { return this.koreanCharWidth }
|
|
getHalfWidthCharWidth () { return this.halfWidthCharWidth }
|
|
getDoubleWidthCharWidth () { return this.doubleWidthCharWidth }
|
|
getDefaultCharWidth () { return this.defaultCharWidth }
|
|
|
|
ratioForCharacter (character) {
|
|
if (isKoreanCharacter(character)) {
|
|
return this.getKoreanCharWidth() / this.getDefaultCharWidth()
|
|
} else if (isHalfWidthCharacter(character)) {
|
|
return this.getHalfWidthCharWidth() / this.getDefaultCharWidth()
|
|
} else if (isDoubleWidthCharacter(character)) {
|
|
return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth()
|
|
} else {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
setDefaultCharWidth (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) {
|
|
if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth }
|
|
if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth }
|
|
if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth }
|
|
if (defaultCharWidth !== this.defaultCharWidth ||
|
|
(doubleWidthCharWidth !== this.doubleWidthCharWidth &&
|
|
halfWidthCharWidth !== this.halfWidthCharWidth &&
|
|
koreanCharWidth !== this.koreanCharWidth)) {
|
|
this.defaultCharWidth = defaultCharWidth
|
|
this.doubleWidthCharWidth = doubleWidthCharWidth
|
|
this.halfWidthCharWidth = halfWidthCharWidth
|
|
this.koreanCharWidth = koreanCharWidth
|
|
if (this.isSoftWrapped()) {
|
|
this.displayLayer.reset({
|
|
softWrapColumn: this.getSoftWrapColumn()
|
|
})
|
|
}
|
|
}
|
|
return defaultCharWidth
|
|
}
|
|
|
|
setHeight (height) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setHeight instead.')
|
|
this.getElement().setHeight(height)
|
|
}
|
|
|
|
getHeight () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getHeight instead.')
|
|
return this.getElement().getHeight()
|
|
}
|
|
|
|
getAutoHeight () { return this.autoHeight != null ? this.autoHeight : true }
|
|
|
|
getAutoWidth () { return this.autoWidth != null ? this.autoWidth : false }
|
|
|
|
setWidth (width) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setWidth instead.')
|
|
this.getElement().setWidth(width)
|
|
}
|
|
|
|
getWidth () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getWidth instead.')
|
|
return this.getElement().getWidth()
|
|
}
|
|
|
|
// Use setScrollTopRow instead of this method
|
|
setFirstVisibleScreenRow (screenRow) {
|
|
this.setScrollTopRow(screenRow)
|
|
}
|
|
|
|
getFirstVisibleScreenRow () {
|
|
return this.getElement().component.getFirstVisibleRow()
|
|
}
|
|
|
|
getLastVisibleScreenRow () {
|
|
return this.getElement().component.getLastVisibleRow()
|
|
}
|
|
|
|
getVisibleRowRange () {
|
|
return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()]
|
|
}
|
|
|
|
// Use setScrollLeftColumn instead of this method
|
|
setFirstVisibleScreenColumn (column) {
|
|
return this.setScrollLeftColumn(column)
|
|
}
|
|
|
|
getFirstVisibleScreenColumn () {
|
|
return this.getElement().component.getFirstVisibleColumn()
|
|
}
|
|
|
|
getScrollTop () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollTop instead.')
|
|
return this.getElement().getScrollTop()
|
|
}
|
|
|
|
setScrollTop (scrollTop) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollTop instead.')
|
|
this.getElement().setScrollTop(scrollTop)
|
|
}
|
|
|
|
getScrollBottom () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollBottom instead.')
|
|
return this.getElement().getScrollBottom()
|
|
}
|
|
|
|
setScrollBottom (scrollBottom) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollBottom instead.')
|
|
this.getElement().setScrollBottom(scrollBottom)
|
|
}
|
|
|
|
getScrollLeft () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollLeft instead.')
|
|
return this.getElement().getScrollLeft()
|
|
}
|
|
|
|
setScrollLeft (scrollLeft) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollLeft instead.')
|
|
this.getElement().setScrollLeft(scrollLeft)
|
|
}
|
|
|
|
getScrollRight () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollRight instead.')
|
|
return this.getElement().getScrollRight()
|
|
}
|
|
|
|
setScrollRight (scrollRight) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollRight instead.')
|
|
this.getElement().setScrollRight(scrollRight)
|
|
}
|
|
|
|
getScrollHeight () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollHeight instead.')
|
|
return this.getElement().getScrollHeight()
|
|
}
|
|
|
|
getScrollWidth () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollWidth instead.')
|
|
return this.getElement().getScrollWidth()
|
|
}
|
|
|
|
getMaxScrollTop () {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::getMaxScrollTop instead.')
|
|
return this.getElement().getMaxScrollTop()
|
|
}
|
|
|
|
getScrollTopRow () {
|
|
return this.getElement().component.getScrollTopRow()
|
|
}
|
|
|
|
setScrollTopRow (scrollTopRow) {
|
|
this.getElement().component.setScrollTopRow(scrollTopRow)
|
|
}
|
|
|
|
getScrollLeftColumn () {
|
|
return this.getElement().component.getScrollLeftColumn()
|
|
}
|
|
|
|
setScrollLeftColumn (scrollLeftColumn) {
|
|
this.getElement().component.setScrollLeftColumn(scrollLeftColumn)
|
|
}
|
|
|
|
intersectsVisibleRowRange (startRow, endRow) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.')
|
|
return this.getElement().intersectsVisibleRowRange(startRow, endRow)
|
|
}
|
|
|
|
selectionIntersectsVisibleRowRange (selection) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.')
|
|
return this.getElement().selectionIntersectsVisibleRowRange(selection)
|
|
}
|
|
|
|
screenPositionForPixelPosition (pixelPosition) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.')
|
|
return this.getElement().screenPositionForPixelPosition(pixelPosition)
|
|
}
|
|
|
|
pixelRectForScreenRange (screenRange) {
|
|
Grim.deprecate('This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.')
|
|
return this.getElement().pixelRectForScreenRange(screenRange)
|
|
}
|
|
|
|
/*
|
|
Section: Utility
|
|
*/
|
|
|
|
inspect () {
|
|
return `<TextEditor ${this.id}>`
|
|
}
|
|
|
|
emitWillInsertTextEvent (text) {
|
|
let result = true
|
|
const cancel = () => { result = false }
|
|
this.emitter.emit('will-insert-text', {cancel, text})
|
|
return result
|
|
}
|
|
|
|
/*
|
|
Section: Language Mode Delegated Methods
|
|
*/
|
|
|
|
suggestedIndentForBufferRow (bufferRow, options) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
return languageMode.suggestedIndentForBufferRow && languageMode.suggestedIndentForBufferRow(bufferRow, options)
|
|
}
|
|
|
|
// Given a buffer row, indent it.
|
|
//
|
|
// * bufferRow - The row {Number}.
|
|
// * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
|
|
autoIndentBufferRow (bufferRow, options) {
|
|
const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options)
|
|
return this.setIndentationForBufferRow(bufferRow, indentLevel, options)
|
|
}
|
|
|
|
// Indents all the rows between two buffer row numbers.
|
|
//
|
|
// * startRow - The row {Number} to start at
|
|
// * endRow - The row {Number} to end at
|
|
autoIndentBufferRows (startRow, endRow) {
|
|
let row = startRow
|
|
while (row <= endRow) {
|
|
this.autoIndentBufferRow(row)
|
|
row++
|
|
}
|
|
}
|
|
|
|
autoDecreaseIndentForBufferRow (bufferRow) {
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
const indentLevel = (
|
|
languageMode.suggestedIndentForEditedBufferRow &&
|
|
languageMode.suggestedIndentForEditedBufferRow(bufferRow)
|
|
)
|
|
if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel)
|
|
}
|
|
|
|
toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) }
|
|
|
|
toggleLineCommentsForBufferRows (start, end) {
|
|
let {
|
|
commentStartString,
|
|
commentEndString
|
|
} = this.buffer.getLanguageMode().commentStringsForPosition(Point(start, 0))
|
|
if (!commentStartString) return
|
|
commentStartString = commentStartString.trim()
|
|
|
|
if (commentEndString) {
|
|
commentEndString = commentEndString.trim()
|
|
const startDelimiterColumnRange = columnRangeForStartDelimiter(
|
|
this.buffer.lineForRow(start),
|
|
commentStartString
|
|
)
|
|
if (startDelimiterColumnRange) {
|
|
const endDelimiterColumnRange = columnRangeForEndDelimiter(
|
|
this.buffer.lineForRow(end),
|
|
commentEndString
|
|
)
|
|
if (endDelimiterColumnRange) {
|
|
this.buffer.transact(() => {
|
|
this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]])
|
|
this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]])
|
|
})
|
|
}
|
|
} else {
|
|
this.buffer.transact(() => {
|
|
const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length
|
|
this.buffer.insert([start, indentLength], commentStartString + ' ')
|
|
this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString)
|
|
})
|
|
}
|
|
} else {
|
|
let hasCommentedLines = false
|
|
let hasUncommentedLines = false
|
|
for (let row = start; row <= end; row++) {
|
|
const line = this.buffer.lineForRow(row)
|
|
if (NON_WHITESPACE_REGEXP.test(line)) {
|
|
if (columnRangeForStartDelimiter(line, commentStartString)) {
|
|
hasCommentedLines = true
|
|
} else {
|
|
hasUncommentedLines = true
|
|
}
|
|
}
|
|
}
|
|
|
|
const shouldUncomment = hasCommentedLines && !hasUncommentedLines
|
|
|
|
if (shouldUncomment) {
|
|
for (let row = start; row <= end; row++) {
|
|
const columnRange = columnRangeForStartDelimiter(
|
|
this.buffer.lineForRow(row),
|
|
commentStartString
|
|
)
|
|
if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]])
|
|
}
|
|
} else {
|
|
let minIndentLevel = Infinity
|
|
let minBlankIndentLevel = Infinity
|
|
for (let row = start; row <= end; row++) {
|
|
const line = this.buffer.lineForRow(row)
|
|
const indentLevel = this.indentLevelForLine(line)
|
|
if (NON_WHITESPACE_REGEXP.test(line)) {
|
|
if (indentLevel < minIndentLevel) minIndentLevel = indentLevel
|
|
} else {
|
|
if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel
|
|
}
|
|
}
|
|
minIndentLevel = Number.isFinite(minIndentLevel)
|
|
? minIndentLevel
|
|
: Number.isFinite(minBlankIndentLevel)
|
|
? minBlankIndentLevel
|
|
: 0
|
|
|
|
const tabLength = this.getTabLength()
|
|
const indentString = ' '.repeat(tabLength * minIndentLevel)
|
|
for (let row = start; row <= end; row++) {
|
|
const line = this.buffer.lineForRow(row)
|
|
if (NON_WHITESPACE_REGEXP.test(line)) {
|
|
const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength())
|
|
this.buffer.insert(Point(row, indentColumn), commentStartString + ' ')
|
|
} else {
|
|
this.buffer.setTextInRange(
|
|
new Range(new Point(row, 0), new Point(row, Infinity)),
|
|
indentString + commentStartString + ' '
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rowRangeForParagraphAtBufferRow (bufferRow) {
|
|
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return
|
|
|
|
const languageMode = this.buffer.getLanguageMode()
|
|
const isCommented = languageMode.isRowCommented(bufferRow)
|
|
|
|
let startRow = bufferRow
|
|
while (startRow > 0) {
|
|
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break
|
|
if (languageMode.isRowCommented(startRow - 1) !== isCommented) break
|
|
startRow--
|
|
}
|
|
|
|
let endRow = bufferRow
|
|
const rowCount = this.getLineCount()
|
|
while (endRow < rowCount) {
|
|
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break
|
|
if (languageMode.isRowCommented(endRow + 1) !== isCommented) break
|
|
endRow++
|
|
}
|
|
|
|
return new Range(new Point(startRow, 0), new Point(endRow, this.buffer.lineLengthForRow(endRow)))
|
|
}
|
|
}
|
|
|
|
function columnForIndentLevel (line, indentLevel, tabLength) {
|
|
let column = 0
|
|
let indentLength = 0
|
|
const goalIndentLength = indentLevel * tabLength
|
|
while (indentLength < goalIndentLength) {
|
|
const char = line[column]
|
|
if (char === '\t') {
|
|
indentLength += tabLength - (indentLength % tabLength)
|
|
} else if (char === ' ') {
|
|
indentLength++
|
|
} else {
|
|
break
|
|
}
|
|
column++
|
|
}
|
|
return column
|
|
}
|
|
|
|
function columnRangeForStartDelimiter (line, delimiter) {
|
|
const startColumn = line.search(NON_WHITESPACE_REGEXP)
|
|
if (startColumn === -1) return null
|
|
if (!line.startsWith(delimiter, startColumn)) return null
|
|
|
|
let endColumn = startColumn + delimiter.length
|
|
if (line[endColumn] === ' ') endColumn++
|
|
return [startColumn, endColumn]
|
|
}
|
|
|
|
function columnRangeForEndDelimiter (line, delimiter) {
|
|
let startColumn = line.lastIndexOf(delimiter)
|
|
if (startColumn === -1) return null
|
|
|
|
const endColumn = startColumn + delimiter.length
|
|
if (NON_WHITESPACE_REGEXP.test(line.slice(endColumn))) return null
|
|
if (line[startColumn - 1] === ' ') startColumn--
|
|
return [startColumn, endColumn]
|
|
}
|
|
|
|
class ChangeEvent {
|
|
constructor ({oldRange, newRange}) {
|
|
this.oldRange = oldRange
|
|
this.newRange = newRange
|
|
}
|
|
|
|
get start () {
|
|
return this.newRange.start
|
|
}
|
|
|
|
get oldExtent () {
|
|
return this.oldRange.getExtent()
|
|
}
|
|
|
|
get newExtent () {
|
|
return this.newRange.getExtent()
|
|
}
|
|
}
|