monaco-editor
Version:
A browser based code editor
207 lines (206 loc) • 10.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LcsDiff } from '../../../../base/common/diff/diff.js';
import { commonPrefixLength, getLeadingWhitespace, splitLines } from '../../../../base/common/strings.js';
import { Range } from '../../../common/core/range.js';
import { GhostText, GhostTextPart } from './ghostText.js';
import { addPositions, lengthOfText } from './utils.js';
export class SingleTextEdit {
constructor(range, text) {
this.range = range;
this.text = text;
}
removeCommonPrefix(model, validModelRange) {
const modelRange = validModelRange ? this.range.intersectRanges(validModelRange) : this.range;
if (!modelRange) {
return this;
}
const valueToReplace = model.getValueInRange(modelRange, 1 /* EndOfLinePreference.LF */);
const commonPrefixLen = commonPrefixLength(valueToReplace, this.text);
const start = addPositions(this.range.getStartPosition(), lengthOfText(valueToReplace.substring(0, commonPrefixLen)));
const text = this.text.substring(commonPrefixLen);
const range = Range.fromPositions(start, this.range.getEndPosition());
return new SingleTextEdit(range, text);
}
augments(base) {
// The augmented completion must replace the base range, but can replace even more
return this.text.startsWith(base.text) && rangeExtends(this.range, base.range);
}
/**
* @param previewSuffixLength Sets where to split `inlineCompletion.text`.
* If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
*/
computeGhostText(model, mode, cursorPosition, previewSuffixLength = 0) {
let edit = this.removeCommonPrefix(model);
if (edit.range.endLineNumber !== edit.range.startLineNumber) {
// This edit might span multiple lines, but the first lines must be a common prefix.
return undefined;
}
const sourceLine = model.getLineContent(edit.range.startLineNumber);
const sourceIndentationLength = getLeadingWhitespace(sourceLine).length;
const suggestionTouchesIndentation = edit.range.startColumn - 1 <= sourceIndentationLength;
if (suggestionTouchesIndentation) {
// source: ··········[······abc]
// ^^^^^^^^^ inlineCompletion.range
// ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
// ^^^^^^ replacedIndentation.length
// ^^^ rangeThatDoesNotReplaceIndentation
// inlineCompletion.text: '··foo'
// ^^ suggestionAddedIndentationLength
const suggestionAddedIndentationLength = getLeadingWhitespace(edit.text).length;
const replacedIndentation = sourceLine.substring(edit.range.startColumn - 1, sourceIndentationLength);
const [startPosition, endPosition] = [edit.range.getStartPosition(), edit.range.getEndPosition()];
const newStartPosition = startPosition.column + replacedIndentation.length <= endPosition.column
? startPosition.delta(0, replacedIndentation.length)
: endPosition;
const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition);
const suggestionWithoutIndentationChange = edit.text.startsWith(replacedIndentation)
// Adds more indentation without changing existing indentation: We can add ghost text for this
? edit.text.substring(replacedIndentation.length)
// Changes or removes existing indentation. Only add ghost text for the non-indentation part.
: edit.text.substring(suggestionAddedIndentationLength);
edit = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange);
}
// This is a single line string
const valueToBeReplaced = model.getValueInRange(edit.range);
const changes = cachingDiff(valueToBeReplaced, edit.text);
if (!changes) {
// No ghost text in case the diff would be too slow to compute
return undefined;
}
const lineNumber = edit.range.startLineNumber;
const parts = new Array();
if (mode === 'prefix') {
const filteredChanges = changes.filter(c => c.originalLength === 0);
if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
// Prefixes only have a single change.
return undefined;
}
}
const previewStartInCompletionText = edit.text.length - previewSuffixLength;
for (const c of changes) {
const insertColumn = edit.range.startColumn + c.originalStart + c.originalLength;
if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === edit.range.startLineNumber && insertColumn < cursorPosition.column) {
// No ghost text before cursor
return undefined;
}
if (c.originalLength > 0) {
return undefined;
}
if (c.modifiedLength === 0) {
continue;
}
const modifiedEnd = c.modifiedStart + c.modifiedLength;
const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
const nonPreviewText = edit.text.substring(c.modifiedStart, nonPreviewTextEnd);
const italicText = edit.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
if (nonPreviewText.length > 0) {
const lines = splitLines(nonPreviewText);
parts.push(new GhostTextPart(insertColumn, lines, false));
}
if (italicText.length > 0) {
const lines = splitLines(italicText);
parts.push(new GhostTextPart(insertColumn, lines, true));
}
}
return new GhostText(lineNumber, parts);
}
}
function rangeExtends(extendingRange, rangeToExtend) {
return rangeToExtend.getStartPosition().equals(extendingRange.getStartPosition())
&& rangeToExtend.getEndPosition().isBeforeOrEqual(extendingRange.getEndPosition());
}
let lastRequest = undefined;
function cachingDiff(originalValue, newValue) {
if ((lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.originalValue) === originalValue && (lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.newValue) === newValue) {
return lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.changes;
}
else {
let changes = smartDiff(originalValue, newValue, true);
if (changes) {
const deletedChars = deletedCharacters(changes);
if (deletedChars > 0) {
// For performance reasons, don't compute diff if there is nothing to improve
const newChanges = smartDiff(originalValue, newValue, false);
if (newChanges && deletedCharacters(newChanges) < deletedChars) {
// Disabling smartness seems to be better here
changes = newChanges;
}
}
}
lastRequest = {
originalValue,
newValue,
changes
};
return changes;
}
}
function deletedCharacters(changes) {
let sum = 0;
for (const c of changes) {
sum += c.originalLength;
}
return sum;
}
/**
* When matching `if ()` with `if (f() = 1) { g(); }`,
* align it like this: `if ( )`
* Not like this: `if ( )`
* Also not like this: `if ( )`.
*
* The parenthesis are preprocessed to ensure that they match correctly.
*/
function smartDiff(originalValue, newValue, smartBracketMatching) {
if (originalValue.length > 5000 || newValue.length > 5000) {
// We don't want to work on strings that are too big
return undefined;
}
function getMaxCharCode(val) {
let maxCharCode = 0;
for (let i = 0, len = val.length; i < len; i++) {
const charCode = val.charCodeAt(i);
if (charCode > maxCharCode) {
maxCharCode = charCode;
}
}
return maxCharCode;
}
const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
function getUniqueCharCode(id) {
if (id < 0) {
throw new Error('unexpected');
}
return maxCharCode + id + 1;
}
function getElements(source) {
let level = 0;
let group = 0;
const characters = new Int32Array(source.length);
for (let i = 0, len = source.length; i < len; i++) {
// TODO support more brackets
if (smartBracketMatching && source[i] === '(') {
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id);
level++;
}
else if (smartBracketMatching && source[i] === ')') {
level = Math.max(level - 1, 0);
const id = group * 100 + level;
characters[i] = getUniqueCharCode(2 * id + 1);
if (level === 0) {
group++;
}
}
else {
characters[i] = source.charCodeAt(i);
}
}
return characters;
}
const elements1 = getElements(originalValue);
const elements2 = getElements(newValue);
return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
}