Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support alwaysOn: false, and an explicit command #39

Merged
merged 4 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ <h3>TypeScript AI autocompletion</h3>

<h3>Python AI autocompletion</h3>
<div id="editor-python"></div>

<h3>TypeScript AI autocompletion (cmd+k to trigger completion)</h3>
<div id="editor-explicit"></div>
</main>
<script type="module" src="./index.ts"></script>
</body>
Expand Down
43 changes: 43 additions & 0 deletions demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import {
codeiumOtherDocumentsConfig,
startCompletion,
Language,
copilotPlugin,
} from "../src/plugin.js";
import { python } from "@codemirror/lang-python";
import { keymap } from "@codemirror/view";

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

new EditorView({
doc: "// Factorial function (explicit trigger)",
extensions: [
basicSetup,
javascript({
typescript: true,
jsx: true,
}),
codeiumOtherDocumentsConfig.of({
override: () => [
{
absolutePath: "https://esm.town/v/foo.ts",
text: `export const foo = 10;

const hiddenValue = "https://macwright.com/"`,
language: Language.TYPESCRIPT,
editorLanguage: "typescript",
},
],
}),
copilotPlugin({
apiKey: "d49954eb-cfba-4992-980f-d8fb37f0e942",
shouldComplete(context) {
if (context.tokenBefore(["String"])) {
return true;
}
const match = context.matchBefore(/(@(?:\w*))(?:[./](\w*))?/);
return !match;
},
alwaysOn: false,
}),
keymap.of([
{
key: "Cmd-k",
run: startCompletion,
},
]),
],
parent: document.querySelector("#editor-explicit")!,
});

