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 TextMateLanguageMode = require('./text-mate-language-mode') const ScopeDescriptor = require('./scope-descriptor') 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 const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" // 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. // // ```js // 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) // Semantics of the readOnly flag have changed since its introduction. // Only respect readOnly2, which has been set with the current readOnly semantics. delete state.readOnly state.readOnly = state.readOnly2 delete state.readOnly2 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++ if (this.id >= nextId) { // Ensure that new editors get unique ids: nextId = this.id + 1 } 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.keyboardInputEnabled = (params.keyboardInputEnabled != null) ? params.keyboardInputEnabled : true this.readOnly = (params.readOnly != null) ? params.readOnly : false this.placeholderText = params.placeholderText this.showLineNumbers = params.showLineNumbers 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.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 if (params.buffer) { this.buffer = params.buffer } else { this.buffer = new TextBuffer({ shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') } }) this.buffer.setLanguageMode(new TextMateLanguageMode({buffer: this.buffer, config: atom.config})) } const languageMode = this.buffer.getLanguageMode() this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => { this.emitter.emit('did-tokenize') }) if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription) 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, role: 'selections'}) } 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 '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) { 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 'readOnly': if (value !== this.readOnly) { this.readOnly = value if (this.component != null) { this.component.scheduleUpdate() } } break case 'keyboardInputEnabled': if (value !== this.keyboardInputEnabled) { this.keyboardInputEnabled = value 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, readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled keyboardInputEnabled: this.keyboardInputEnabled, editorWidthInChars: this.editorWidthInChars, width: this.width, 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().grammar) }) } // 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) { if (visible) { const languageMode = this.buffer.getLanguageMode() if (languageMode.startTokenizing) languageMode.startTokenizing() } } setMini (mini) { this.update({mini}) } isMini () { return this.mini } setReadOnly (readOnly) { this.update({readOnly}) } isReadOnly () { return this.readOnly } enableKeyboardInput (enabled) { this.update({keyboardInputEnabled: enabled}) } isKeyboardInputEnabled () { return this.keyboardInputEnabled } 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, // * "" when it is the only editing buffer with this file name. // * "" 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 // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. setText (text, options = {}) { if (!this.ensureWritable('setText', options)) return 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) *Deprecated* {String} 'skip' will skip the undo system. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead. // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) // // Returns the {Range} of the newly-inserted text. setTextInBufferRange (range, text, options = {}) { if (!this.ensureWritable('setTextInBufferRange', options)) return 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.ensureWritable('insertText', options)) return if (!this.emitWillInsertTextEvent(text)) return false let groupLastChanges = false if (options.undo === 'skip') { options = Object.assign({}, options) delete options.undo groupLastChanges = true } const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0 if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent() if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent() const result = this.mutateSelectedText(selection => { const range = selection.insertText(text, options) const didInsertEvent = {text, range} this.emitter.emit('did-insert-text', didInsertEvent) return range }, groupingInterval) if (groupLastChanges) this.buffer.groupLastChanges() return result } // Essential: For each selection, replace the selected text with a newline. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) delete (options = {}) { if (!this.ensureWritable('delete', options)) return return this.mutateSelectedText(selection => selection.delete(options)) } // Essential: For each selection, if the selection is empty, delete the character // preceding the cursor. Otherwise delete the selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) backspace (options = {}) { if (!this.ensureWritable('backspace', options)) return return this.mutateSelectedText(selection => selection.backspace(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineUp (options = {}) { if (!this.ensureWritable('moveLineUp', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineDown (options = {}) { if (!this.ensureWritable('moveLineDown', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionLeft (options = {}) { if (!this.ensureWritable('moveSelectionLeft', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionRight (options = {}) { if (!this.ensureWritable('moveSelectionRight', options)) return 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) }) } } // Duplicate all lines containing active selections. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) duplicateLines (options = {}) { if (!this.ensureWritable('duplicateLines', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) transpose (options = {}) { if (!this.ensureWritable('transpose', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) upperCase (options = {}) { if (!this.ensureWritable('upperCase', options)) return this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) lowerCase (options = {}) { if (!this.ensureWritable('lowerCase', options)) return this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase(options)) } // Extended: Toggle line comments for rows intersecting selections. // // If the current grammar doesn't support comments, does nothing. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) toggleLineCommentsInSelection (options = {}) { if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return this.mutateSelectedText(selection => selection.toggleLineComments(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) joinLines (options = {}) { if (!this.ensureWritable('joinLines', options)) return this.mutateSelectedText(selection => selection.joinLines()) } // Extended: For each cursor, insert a newline at beginning the following line. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineBelow (options = {}) { if (!this.ensureWritable('insertNewlineBelow', options)) return this.transact(() => { this.moveToEndOfLine() this.insertNewline(options) }) } // Extended: For each cursor, insert a newline at the end of the preceding line. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineAbove (options = {}) { if (!this.ensureWritable('insertNewlineAbove', options)) return this.transact(() => { const bufferRow = this.getCursorBufferPosition().row const indentLevel = this.indentationForBufferRow(bufferRow) const onFirstLine = bufferRow === 0 this.moveToBeginningOfLine() this.moveLeft() this.insertNewline(options) 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfWord (options = {}) { if (!this.ensureWritable('deleteToBeginningOfWord', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options)) } // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the // previous word boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToPreviousWordBoundary (options = {}) { if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options)) } // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the // next word boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToNextWordBoundary (options = {}) { if (!this.ensureWritable('deleteToNextWordBoundary', options)) return this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options)) } // Extended: For each selection, if the selection is empty, delete all characters // of the containing subword following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfSubword (options = {}) { if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options)) } // Extended: For each selection, if the selection is empty, delete all characters // of the containing subword following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfSubword (options = {}) { if (!this.ensureWritable('deleteToEndOfSubword', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfLine (options = {}) { if (!this.ensureWritable('deleteToBeginningOfLine', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfLine (options = {}) { if (!this.ensureWritable('deleteToEndOfLine', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfLine(options)) } // Extended: For each selection, if the selection is empty, delete all characters // of the containing word following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfWord (options = {}) { if (!this.ensureWritable('deleteToEndOfWord', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfWord(options)) } // Extended: Delete all lines intersecting selections. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteLine (options = {}) { if (!this.ensureWritable('deleteLine', options)) return this.mergeSelectionsOnSameRows() this.mutateSelectedText(selection => selection.deleteLine(options)) } // Private: Ensure that this editor is not marked read-only before allowing a buffer modification to occur. If // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error. ensureWritable (methodName, opts) { if (!opts.bypassReadOnly && this.isReadOnly()) { if (atom.inDevMode() || atom.inSpecMode()) { const e = new Error('Attempt to mutate a read-only TextEditor') e.detail = `Your package is attempting to call ${methodName} on an editor that has been marked read-only. ` + 'Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before attempting ' + 'modifications.' throw e } return false } return true } /* Section: History */ // Essential: Undo the last change. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) undo (options = {}) { if (!this.ensureWritable('undo', options)) return this.avoidMergingSelections(() => this.buffer.undo({selectionsMarkerLayer: this.selectionsMarkerLayer})) this.getLastSelection().autoscroll() } // Essential: Redo the last change. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) redo (options = {}) { if (!this.ensureWritable('redo', options)) return this.avoidMergingSelections(() => this.buffer.redo({selectionsMarkerLayer: this.selectionsMarkerLayer})) 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) { const options = {selectionsMarkerLayer: this.selectionsMarkerLayer} if (typeof groupingInterval === 'function') { fn = groupingInterval } else { options.groupingInterval = groupingInterval } return this.buffer.transact(options, 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({selectionsMarkerLayer: this.selectionsMarkerLayer}) } // 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, {selectionsMarkerLayer: this.selectionsMarkerLayer}) } /* 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 // // ```js // 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 // // ```js // 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 //
// //
//
// ``` // * __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 {Cursor}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: For each selection, select the syntax node that contains // that selection. selectLargerSyntaxNode () { const languageMode = this.buffer.getLanguageMode() if (!languageMode.getRangeForSyntaxNodeContainingRange) return this.expandSelectionsForward(selection => { const currentRange = selection.getBufferRange() const newRange = languageMode.getRangeForSyntaxNodeContainingRange(currentRange) if (newRange) { if (!selection._rangeStack) selection._rangeStack = [] selection._rangeStack.push(currentRange) selection.setBufferRange(newRange) } }) } // Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}. selectSmallerSyntaxNode () { this.expandSelectionsForward(selection => { if (selection._rangeStack) { const lastRange = selection._rangeStack[selection._rangeStack.length - 1] if (lastRange && selection.getBufferRange().containsRange(lastRange)) { selection._rangeStack.length-- selection.setBufferRange(lastRange) } } }) } // 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() const hasIsRowCommented = languageMode.isRowCommented for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) { if (hasIsRowCommented && 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indentSelectedRows (options = {}) { if (!this.ensureWritable('indentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.indentSelectedRows(options)) } // Extended: Outdent rows intersecting selections by one level. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. outdentSelectedRows (options = {}) { if (!this.ensureWritable('outdentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.outdentSelectedRows(options)) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. autoIndentSelectedRows (options = {}) { if (!this.ensureWritable('autoIndentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options)) } // Indent all lines intersecting selections. See {Selection::indent} for more // information. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indent (options = {}) { if (!this.ensureWritable('indent', options)) return 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 } // Deprecated: 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) { const buffer = this.getBuffer() buffer.setLanguageMode(atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer)) } // Experimental: Get a notification when async tokenization is completed. onDidTokenize (callback) { return this.emitter.on('did-tokenize', 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, this method returns a {ScopeDescriptor} with // the following scopes array: // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]` // // * `bufferPosition` A {Point} or {Array} of `[row, column]`. // // Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition (bufferPosition) { const languageMode = this.buffer.getLanguageMode() return languageMode.scopeDescriptorForPosition ? languageMode.scopeDescriptorForPosition(bufferPosition) : new ScopeDescriptor({scopes: ['text']}) } // 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutSelectedText (options = {}) { if (!this.ensureWritable('cutSelectedText', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { if (selection.isEmpty()) { selection.selectLine() selection.cut(maintainClipboard, true, options.bypassReadOnly) } else { selection.cut(maintainClipboard, false, options.bypassReadOnly) } 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 = {}) { if (!this.ensureWritable('parseText', options)) return 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfLine (options = {}) { if (!this.ensureWritable('cutToEndOfLine', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfLine(maintainClipboard, options) 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. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfBufferLine (options = {}) { if (!this.ensureWritable('cutToEndOfBufferLine', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfBufferLine(maintainClipboard, options) 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), this.getTabLength()) ) 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} starting at 0. 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 || Point(0, 0)) ) || DEFAULT_NON_WORD_CHARACTERS } /* Section: Event Handlers */ handleLanguageModeChange () { this.unfoldAll() if (this.languageModeSubscription) { this.languageModeSubscription.dispose() this.disposables.remove(this.languageModeSubscription) } const languageMode = this.buffer.getLanguageMode() if (this.component && this.component.visible && languageMode.startTokenizing) { languageMode.startTokenizing() } this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => { this.emitter.emit('did-tokenize') }) if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription) this.emitter.emit('did-change-grammar', languageMode.grammar) } /* 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 `` } 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, this.getTabLength(), 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, this.getTabLength()) ) if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel) } toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) } toggleLineCommentsForBufferRows (start, end) { const languageMode = this.buffer.getLanguageMode() let {commentStartString, commentEndString} = languageMode.commentStringsForPosition && languageMode.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 indentString = this.buildIndentString(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() } }