Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/isInlineSuggestion.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Position, Range, TextDocument } from 'vscode';
7
8
export interface InlineSuggestionEdit {
9
readonly range: Range;
10
readonly newText: string;
11
}
12
13
/**
14
* Determines whether an edit can be displayed as an inline suggestion (ghost text).
15
* If so, returns the (possibly adjusted) range and text that touches the cursor position,
16
* which is required for VS Code to render ghost text.
17
*/
18
export function toInlineSuggestion(cursorPos: Position, doc: TextDocument, range: Range, newText: string, advanced: boolean = true): InlineSuggestionEdit | undefined {
19
// Special case: a multi-line insertion that starts on the line *after* the cursor
20
// can be re-expressed as a pure insertion at the cursor.
21
const nextLineInsertion = tryAdjustNextLineInsertion(cursorPos, doc, range, newText);
22
if (nextLineInsertion) {
23
return nextLineInsertion;
24
}
25
26
// If the range spans multiple lines, try to collapse it to a single line by
27
// trimming a shared prefix up to a newline boundary.
28
if (advanced && range.start.line !== range.end.line) {
29
({ range, newText } = stripCommonLinePrefix(doc, range, newText));
30
}
31
32
// Ghost text requires the edit to be on the cursor's line.
33
if (range.start.line !== range.end.line || range.start.line !== cursorPos.line) {
34
return undefined;
35
}
36
37
return validateSameLineGhostText(cursorPos, doc, range, newText);
38
}
39
40
/**
41
* If the cursor is at the end of a line and the edit is an empty-range insertion
42
* at column 0 of the next line, rewrite it as a pure insertion at the cursor
43
* position. This is allowed when either:
44
* - `newText` ends with a newline (any existing content on the target line is
45
* pushed onto the following line), or
46
* - `newText` contains a newline and the target line is fully consumed by the
47
* insertion (no leftover content after the insertion).
48
*/
49
function tryAdjustNextLineInsertion(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined {
50
if (!range.isEmpty) {
51
return undefined;
52
}
53
if (cursorPos.line + 1 !== range.start.line || range.start.character !== 0) {
54
return undefined;
55
}
56
if (doc.lineAt(cursorPos.line).text.length !== cursorPos.character) {
57
return undefined; // cursor is not at the end of the line
58
}
59
60
const targetLineFullyConsumed = doc.lineAt(range.end.line).text.length === range.end.character;
61
const noLeftoverAfterInsertion = newText.endsWith('\n') || (newText.includes('\n') && targetLineFullyConsumed);
62
if (!noLeftoverAfterInsertion) {
63
return undefined;
64
}
65
66
// Use an empty range at the cursor so the suggestion is a pure insertion.
67
// The original line terminator between the cursor and `range.start` is preserved
68
// in the document, so:
69
// - prepend that terminator to `newText` (it lives in the doc, not in the edit), and
70
// - drop a single trailing line ending from `newText` to avoid an extra blank line.
71
// CRLF-safe so we don't leak a dangling '\r' into the suggestion.
72
const lineBreak = doc.getText(new Range(cursorPos, range.start));
73
const trimmedNewText = newText.replace(/\r?\n$/, '');
74
return { range: new Range(cursorPos, cursorPos), newText: lineBreak + trimmedNewText };
75
}
76
77
/**
78
* Strip the longest shared prefix that ends on a newline boundary from both sides
79
* of a multi-line edit. This often shrinks the range so it fits on a single line,
80
* which is required for ghost text rendering.
81
*/
82
function stripCommonLinePrefix(doc: TextDocument, range: Range, newText: string): { range: Range; newText: string } {
83
const replacedText = doc.getText(range);
84
const maxLen = Math.min(replacedText.length, newText.length);
85
let commonLen = 0;
86
while (commonLen < maxLen && replacedText[commonLen] === newText[commonLen]) {
87
commonLen++;
88
}
89
if (commonLen === 0) {
90
return { range, newText };
91
}
92
const lastNewline = replacedText.lastIndexOf('\n', commonLen - 1);
93
if (lastNewline < 0) {
94
return { range, newText };
95
}
96
const strippedLen = lastNewline + 1;
97
const newStart = doc.positionAt(doc.offsetAt(range.start) + strippedLen);
98
return { range: new Range(newStart, range.end), newText: newText.substring(strippedLen) };
99
}
100
101
/**
102
* Validate that a single-line edit can be rendered as ghost text at the cursor:
103
* - the cursor is at or after `range.start`
104
* - everything before the cursor in the replaced text matches `newText`
105
* - the replaced text is a subword of `newText` (i.e. only insertions are needed)
106
*/
107
function validateSameLineGhostText(cursorPos: Position, doc: TextDocument, range: Range, newText: string): InlineSuggestionEdit | undefined {
108
const replacedText = doc.getText(range);
109
const cursorOffsetInReplacedText = cursorPos.character - range.start.character;
110
if (cursorOffsetInReplacedText < 0) {
111
return undefined;
112
}
113
if (replacedText.substring(0, cursorOffsetInReplacedText) !== newText.substring(0, cursorOffsetInReplacedText)) {
114
return undefined;
115
}
116
if (!isSubword(replacedText, newText)) {
117
return undefined;
118
}
119
return { range, newText };
120
}
121
122
/**
123
* a is subword of b if a can be obtained by removing characters from b
124
*/
125
export function isSubword(a: string, b: string): boolean {
126
for (let aIdx = 0, bIdx = 0; aIdx < a.length; bIdx++) {
127
if (bIdx >= b.length) {
128
return false;
129
}
130
if (a[aIdx] === b[bIdx]) {
131
aIdx++;
132
}
133
}
134
return true;
135
}
136
137
138