@singapore-editor/lsp-plugin 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -0
- package/dist/bufferSync.d.ts +12 -0
- package/dist/bufferSync.d.ts.map +1 -0
- package/dist/bufferSync.js +35 -0
- package/dist/bufferSync.js.map +1 -0
- package/dist/codeActions.d.ts +83 -0
- package/dist/codeActions.d.ts.map +1 -0
- package/dist/codeActions.js +294 -0
- package/dist/codeActions.js.map +1 -0
- package/dist/colorAlpha.d.ts +4 -0
- package/dist/colorAlpha.d.ts.map +1 -0
- package/dist/colorAlpha.js +28 -0
- package/dist/colorAlpha.js.map +1 -0
- package/dist/completion.d.ts +124 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +609 -0
- package/dist/completion.js.map +1 -0
- package/dist/completionCommit.d.ts +19 -0
- package/dist/completionCommit.d.ts.map +1 -0
- package/dist/completionCommit.js +57 -0
- package/dist/completionCommit.js.map +1 -0
- package/dist/completionController.d.ts +89 -0
- package/dist/completionController.d.ts.map +1 -0
- package/dist/completionController.js +376 -0
- package/dist/completionController.js.map +1 -0
- package/dist/completionProviders.d.ts +50 -0
- package/dist/completionProviders.d.ts.map +1 -0
- package/dist/completionProviders.js +64 -0
- package/dist/completionProviders.js.map +1 -0
- package/dist/connectionContext.d.ts +7 -0
- package/dist/connectionContext.d.ts.map +1 -0
- package/dist/definitionLinkController.d.ts +54 -0
- package/dist/definitionLinkController.d.ts.map +1 -0
- package/dist/definitionLinkController.js +221 -0
- package/dist/definitionLinkController.js.map +1 -0
- package/dist/definitionNavigation.d.ts +76 -0
- package/dist/definitionNavigation.d.ts.map +1 -0
- package/dist/definitionNavigation.js +143 -0
- package/dist/definitionNavigation.js.map +1 -0
- package/dist/diagnosticNotes.d.ts +7 -0
- package/dist/diagnosticNotes.d.ts.map +1 -0
- package/dist/diagnosticNotes.js +37 -0
- package/dist/diagnosticNotes.js.map +1 -0
- package/dist/diagnosticProjection.d.ts +31 -0
- package/dist/diagnosticProjection.d.ts.map +1 -0
- package/dist/diagnosticProjection.js +79 -0
- package/dist/diagnosticProjection.js.map +1 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +94 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/diagnosticsPresenter.d.ts +64 -0
- package/dist/diagnosticsPresenter.d.ts.map +1 -0
- package/dist/diagnosticsPresenter.js +305 -0
- package/dist/diagnosticsPresenter.js.map +1 -0
- package/dist/document.d.ts +69 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +190 -0
- package/dist/document.js.map +1 -0
- package/dist/documentHighlightController.d.ts +34 -0
- package/dist/documentHighlightController.d.ts.map +1 -0
- package/dist/documentHighlightController.js +101 -0
- package/dist/documentHighlightController.js.map +1 -0
- package/dist/documentSnapshot.d.ts +9 -0
- package/dist/documentSnapshot.d.ts.map +1 -0
- package/dist/documentSnapshot.js +31 -0
- package/dist/documentSnapshot.js.map +1 -0
- package/dist/documentSync.d.ts +53 -0
- package/dist/documentSync.d.ts.map +1 -0
- package/dist/documentSync.js +334 -0
- package/dist/documentSync.js.map +1 -0
- package/dist/documentSyncController.d.ts +26 -0
- package/dist/documentSyncController.d.ts.map +1 -0
- package/dist/documentSyncController.js +24 -0
- package/dist/documentSyncController.js.map +1 -0
- package/dist/formatOnType.d.ts +43 -0
- package/dist/formatOnType.d.ts.map +1 -0
- package/dist/formatOnType.js +198 -0
- package/dist/formatOnType.js.map +1 -0
- package/dist/formatting.d.ts +18 -0
- package/dist/formatting.d.ts.map +1 -0
- package/dist/formatting.js +52 -0
- package/dist/formatting.js.map +1 -0
- package/dist/fuzzyMatch.d.ts +22 -0
- package/dist/fuzzyMatch.d.ts.map +1 -0
- package/dist/fuzzyMatch.js +216 -0
- package/dist/fuzzyMatch.js.map +1 -0
- package/dist/hoverParticipant.d.ts +24 -0
- package/dist/hoverParticipant.d.ts.map +1 -0
- package/dist/hoverParticipant.js +156 -0
- package/dist/hoverParticipant.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/keyCommands.d.ts +19 -0
- package/dist/keyCommands.d.ts.map +1 -0
- package/dist/keyCommands.js +21 -0
- package/dist/keyCommands.js.map +1 -0
- package/dist/lane.d.ts +28 -0
- package/dist/lane.d.ts.map +1 -0
- package/dist/lane.js +138 -0
- package/dist/lane.js.map +1 -0
- package/dist/lspConnection.d.ts +94 -0
- package/dist/lspConnection.d.ts.map +1 -0
- package/dist/lspConnection.js +195 -0
- package/dist/lspConnection.js.map +1 -0
- package/dist/lspConnectionPool.d.ts +34 -0
- package/dist/lspConnectionPool.d.ts.map +1 -0
- package/dist/lspConnectionPool.js +255 -0
- package/dist/lspConnectionPool.js.map +1 -0
- package/dist/minimalEdits.d.ts +21 -0
- package/dist/minimalEdits.d.ts.map +1 -0
- package/dist/minimalEdits.js +181 -0
- package/dist/minimalEdits.js.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +38 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugin.d.ts +100 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +837 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.styles.d.ts +31 -0
- package/dist/plugin.styles.d.ts.map +1 -0
- package/dist/plugin.styles.js +102 -0
- package/dist/plugin.styles.js.map +1 -0
- package/dist/pluginTypes.d.ts +41 -0
- package/dist/pluginTypes.d.ts.map +1 -0
- package/dist/pluginTypes.js +0 -0
- package/dist/pullDiagnostics.d.ts +30 -0
- package/dist/pullDiagnostics.d.ts.map +1 -0
- package/dist/pullDiagnostics.js +94 -0
- package/dist/pullDiagnostics.js.map +1 -0
- package/dist/renameWidget.d.ts +30 -0
- package/dist/renameWidget.d.ts.map +1 -0
- package/dist/renameWidget.js +117 -0
- package/dist/renameWidget.js.map +1 -0
- package/dist/semanticTokenDecoder.d.ts +81 -0
- package/dist/semanticTokenDecoder.d.ts.map +1 -0
- package/dist/semanticTokenDecoder.js +132 -0
- package/dist/semanticTokenDecoder.js.map +1 -0
- package/dist/semanticTokens.d.ts +47 -0
- package/dist/semanticTokens.d.ts.map +1 -0
- package/dist/semanticTokens.js +68 -0
- package/dist/semanticTokens.js.map +1 -0
- package/dist/serverSet.d.ts +69 -0
- package/dist/serverSet.d.ts.map +1 -0
- package/dist/serverSet.js +331 -0
- package/dist/serverSet.js.map +1 -0
- package/dist/signatureHelp.d.ts +40 -0
- package/dist/signatureHelp.d.ts.map +1 -0
- package/dist/signatureHelp.js +108 -0
- package/dist/signatureHelp.js.map +1 -0
- package/dist/signatureHelpController.d.ts +52 -0
- package/dist/signatureHelpController.d.ts.map +1 -0
- package/dist/signatureHelpController.js +160 -0
- package/dist/signatureHelpController.js.map +1 -0
- package/dist/sourceText.d.ts +22 -0
- package/dist/sourceText.d.ts.map +1 -0
- package/dist/sourceText.js +70 -0
- package/dist/sourceText.js.map +1 -0
- package/dist/types.d.ts +238 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/viewDocumentSnapshot.d.ts +5 -0
- package/dist/viewDocumentSnapshot.d.ts.map +1 -0
- package/dist/viewDocumentSnapshot.js +12 -0
- package/dist/viewDocumentSnapshot.js.map +1 -0
- package/dist/websocket.d.ts +6 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +4 -0
- package/dist/workspaceEdit.d.ts +66 -0
- package/dist/workspaceEdit.d.ts.map +1 -0
- package/dist/workspaceEdit.js +276 -0
- package/dist/workspaceEdit.js.map +1 -0
- package/dist/workspaceEditProvenance.d.ts +5 -0
- package/dist/workspaceEditProvenance.d.ts.map +1 -0
- package/dist/workspaceEditProvenance.js +16 -0
- package/dist/workspaceEditProvenance.js.map +1 -0
- package/dist/workspaceTextEdits.d.ts +60 -0
- package/dist/workspaceTextEdits.d.ts.map +1 -0
- package/dist/workspaceTextEdits.js +395 -0
- package/dist/workspaceTextEdits.js.map +1 -0
- package/package.json +118 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { rangeAroundOffset, textReadSnapshotOf } from "./sourceText.js";
|
|
2
|
+
import { fuzzyMatch, looseFuzzyMatch } from "./fuzzyMatch.js";
|
|
3
|
+
import { lspPositionToOffsetInSnapshot } from "@singapore-editor/lsp";
|
|
4
|
+
import { createEditorCapabilityToken, parseSnippet, snippetInitialSelection } from "@singapore-editor/core/extensions";
|
|
5
|
+
import { createAnchoredSurface } from "@singapore-editor/plugin-ui/anchored-surface";
|
|
6
|
+
//#region src/completion.ts
|
|
7
|
+
var LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE_ID = "editor.lsp-plugin.completion-edit";
|
|
8
|
+
var COMPLETION_REQUEST_DEBOUNCE_MS = 80;
|
|
9
|
+
var LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE = createEditorCapabilityToken(LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE_ID);
|
|
10
|
+
var COMPLETION_WIDGET_GAP_PX = 4;
|
|
11
|
+
var COMPLETION_WIDGET_WIDTH_PX = 320;
|
|
12
|
+
var COMPLETION_WIDGET_MAX_HEIGHT_PX = 280;
|
|
13
|
+
var COMPLETION_WIDGET_MARGIN_PX = 12;
|
|
14
|
+
var COMPLETION_WIDGET_CLASS_NAMESPACE = "lsp-plugin";
|
|
15
|
+
var COMPLETION_TRIGGER_CHARACTERS = /* @__PURE__ */ new Set([
|
|
16
|
+
".",
|
|
17
|
+
"\"",
|
|
18
|
+
"'",
|
|
19
|
+
"`",
|
|
20
|
+
"/",
|
|
21
|
+
"@",
|
|
22
|
+
"<",
|
|
23
|
+
"#"
|
|
24
|
+
]);
|
|
25
|
+
var COMPLETION_THEME_VARIABLES = [
|
|
26
|
+
"--editor-background",
|
|
27
|
+
"--editor-popup-background",
|
|
28
|
+
"--editor-foreground",
|
|
29
|
+
"--editor-caret-color"
|
|
30
|
+
];
|
|
31
|
+
function createCompletionWidgetController(options) {
|
|
32
|
+
const classNames = completionWidgetClassNames(options.classNamespace);
|
|
33
|
+
const element = createCompletionWidgetElement(options.document, classNames);
|
|
34
|
+
options.document.body.append(element);
|
|
35
|
+
const surface = createAnchoredSurface({
|
|
36
|
+
element,
|
|
37
|
+
anchorClassName: `${classNames.root}-anchor`,
|
|
38
|
+
preferredPlacement: "bottom",
|
|
39
|
+
alignment: "start",
|
|
40
|
+
gapPx: COMPLETION_WIDGET_GAP_PX,
|
|
41
|
+
viewportMarginPx: COMPLETION_WIDGET_MARGIN_PX,
|
|
42
|
+
maxHeightPx: COMPLETION_WIDGET_MAX_HEIGHT_PX
|
|
43
|
+
});
|
|
44
|
+
let items = [];
|
|
45
|
+
let selectedIndex = 0;
|
|
46
|
+
let disposed = false;
|
|
47
|
+
const render = () => {
|
|
48
|
+
element.replaceChildren();
|
|
49
|
+
for (let index = 0; index < items.length; index += 1) element.append(completionRowElement(element.ownerDocument, items[index], index, classNames));
|
|
50
|
+
syncSelectedRows(element, selectedIndex, classNames);
|
|
51
|
+
};
|
|
52
|
+
const show = (showOptions) => {
|
|
53
|
+
items = showOptions.items;
|
|
54
|
+
selectedIndex = clampIndex(showOptions.selectedIndex ?? 0, items.length);
|
|
55
|
+
syncEditorThemeVariables(element, options.themeSource);
|
|
56
|
+
render();
|
|
57
|
+
element.hidden = items.length === 0;
|
|
58
|
+
if (element.hidden) surface.release();
|
|
59
|
+
else surface.place(showOptions.anchor);
|
|
60
|
+
};
|
|
61
|
+
const reanchor = (anchor) => {
|
|
62
|
+
if (element.hidden) return;
|
|
63
|
+
surface.place(anchor);
|
|
64
|
+
};
|
|
65
|
+
const hide = () => {
|
|
66
|
+
element.hidden = true;
|
|
67
|
+
surface.release();
|
|
68
|
+
element.replaceChildren();
|
|
69
|
+
items = [];
|
|
70
|
+
selectedIndex = 0;
|
|
71
|
+
};
|
|
72
|
+
const moveSelection = (delta) => {
|
|
73
|
+
if (items.length === 0) return;
|
|
74
|
+
selectedIndex = wrapIndex(selectedIndex + delta, items.length);
|
|
75
|
+
syncSelectedRows(element, selectedIndex, classNames);
|
|
76
|
+
};
|
|
77
|
+
const selectedItem = () => items[selectedIndex] ?? null;
|
|
78
|
+
const handlePointerDown = (event) => {
|
|
79
|
+
const row = completionRowTarget(event.target, classNames);
|
|
80
|
+
if (!row) return;
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
event.stopPropagation();
|
|
83
|
+
selectedIndex = rowIndex(row);
|
|
84
|
+
syncSelectedRows(element, selectedIndex, classNames);
|
|
85
|
+
options.onSelect(selectedIndex);
|
|
86
|
+
};
|
|
87
|
+
element.addEventListener("pointerdown", handlePointerDown);
|
|
88
|
+
const dispose = () => {
|
|
89
|
+
if (disposed) return;
|
|
90
|
+
disposed = true;
|
|
91
|
+
element.removeEventListener("pointerdown", handlePointerDown);
|
|
92
|
+
surface.dispose();
|
|
93
|
+
element.remove();
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
show,
|
|
97
|
+
reanchor,
|
|
98
|
+
hide,
|
|
99
|
+
isVisible: () => !element.hidden,
|
|
100
|
+
containsTarget: (target) => target instanceof Node && element.contains(target),
|
|
101
|
+
moveSelection,
|
|
102
|
+
selectedItem,
|
|
103
|
+
dispose
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function completionListResult(result) {
|
|
107
|
+
if (!result) return {
|
|
108
|
+
items: [],
|
|
109
|
+
isIncomplete: false
|
|
110
|
+
};
|
|
111
|
+
if (isCompletionList(result)) return {
|
|
112
|
+
items: result.items ?? [],
|
|
113
|
+
isIncomplete: result.isIncomplete === true
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
items: result,
|
|
117
|
+
isIncomplete: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function completionTriggerFromChange(change) {
|
|
121
|
+
if (!change || change.kind !== "edit") return null;
|
|
122
|
+
if (change.edits.length !== 1) return null;
|
|
123
|
+
const edit = change.edits[0];
|
|
124
|
+
if (!edit) return null;
|
|
125
|
+
const character = typedCharacter(edit);
|
|
126
|
+
if (!character) return null;
|
|
127
|
+
if (COMPLETION_TRIGGER_CHARACTERS.has(character)) return {
|
|
128
|
+
triggerKind: 2,
|
|
129
|
+
triggerCharacter: character
|
|
130
|
+
};
|
|
131
|
+
if (isIdentifierCharacter(character)) return { triggerKind: 1 };
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The character the user typed, out of an edit that may have written more than it.
|
|
136
|
+
*
|
|
137
|
+
* A delimiter that closes itself arrives as the pair in a single insertion with the caret left
|
|
138
|
+
* between the halves — which is exactly the keystroke a path or a member list is wanted for, so
|
|
139
|
+
* reading only the one-character edits would leave the quote triggers unreachable while the editor
|
|
140
|
+
* completes them. An insertion of one trigger character twice over is the whole of that shape;
|
|
141
|
+
* anything else two characters wide is text arriving from somewhere other than the keyboard.
|
|
142
|
+
*/
|
|
143
|
+
function typedCharacter(edit) {
|
|
144
|
+
if (edit.text.length === 1) return edit.text;
|
|
145
|
+
if (edit.from !== edit.to || edit.text.length !== 2) return null;
|
|
146
|
+
const opener = edit.text[0] ?? "";
|
|
147
|
+
if (opener !== edit.text[1]) return null;
|
|
148
|
+
return COMPLETION_TRIGGER_CHARACTERS.has(opener) ? opener : null;
|
|
149
|
+
}
|
|
150
|
+
function completionApplication(request, item) {
|
|
151
|
+
const primary = completionPrimaryEdit(request, item);
|
|
152
|
+
if (!primary) return null;
|
|
153
|
+
const additional = additionalCompletionEdits(request, item.additionalTextEdits ?? []);
|
|
154
|
+
const edits = [primary.edit, ...additional];
|
|
155
|
+
const head = completionSelectionHead(primary.edit, additional);
|
|
156
|
+
const snippet = primary.snippet;
|
|
157
|
+
if (!snippet) return {
|
|
158
|
+
edits,
|
|
159
|
+
selection: {
|
|
160
|
+
anchor: head,
|
|
161
|
+
head,
|
|
162
|
+
affinity: request.caretAffinity
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const insertionOffset = head - primary.edit.text.length;
|
|
166
|
+
const first = snippetInitialSelection(snippet, insertionOffset);
|
|
167
|
+
return {
|
|
168
|
+
edits,
|
|
169
|
+
selection: {
|
|
170
|
+
anchor: first.start,
|
|
171
|
+
head: first.end,
|
|
172
|
+
affinity: request.caretAffinity
|
|
173
|
+
},
|
|
174
|
+
snippetStops: snippetStopRanges(snippet, insertionOffset)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether an item could be applied at all against the text it was answered for.
|
|
179
|
+
*
|
|
180
|
+
* The ranges an item carries are positions in that text, and one that spans lines or that the caret
|
|
181
|
+
* was never inside describes a document this client did not ask about. Nothing downstream can rescue
|
|
182
|
+
* such an item, so a row for it would be one the reader can press Enter on and get nothing for.
|
|
183
|
+
*/
|
|
184
|
+
function completionItemApplies(request, item) {
|
|
185
|
+
const textEdit = completionTextEdit(item);
|
|
186
|
+
if (!textEdit) return true;
|
|
187
|
+
return completionEditRange(request, textEdit.range) !== null;
|
|
188
|
+
}
|
|
189
|
+
function completionAnchorRange(length, offset) {
|
|
190
|
+
if (length === 0) return {
|
|
191
|
+
start: 0,
|
|
192
|
+
end: 0
|
|
193
|
+
};
|
|
194
|
+
const end = Math.max(1, Math.min(offset, length));
|
|
195
|
+
return {
|
|
196
|
+
start: end - 1,
|
|
197
|
+
end
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function createCompletionEditFeature(context, timingName) {
|
|
201
|
+
return { applyCompletion(application) {
|
|
202
|
+
if (!context.hasDocument()) return false;
|
|
203
|
+
context.applyEdits(application.edits, timingName, application.selection);
|
|
204
|
+
if (application.snippetStops) context.startSnippetSession(application.snippetStops);
|
|
205
|
+
context.focusEditor();
|
|
206
|
+
return true;
|
|
207
|
+
} };
|
|
208
|
+
}
|
|
209
|
+
function createCompletionWidgetElement(document, classNames) {
|
|
210
|
+
const element = document.createElement("div");
|
|
211
|
+
element.className = classNames.root;
|
|
212
|
+
element.hidden = true;
|
|
213
|
+
element.setAttribute("role", "listbox");
|
|
214
|
+
Object.assign(element.style, {
|
|
215
|
+
zIndex: "1001",
|
|
216
|
+
width: `${COMPLETION_WIDGET_WIDTH_PX}px`,
|
|
217
|
+
maxWidth: `calc(100vw - 24px)`,
|
|
218
|
+
maxHeight: `${COMPLETION_WIDGET_MAX_HEIGHT_PX}px`,
|
|
219
|
+
overflowX: "hidden",
|
|
220
|
+
overflowY: "auto",
|
|
221
|
+
padding: "3px",
|
|
222
|
+
border: "1px solid color-mix(in srgb, var(--editor-foreground, #d4d4d8) 22%, transparent)",
|
|
223
|
+
borderRadius: "6px",
|
|
224
|
+
boxSizing: "border-box",
|
|
225
|
+
background: "var(--editor-popup-background, color-mix(in srgb, var(--editor-background, #18181b) 96%, var(--editor-foreground, #e4e4e7) 4%))",
|
|
226
|
+
color: "var(--editor-foreground, #e4e4e7)",
|
|
227
|
+
boxShadow: "0 12px 30px color-mix(in srgb, var(--editor-background, #000000) 60%, transparent)",
|
|
228
|
+
font: "12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
229
|
+
userSelect: "none",
|
|
230
|
+
scrollbarGutter: "stable"
|
|
231
|
+
});
|
|
232
|
+
return element;
|
|
233
|
+
}
|
|
234
|
+
function completionRowElement(document, item, index, classNames) {
|
|
235
|
+
const row = document.createElement("div");
|
|
236
|
+
row.className = classNames.item;
|
|
237
|
+
row.dataset.index = String(index);
|
|
238
|
+
row.setAttribute("role", "option");
|
|
239
|
+
Object.assign(row.style, {
|
|
240
|
+
display: "grid",
|
|
241
|
+
gridTemplateColumns: "44px minmax(0, 1fr) auto",
|
|
242
|
+
alignItems: "center",
|
|
243
|
+
gap: "8px",
|
|
244
|
+
minHeight: "24px",
|
|
245
|
+
padding: "2px 7px",
|
|
246
|
+
borderRadius: "4px",
|
|
247
|
+
cursor: "default",
|
|
248
|
+
whiteSpace: "nowrap"
|
|
249
|
+
});
|
|
250
|
+
row.append(completionKindElement(document, item.kind), completionLabelElement(document, item, classNames), completionDetailElement(document, item));
|
|
251
|
+
return row;
|
|
252
|
+
}
|
|
253
|
+
function completionKindElement(document, kind) {
|
|
254
|
+
const element = document.createElement("span");
|
|
255
|
+
element.textContent = completionKindLabel(kind);
|
|
256
|
+
Object.assign(element.style, {
|
|
257
|
+
minWidth: "0",
|
|
258
|
+
overflow: "hidden",
|
|
259
|
+
color: "color-mix(in srgb, var(--editor-foreground, #e4e4e7) 62%, transparent)",
|
|
260
|
+
fontSize: "10px",
|
|
261
|
+
textTransform: "uppercase"
|
|
262
|
+
});
|
|
263
|
+
return element;
|
|
264
|
+
}
|
|
265
|
+
function completionLabelElement(document, item, classNames) {
|
|
266
|
+
const element = document.createElement("span");
|
|
267
|
+
Object.assign(element.style, {
|
|
268
|
+
minWidth: "0",
|
|
269
|
+
overflow: "hidden",
|
|
270
|
+
textOverflow: "ellipsis"
|
|
271
|
+
});
|
|
272
|
+
appendCompletionLabel(element, item.label, completionLabelMatches.get(item) ?? [], classNames);
|
|
273
|
+
const detail = item.labelDetails?.detail;
|
|
274
|
+
if (detail) element.append(detail);
|
|
275
|
+
return element;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The label, with each run of characters the filter matched marked up.
|
|
279
|
+
*
|
|
280
|
+
* A list that reorders itself as the user types owes them the reason it did: with nothing marked, an
|
|
281
|
+
* entry that outranks the one above it looks arbitrary rather than earned.
|
|
282
|
+
*/
|
|
283
|
+
function appendCompletionLabel(element, label, positions, classNames) {
|
|
284
|
+
let plainStart = 0;
|
|
285
|
+
let cursor = 0;
|
|
286
|
+
while (cursor < positions.length) {
|
|
287
|
+
const start = positions[cursor] ?? 0;
|
|
288
|
+
let end = start + 1;
|
|
289
|
+
while (positions[cursor + 1] === end) {
|
|
290
|
+
end += 1;
|
|
291
|
+
cursor += 1;
|
|
292
|
+
}
|
|
293
|
+
cursor += 1;
|
|
294
|
+
if (start > plainStart) element.append(label.slice(plainStart, start));
|
|
295
|
+
element.append(completionMatchElement(element.ownerDocument, label.slice(start, end), classNames));
|
|
296
|
+
plainStart = end;
|
|
297
|
+
}
|
|
298
|
+
if (plainStart < label.length) element.append(label.slice(plainStart));
|
|
299
|
+
}
|
|
300
|
+
function completionMatchElement(document, text, classNames) {
|
|
301
|
+
const element = document.createElement("span");
|
|
302
|
+
element.className = classNames.match;
|
|
303
|
+
element.textContent = text;
|
|
304
|
+
Object.assign(element.style, {
|
|
305
|
+
color: "color-mix(in srgb, var(--editor-caret-color, #60a5fa) 80%, var(--editor-foreground, #e4e4e7))",
|
|
306
|
+
fontWeight: "600"
|
|
307
|
+
});
|
|
308
|
+
return element;
|
|
309
|
+
}
|
|
310
|
+
function completionDetailElement(document, item) {
|
|
311
|
+
const element = document.createElement("span");
|
|
312
|
+
element.textContent = item.labelDetails?.description ?? item.detail ?? "";
|
|
313
|
+
Object.assign(element.style, {
|
|
314
|
+
minWidth: "0",
|
|
315
|
+
maxWidth: "140px",
|
|
316
|
+
overflow: "hidden",
|
|
317
|
+
color: "color-mix(in srgb, var(--editor-foreground, #e4e4e7) 58%, transparent)",
|
|
318
|
+
textOverflow: "ellipsis"
|
|
319
|
+
});
|
|
320
|
+
return element;
|
|
321
|
+
}
|
|
322
|
+
function syncSelectedRows(element, selectedIndex, classNames) {
|
|
323
|
+
const rows = element.querySelectorAll(`.${classNames.item}`);
|
|
324
|
+
for (const row of rows) {
|
|
325
|
+
const selected = rowIndex(row) === selectedIndex;
|
|
326
|
+
syncSelectedRow(row, selected);
|
|
327
|
+
if (selected) scrollRowIntoView(element, row);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Keeps the focused row inside the box.
|
|
332
|
+
*
|
|
333
|
+
* A hundred rows are offered in a box a dozen of them fit in, and the widget never takes the focus,
|
|
334
|
+
* so there is no scrollport the browser is moving on anyone's behalf: walking off the bottom edge —
|
|
335
|
+
* or wrapping to the end with a single ArrowUp — would leave a list with no highlight in it while
|
|
336
|
+
* Enter commits a row that was never on screen.
|
|
337
|
+
*/
|
|
338
|
+
function scrollRowIntoView(element, row) {
|
|
339
|
+
const top = row.offsetTop;
|
|
340
|
+
const bottom = top + row.offsetHeight;
|
|
341
|
+
if (top < element.scrollTop) element.scrollTop = top;
|
|
342
|
+
else if (bottom > element.scrollTop + element.clientHeight) element.scrollTop = bottom - element.clientHeight;
|
|
343
|
+
}
|
|
344
|
+
function syncSelectedRow(row, selected) {
|
|
345
|
+
row.setAttribute("aria-selected", selected ? "true" : "false");
|
|
346
|
+
row.style.background = selected ? "color-mix(in srgb, var(--editor-caret-color, #60a5fa) 26%, transparent)" : "transparent";
|
|
347
|
+
}
|
|
348
|
+
function syncEditorThemeVariables(element, source) {
|
|
349
|
+
const style = source.ownerDocument.defaultView?.getComputedStyle(source);
|
|
350
|
+
if (!style) return;
|
|
351
|
+
for (const variable of COMPLETION_THEME_VARIABLES) {
|
|
352
|
+
const value = style.getPropertyValue(variable);
|
|
353
|
+
if (value) {
|
|
354
|
+
element.style.setProperty(variable, value);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
element.style.removeProperty(variable);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function completionPrimaryEdit(request, item) {
|
|
361
|
+
const textEdit = completionTextEdit(item);
|
|
362
|
+
const range = textEdit ? completionEditRange(request, textEdit.range) : defaultCompletionReplacementRange(request.document, request.offset);
|
|
363
|
+
if (!range) return null;
|
|
364
|
+
const source = textEdit ? textEdit.newText : item.insertText ?? item.label;
|
|
365
|
+
const snippet = item.insertTextFormat === 2 ? parseSnippet(source, { insertion: {
|
|
366
|
+
textSnapshot: textReadSnapshotOf(request.document),
|
|
367
|
+
offset: range.start
|
|
368
|
+
} }) : null;
|
|
369
|
+
return {
|
|
370
|
+
snippet,
|
|
371
|
+
edit: {
|
|
372
|
+
from: range.start,
|
|
373
|
+
to: Math.max(range.start, range.end + request.caretOffset - request.offset),
|
|
374
|
+
text: snippet?.text ?? source
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function completionTextEdit(item) {
|
|
379
|
+
const textEdit = item.textEdit;
|
|
380
|
+
if (!textEdit) return null;
|
|
381
|
+
if ("range" in textEdit) return textEdit;
|
|
382
|
+
return {
|
|
383
|
+
range: textEdit.replace,
|
|
384
|
+
newText: textEdit.newText
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The item's replacement range in offsets, or null when it describes a document we never asked
|
|
389
|
+
* about: a range spanning lines, or one the caret was not inside, leaves nothing for the typed
|
|
390
|
+
* characters to be measured against.
|
|
391
|
+
*/
|
|
392
|
+
function completionEditRange(request, range) {
|
|
393
|
+
if (range.start.line !== range.end.line) return null;
|
|
394
|
+
const start = lspPositionToOffsetInSnapshot(request.document, range.start);
|
|
395
|
+
const end = lspPositionToOffsetInSnapshot(request.document, range.end);
|
|
396
|
+
if (start > request.offset || end < request.offset) return null;
|
|
397
|
+
return {
|
|
398
|
+
start,
|
|
399
|
+
end
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function additionalCompletionEdits(request, edits) {
|
|
403
|
+
return edits.map((edit) => {
|
|
404
|
+
const from = lspPositionToOffsetInSnapshot(request.document, edit.range.start);
|
|
405
|
+
const to = lspPositionToOffsetInSnapshot(request.document, edit.range.end);
|
|
406
|
+
const shift = from < request.offset ? 0 : request.caretOffset - request.offset;
|
|
407
|
+
return {
|
|
408
|
+
from: from + shift,
|
|
409
|
+
to: to + shift,
|
|
410
|
+
text: edit.newText
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function completionSelectionHead(primary, additional) {
|
|
415
|
+
let head = primary.from + primary.text.length;
|
|
416
|
+
for (const edit of additional) {
|
|
417
|
+
if (edit.to > primary.from) continue;
|
|
418
|
+
head += edit.text.length - (edit.to - edit.from);
|
|
419
|
+
}
|
|
420
|
+
return Math.max(0, head);
|
|
421
|
+
}
|
|
422
|
+
function defaultCompletionReplacementRange(document, offset) {
|
|
423
|
+
const clamped = Math.max(0, Math.min(offset, document.textSnapshot.length));
|
|
424
|
+
return rangeAroundOffset(document, clamped, identifierRunAt);
|
|
425
|
+
}
|
|
426
|
+
function identifierRunAt(text, offset) {
|
|
427
|
+
let start = offset;
|
|
428
|
+
while (start > 0 && isIdentifierCharacter(text[start - 1] ?? "")) start -= 1;
|
|
429
|
+
let end = offset;
|
|
430
|
+
while (end < text.length && isIdentifierCharacter(text[end] ?? "")) end += 1;
|
|
431
|
+
return {
|
|
432
|
+
start,
|
|
433
|
+
end
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** Every tab stop in visit order and in document offsets, or undefined when there are none. */
|
|
437
|
+
function snippetStopRanges(snippet, insertionOffset) {
|
|
438
|
+
const stops = snippet.stops.flatMap((stop) => {
|
|
439
|
+
const caret = stop.ranges.findIndex((candidate) => !candidate.transform);
|
|
440
|
+
const range = stop.ranges[caret];
|
|
441
|
+
if (!range) return [];
|
|
442
|
+
const mirrors = stop.ranges.flatMap((mirror, index) => index === caret ? [] : [shiftedSnippetRange(mirror, insertionOffset)]);
|
|
443
|
+
const shifted = shiftedSnippetRange(range, insertionOffset);
|
|
444
|
+
return [mirrors.length > 0 ? {
|
|
445
|
+
...shifted,
|
|
446
|
+
mirrors
|
|
447
|
+
} : shifted];
|
|
448
|
+
});
|
|
449
|
+
return stops.length > 0 ? stops : void 0;
|
|
450
|
+
}
|
|
451
|
+
function shiftedSnippetRange(range, insertionOffset) {
|
|
452
|
+
const shifted = {
|
|
453
|
+
end: insertionOffset + range.end,
|
|
454
|
+
start: insertionOffset + range.start
|
|
455
|
+
};
|
|
456
|
+
return range.transform ? {
|
|
457
|
+
...shifted,
|
|
458
|
+
transform: range.transform
|
|
459
|
+
} : shifted;
|
|
460
|
+
}
|
|
461
|
+
function completionRowTarget(target, classNames) {
|
|
462
|
+
if (!(target instanceof Element)) return null;
|
|
463
|
+
return target.closest(`.${classNames.item}`);
|
|
464
|
+
}
|
|
465
|
+
function completionWidgetClassNames(namespace = COMPLETION_WIDGET_CLASS_NAMESPACE) {
|
|
466
|
+
const root = `editor-${namespace}-completion`;
|
|
467
|
+
return {
|
|
468
|
+
root,
|
|
469
|
+
item: `${root}-item`,
|
|
470
|
+
match: `${root}-match`
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function rowIndex(row) {
|
|
474
|
+
const parsed = Number(row.dataset.index);
|
|
475
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
476
|
+
}
|
|
477
|
+
function clampIndex(index, length) {
|
|
478
|
+
if (length <= 0) return 0;
|
|
479
|
+
return Math.min(Math.max(0, index), length - 1);
|
|
480
|
+
}
|
|
481
|
+
function wrapIndex(index, length) {
|
|
482
|
+
if (length <= 0) return 0;
|
|
483
|
+
return (index % length + length) % length;
|
|
484
|
+
}
|
|
485
|
+
function completionKindLabel(kind) {
|
|
486
|
+
if (kind === 2) return "meth";
|
|
487
|
+
if (kind === 3) return "fn";
|
|
488
|
+
if (kind === 5) return "field";
|
|
489
|
+
if (kind === 6) return "var";
|
|
490
|
+
if (kind === 7) return "class";
|
|
491
|
+
if (kind === 8) return "iface";
|
|
492
|
+
if (kind === 9) return "mod";
|
|
493
|
+
if (kind === 10) return "prop";
|
|
494
|
+
if (kind === 13) return "enum";
|
|
495
|
+
if (kind === 14) return "key";
|
|
496
|
+
if (kind === 20) return "member";
|
|
497
|
+
return "text";
|
|
498
|
+
}
|
|
499
|
+
function isIdentifierCharacter(value) {
|
|
500
|
+
return /^[A-Za-z0-9_$]$/.test(value);
|
|
501
|
+
}
|
|
502
|
+
function isCompletionList(value) {
|
|
503
|
+
return !Array.isArray(value);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Whether an accepted item still needs a `completionItem/resolve` round-trip.
|
|
507
|
+
*
|
|
508
|
+
* Servers routinely send a list whose items carry only a label, deferring documentation and — the
|
|
509
|
+
* one that matters — the import edit until resolve. Applying such an item directly is what silently
|
|
510
|
+
* drops auto-imports.
|
|
511
|
+
*/
|
|
512
|
+
function completionNeedsResolve(item, serverCapabilities) {
|
|
513
|
+
if (item.additionalTextEdits !== void 0) return false;
|
|
514
|
+
return Boolean(serverCapabilities?.completionProvider?.resolveProvider);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Word being typed at `offset`, which is what the list should be filtered against. The server's
|
|
518
|
+
* answer is for the moment the request was sent; the user has usually typed more since.
|
|
519
|
+
*/
|
|
520
|
+
function completionPrefix(text, offset) {
|
|
521
|
+
let start = Math.max(0, Math.min(offset, text.length));
|
|
522
|
+
while (start > 0 && isIdentifierCharacter(text[start - 1] ?? "")) start -= 1;
|
|
523
|
+
return text.slice(start, Math.max(0, Math.min(offset, text.length)));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Where each ranked item's label matched, kept beside the list instead of inside it.
|
|
527
|
+
*
|
|
528
|
+
* The items in a list are the server's own objects, and one of them goes back to the server on a
|
|
529
|
+
* resolve; the runs to mark are this client's reading of them. The widget is handed the items and
|
|
530
|
+
* nothing else, so this is where it reads that from.
|
|
531
|
+
*/
|
|
532
|
+
var completionLabelMatches = /* @__PURE__ */ new WeakMap();
|
|
533
|
+
/**
|
|
534
|
+
* Each server list's last filter, hung on the list itself.
|
|
535
|
+
*
|
|
536
|
+
* A narrowing is an answer about the one list it was computed from, and that list is held by a
|
|
537
|
+
* single session in a single editor. Here it is out of reach of a second editor filtering its own
|
|
538
|
+
* list, and it is released along with the session holding the list; in a variable of its own it
|
|
539
|
+
* would be neither.
|
|
540
|
+
*/
|
|
541
|
+
var completionRankings = /* @__PURE__ */ new WeakMap();
|
|
542
|
+
/**
|
|
543
|
+
* Filters and orders a completion list against what has been typed.
|
|
544
|
+
*
|
|
545
|
+
* An item earns its place by where the typed characters landed in it, so a word start, a camel-case
|
|
546
|
+
* hump and the far side of a separator all outrank the same letters found in the middle of a word.
|
|
547
|
+
* Ties fall back to the server's own `sortText`, which is how a server expresses relevance it knows
|
|
548
|
+
* and the client does not.
|
|
549
|
+
*/
|
|
550
|
+
function rankCompletionItems(items, prefix) {
|
|
551
|
+
if (prefix.length === 0) {
|
|
552
|
+
completionRankings.delete(items);
|
|
553
|
+
for (const item of items) completionLabelMatches.delete(item);
|
|
554
|
+
return items;
|
|
555
|
+
}
|
|
556
|
+
const scored = [];
|
|
557
|
+
for (const item of narrowingSource(items, prefix)) {
|
|
558
|
+
const match = fuzzyMatch(prefix, item.filterText ?? item.label);
|
|
559
|
+
if (!match) continue;
|
|
560
|
+
completionLabelMatches.set(item, completionLabelPositions(item, prefix, match));
|
|
561
|
+
scored.push({
|
|
562
|
+
item,
|
|
563
|
+
score: match.score
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const ranked = scored.sort((left, right) => right.score - left.score || compareCompletionOrder(left.item, right.item)).map((entry) => entry.item);
|
|
567
|
+
completionRankings.set(items, {
|
|
568
|
+
prefix,
|
|
569
|
+
ranked
|
|
570
|
+
});
|
|
571
|
+
return ranked;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The items worth scoring for this word: the ones that survived the shorter word, when this word is
|
|
575
|
+
* that word plus what has been typed since.
|
|
576
|
+
*
|
|
577
|
+
* Typing a character can only narrow, never widen — no item the longer word matches was passed over
|
|
578
|
+
* by the shorter one — so a server's list is scored in full once for the word it answered, and for
|
|
579
|
+
* every keystroke the list stays up after that, only where it is still standing.
|
|
580
|
+
*/
|
|
581
|
+
function narrowingSource(items, prefix) {
|
|
582
|
+
const previous = completionRankings.get(items);
|
|
583
|
+
if (!previous) return items;
|
|
584
|
+
if (prefix.length <= previous.prefix.length || !prefix.startsWith(previous.prefix)) return items;
|
|
585
|
+
return previous.ranked;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The label positions to mark for an item, given the match its filter text earned.
|
|
589
|
+
*
|
|
590
|
+
* An item may be filtered by one string and displayed as another, and positions in the string that
|
|
591
|
+
* was matched address nothing in the string on screen. Such a label is matched again on its own
|
|
592
|
+
* terms, and loosely: part of what was typed has no reason to appear in it at all, and marking the
|
|
593
|
+
* part that does beats marking nothing. The rank stays the one the filter text earned.
|
|
594
|
+
*/
|
|
595
|
+
function completionLabelPositions(item, prefix, match) {
|
|
596
|
+
const filterText = item.filterText;
|
|
597
|
+
if (filterText === void 0) return match.positions;
|
|
598
|
+
if (filterText.toLowerCase() === item.label.toLowerCase()) return match.positions;
|
|
599
|
+
return looseFuzzyMatch(prefix, item.label)?.positions ?? [];
|
|
600
|
+
}
|
|
601
|
+
function compareCompletionOrder(left, right) {
|
|
602
|
+
const leftKey = left.sortText ?? left.label;
|
|
603
|
+
const rightKey = right.sortText ?? right.label;
|
|
604
|
+
return leftKey.localeCompare(rightKey);
|
|
605
|
+
}
|
|
606
|
+
//#endregion
|
|
607
|
+
export { COMPLETION_REQUEST_DEBOUNCE_MS, LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE, LANGUAGE_SERVER_COMPLETION_EDIT_FEATURE_ID, completionAnchorRange, completionApplication, completionItemApplies, completionListResult, completionNeedsResolve, completionPrefix, completionTriggerFromChange, createCompletionEditFeature, createCompletionWidgetController, rankCompletionItems };
|
|
608
|
+
|
|
609
|
+
//# sourceMappingURL=completion.js.map
|