/
githubmirror
/
atom
Обзор
Документация
Войти
/
githubmirror
/
atom
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/text-editor.js
5 855 строк
197 KB
sadick254
Fix linting
24 май 2021, 18:12
Не верифицирован
24 май 2021, 18:12
59b467e
Код
Авторство
О чём код?
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', type: '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.updateAutoIndent(value, false); break; case 'autoIndentOnPaste': this.updateAutoIndentOnPaste(value, false); break; case 'undoGroupingInterval': this.updateUndoGroupingInterval(value, false); break; case 'scrollSensitivity': this.updateScrollSensitivity(value, false); break; case 'encoding': this.updateEncoding(value, false); break; case 'softTabs': this.updateSoftTabs(value, false); break; case 'atomicSoftTabs': this.updateAtomicSoftTabs(value, false, displayLayerParams); break; case 'tabLength': this.updateTabLength(value, false, displayLayerParams); break; case 'softWrapped': this.updateSoftWrapped(value, false, displayLayerParams); break; case 'softWrapHangingIndentLength': this.updateSoftWrapHangingIndentLength( value, false, displayLayerParams ); break; case 'softWrapAtPreferredLineLength': this.updateSoftWrapAtPreferredLineLength( value, false, displayLayerParams ); break; case 'preferredLineLength': this.updatePreferredLineLength(value, false, displayLayerParams); break; case 'maxScreenLineLength': this.updateMaxScreenLineLength(value, false, displayLayerParams); break; case 'mini': this.updateMini(value, false, displayLayerParams); break; case 'readOnly': this.updateReadOnly(value, false); break; case 'keyboardInputEnabled': this.updateKeyboardInputEnabled(value, false); break; case 'placeholderText': this.updatePlaceholderText(value, false); break; case 'lineNumberGutterVisible': this.updateLineNumberGutterVisible(value, false); break; case 'showIndentGuide': this.updateShowIndentGuide(value, false, displayLayerParams); break; case 'showLineNumbers': this.updateShowLineNumbers(value, false); break; case 'showInvisibles': this.updateShowInvisibles(value, false, displayLayerParams); break; case 'invisibles': this.updateInvisibles(value, false, displayLayerParams); break; case 'editorWidthInChars': this.updateEditorWidthInChars(value, false, displayLayerParams); break; case 'width': this.updateWidth(value, false, displayLayerParams); break; case 'scrollPastEnd': this.updateScrollPastEnd(value, false); break; case 'autoHeight': this.updateAutoHight(value, false); break; case 'autoWidth': this.updateAutoWidth(value, false); break; case 'showCursorOnSelection': this.updateShowCursorOnSelection(value, false); break; default: if (param !== 'ref' && param !== 'key') { throw new TypeError(`Invalid TextEditor parameter: '${param}'`); } } } return this.finishUpdate(displayLayerParams); } finishUpdate(displayLayerParams = {}) { this.displayLayer.reset(displayLayerParams); if (this.component) { return this.component.getNextUpdatePromise(); } else { return Promise.resolve(); } } updateAutoIndent(value, finish) { this.autoIndent = value; if (finish) this.finishUpdate(); } updateAutoIndentOnPaste(value, finish) { this.autoIndentOnPaste = value; if (finish) this.finishUpdate(); } updateUndoGroupingInterval(value, finish) { this.undoGroupingInterval = value; if (finish) this.finishUpdate(); } updateScrollSensitivity(value, finish) { this.scrollSensitivity = value; if (finish) this.finishUpdate(); } updateEncoding(value, finish) { this.buffer.setEncoding(value); if (finish) this.finishUpdate(); } updateSoftTabs(value, finish) { if (value !== this.softTabs) { this.softTabs = value; } if (finish) this.finishUpdate(); } updateAtomicSoftTabs(value, finish, displayLayerParams = {}) { if (value !== this.displayLayer.atomicSoftTabs) { displayLayerParams.atomicSoftTabs = value; } if (finish) this.finishUpdate(displayLayerParams); } updateTabLength(value, finish, displayLayerParams = {}) { if (value > 0 && value !== this.displayLayer.tabLength) { displayLayerParams.tabLength = value; } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapped(value, finish, displayLayerParams = {}) { if (value !== this.softWrapped) { this.softWrapped = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped()); } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapHangingIndentLength(value, finish, displayLayerParams = {}) { if (value !== this.displayLayer.softWrapHangingIndent) { displayLayerParams.softWrapHangingIndent = value; } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapAtPreferredLineLength(value, finish, displayLayerParams = {}) { if (value !== this.softWrapAtPreferredLineLength) { this.softWrapAtPreferredLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updatePreferredLineLength(value, finish, displayLayerParams = {}) { if (value !== this.preferredLineLength) { this.preferredLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateMaxScreenLineLength(value, finish, displayLayerParams = {}) { if (value !== this.maxScreenLineLength) { this.maxScreenLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateMini(value, finish, displayLayerParams = {}) { 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(); } } if (finish) this.finishUpdate(displayLayerParams); } updateReadOnly(value, finish) { if (value !== this.readOnly) { this.readOnly = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updateKeyboardInputEnabled(value, finish) { if (value !== this.keyboardInputEnabled) { this.keyboardInputEnabled = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updatePlaceholderText(value, finish) { if (value !== this.placeholderText) { this.placeholderText = value; this.emitter.emit('did-change-placeholder-text', value); } if (finish) this.finishUpdate(); } updateLineNumberGutterVisible(value, finish) { 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() ); } if (finish) this.finishUpdate(); } updateShowIndentGuide(value, finish, displayLayerParams = {}) { if (value !== this.showIndentGuide) { this.showIndentGuide = value; displayLayerParams.showIndentGuides = this.doesShowIndentGuide(); } if (finish) this.finishUpdate(displayLayerParams); } updateShowLineNumbers(value, finish) { if (value !== this.showLineNumbers) { this.showLineNumbers = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updateShowInvisibles(value, finish, displayLayerParams = {}) { if (value !== this.showInvisibles) { this.showInvisibles = value; displayLayerParams.invisibles = this.getInvisibles(); } if (finish) this.finishUpdate(displayLayerParams); } updateInvisibles(value, finish, displayLayerParams = {}) { if (!_.isEqual(value, this.invisibles)) { this.invisibles = value; displayLayerParams.invisibles = this.getInvisibles(); } if (finish) this.finishUpdate(displayLayerParams); } updateEditorWidthInChars(value, finish, displayLayerParams = {}) { if (value > 0 && value !== this.editorWidthInChars) { this.editorWidthInChars = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateWidth(value, finish, displayLayerParams = {}) { if (value !== this.width) { this.width = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateScrollPastEnd(value, finish) { if (value !== this.scrollPastEnd) { this.scrollPastEnd = value; if (this.component) this.component.scheduleUpdate(); } if (finish) this.finishUpdate(); } updateAutoHight(value, finish) { if (value !== this.autoHeight) { this.autoHeight = value; } if (finish) this.finishUpdate(); } updateAutoWidth(value, finish) { if (value !== this.autoWidth) { this.autoWidth = value; } if (finish) this.finishUpdate(); } updateShowCursorOnSelection(value, finish) { if (value !== this.showCursorOnSelection) { this.showCursorOnSelection = value; if (this.component) this.component.scheduleUpdate(); } if (finish) this.finishUpdate(); } 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); } // 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.updateMini(mini, true); } isMini() { return this.mini; } setReadOnly(readOnly) { this.updateReadOnly(readOnly, true); } isReadOnly() { return this.readOnly; } enableKeyboardInput(enabled) { this.updateKeyboardInputEnabled(enabled, true); } isKeyboardInputEnabled() { return this.keyboardInputEnabled; } onDidChangeMini(callback) { return this.emitter.on('did-change-mini', callback); } setLineNumberGutterVisible(lineNumberGutterVisible) { this.updateLineNumberGutterVisible(lineNumberGutterVisible, true); } isLineNumberGutterVisible() { return this.lineNumberGutter.isVisible(); } anyLineNumberGutterVisible() { return this.getGutters().some( gutter => gutter.type === 'line-number' && gutter.visible ); } 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.updateEditorWidthInChars(editorWidthInChars, true); } // 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: Buffer */ // Essential: Retrieves the current {TextBuffer}. getBuffer() { return this.buffer; } /* Section: File Details */ // Essential: Get the editor's title for display in other parts of the // UI such as the tabs. // // If the editor's buffer is saved, its title is the file name. If it is // unsaved, its title is "untitled". // // Returns a {String}. getTitle() { return this.getFileName() || 'untitled'; } // Essential: Get unique title for display in other parts of the UI, such as // the window title. // // If the editor's buffer is unsaved, its title is "untitled" // If the editor's buffer is saved, its unique title is formatted as one // of the following, // * "<filename>" when it is the only editing buffer with this file name. // * "<filename> — <unique-dir-prefix>" when other buffers have this file name. // // Returns a {String} getLongTitle() { if (this.getPath()) { const fileName = this.getFileName(); let myPathSegments; const openEditorPathSegmentsWithSameFilename = []; for (const textEditor of atom.workspace.getTextEditors()) { if (textEditor.getFileName() === fileName) { const pathSegments = fs .tildify(textEditor.getDirectoryPath()) .split(path.sep); openEditorPathSegmentsWithSameFilename.push(pathSegments); if (textEditor === this) myPathSegments = pathSegments; } } if ( !myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1 ) return fileName; let commonPathSegmentCount; for (let i = 0, { length } = myPathSegments; i < length; i++) { const myPathSegment = myPathSegments[i]; if ( openEditorPathSegmentsWithSameFilename.some( segments => segments.length === i + 1 || segments[i] !== myPathSegment ) ) { commonPathSegmentCount = i; break; } } return `${fileName} \u2014 ${path.join( ...myPathSegments.slice(commonPathSegmentCount) )}`; } else { return 'untitled'; } } // Essential: Returns the {String} path of this editor's text buffer. getPath() { return this.buffer.getPath(); } getFileName() { const fullPath = this.getPath(); if (fullPath) return path.basename(fullPath); } getDirectoryPath() { const fullPath = this.getPath(); if (fullPath) return path.dirname(fullPath); } // Extended: Returns the {String} character set encoding of this editor's text // buffer. getEncoding() { return this.buffer.getEncoding(); } // Extended: Set the character set encoding to use in this editor's text // buffer. // // * `encoding` The {String} character set encoding name such as 'utf8' setEncoding(encoding) { this.buffer.setEncoding(encoding); } // Essential: Returns {Boolean} `true` if this editor has been modified. isModified() { return this.buffer.isModified(); } // Essential: Returns {Boolean} `true` if this editor has no content. isEmpty() { return this.buffer.isEmpty(); } /* Section: File Operations */ // Essential: Saves the editor's text buffer. // // See {TextBuffer::save} for more details. save() { return this.buffer.save(); } // Essential: Saves the editor's text buffer as the given path. // // See {TextBuffer::saveAs} for more details. // // * `filePath` A {String} path. saveAs(filePath) { return this.buffer.saveAs(filePath); } // Determine whether the user should be prompted to save before closing // this editor. shouldPromptToSave({ windowCloseRequested, projectHasPaths } = {}) { if ( windowCloseRequested && projectHasPaths && atom.stateStore.isConnected() ) { return this.buffer.isInConflict(); } else { return this.isModified() && !this.buffer.hasMultipleEditors(); } } // Returns an {Object} to configure dialog shown when this editor is saved // via {Pane::saveItemAs}. getSaveDialogOptions() { return {}; } /* Section: Reading Text */ // Essential: Returns a {String} representing the entire contents of the editor. getText() { return this.buffer.getText(); } // Essential: Get the text in the given {Range} in buffer coordinates. // // * `range` A {Range} or range-compatible {Array}. // // Returns a {String}. getTextInBufferRange(range) { return this.buffer.getTextInRange(range); } // Essential: Returns a {Number} representing the number of lines in the buffer. getLineCount() { return this.buffer.getLineCount(); } // Essential: Returns a {Number} representing the number of screen lines in the // editor. This accounts for folds. getScreenLineCount() { return this.displayLayer.getScreenLineCount(); } getApproximateScreenLineCount() { return this.displayLayer.getApproximateScreenLineCount(); } // Essential: Returns a {Number} representing the last zero-indexed buffer row // number of the editor. getLastBufferRow() { return this.buffer.getLastRow(); } // Essential: Returns a {Number} representing the last zero-indexed screen row // number of the editor. getLastScreenRow() { return this.getScreenLineCount() - 1; } // Essential: Returns a {String} representing the contents of the line at the // given buffer row. // // * `bufferRow` A {Number} representing a zero-indexed buffer row. lineTextForBufferRow(bufferRow) { return this.buffer.lineForRow(bufferRow); } // Essential: Returns a {String} representing the contents of the line at the // given screen row. // // * `screenRow` A {Number} representing a zero-indexed screen row. lineTextForScreenRow(screenRow) { const screenLine = this.screenLineForScreenRow(screenRow); if (screenLine) return screenLine.lineText; } logScreenLines(start = 0, end = this.getLastScreenRow()) { for (let row = start; row <= end; row++) { const line = this.lineTextForScreenRow(row); console.log(row, this.bufferRowForScreenRow(row), line, line.length); } } tokensForScreenRow(screenRow) { const tokens = []; let lineTextIndex = 0; const currentTokenScopes = []; const { lineText, tags } = this.screenLineForScreenRow(screenRow); for (const tag of tags) { if (this.displayLayer.isOpenTag(tag)) { currentTokenScopes.push(this.displayLayer.classNameForTag(tag)); } else if (this.displayLayer.isCloseTag(tag)) { currentTokenScopes.pop(); } else { tokens.push({ text: lineText.substr(lineTextIndex, tag), scopes: currentTokenScopes.slice() }); lineTextIndex += tag; } } return tokens; } screenLineForScreenRow(screenRow) { return this.displayLayer.getScreenLine(screenRow); } bufferRowForScreenRow(screenRow) { return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row; } bufferRowsForScreenRows(startScreenRow, endScreenRow) { return this.displayLayer.bufferRowsForScreenRows( startScreenRow, endScreenRow + 1 ); } screenRowForBufferRow(row) { return this.displayLayer.translateBufferPosition(Point(row, 0)).row; } getRightmostScreenPosition() { return this.displayLayer.getRightmostScreenPosition(); } getApproximateRightmostScreenPosition() { return this.displayLayer.getApproximateRightmostScreenPosition(); } getMaxScreenLineLength() { return this.getRightmostScreenPosition().column; } getLongestScreenRow() { return this.getRightmostScreenPosition().row; } getApproximateLongestScreenRow() { return this.getApproximateRightmostScreenPosition().row; } lineLengthForScreenRow(screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow); } // Returns the range for the given buffer row. // // * `row` A row {Number}. // * `options` (optional) An options hash with an `includeNewline` key. // // Returns a {Range}. bufferRangeForBufferRow(row, options) { return this.buffer.rangeForRow(row, options && options.includeNewline); } // Get the text in the given {Range}. // // Returns a {String}. getTextInRange(range) { return this.buffer.getTextInRange(range); } // {Delegates to: TextBuffer.isRowBlank} isBufferRowBlank(bufferRow) { return this.buffer.isRowBlank(bufferRow); } // {Delegates to: TextBuffer.nextNonBlankRow} nextNonBlankBufferRow(bufferRow) { return this.buffer.nextNonBlankRow(bufferRow); } // {Delegates to: TextBuffer.getEndPosition} getEofBufferPosition() { return this.buffer.getEndPosition(); } // Essential: Get the {Range} of the paragraph surrounding the most recently added // cursor. // // Returns a {Range}. getCurrentParagraphBufferRange() { return this.getLastCursor().getCurrentParagraphBufferRange(); } /* Section: Mutating Text */ // Essential: Replaces the entire contents of the buffer with the given {String}. // // * `text` A {String} to replace with // * `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 the given CSS `class` to the lines overlapping the rows // spanned by the marker. // * __line-number__: Adds the given CSS `class` to the line numbers overlapping // the rows spanned by the marker // * __text__: Injects spans into all text overlapping the marked range, then adds // the given `class` or `style` to these spans. Use this to manipulate the foreground // color or styling of text in a range. // * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor // containing nested divs that cover the marked region. For example, when the user // selects text, the selection is implemented with a highlight decoration. The structure // of this highlight will be: // ```html // <div class="highlight <your-class>"> // <!-- Will be one region for each row in the range. Spans 2 lines? There will be 2 regions. --> // <div class="region"></div> // </div> // ``` // * __overlay__: Positions the view associated with the given item at the head // or tail of the given `DisplayMarker`, depending on the `position` property. // * __gutter__: 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 {DisplayMarker}, depending on the `position` property. // Block decorations at the same screen row are ordered by their `order` property. // * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations // are created for the same marker, their class strings and style objects are combined // into a single cursor. This decoration type may be used to style existing cursors // by passing in their markers or to render artificial cursors that don't actually // exist in the model by passing a marker that isn't associated with a real cursor. // // ## 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` Determines the behavior and appearance of this {Decoration}. Supported decoration types // and their uses are listed above. // * `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. // * `order` (optional) Only applicable to decorations of type `block`. Controls // where the view is positioned relative to other block decorations at the // same screen row. If unspecified, block decorations render oldest to newest. // * `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 the created {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.updateSoftTabs(this.softTabs, true); } // 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.updateTabLength(tabLength, true); } // Returns an {Object} representing the current invisible character // substitutions for this editor, whose keys are names of invisible characters // and whose values are 1-character {Strings}s that are displayed in place of // those invisible characters 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.updateSoftWrapped(softWrapped, true); 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'] }); } // Essential: Get the syntactic tree {ScopeDescriptor} for the given position in buffer // coordinates or the syntactic {ScopeDescriptor} for TextMate language mode // // For example, if called with a position inside the parameter list of a // JavaScript class function, this method returns a {ScopeDescriptor} with // the following syntax nodes array: // `["source.js", "program", "expression_statement", "assignment_expression", "class", "class_body", "method_definition", "formal_parameters", "identifier"]` // if tree-sitter is used // and the following scopes array: // `["source.js"]` // if textmate is used // // * `bufferPosition` A {Point} or {Array} of `[row, column]`. // // Returns a {ScopeDescriptor}. syntaxTreeScopeDescriptorForBufferPosition(bufferPosition) { const languageMode = this.buffer.getLanguageMode(); return languageMode.syntaxTreeScopeDescriptorForPosition ? languageMode.syntaxTreeScopeDescriptorForPosition(bufferPosition) : this.scopeDescriptorForBufferPosition(bufferPosition); } // Extended: Get the range in buffer coordinates of all tokens surrounding the // cursor that match the given scope selector. // // For example, if you wanted to find the string surrounding the cursor, you // could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. // // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` // // Returns a {Range}. bufferRangeForScopeAtCursor(scopeSelector) { return this.bufferRangeForScopeAtPosition( scopeSelector, this.getCursorBufferPosition() ); } // Extended: Get the range in buffer coordinates of all tokens surrounding the // given position in buffer coordinates that match the given scope selector. // // For example, if you wanted to find the string surrounding the cursor, you // could call `editor.bufferRangeForScopeAtPosition(".string.quoted", this.getCursorBufferPosition())`. // // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` // * `bufferPosition` A {Point} or {Array} of [row, column] // // Returns a {Range}. bufferRangeForScopeAtPosition(scopeSelector, bufferPosition) { return this.buffer .getLanguageMode() .bufferRangeForScopeAtPosition(scopeSelector, bufferPosition); } // 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(); } // Get the syntax nodes at the cursor. getCursorSyntaxTreeScope() { return this.getLastCursor().getSyntaxTreeScopeDescriptor(); } 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(); if (result.length > 0) 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) // * `type` (optional) {String} specifying the type of gutter to create. `'decorated'` // gutters are useful as a destination for decorations created with {Gutter::decorateMarker}. // `'line-number'` gutters. // * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element. // * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number // element. Should return a {String} that will be used to label the corresponding line. // * `lineData` an {Object} containing information about each line to label. // * `bufferRow` {Number} indicating the zero-indexed buffer index of this line. // * `screenRow` {Number} indicating the zero-indexed screen index. // * `foldable` {Boolean} that is `true` if a fold may be created here. // * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row. // * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row. // * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number // element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the // clicked buffer row. // * `lineData` an {Object} containing information about the line that's being clicked. // * `bufferRow` {Number} of the originating line element // * `screenRow` {Number} // * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within // within this `type: 'line-number'` {Gutter}. // * `lineData` an {Object} containing information about the line that's being clicked. // * `bufferRow` {Number} of the originating line element // * `screenRow` {Number} // // 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.updatePlaceholderText(placeholderText, true); } pixelPositionForBufferPosition(bufferPosition) { Grim.deprecate( 'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead' ); return this.getElement().pixelPositionForBufferPosition(bufferPosition); } pixelPositionForScreenPosition(screenPosition) { Grim.deprecate( 'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead' ); return this.getElement().pixelPositionForScreenPosition(screenPosition); } getVerticalScrollMargin() { const maxScrollMargin = Math.floor( (this.height / this.getLineHeightInPixels() - 1) / 2 ); return Math.min(this.verticalScrollMargin, maxScrollMargin); } setVerticalScrollMargin(verticalScrollMargin) { this.verticalScrollMargin = verticalScrollMargin; return this.verticalScrollMargin; } getHorizontalScrollMargin() { return Math.min( this.horizontalScrollMargin, Math.floor((this.width / this.getDefaultCharWidth() - 1) / 2) ); } setHorizontalScrollMargin(horizontalScrollMargin) { this.horizontalScrollMargin = horizontalScrollMargin; return this.horizontalScrollMargin; } getLineHeightInPixels() { return this.lineHeightInPixels; } setLineHeightInPixels(lineHeightInPixels) { this.lineHeightInPixels = lineHeightInPixels; return this.lineHeightInPixels; } getKoreanCharWidth() { return this.koreanCharWidth; } getHalfWidthCharWidth() { return this.halfWidthCharWidth; } getDoubleWidthCharWidth() { return this.doubleWidthCharWidth; } getDefaultCharWidth() { return this.defaultCharWidth; } ratioForCharacter(character) { if (isKoreanCharacter(character)) { return this.getKoreanCharWidth() / this.getDefaultCharWidth(); } else if (isHalfWidthCharacter(character)) { return this.getHalfWidthCharWidth() / this.getDefaultCharWidth(); } else if (isDoubleWidthCharacter(character)) { return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth(); } else { return 1; } } setDefaultCharWidth( defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth ) { if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth; } if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth; } if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth; } if ( defaultCharWidth !== this.defaultCharWidth || (doubleWidthCharWidth !== this.doubleWidthCharWidth && halfWidthCharWidth !== this.halfWidthCharWidth && koreanCharWidth !== this.koreanCharWidth) ) { this.defaultCharWidth = defaultCharWidth; this.doubleWidthCharWidth = doubleWidthCharWidth; this.halfWidthCharWidth = halfWidthCharWidth; this.koreanCharWidth = koreanCharWidth; if (this.isSoftWrapped()) { this.displayLayer.reset({ softWrapColumn: this.getSoftWrapColumn() }); } } return defaultCharWidth; } setHeight(height) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setHeight instead.' ); this.getElement().setHeight(height); } getHeight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getHeight instead.' ); return this.getElement().getHeight(); } getAutoHeight() { return this.autoHeight != null ? this.autoHeight : true; } getAutoWidth() { return this.autoWidth != null ? this.autoWidth : false; } setWidth(width) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setWidth instead.' ); this.getElement().setWidth(width); } getWidth() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getWidth instead.' ); return this.getElement().getWidth(); } // Use setScrollTopRow instead of this method setFirstVisibleScreenRow(screenRow) { this.setScrollTopRow(screenRow); } getFirstVisibleScreenRow() { return this.getElement().component.getFirstVisibleRow(); } getLastVisibleScreenRow() { return this.getElement().component.getLastVisibleRow(); } getVisibleRowRange() { return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()]; } // Use setScrollLeftColumn instead of this method setFirstVisibleScreenColumn(column) { return this.setScrollLeftColumn(column); } getFirstVisibleScreenColumn() { return this.getElement().component.getFirstVisibleColumn(); } getScrollTop() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollTop instead.' ); return this.getElement().getScrollTop(); } setScrollTop(scrollTop) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollTop instead.' ); this.getElement().setScrollTop(scrollTop); } getScrollBottom() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollBottom instead.' ); return this.getElement().getScrollBottom(); } setScrollBottom(scrollBottom) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollBottom instead.' ); this.getElement().setScrollBottom(scrollBottom); } getScrollLeft() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollLeft instead.' ); return this.getElement().getScrollLeft(); } setScrollLeft(scrollLeft) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollLeft instead.' ); this.getElement().setScrollLeft(scrollLeft); } getScrollRight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollRight instead.' ); return this.getElement().getScrollRight(); } setScrollRight(scrollRight) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollRight instead.' ); this.getElement().setScrollRight(scrollRight); } getScrollHeight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollHeight instead.' ); return this.getElement().getScrollHeight(); } getScrollWidth() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollWidth instead.' ); return this.getElement().getScrollWidth(); } getMaxScrollTop() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getMaxScrollTop instead.' ); return this.getElement().getMaxScrollTop(); } getScrollTopRow() { return this.getElement().component.getScrollTopRow(); } setScrollTopRow(scrollTopRow) { this.getElement().component.setScrollTopRow(scrollTopRow); } getScrollLeftColumn() { return this.getElement().component.getScrollLeftColumn(); } setScrollLeftColumn(scrollLeftColumn) { this.getElement().component.setScrollLeftColumn(scrollLeftColumn); } intersectsVisibleRowRange(startRow, endRow) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.' ); return this.getElement().intersectsVisibleRowRange(startRow, endRow); } selectionIntersectsVisibleRowRange(selection) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.' ); return this.getElement().selectionIntersectsVisibleRowRange(selection); } screenPositionForPixelPosition(pixelPosition) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.' ); return this.getElement().screenPositionForPixelPosition(pixelPosition); } pixelRectForScreenRange(screenRange) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.' ); return this.getElement().pixelRectForScreenRange(screenRange); } /* Section: Utility */ inspect() { return `<TextEditor ${this.id}>`; } emitWillInsertTextEvent(text) { let result = true; const cancel = () => { result = false; }; this.emitter.emit('will-insert-text', { cancel, text }); return result; } /* Section: Language Mode Delegated Methods */ suggestedIndentForBufferRow(bufferRow, options) { const languageMode = this.buffer.getLanguageMode(); return ( languageMode.suggestedIndentForBufferRow && languageMode.suggestedIndentForBufferRow( bufferRow, 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, options = {}) { const languageMode = this.buffer.getLanguageMode(); let { commentStartString, commentEndString } = (languageMode.commentStringsForPosition && languageMode.commentStringsForPosition(new 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 ); // Prevent the cursor from selecting / passing the delimiters // See https://github.com/atom/atom/pull/17519 if (options.correctSelection && options.selection) { const endLineLength = this.buffer.lineLengthForRow(end); const oldRange = options.selection.getBufferRange(); if (oldRange.isEmpty()) { if (oldRange.start.column === endLineLength) { const endCol = endLineLength - commentEndString.length - 1; options.selection.setBufferRange( [[end, endCol], [end, endCol]], { autoscroll: false } ); } } else { const startDelta = oldRange.start.column === indentLength ? [0, commentStartString.length + 1] : [0, 0]; const endDelta = oldRange.end.column === endLineLength ? [0, -commentEndString.length - 1] : [0, 0]; options.selection.setBufferRange( oldRange.translate(startDelta, endDelta), { autoscroll: false } ); } } }); } } 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 + 1 < 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(); } }