Skip to content
This repository was archived by the owner on Sep 19, 2025. It is now read-only.

Commit 11a967d

Browse files
authored
Support alwaysOn: false, and an explicit command (#39)
* Add always-on option * Support explicit completion request * Reorganize files, split requestCompletion into its own file * Fix comment
1 parent 450fb92 commit 11a967d

File tree

7 files changed

+148
-74
lines changed

7 files changed

+148
-74
lines changed

demo/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ <h3>TypeScript AI autocompletion</h3>
2424

2525
<h3>Python AI autocompletion</h3>
2626
<div id="editor-python"></div>
27+
28+
<h3>TypeScript AI autocompletion (cmd+k to trigger completion)</h3>
29+
<div id="editor-explicit"></div>
2730
</main>
2831
<script type="module" src="./index.ts"></script>
2932
</body>

demo/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { EditorView, basicSetup } from "codemirror";
22
import { javascript } from "@codemirror/lang-javascript";
33
import {
44
codeiumOtherDocumentsConfig,
5+
startCompletion,
56
Language,
67
copilotPlugin,
78
} from "../src/plugin.js";
89
import { python } from "@codemirror/lang-python";
10+
import { keymap } from "@codemirror/view";
911

1012
new EditorView({
1113
doc: "// Factorial function",
@@ -41,6 +43,47 @@ const hiddenValue = "https://macwright.com/"`,
4143
parent: document.querySelector("#editor")!,
4244
});
4345

46+
new EditorView({
47+
doc: "// Factorial function (explicit trigger)",
48+
extensions: [
49+
basicSetup,
50+
javascript({
51+
typescript: true,
52+
jsx: true,
53+
}),
54+
codeiumOtherDocumentsConfig.of({
55+
override: () => [
56+
{
57+
absolutePath: "https://esm.town/v/foo.ts",
58+
text: `export const foo = 10;
59+
60+
const hiddenValue = "https://macwright.com/"`,
61+
language: Language.TYPESCRIPT,
62+
editorLanguage: "typescript",
63+
},
64+
],
65+
}),
66+
copilotPlugin({
67+
apiKey: "d49954eb-cfba-4992-980f-d8fb37f0e942",
68+
shouldComplete(context) {
69+
if (context.tokenBefore(["String"])) {
70+
return true;
71+
}
72+
const match = context.matchBefore(/(@(?:\w*))(?:[./](\w*))?/);
73+
return !match;
74+
},
75+
alwaysOn: false,
76+
}),
77+
keymap.of([
78+
{
79+
key: "Cmd-k",
80+
run: startCompletion,
81+
},
82+
]),
83+
],
84+
parent: document.querySelector("#editor-explicit")!,
85+
});
86+
4487
new EditorView({
4588
doc: "def hi_python():",
4689
extensions: [

src/commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
addSuggestions,
88
clearSuggestion,
99
} from "./effects.js";
10+
import { requestCompletion } from "./requestCompletion.js";
1011

1112
/**
1213
* Accepting a suggestion: we remove the ghost text, which
@@ -174,3 +175,8 @@ export function sameKeyCommand(view: EditorView, key: string) {
174175
}
175176
return rejectSuggestionCommand(view);
176177
}
178+
179+
export const startCompletion: Command = (view: EditorView) => {
180+
requestCompletion(view);
181+
return true;
182+
};

src/completionRequester.ts

Lines changed: 7 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import { CompletionContext, completionStatus } from "@codemirror/autocomplete";
2-
import { ChangeSet, Transaction } from "@codemirror/state";
32
import { EditorView, type ViewUpdate } from "@codemirror/view";
4-
import { completionsToChangeSpec, getCodeiumCompletions } from "./codeium.js";
5-
import {
6-
acceptSuggestion,
7-
addSuggestions,
8-
clearSuggestion,
9-
} from "./effects.js";
3+
import { acceptSuggestion, clearSuggestion } from "./effects.js";
104
import { completionDecoration } from "./completionDecoration.js";
11-
import { copilotEvent, copilotIgnore } from "./annotations.js";
12-
import { codeiumConfig, codeiumOtherDocumentsConfig } from "./config.js";
5+
import { copilotEvent } from "./annotations.js";
6+
import { codeiumConfig } from "./config.js";
7+
import { requestCompletion } from "./requestCompletion.js";
138

149
/**
1510
* To request a completion, the document needs to have been
@@ -57,7 +52,7 @@ export function completionRequester() {
5752

5853
return EditorView.updateListener.of((update: ViewUpdate) => {
5954
const config = update.view.state.facet(codeiumConfig);
60-
const { override } = update.view.state.facet(codeiumOtherDocumentsConfig);
55+
if (!config.alwaysOn) return;
6156

6257
if (!shouldRequestCompletion(update)) return;
6358

@@ -85,76 +80,14 @@ export function completionRequester() {
8580
return;
8681
}
8782

88-
const source = state.doc.toString();
89-
9083
// Set a new timeout to request completion
9184
timeout = setTimeout(async () => {
9285
// Check if the position has changed
9386
if (pos !== lastPos) return;
9487

95-
const otherDocuments = await override();
96-
97-
// Request completion from the server
98-
try {
99-
const completionResult = await getCodeiumCompletions({
100-
text: source,
101-
cursorOffset: pos,
102-
config,
103-
otherDocuments,
104-
});
105-
106-
if (
107-
!completionResult ||
108-
completionResult.completionItems.length === 0
109-
) {
110-
return;
111-
}
112-
113-
// Check if the position is still the same. If
114-
// it has changed, ignore the code that we just
115-
// got from the API and don't show anything.
116-
if (
117-
!(
118-
pos === lastPos &&
119-
completionStatus(update.view.state) !== "active" &&
120-
update.view.hasFocus
121-
)
122-
) {
123-
return;
124-
}
125-
126-
// Dispatch an effect to add the suggestion
127-
// If the completion starts before the end of the line,
128-
// check the end of the line with the end of the completion
129-
const changeSpecs = completionsToChangeSpec(completionResult);
130-
131-
const index = 0;
132-
const firstSpec = changeSpecs.at(index);
133-
if (!firstSpec) return;
134-
const insertChangeSet = ChangeSet.of(firstSpec, state.doc.length);
135-
const reverseChangeSet = insertChangeSet.invert(state.doc);
136-
137-
update.view.dispatch({
138-
changes: insertChangeSet,
139-
effects: addSuggestions.of({
140-
index,
141-
reverseChangeSet,
142-
changeSpecs,
143-
}),
144-
annotations: [
145-
copilotIgnore.of(null),
146-
copilotEvent.of(null),
147-
Transaction.addToHistory.of(false),
148-
],
149-
});
150-
} catch (error) {
151-
console.warn("copilot completion failed", error);
152-
// Javascript wait for 500ms for some reason is necessary here.
153-
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
154-
155-
await new Promise((resolve) => setTimeout(resolve, 300));
156-
}
88+
await requestCompletion(update.view, lastPos);
15789
}, config.timeout);
90+
15891
// Update the last position
15992
lastPos = pos;
16093
});

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export interface CodeiumConfig {
3737
* when there are multiple suggestions to cycle through.
3838
*/
3939
widgetClass?: typeof DefaultCycleWidget | null;
40+
41+
/**
42+
* Always request completions after a delay
43+
*/
44+
alwaysOn?: boolean;
4045
}
4146

4247
export const codeiumConfig = Facet.define<
@@ -50,6 +55,7 @@ export const codeiumConfig = Facet.define<
5055
language: Language.TYPESCRIPT,
5156
timeout: 150,
5257
widgetClass: DefaultCycleWidget,
58+
alwaysOn: true,
5359
},
5460
{},
5561
);

src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
rejectSuggestionCommand,
88
acceptSuggestionCommand,
99
nextSuggestionCommand,
10+
startCompletion,
1011
} from "./commands.js";
1112
import {
1213
type CodeiumConfig,
@@ -112,6 +113,7 @@ export {
112113
codeiumConfig,
113114
codeiumOtherDocumentsConfig,
114115
nextSuggestionCommand,
116+
startCompletion,
115117
type CodeiumOtherDocumentsConfig,
116118
type CodeiumConfig,
117119
};

src/requestCompletion.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { completionStatus } from "@codemirror/autocomplete";
2+
import { ChangeSet, Transaction } from "@codemirror/state";
3+
import type { EditorView } from "@codemirror/view";
4+
import { completionsToChangeSpec, getCodeiumCompletions } from "./codeium.js";
5+
import { addSuggestions } from "./effects.js";
6+
import { copilotEvent, copilotIgnore } from "./annotations.js";
7+
import { codeiumConfig, codeiumOtherDocumentsConfig } from "./config.js";
8+
9+
/**
10+
* Inner 'requestCompletion' API, which can optionally
11+
* be run all the time if you set `alwaysOn`
12+
*/
13+
export async function requestCompletion(view: EditorView, lastPos?: number) {
14+
const config = view.state.facet(codeiumConfig);
15+
const { override } = view.state.facet(codeiumOtherDocumentsConfig);
16+
17+
const otherDocuments = await override();
18+
19+
// Get the current position and source
20+
const state = view.state;
21+
const pos = state.selection.main.head;
22+
const source = state.doc.toString();
23+
24+
// Request completion from the server
25+
try {
26+
const completionResult = await getCodeiumCompletions({
27+
text: source,
28+
cursorOffset: pos,
29+
config,
30+
otherDocuments,
31+
});
32+
33+
if (!completionResult || completionResult.completionItems.length === 0) {
34+
return;
35+
}
36+
37+
// Check if the position is still the same. If
38+
// it has changed, ignore the code that we just
39+
// got from the API and don't show anything.
40+
if (
41+
!(
42+
(lastPos === undefined || pos === lastPos) &&
43+
completionStatus(view.state) !== "active" &&
44+
view.hasFocus
45+
)
46+
) {
47+
return;
48+
}
49+
50+
// Dispatch an effect to add the suggestion
51+
// If the completion starts before the end of the line,
52+
// check the end of the line with the end of the completion
53+
const changeSpecs = completionsToChangeSpec(completionResult);
54+
55+
const index = 0;
56+
const firstSpec = changeSpecs.at(index);
57+
if (!firstSpec) return;
58+
const insertChangeSet = ChangeSet.of(firstSpec, state.doc.length);
59+
const reverseChangeSet = insertChangeSet.invert(state.doc);
60+
61+
view.dispatch({
62+
changes: insertChangeSet,
63+
effects: addSuggestions.of({
64+
index,
65+
reverseChangeSet,
66+
changeSpecs,
67+
}),
68+
annotations: [
69+
copilotIgnore.of(null),
70+
copilotEvent.of(null),
71+
Transaction.addToHistory.of(false),
72+
],
73+
});
74+
} catch (error) {
75+
console.warn("copilot completion failed", error);
76+
// Javascript wait for 300ms for some reason is necessary here.
77+
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG
78+
79+
await new Promise((resolve) => setTimeout(resolve, 300));
80+
}
81+
}

0 commit comments

Comments
 (0)