Skip to content

Commit

Permalink
Add ability to cycle through completions
Browse files Browse the repository at this point in the history
  • Loading branch information
tmcw committed Jun 12, 2024
1 parent 3b3a97f commit 12ef88a
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 14 deletions.
8 changes: 1 addition & 7 deletions demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@ import {
import { python } from "@codemirror/lang-python";

new EditorView({
doc: `let hasAnError: string = 10;
function increment(num: number) {
return num + 1;
}
increment('not a number');`,
doc: "// Factorial function",
extensions: [
basicSetup,
javascript({
Expand Down
86 changes: 83 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Transaction, EditorSelection } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import { Transaction, EditorSelection, ChangeSet } from "@codemirror/state";
import type { Command, EditorView } from "@codemirror/view";
import { copilotEvent, copilotIgnore } from "./annotations.js";
import { completionDecoration } from "./completionDecoration.js";
import { acceptSuggestion, clearSuggestion } from "./effects.js";
import {
acceptSuggestion,
addSuggestions,
clearSuggestion,
} from "./effects.js";

/**
* Accepting a suggestion: we remove the ghost text, which
Expand Down Expand Up @@ -53,6 +57,82 @@ export function acceptSuggestionCommand(view: EditorView) {
return true;
}

/**
* Rejecting a suggestion: this looks at the currently-shown suggestion
* and reverses it, clears the suggestion, and makes sure
* that we don't add that clearing transaction to history and we don't
* trigger a new suggestion because of it.
*/
export const nextSuggestionCommand: Command = (view: EditorView) => {
const { state } = view;
// We delete the suggestion, then carry through with the original keypress
const stateField = state.field(completionDecoration);

if (!stateField) {
return false;
}

const { changeSpecs } = stateField;

if (changeSpecs.length < 2) {
return false;
}

// Loop through next suggestion.
const index = (stateField.index + 1) % changeSpecs.length;
const nextSpec = changeSpecs.at(index);
if (!nextSpec) {
return false;
}

/**
* First, get the original document, by applying the stored
* reverse changeset against the currently-displayed document.
*/
const originalDocument = stateField.reverseChangeSet.apply(state.doc);

/**
* Get the changeset that we will apply that will
*
* 1. Reverse the currently-displayed suggestion, to get us back to
* the original document
* 2. Apply the next suggestion.
*
* It does both in the same changeset, so there is no flickering.
*/
const reverseCurrentSuggestionAndApplyNext = ChangeSet.of(
stateField.reverseChangeSet,
state.doc.length,
).compose(ChangeSet.of(nextSpec, originalDocument.length));

/**
* Generate the next changeset
*/
const nextSet = ChangeSet.of(nextSpec, originalDocument.length);
const reverseChangeSet = nextSet.invert(originalDocument);

view.dispatch({
changes: reverseCurrentSuggestionAndApplyNext,
effects: addSuggestions.of({
index,
reverseChangeSet,
changeSpecs,
}),
annotations: [
// Tell upstream integrations to ignore this
// change.
copilotIgnore.of(null),
// Tell ourselves not to request a completion
// because of this change.
copilotEvent.of(null),
// Don't add this to history.
Transaction.addToHistory.of(false),
],
});

return true;
};

/**
* Rejecting a suggestion: this looks at the currently-shown suggestion
* and reverses it, clears the suggestion, and makes sure
Expand Down
3 changes: 2 additions & 1 deletion src/completionDecoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const completionDecoration = StateField.define<CompletionState>({
update(state: CompletionState, transaction: Transaction) {
for (const effect of transaction.effects) {
if (effect.is(addSuggestions)) {
const { changeSpecs, index } = effect.value;
const { changeSpecs, index, originalDocument } = effect.value;

Check failure on line 29 in src/completionDecoration.ts

View workflow job for this annotation

GitHub Actions / Test

Property 'originalDocument' does not exist on type 'AddSuggestionsState'.

// NOTE: here we're adjusting the decoration range
// to refer to locations in the document _after_ we've
Expand All @@ -45,6 +45,7 @@ export const completionDecoration = StateField.define<CompletionState>({
index,
decorations,
changeSpecs,
originalDocument,
reverseChangeSet: effect.value.reverseChangeSet,
};
}
Expand Down
25 changes: 23 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { EditorView, type ViewUpdate } from "@codemirror/view";
import { EditorView, keymap, type ViewUpdate } from "@codemirror/view";
import { type Extension, Prec } from "@codemirror/state";
import { completionDecoration } from "./completionDecoration.js";
import { completionRequester } from "./completionRequester.js";
import {
sameKeyCommand,
rejectSuggestionCommand,
acceptSuggestionCommand,
nextSuggestionCommand,
} from "./commands.js";
import {
type CodeiumConfig,
Expand Down Expand Up @@ -45,6 +46,13 @@ function isDecorationClicked(view: EditorView): boolean {
function completionPlugin() {
return EditorView.domEventHandlers({
keydown(event, view) {
// Ideally, we handle infighting between
// the nextSuggestionCommand and this handler
// by using precedence, but I can't get that to work
// yet.
if (event.key === "]" && event.ctrlKey) {
return false;
}
if (
event.key !== "Shift" &&
event.key !== "Control" &&
Expand All @@ -64,6 +72,18 @@ function completionPlugin() {
});
}

/**
* Next completion map
*/
function nextCompletionPlugin() {
return keymap.of([
{
key: "Ctrl-]",
run: nextSuggestionCommand,
},
]);
}

/**
* Changing the editor's focus - blurring it by clicking outside -
* rejects the suggestion
Expand Down Expand Up @@ -93,8 +113,9 @@ export function copilotPlugin(config: CodeiumConfig): Extension {
return [
codeiumConfig.of(config),
completionDecoration,
Prec.highest(completionPlugin()),
Prec.highest(nextCompletionPlugin()),
Prec.highest(viewCompletionPlugin()),
Prec.high(completionPlugin()),
completionRequester(),
];
}
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChangeSet } from "@codemirror/state";
import type { ChangeSet, Text } from "@codemirror/state";
import type { DecorationSet } from "@codemirror/view";

/**
Expand All @@ -7,6 +7,7 @@ import type { DecorationSet } from "@codemirror/view";
*/
export type CompletionState = null | {
index: number;
originalDocument: Text;
reverseChangeSet: ChangeSet;
changeSpecs: SimpleChangeSpec[][];
decorations: DecorationSet;
Expand Down

0 comments on commit 12ef88a

Please sign in to comment.