mirror of https://github.com/atom/atom.git
5693 lines
193 KiB
JavaScript
5693 lines
193 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.autoIndent = value;
|
|
break;
|
|
|
|
case 'autoIndentOnPaste':
|
|
this.autoIndentOnPaste = value;
|
|
break;
|
|
|
|
case 'undoGroupingInterval':
|
|
this.undoGroupingInterval = value;
|
|
break;
|
|
|
|
case 'scrollSensitivity':
|
|
this.scrollSensitivity = value;
|
|
break;
|
|
|
|
case 'encoding':
|
|
this.buffer.setEncoding(value);
|
|
break;
|
|
|
|
case 'softTabs':
|
|
if (value !== this.softTabs) {
|
|
this.softTabs = value;
|
|
}
|
|
break;
|
|
|
|
case 'atomicSoftTabs':
|
|
if (value !== this.displayLayer.atomicSoftTabs) {
|
|
displayLayerParams.atomicSoftTabs = value;
|
|
}
|
|
break;
|
|
|
|
case 'tabLength':
|
|
if (value > 0 && value !== this.displayLayer.tabLength) {
|
|
displayLayerParams.tabLength = value;
|
|
}
|
|
break;
|
|
|
|
case 'softWrapped':
|
|
if (value !== this.softWrapped) {
|
|
this.softWrapped = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped());
|
|
}
|
|
break;
|
|
|
|
case 'softWrapHangingIndentLength':
|
|
if (value !== this.displayLayer.softWrapHangingIndent) {
|
|
displayLayerParams.softWrapHangingIndent = value;
|
|
}
|
|
break;
|
|
|
|
case 'softWrapAtPreferredLineLength':
|
|
if (value !== this.softWrapAtPreferredLineLength) {
|
|
this.softWrapAtPreferredLineLength = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
}
|
|
break;
|
|
|
|
case 'preferredLineLength':
|
|
if (value !== this.preferredLineLength) {
|
|
this.preferredLineLength = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
}
|
|
break;
|
|
|
|
case 'maxScreenLineLength':
|
|
if (value !== this.maxScreenLineLength) {
|
|
this.maxScreenLineLength = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
}
|
|
break;
|
|
|
|
case 'mini':
|
|
if (value !== this.mini) {
|
|
this.mini = value;
|
|
this.emitter.emit('did-change-mini', value);
|
|
displayLayerParams.invisibles = this.getInvisibles();
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
displayLayerParams.showIndentGuides = this.doesShowIndentGuide();
|
|
if (this.mini) {
|
|
for (let decoration of this.cursorLineDecorations) {
|
|
decoration.destroy();
|
|
}
|
|
this.cursorLineDecorations = null;
|
|
} else {
|
|
this.decorateCursorLine();
|
|
}
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'readOnly':
|
|
if (value !== this.readOnly) {
|
|
this.readOnly = value;
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'keyboardInputEnabled':
|
|
if (value !== this.keyboardInputEnabled) {
|
|
this.keyboardInputEnabled = value;
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'placeholderText':
|
|
if (value !== this.placeholderText) {
|
|
this.placeholderText = value;
|
|
this.emitter.emit('did-change-placeholder-text', value);
|
|
}
|
|
break;
|
|
|
|
case 'lineNumberGutterVisible':
|
|
if (value !== this.lineNumberGutterVisible) {
|
|
if (value) {
|
|
this.lineNumberGutter.show();
|
|
} else {
|
|
this.lineNumberGutter.hide();
|
|
}
|
|
this.emitter.emit(
|
|
'did-change-line-number-gutter-visible',
|
|
this.lineNumberGutter.isVisible()
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'showIndentGuide':
|
|
if (value !== this.showIndentGuide) {
|
|
this.showIndentGuide = value;
|
|
displayLayerParams.showIndentGuides = this.doesShowIndentGuide();
|
|
}
|
|
break;
|
|
|
|
case 'showLineNumbers':
|
|
if (value !== this.showLineNumbers) {
|
|
this.showLineNumbers = value;
|
|
if (this.component != null) {
|
|
this.component.scheduleUpdate();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'showInvisibles':
|
|
if (value !== this.showInvisibles) {
|
|
this.showInvisibles = value;
|
|
displayLayerParams.invisibles = this.getInvisibles();
|
|
}
|
|
break;
|
|
|
|
case 'invisibles':
|
|
if (!_.isEqual(value, this.invisibles)) {
|
|
this.invisibles = value;
|
|
displayLayerParams.invisibles = this.getInvisibles();
|
|
}
|
|
break;
|
|
|
|
case 'editorWidthInChars':
|
|
if (value > 0 && value !== this.editorWidthInChars) {
|
|
this.editorWidthInChars = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
}
|
|
break;
|
|
|
|
case 'width':
|
|
if (value !== this.width) {
|
|
this.width = value;
|
|
displayLayerParams.softWrapColumn = this.getSoftWrapColumn();
|
|
}
|
|
break;
|
|
|
|
case 'scrollPastEnd':
|
|
if (value !== this.scrollPastEnd) {
|
|
this.scrollPastEnd = value;
|
|
if (this.component) this.component.scheduleUpdate();
|
|
}
|
|
break;
|
|
|
|
case 'autoHeight':
|
|
if (value !== this.autoHeight) {
|
|
this.autoHeight = value;
|
|
}
|
|
break;
|
|
|
|
case 'autoWidth':
|
|
if (value !== this.autoWidth) {
|
|
this.autoWidth = value;
|
|
}
|
|
break;
|
|
|
|
case 'showCursorOnSelection':
|
|
if (value !== this.showCursorOnSelection) {
|
|
this.showCursorOnSelection = value;
|
|
if (this.component) this.component.scheduleUpdate();
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (param !== 'ref' && param !== 'key') {
|
|
throw new TypeError(`Invalid TextEditor parameter: '${param}'`);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.displayLayer.reset(displayLayerParams);
|
|
|
|
if (this.component) {
|
|
return this.component.getNextUpdatePromise();
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
scheduleComponentUpdate() {
|
|
if (this.component) this.component.scheduleUpdate();
|
|
}
|
|
|
|
serialize() {
|
|
return {
|
|
deserializer: 'TextEditor',
|
|
version: SERIALIZATION_VERSION,
|
|
|
|
displayLayerId: this.displayLayer.id,
|
|
selectionsMarkerLayerId: this.selectionsMarkerLayer.id,
|
|
|
|
initialScrollTopRow: this.getScrollTopRow(),
|
|
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
|
|
|
tabLength: this.displayLayer.tabLength,
|
|
atomicSoftTabs: this.displayLayer.atomicSoftTabs,
|
|
softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent,
|
|
|
|
id: this.id,
|
|
bufferId: this.buffer.id,
|
|
softTabs: this.softTabs,
|
|
softWrapped: this.softWrapped,
|
|
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
|
|
preferredLineLength: this.preferredLineLength,
|
|
mini: this.mini,
|
|
readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled
|
|
keyboardInputEnabled: this.keyboardInputEnabled,
|
|
editorWidthInChars: this.editorWidthInChars,
|
|
width: this.width,
|
|
maxScreenLineLength: this.maxScreenLineLength,
|
|
registered: this.registered,
|
|
invisibles: this.invisibles,
|
|
showInvisibles: this.showInvisibles,
|
|
showIndentGuide: this.showIndentGuide,
|
|
autoHeight: this.autoHeight,
|
|
autoWidth: this.autoWidth
|
|
};
|
|
}
|
|
|
|
subscribeToBuffer() {
|
|
this.buffer.retain();
|
|
this.disposables.add(
|
|
this.buffer.onDidChangeLanguageMode(
|
|
this.handleLanguageModeChange.bind(this)
|
|
)
|
|
);
|
|
this.disposables.add(
|
|
this.buffer.onDidChangePath(() => {
|
|
this.emitter.emit('did-change-title', this.getTitle());
|
|
this.emitter.emit('did-change-path', this.getPath());
|
|
})
|
|
);
|
|
this.disposables.add(
|
|
this.buffer.onDidChangeEncoding(() => {
|
|
this.emitter.emit('did-change-encoding', this.getEncoding());
|
|
})
|
|
);
|
|
this.disposables.add(this.buffer.onDidDestroy(() => this.destroy()));
|
|
this.disposables.add(
|
|
this.buffer.onDidChangeModified(() => {
|
|
if (!this.hasTerminatedPendingState && this.buffer.isModified())
|
|
this.terminatePendingState();
|
|
})
|
|
);
|
|
}
|
|
|
|
terminatePendingState() {
|
|
if (!this.hasTerminatedPendingState)
|
|
this.emitter.emit('did-terminate-pending-state');
|
|
this.hasTerminatedPendingState = true;
|
|
}
|
|
|
|
onDidTerminatePendingState(callback) {
|
|
return this.emitter.on('did-terminate-pending-state', callback);
|
|
}
|
|
|
|
subscribeToDisplayLayer() {
|
|
this.disposables.add(
|
|
this.displayLayer.onDidChange(changes => {
|
|
this.mergeIntersectingSelections();
|
|
if (this.component) this.component.didChangeDisplayLayer(changes);
|
|
this.emitter.emit(
|
|
'did-change',
|
|
changes.map(change => new ChangeEvent(change))
|
|
);
|
|
})
|
|
);
|
|
this.disposables.add(
|
|
this.displayLayer.onDidReset(() => {
|
|
this.mergeIntersectingSelections();
|
|
if (this.component) this.component.didResetDisplayLayer();
|
|
this.emitter.emit('did-change', {});
|
|
})
|
|
);
|
|
this.disposables.add(
|
|
this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this))
|
|
);
|
|
return this.disposables.add(
|
|
this.selectionsMarkerLayer.onDidUpdate(() =>
|
|
this.component != null
|
|
? this.component.didUpdateSelections()
|
|
: undefined
|
|
)
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
if (!this.alive) return;
|
|
this.alive = false;
|
|
this.disposables.dispose();
|
|
this.displayLayer.destroy();
|
|
for (let selection of this.selections.slice()) {
|
|
selection.destroy();
|
|
}
|
|
this.buffer.release();
|
|
this.gutterContainer.destroy();
|
|
this.emitter.emit('did-destroy');
|
|
this.emitter.clear();
|
|
if (this.component) this.component.element.component = null;
|
|
this.component = null;
|
|
this.lineNumberGutter.element = null;
|
|
}
|
|
|
|
isAlive() {
|
|
return this.alive;
|
|
}
|
|
|
|
isDestroyed() {
|
|
return !this.alive;
|
|
}
|
|
|
|
/*
|
|
Section: Event Subscription
|
|
*/
|
|
|
|
// Essential: Calls your `callback` when the buffer's title has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeTitle(callback) {
|
|
return this.emitter.on('did-change-title', callback);
|
|
}
|
|
|
|
// Essential: Calls your `callback` when the buffer's path, and therefore title, has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePath(callback) {
|
|
return this.emitter.on('did-change-path', callback);
|
|
}
|
|
|
|
// Essential: Invoke the given callback synchronously when the content of the
|
|
// buffer changes.
|
|
//
|
|
// Because observers are invoked synchronously, it's important not to perform
|
|
// any expensive operations via this method. Consider {::onDidStopChanging} to
|
|
// delay expensive operations until after changes stop occurring.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChange(callback) {
|
|
return this.emitter.on('did-change', callback);
|
|
}
|
|
|
|
// Essential: Invoke `callback` when the buffer's contents change. It is
|
|
// emit asynchronously 300ms after the last buffer change. This is a good place
|
|
// to handle changes to the buffer without compromising typing performance.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidStopChanging(callback) {
|
|
return this.getBuffer().onDidStopChanging(callback);
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a {Cursor} is moved. If there are
|
|
// multiple cursors, your callback will be called for each cursor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` {Object}
|
|
// * `oldBufferPosition` {Point}
|
|
// * `oldScreenPosition` {Point}
|
|
// * `newBufferPosition` {Point}
|
|
// * `newScreenPosition` {Point}
|
|
// * `textChanged` {Boolean}
|
|
// * `cursor` {Cursor} that triggered the event
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeCursorPosition(callback) {
|
|
return this.emitter.on('did-change-cursor-position', callback);
|
|
}
|
|
|
|
// Essential: Calls your `callback` when a selection's screen range changes.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` {Object}
|
|
// * `oldBufferRange` {Range}
|
|
// * `oldScreenRange` {Range}
|
|
// * `newBufferRange` {Range}
|
|
// * `newScreenRange` {Range}
|
|
// * `selection` {Selection} that triggered the event
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeSelectionRange(callback) {
|
|
return this.emitter.on('did-change-selection-range', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when soft wrap was enabled or disabled.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeSoftWrapped(callback) {
|
|
return this.emitter.on('did-change-soft-wrapped', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the buffer's encoding has changed.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeEncoding(callback) {
|
|
return this.emitter.on('did-change-encoding', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the grammar that interprets and
|
|
// colorizes the text has been changed. Immediately calls your callback with
|
|
// the current grammar.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `grammar` {Grammar}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeGrammar(callback) {
|
|
callback(this.getGrammar());
|
|
return this.onDidChangeGrammar(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the grammar that interprets and
|
|
// colorizes the text has been changed.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `grammar` {Grammar}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeGrammar(callback) {
|
|
return this.buffer.onDidChangeLanguageMode(() => {
|
|
callback(this.buffer.getLanguageMode().grammar);
|
|
});
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the result of {::isModified} changes.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeModified(callback) {
|
|
return this.getBuffer().onDidChangeModified(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the buffer's underlying file changes on
|
|
// disk at a moment when the result of {::isModified} is true.
|
|
//
|
|
// * `callback` {Function}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidConflict(callback) {
|
|
return this.getBuffer().onDidConflict(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` before text has been inserted.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` event {Object}
|
|
// * `text` {String} text to be inserted
|
|
// * `cancel` {Function} Call to prevent the text from being inserted
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onWillInsertText(callback) {
|
|
return this.emitter.on('will-insert-text', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` after text has been inserted.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `event` event {Object}
|
|
// * `text` {String} text to be inserted
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidInsertText(callback) {
|
|
return this.emitter.on('did-insert-text', callback);
|
|
}
|
|
|
|
// Essential: Invoke the given callback after the buffer is saved to disk.
|
|
//
|
|
// * `callback` {Function} to be called after the buffer is saved.
|
|
// * `event` {Object} with the following keys:
|
|
// * `path` The path to which the buffer was saved.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidSave(callback) {
|
|
return this.getBuffer().onDidSave(callback);
|
|
}
|
|
|
|
// Essential: Invoke the given callback when the editor is destroyed.
|
|
//
|
|
// * `callback` {Function} to be called when the editor is destroyed.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidDestroy(callback) {
|
|
return this.emitter.once('did-destroy', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is added to the editor.
|
|
// Immediately calls your callback for each existing cursor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeCursors(callback) {
|
|
this.getCursors().forEach(callback);
|
|
return this.onDidAddCursor(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddCursor(callback) {
|
|
return this.emitter.on('did-add-cursor', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Cursor} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `cursor` {Cursor} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveCursor(callback) {
|
|
return this.emitter.on('did-remove-cursor', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is added to the editor.
|
|
// Immediately calls your callback for each existing selection.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeSelections(callback) {
|
|
this.getSelections().forEach(callback);
|
|
return this.onDidAddSelection(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddSelection(callback) {
|
|
return this.emitter.on('did-add-selection', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Selection} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `selection` {Selection} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveSelection(callback) {
|
|
return this.emitter.on('did-remove-selection', callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` with each {Decoration} added to the editor.
|
|
// Calls your `callback` immediately for any existing decorations.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration}
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeDecorations(callback) {
|
|
return this.decorationManager.observeDecorations(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Decoration} is added to the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration} that was added
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddDecoration(callback) {
|
|
return this.decorationManager.onDidAddDecoration(callback);
|
|
}
|
|
|
|
// Extended: Calls your `callback` when a {Decoration} is removed from the editor.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `decoration` {Decoration} that was removed
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveDecoration(callback) {
|
|
return this.decorationManager.onDidRemoveDecoration(callback);
|
|
}
|
|
|
|
// Called by DecorationManager when a decoration is added.
|
|
didAddDecoration(decoration) {
|
|
if (this.component && decoration.isType('block')) {
|
|
this.component.addBlockDecoration(decoration);
|
|
}
|
|
}
|
|
|
|
// Extended: Calls your `callback` when the placeholder text is changed.
|
|
//
|
|
// * `callback` {Function}
|
|
// * `placeholderText` {String} new text
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePlaceholderText(callback) {
|
|
return this.emitter.on('did-change-placeholder-text', callback);
|
|
}
|
|
|
|
onDidChangeScrollTop(callback) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.'
|
|
);
|
|
return this.getElement().onDidChangeScrollTop(callback);
|
|
}
|
|
|
|
onDidChangeScrollLeft(callback) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.'
|
|
);
|
|
return this.getElement().onDidChangeScrollLeft(callback);
|
|
}
|
|
|
|
onDidRequestAutoscroll(callback) {
|
|
return this.emitter.on('did-request-autoscroll', callback);
|
|
}
|
|
|
|
// TODO Remove once the tabs package no longer uses .on subscriptions
|
|
onDidChangeIcon(callback) {
|
|
return this.emitter.on('did-change-icon', callback);
|
|
}
|
|
|
|
onDidUpdateDecorations(callback) {
|
|
return this.decorationManager.onDidUpdateDecorations(callback);
|
|
}
|
|
|
|
// Retrieves the current buffer's URI.
|
|
getURI() {
|
|
return this.buffer.getUri();
|
|
}
|
|
|
|
// Create an {TextEditor} with its initial state based on this object
|
|
copy() {
|
|
const displayLayer = this.displayLayer.copy();
|
|
const selectionsMarkerLayer = displayLayer.getMarkerLayer(
|
|
this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id
|
|
);
|
|
const softTabs = this.getSoftTabs();
|
|
return new TextEditor({
|
|
buffer: this.buffer,
|
|
selectionsMarkerLayer,
|
|
softTabs,
|
|
suppressCursorCreation: true,
|
|
tabLength: this.getTabLength(),
|
|
initialScrollTopRow: this.getScrollTopRow(),
|
|
initialScrollLeftColumn: this.getScrollLeftColumn(),
|
|
assert: this.assert,
|
|
displayLayer,
|
|
grammar: this.getGrammar(),
|
|
autoWidth: this.autoWidth,
|
|
autoHeight: this.autoHeight,
|
|
showCursorOnSelection: this.showCursorOnSelection
|
|
});
|
|
}
|
|
|
|
// Controls visibility based on the given {Boolean}.
|
|
setVisible(visible) {
|
|
if (visible) {
|
|
const languageMode = this.buffer.getLanguageMode();
|
|
if (languageMode.startTokenizing) languageMode.startTokenizing();
|
|
}
|
|
}
|
|
|
|
setMini(mini) {
|
|
this.update({ mini });
|
|
}
|
|
|
|
isMini() {
|
|
return this.mini;
|
|
}
|
|
|
|
setReadOnly(readOnly) {
|
|
this.update({ readOnly });
|
|
}
|
|
|
|
isReadOnly() {
|
|
return this.readOnly;
|
|
}
|
|
|
|
enableKeyboardInput(enabled) {
|
|
this.update({ keyboardInputEnabled: enabled });
|
|
}
|
|
|
|
isKeyboardInputEnabled() {
|
|
return this.keyboardInputEnabled;
|
|
}
|
|
|
|
onDidChangeMini(callback) {
|
|
return this.emitter.on('did-change-mini', callback);
|
|
}
|
|
|
|
setLineNumberGutterVisible(lineNumberGutterVisible) {
|
|
this.update({ lineNumberGutterVisible });
|
|
}
|
|
|
|
isLineNumberGutterVisible() {
|
|
return this.lineNumberGutter.isVisible();
|
|
}
|
|
|
|
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.update({ editorWidthInChars });
|
|
}
|
|
|
|
// Returns the editor width in characters.
|
|
getEditorWidthInChars() {
|
|
if (this.width != null && this.defaultCharWidth > 0) {
|
|
return Math.max(0, Math.floor(this.width / this.defaultCharWidth));
|
|
} else {
|
|
return this.editorWidthInChars;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: 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 actaully
|
|
// 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.update({ softTabs: this.softTabs });
|
|
}
|
|
|
|
// Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor.
|
|
hasAtomicSoftTabs() {
|
|
return this.displayLayer.atomicSoftTabs;
|
|
}
|
|
|
|
// Essential: Toggle soft tabs for this editor
|
|
toggleSoftTabs() {
|
|
this.setSoftTabs(!this.getSoftTabs());
|
|
}
|
|
|
|
// Essential: Get the on-screen length of tab characters.
|
|
//
|
|
// Returns a {Number}.
|
|
getTabLength() {
|
|
return this.displayLayer.tabLength;
|
|
}
|
|
|
|
// Essential: Set the on-screen length of tab characters. Setting this to a
|
|
// {Number} This will override the `editor.tabLength` setting.
|
|
//
|
|
// * `tabLength` {Number} length of a single tab. Setting to `null` will
|
|
// fallback to using the `editor.tabLength` config setting
|
|
setTabLength(tabLength) {
|
|
this.update({ tabLength });
|
|
}
|
|
|
|
// Returns an {Object} representing the current invisible character
|
|
// substitutions for this editor, 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.update({ softWrapped });
|
|
return this.isSoftWrapped();
|
|
}
|
|
|
|
getPreferredLineLength() {
|
|
return this.preferredLineLength;
|
|
}
|
|
|
|
// Essential: Toggle soft wrapping for this editor
|
|
//
|
|
// Returns a {Boolean}.
|
|
toggleSoftWrapped() {
|
|
return this.setSoftWrapped(!this.isSoftWrapped());
|
|
}
|
|
|
|
// Essential: Gets the column at which column will soft wrap
|
|
getSoftWrapColumn() {
|
|
if (this.isSoftWrapped() && !this.mini) {
|
|
if (this.softWrapAtPreferredLineLength) {
|
|
return Math.min(this.getEditorWidthInChars(), this.preferredLineLength);
|
|
} else {
|
|
return this.getEditorWidthInChars();
|
|
}
|
|
} else {
|
|
return this.maxScreenLineLength;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Indentation
|
|
*/
|
|
|
|
// Essential: Get the indentation level of the given buffer row.
|
|
//
|
|
// Determines how deeply the given row is indented based on the soft tabs and
|
|
// tab length settings of this editor. Note that if soft tabs are enabled and
|
|
// the tab length is 2, a row with 4 leading spaces would have an indentation
|
|
// level of 2.
|
|
//
|
|
// * `bufferRow` A {Number} indicating the buffer row.
|
|
//
|
|
// Returns a {Number}.
|
|
indentationForBufferRow(bufferRow) {
|
|
return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow));
|
|
}
|
|
|
|
// Essential: Set the indentation level for the given buffer row.
|
|
//
|
|
// Inserts or removes hard tabs or spaces based on the soft tabs and tab length
|
|
// settings of this editor in order to bring it to the given indentation level.
|
|
// Note that if soft tabs are enabled and the tab length is 2, a row with 4
|
|
// leading spaces would have an indentation level of 2.
|
|
//
|
|
// * `bufferRow` A {Number} indicating the buffer row.
|
|
// * `newLevel` A {Number} indicating the new indentation level.
|
|
// * `options` (optional) An {Object} with the following keys:
|
|
// * `preserveLeadingWhitespace` `true` to preserve any whitespace already at
|
|
// the beginning of the line (default: false).
|
|
setIndentationForBufferRow(
|
|
bufferRow,
|
|
newLevel,
|
|
{ preserveLeadingWhitespace } = {}
|
|
) {
|
|
let endColumn;
|
|
if (preserveLeadingWhitespace) {
|
|
endColumn = 0;
|
|
} else {
|
|
endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length;
|
|
}
|
|
const newIndentString = this.buildIndentString(newLevel);
|
|
return this.buffer.setTextInRange(
|
|
[[bufferRow, 0], [bufferRow, endColumn]],
|
|
newIndentString
|
|
);
|
|
}
|
|
|
|
// Extended: Indent rows intersecting selections by one level.
|
|
//
|
|
// * `options` (optional) {Object}
|
|
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
|
|
indentSelectedRows(options = {}) {
|
|
if (!this.ensureWritable('indentSelectedRows', options)) return;
|
|
return this.mutateSelectedText(selection =>
|
|
selection.indentSelectedRows(options)
|
|
);
|
|
}
|
|
|
|
// Extended: Outdent rows intersecting selections by one level.
|
|
//
|
|
// * `options` (optional) {Object}
|
|
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
|
|
outdentSelectedRows(options = {}) {
|
|
if (!this.ensureWritable('outdentSelectedRows', options)) return;
|
|
return this.mutateSelectedText(selection =>
|
|
selection.outdentSelectedRows(options)
|
|
);
|
|
}
|
|
|
|
// Extended: Get the indentation level of the given line of text.
|
|
//
|
|
// Determines how deeply the given line is indented based on the soft tabs and
|
|
// tab length settings of this editor. Note that if soft tabs are enabled and
|
|
// the tab length is 2, a row with 4 leading spaces would have an indentation
|
|
// level of 2.
|
|
//
|
|
// * `line` A {String} representing a line of text.
|
|
//
|
|
// Returns a {Number}.
|
|
indentLevelForLine(line) {
|
|
const tabLength = this.getTabLength();
|
|
let indentLength = 0;
|
|
for (let i = 0, { length } = line; i < length; i++) {
|
|
const char = line[i];
|
|
if (char === '\t') {
|
|
indentLength += tabLength - (indentLength % tabLength);
|
|
} else if (char === ' ') {
|
|
indentLength++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return indentLength / tabLength;
|
|
}
|
|
|
|
// Extended: Indent rows intersecting selections based on the grammar's suggested
|
|
// indent level.
|
|
//
|
|
// * `options` (optional) {Object}
|
|
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
|
|
autoIndentSelectedRows(options = {}) {
|
|
if (!this.ensureWritable('autoIndentSelectedRows', options)) return;
|
|
return this.mutateSelectedText(selection =>
|
|
selection.autoIndentSelectedRows(options)
|
|
);
|
|
}
|
|
|
|
// Indent all lines intersecting selections. See {Selection::indent} for more
|
|
// information.
|
|
//
|
|
// * `options` (optional) {Object}
|
|
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
|
|
indent(options = {}) {
|
|
if (!this.ensureWritable('indent', options)) return;
|
|
if (options.autoIndent == null)
|
|
options.autoIndent = this.shouldAutoIndent();
|
|
this.mutateSelectedText(selection => selection.indent(options));
|
|
}
|
|
|
|
// Constructs the string used for indents.
|
|
buildIndentString(level, column = 0) {
|
|
if (this.getSoftTabs()) {
|
|
const tabStopViolation = column % this.getTabLength();
|
|
return _.multiplyString(
|
|
' ',
|
|
Math.floor(level * this.getTabLength()) - tabStopViolation
|
|
);
|
|
} else {
|
|
const excessWhitespace = _.multiplyString(
|
|
' ',
|
|
Math.round((level - Math.floor(level)) * this.getTabLength())
|
|
);
|
|
return _.multiplyString('\t', Math.floor(level)) + excessWhitespace;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Section: Grammars
|
|
*/
|
|
|
|
// Essential: Get the current {Grammar} of this editor.
|
|
getGrammar() {
|
|
const languageMode = this.buffer.getLanguageMode();
|
|
return (
|
|
(languageMode.getGrammar && languageMode.getGrammar()) || NullGrammar
|
|
);
|
|
}
|
|
|
|
// Deprecated: Set the current {Grammar} of this editor.
|
|
//
|
|
// Assigning a grammar will cause the editor to re-tokenize based on the new
|
|
// grammar.
|
|
//
|
|
// * `grammar` {Grammar}
|
|
setGrammar(grammar) {
|
|
const buffer = this.getBuffer();
|
|
buffer.setLanguageMode(
|
|
atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer)
|
|
);
|
|
}
|
|
|
|
// Experimental: Get a notification when async tokenization is completed.
|
|
onDidTokenize(callback) {
|
|
return this.emitter.on('did-tokenize', callback);
|
|
}
|
|
|
|
/*
|
|
Section: Managing Syntax Scopes
|
|
*/
|
|
|
|
// Essential: Returns a {ScopeDescriptor} that includes this editor's language.
|
|
// e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
|
|
// {Config::get} to get language specific config values.
|
|
getRootScopeDescriptor() {
|
|
return this.buffer.getLanguageMode().rootScopeDescriptor;
|
|
}
|
|
|
|
// Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer
|
|
// coordinates. Useful with {Config::get}.
|
|
//
|
|
// For example, if called with a position inside the parameter list of an
|
|
// anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with
|
|
// the following scopes array:
|
|
// `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]`
|
|
//
|
|
// * `bufferPosition` A {Point} or {Array} of `[row, column]`.
|
|
//
|
|
// Returns a {ScopeDescriptor}.
|
|
scopeDescriptorForBufferPosition(bufferPosition) {
|
|
const languageMode = this.buffer.getLanguageMode();
|
|
return languageMode.scopeDescriptorForPosition
|
|
? languageMode.scopeDescriptorForPosition(bufferPosition)
|
|
: new ScopeDescriptor({ scopes: ['text'] });
|
|
}
|
|
|
|
// 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.update({ placeholderText });
|
|
}
|
|
|
|
pixelPositionForBufferPosition(bufferPosition) {
|
|
Grim.deprecate(
|
|
'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead'
|
|
);
|
|
return this.getElement().pixelPositionForBufferPosition(bufferPosition);
|
|
}
|
|
|
|
pixelPositionForScreenPosition(screenPosition) {
|
|
Grim.deprecate(
|
|
'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead'
|
|
);
|
|
return this.getElement().pixelPositionForScreenPosition(screenPosition);
|
|
}
|
|
|
|
getVerticalScrollMargin() {
|
|
const maxScrollMargin = Math.floor(
|
|
(this.height / this.getLineHeightInPixels() - 1) / 2
|
|
);
|
|
return Math.min(this.verticalScrollMargin, maxScrollMargin);
|
|
}
|
|
|
|
setVerticalScrollMargin(verticalScrollMargin) {
|
|
this.verticalScrollMargin = verticalScrollMargin;
|
|
return this.verticalScrollMargin;
|
|
}
|
|
|
|
getHorizontalScrollMargin() {
|
|
return Math.min(
|
|
this.horizontalScrollMargin,
|
|
Math.floor((this.width / this.getDefaultCharWidth() - 1) / 2)
|
|
);
|
|
}
|
|
setHorizontalScrollMargin(horizontalScrollMargin) {
|
|
this.horizontalScrollMargin = horizontalScrollMargin;
|
|
return this.horizontalScrollMargin;
|
|
}
|
|
|
|
getLineHeightInPixels() {
|
|
return this.lineHeightInPixels;
|
|
}
|
|
setLineHeightInPixels(lineHeightInPixels) {
|
|
this.lineHeightInPixels = lineHeightInPixels;
|
|
return this.lineHeightInPixels;
|
|
}
|
|
|
|
getKoreanCharWidth() {
|
|
return this.koreanCharWidth;
|
|
}
|
|
getHalfWidthCharWidth() {
|
|
return this.halfWidthCharWidth;
|
|
}
|
|
getDoubleWidthCharWidth() {
|
|
return this.doubleWidthCharWidth;
|
|
}
|
|
getDefaultCharWidth() {
|
|
return this.defaultCharWidth;
|
|
}
|
|
|
|
ratioForCharacter(character) {
|
|
if (isKoreanCharacter(character)) {
|
|
return this.getKoreanCharWidth() / this.getDefaultCharWidth();
|
|
} else if (isHalfWidthCharacter(character)) {
|
|
return this.getHalfWidthCharWidth() / this.getDefaultCharWidth();
|
|
} else if (isDoubleWidthCharacter(character)) {
|
|
return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth();
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
setDefaultCharWidth(
|
|
defaultCharWidth,
|
|
doubleWidthCharWidth,
|
|
halfWidthCharWidth,
|
|
koreanCharWidth
|
|
) {
|
|
if (doubleWidthCharWidth == null) {
|
|
doubleWidthCharWidth = defaultCharWidth;
|
|
}
|
|
if (halfWidthCharWidth == null) {
|
|
halfWidthCharWidth = defaultCharWidth;
|
|
}
|
|
if (koreanCharWidth == null) {
|
|
koreanCharWidth = defaultCharWidth;
|
|
}
|
|
if (
|
|
defaultCharWidth !== this.defaultCharWidth ||
|
|
(doubleWidthCharWidth !== this.doubleWidthCharWidth &&
|
|
halfWidthCharWidth !== this.halfWidthCharWidth &&
|
|
koreanCharWidth !== this.koreanCharWidth)
|
|
) {
|
|
this.defaultCharWidth = defaultCharWidth;
|
|
this.doubleWidthCharWidth = doubleWidthCharWidth;
|
|
this.halfWidthCharWidth = halfWidthCharWidth;
|
|
this.koreanCharWidth = koreanCharWidth;
|
|
if (this.isSoftWrapped()) {
|
|
this.displayLayer.reset({
|
|
softWrapColumn: this.getSoftWrapColumn()
|
|
});
|
|
}
|
|
}
|
|
return defaultCharWidth;
|
|
}
|
|
|
|
setHeight(height) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setHeight instead.'
|
|
);
|
|
this.getElement().setHeight(height);
|
|
}
|
|
|
|
getHeight() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getHeight instead.'
|
|
);
|
|
return this.getElement().getHeight();
|
|
}
|
|
|
|
getAutoHeight() {
|
|
return this.autoHeight != null ? this.autoHeight : true;
|
|
}
|
|
|
|
getAutoWidth() {
|
|
return this.autoWidth != null ? this.autoWidth : false;
|
|
}
|
|
|
|
setWidth(width) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setWidth instead.'
|
|
);
|
|
this.getElement().setWidth(width);
|
|
}
|
|
|
|
getWidth() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getWidth instead.'
|
|
);
|
|
return this.getElement().getWidth();
|
|
}
|
|
|
|
// Use setScrollTopRow instead of this method
|
|
setFirstVisibleScreenRow(screenRow) {
|
|
this.setScrollTopRow(screenRow);
|
|
}
|
|
|
|
getFirstVisibleScreenRow() {
|
|
return this.getElement().component.getFirstVisibleRow();
|
|
}
|
|
|
|
getLastVisibleScreenRow() {
|
|
return this.getElement().component.getLastVisibleRow();
|
|
}
|
|
|
|
getVisibleRowRange() {
|
|
return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()];
|
|
}
|
|
|
|
// Use setScrollLeftColumn instead of this method
|
|
setFirstVisibleScreenColumn(column) {
|
|
return this.setScrollLeftColumn(column);
|
|
}
|
|
|
|
getFirstVisibleScreenColumn() {
|
|
return this.getElement().component.getFirstVisibleColumn();
|
|
}
|
|
|
|
getScrollTop() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollTop instead.'
|
|
);
|
|
return this.getElement().getScrollTop();
|
|
}
|
|
|
|
setScrollTop(scrollTop) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setScrollTop instead.'
|
|
);
|
|
this.getElement().setScrollTop(scrollTop);
|
|
}
|
|
|
|
getScrollBottom() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollBottom instead.'
|
|
);
|
|
return this.getElement().getScrollBottom();
|
|
}
|
|
|
|
setScrollBottom(scrollBottom) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setScrollBottom instead.'
|
|
);
|
|
this.getElement().setScrollBottom(scrollBottom);
|
|
}
|
|
|
|
getScrollLeft() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollLeft instead.'
|
|
);
|
|
return this.getElement().getScrollLeft();
|
|
}
|
|
|
|
setScrollLeft(scrollLeft) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setScrollLeft instead.'
|
|
);
|
|
this.getElement().setScrollLeft(scrollLeft);
|
|
}
|
|
|
|
getScrollRight() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollRight instead.'
|
|
);
|
|
return this.getElement().getScrollRight();
|
|
}
|
|
|
|
setScrollRight(scrollRight) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::setScrollRight instead.'
|
|
);
|
|
this.getElement().setScrollRight(scrollRight);
|
|
}
|
|
|
|
getScrollHeight() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollHeight instead.'
|
|
);
|
|
return this.getElement().getScrollHeight();
|
|
}
|
|
|
|
getScrollWidth() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getScrollWidth instead.'
|
|
);
|
|
return this.getElement().getScrollWidth();
|
|
}
|
|
|
|
getMaxScrollTop() {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::getMaxScrollTop instead.'
|
|
);
|
|
return this.getElement().getMaxScrollTop();
|
|
}
|
|
|
|
getScrollTopRow() {
|
|
return this.getElement().component.getScrollTopRow();
|
|
}
|
|
|
|
setScrollTopRow(scrollTopRow) {
|
|
this.getElement().component.setScrollTopRow(scrollTopRow);
|
|
}
|
|
|
|
getScrollLeftColumn() {
|
|
return this.getElement().component.getScrollLeftColumn();
|
|
}
|
|
|
|
setScrollLeftColumn(scrollLeftColumn) {
|
|
this.getElement().component.setScrollLeftColumn(scrollLeftColumn);
|
|
}
|
|
|
|
intersectsVisibleRowRange(startRow, endRow) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.'
|
|
);
|
|
return this.getElement().intersectsVisibleRowRange(startRow, endRow);
|
|
}
|
|
|
|
selectionIntersectsVisibleRowRange(selection) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.'
|
|
);
|
|
return this.getElement().selectionIntersectsVisibleRowRange(selection);
|
|
}
|
|
|
|
screenPositionForPixelPosition(pixelPosition) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.'
|
|
);
|
|
return this.getElement().screenPositionForPixelPosition(pixelPosition);
|
|
}
|
|
|
|
pixelRectForScreenRange(screenRange) {
|
|
Grim.deprecate(
|
|
'This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.'
|
|
);
|
|
return this.getElement().pixelRectForScreenRange(screenRange);
|
|
}
|
|
|
|
/*
|
|
Section: Utility
|
|
*/
|
|
|
|
inspect() {
|
|
return `<TextEditor ${this.id}>`;
|
|
}
|
|
|
|
emitWillInsertTextEvent(text) {
|
|
let result = true;
|
|
const cancel = () => {
|
|
result = false;
|
|
};
|
|
this.emitter.emit('will-insert-text', { cancel, text });
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Section: Language Mode Delegated Methods
|
|
*/
|
|
|
|
suggestedIndentForBufferRow(bufferRow, options) {
|
|
const languageMode = this.buffer.getLanguageMode();
|
|
return (
|
|
languageMode.suggestedIndentForBufferRow &&
|
|
languageMode.suggestedIndentForBufferRow(
|
|
bufferRow,
|
|
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();
|
|
}
|
|
}
|