mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Refactors the codes that were responsible for updating different parts of TextEditor out of update function, and, then, it uses those refactored functions directly, which bypasses the loop and switch case inside update. `finishUpdate` is also refactored to allow finishing update of TextEditor from inside the update functions without the need for creating objects outside and passing them in.
5846 lines
197 KiB
JavaScript
5846 lines
197 KiB
JavaScript
const _ = require('underscore-plus');
|
|
const path = require('path');
|
|
const fs = require('fs-plus');
|
|
const Grim = require('grim');
|
|
const dedent = require('dedent');
|
|
const { CompositeDisposable, Disposable, Emitter } = require('event-kit');
|
|
const TextBuffer = require('text-buffer');
|
|
const { Point, Range } = TextBuffer;
|
|
const DecorationManager = require('./decoration-manager');
|
|
const Cursor = require('./cursor');
|
|
const Selection = require('./selection');
|
|
const NullGrammar = require('./null-grammar');
|
|
const 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()
|
|
);
|
|
}
|
|
|
|
bufferRangeForScopeAtPosition(scopeSelector, position) {
|
|
return this.buffer
|
|
.getLanguageMode()
|
|
.bufferRangeForScopeAtPosition(scopeSelector, position);
|
|
}
|
|
|
|
// Extended: Determine if the given row is entirely a comment
|
|
isBufferRowCommented(bufferRow) {
|
|
const match = this.lineTextForBufferRow(bufferRow).match(/\S/);
|
|
if (match) {
|
|
if (!this.commentScopeSelector)
|
|
this.commentScopeSelector = new TextMateScopeSelector('comment.*');
|
|
return this.commentScopeSelector.matches(
|
|
this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get the scope descriptor at the cursor.
|
|
getCursorScope() {
|
|
return this.getLastCursor().getScopeDescriptor();
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|