new EditorView({
doc: "def hi_python():",
extensions: [
Expand Down
6 changes: 6 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
addSuggestions,
clearSuggestion,
} from "./effects.js";
import { requestCompletion } from "./requestCompletion.js";

/**
* Accepting a suggestion: we remove the ghost text, which
Expand Down Expand Up @@ -174,3 +175,8 @@ export function sameKeyCommand(view: EditorView, key: string) {
}
return rejectSuggestionCommand(view);
}

export const startCompletion: Command = (view: EditorView) => {
requestCompletion(view);
return true;
};
81 changes: 7 additions & 74 deletions src/completionRequester.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { CompletionContext, completionStatus } from "@codemirror/autocomplete";
import { ChangeSet, Transaction } from "@codemirror/state";
import { EditorView, type ViewUpdate } from "@codemirror/view";
import { completionsToChangeSpec, getCodeiumCompletions } from "./codeium.js";
import {
acceptSuggestion,
addSuggestions,
clearSuggestion,
} from "./effects.js";
import { acceptSuggestion, clearSuggestion } from "./effects.js";
import { completionDecoration } from "./completionDecoration.js";
import { copilotEvent, copilotIgnore } from "./annotations.js";
import { codeiumConfig, codeiumOtherDocumentsConfig } from "./config.js";
import { copilotEvent } from "./annotations.js";
import { codeiumConfig } from "./config.js";
import { requestCompletion } from "./requestCompletion.js";

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

return EditorView.updateListener.of((update: ViewUpdate) => {
const config = update.view.state.facet(codeiumConfig);
const { override } = update.view.state.facet(codeiumOtherDocumentsConfig);
if (!config.alwaysOn) return;

if (!shouldRequestCompletion(update)) return;

Expand Down Expand Up @@ -85,76 +80,14 @@ export function completionRequester() {
return;
}

const source = state.doc.toString();

// Set a new timeout to request completion
timeout = setTimeout(async () => {
// Check if the position has changed
if (pos !== lastPos) return;

const otherDocuments = await override();

// Request completion from the server
try {
const completionResult = await getCodeiumCompletions({
text: source,
cursorOffset: pos,
config,
otherDocuments,
});

if (
!completionResult ||
completionResult.completionItems.length === 0
) {
return;
}

// Check if the position is still the same. If
// it has changed, ignore the code that we just
// got from the API and don't show anything.
if (
!(
pos === lastPos &&
completionStatus(update.view.state) !== "active" &&
update.view.hasFocus
)
) {
return;
}

// Dispatch an effect to add the suggestion
// If the completion starts before the end of the line,
// check the end of the line with the end of the completion
const changeSpecs = completionsToChangeSpec(completionResult);

const index = 0;
const firstSpec = changeSpecs.at(index);
if (!firstSpec) return;
const insertChangeSet = ChangeSet.of(firstSpec, state.doc.length);
const reverseChangeSet = insertChangeSet.invert(state.doc);

update.view.dispatch({
changes: insertChangeSet,
effects: addSuggestions.of({
index,
reverseChangeSet,
changeSpecs,
}),
annotations: [
copilotIgnore.of(null),
copilotEvent.of(null),
Transaction.addToHistory.of(false),
],
});
} catch (error) {
console.warn("copilot completion failed", error);
// Javascript wait for 500ms for some reason is necessary here.
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG

await new Promise((resolve) => setTimeout(resolve, 300));
}
await requestCompletion(update.view, lastPos);
}, config.timeout);

// Update the last position
lastPos = pos;
});
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface CodeiumConfig {
* when there are multiple suggestions to cycle through.
*/
widgetClass?: typeof DefaultCycleWidget | null;

/**
* Always request completions after a delay
*/
alwaysOn?: boolean;
}

export const codeiumConfig = Facet.define<
Expand All @@ -50,6 +55,7 @@ export const codeiumConfig = Facet.define<
language: Language.TYPESCRIPT,
timeout: 150,
widgetClass: DefaultCycleWidget,
alwaysOn: true,
},
{},
);
Expand Down
2 changes: 2 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
rejectSuggestionCommand,
acceptSuggestionCommand,
nextSuggestionCommand,
startCompletion,
} from "./commands.js";
import {
type CodeiumConfig,
Expand Down Expand Up @@ -112,6 +113,7 @@ export {
codeiumConfig,
codeiumOtherDocumentsConfig,
nextSuggestionCommand,
startCompletion,
type CodeiumOtherDocumentsConfig,
type CodeiumConfig,
};
Expand Down
81 changes: 81 additions & 0 deletions src/requestCompletion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { completionStatus } from "@codemirror/autocomplete";
import { ChangeSet, Transaction } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import { completionsToChangeSpec, getCodeiumCompletions } from "./codeium.js";
import { addSuggestions } from "./effects.js";
import { copilotEvent, copilotIgnore } from "./annotations.js";
import { codeiumConfig, codeiumOtherDocumentsConfig } from "./config.js";

/**
* Inner 'requestCompletion' API, which can optionally
* be run all the time if you set `alwaysOn`
*/
export async function requestCompletion(view: EditorView, lastPos?: number) {
const config = view.state.facet(codeiumConfig);
const { override } = view.state.facet(codeiumOtherDocumentsConfig);

const otherDocuments = await override();

// Get the current position and source
const state = view.state;
const pos = state.selection.main.head;
const source = state.doc.toString();

// Request completion from the server
try {
const completionResult = await getCodeiumCompletions({
text: source,
cursorOffset: pos,
config,
otherDocuments,
});

if (!completionResult || completionResult.completionItems.length === 0) {
return;
}

// Check if the position is still the same. If
// it has changed, ignore the code that we just
// got from the API and don't show anything.
if (
!(
(lastPos === undefined || pos === lastPos) &&
completionStatus(view.state) !== "active" &&
view.hasFocus
)
) {
return;
}

// Dispatch an effect to add the suggestion
// If the completion starts before the end of the line,
// check the end of the line with the end of the completion
const changeSpecs = completionsToChangeSpec(completionResult);

const index = 0;
const firstSpec = changeSpecs.at(index);
if (!firstSpec) return;
const insertChangeSet = ChangeSet.of(firstSpec, state.doc.length);
const reverseChangeSet = insertChangeSet.invert(state.doc);

view.dispatch({
changes: insertChangeSet,
effects: addSuggestions.of({
index,
reverseChangeSet,
changeSpecs,
}),
annotations: [
copilotIgnore.of(null),
copilotEvent.of(null),
Transaction.addToHistory.of(false),
],
});
} catch (error) {
console.warn("copilot completion failed", error);
// Javascript wait for 300ms for some reason is necessary here.
// TODO - FIGURE OUT WHY THIS RESOLVES THE BUG

await new Promise((resolve) => setTimeout(resolve, 300));
}
}