-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix demo copy, refactor big plugin file into sub-files
- Loading branch information
Showing
9 changed files
with
347 additions
and
326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { Annotation } from "@codemirror/state"; | ||
|
||
export const copilotEvent = Annotation.define<null>(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Transaction, EditorSelection } from "@codemirror/state"; | ||
import { EditorView } from "@codemirror/view"; | ||
import { copilotEvent } from "./annotations.js"; | ||
import { completionDecoration } from "./completionDecoration.js"; | ||
import { acceptSuggestion, clearSuggestion } from "./effects.js"; | ||
|
||
export function acceptSuggestionCommand(view: EditorView) { | ||
// We delete the ghost text and insert the suggestion. | ||
// We also set the cursor to the end of the suggestion. | ||
const stateField = view.state.field(completionDecoration)!; | ||
const ghostTexts = stateField.ghostTexts; | ||
|
||
if (!ghostTexts) { | ||
return false; | ||
} | ||
|
||
const reverseReverseChangeSet = stateField.reverseChangeSet?.invert( | ||
view.state.doc, | ||
); | ||
|
||
// This is removing the previous ghost text. Don't | ||
// add this to history. | ||
view.dispatch({ | ||
changes: stateField.reverseChangeSet, | ||
effects: acceptSuggestion.of(null), | ||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], | ||
}); | ||
|
||
let lastIndex = 0; | ||
reverseReverseChangeSet?.iterChangedRanges((_fromA, _toA, _fromB, toB) => { | ||
lastIndex = toB; | ||
}); | ||
|
||
view.dispatch({ | ||
changes: reverseReverseChangeSet, | ||
selection: EditorSelection.cursor(lastIndex), | ||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(true)], | ||
}); | ||
|
||
return true; | ||
} | ||
|
||
export function rejectSuggestionCommand(view: EditorView) { | ||
// We delete the suggestion, then carry through with the original keypress | ||
const stateField = view.state.field(completionDecoration)!; | ||
const ghostTexts = stateField.ghostTexts; | ||
|
||
if (!ghostTexts?.length) { | ||
return false; | ||
} | ||
|
||
view.dispatch({ | ||
changes: stateField.reverseChangeSet, | ||
effects: clearSuggestion.of(null), | ||
annotations: [copilotEvent.of(null), Transaction.addToHistory.of(false)], | ||
}); | ||
|
||
return false; | ||
} | ||
|
||
// TODO: this isn't full reimplemented yet. | ||
export function sameKeyCommand(view: EditorView, key: string) { | ||
// When we type a key that is the same as the first letter of the suggestion, we delete the first letter of the suggestion and carry through with the original keypress | ||
const ghostTexts = view.state.field(completionDecoration)!.ghostTexts; | ||
|
||
if (!ghostTexts || !ghostTexts.length) { | ||
return false; | ||
} | ||
|
||
if (key === "Tab") { | ||
return acceptSuggestionCommand(view); | ||
} else { | ||
return rejectSuggestionCommand(view); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { StateField, EditorState, Transaction } from "@codemirror/state"; | ||
import { Decoration, EditorView } from "@codemirror/view"; | ||
import { | ||
addSuggestions, | ||
acceptSuggestion, | ||
clearSuggestion, | ||
} from "./effects.js"; | ||
import { CompletionState } from "./types.js"; | ||
|
||
const ghostMark = Decoration.mark({ class: "cm-ghostText" }); | ||
|
||
export const completionDecoration = StateField.define<CompletionState>({ | ||
create(_state: EditorState) { | ||
return { ghostTexts: null }; | ||
}, | ||
update(state: CompletionState, transaction: Transaction) { | ||
for (const effect of transaction.effects) { | ||
if (effect.is(addSuggestions)) { | ||
// When adding a suggestion, we set th ghostText | ||
|
||
// NOTE: here we're adjusting the decoration range | ||
// to refer to locations in the document _after_ we've | ||
// inserted the text. | ||
let decorationOffset = 0; | ||
const decorations = Decoration.set( | ||
effect.value.suggestions.map((suggestion) => { | ||
const endGhostText = | ||
suggestion.cursorPos + suggestion.displayText.length; | ||
let range = ghostMark.range( | ||
decorationOffset + suggestion.cursorPos, | ||
decorationOffset + endGhostText, | ||
); | ||
decorationOffset += suggestion.displayText.length; | ||
return range; | ||
}), | ||
); | ||
|
||
// TODO | ||
return { | ||
decorations, | ||
reverseChangeSet: effect.value.reverseChangeSet, | ||
ghostTexts: effect.value.suggestions.map((suggestion) => { | ||
const endGhostText = | ||
suggestion.cursorPos + suggestion.displayText.length; | ||
return { | ||
text: suggestion.text, | ||
displayText: suggestion.text, | ||
startPos: suggestion.startPos, | ||
endPos: suggestion.endPos, | ||
decorations, | ||
// TODO: what's the difference between this | ||
// and startPos? | ||
displayPos: suggestion.cursorPos, | ||
endReplacement: suggestion.endReplacement, | ||
endGhostText, | ||
}; | ||
}), | ||
}; | ||
} else if (effect.is(acceptSuggestion)) { | ||
if (state.ghostTexts) { | ||
return { ghostTexts: null }; | ||
} | ||
} else if (effect.is(clearSuggestion)) { | ||
return { ghostTexts: null }; | ||
} | ||
} | ||
|
||
return state; | ||
}, | ||
provide: (field) => | ||
EditorView.decorations.from(field, (value) => { | ||
return value.decorations || Decoration.none; | ||
}), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { completionStatus } from "@codemirror/autocomplete"; | ||
import { ChangeSet, Transaction } from "@codemirror/state"; | ||
import { EditorView, ViewUpdate } from "@codemirror/view"; | ||
import { getCodeiumCompletions } from "./codeium.js"; | ||
import { | ||
acceptSuggestion, | ||
addSuggestions, | ||
clearSuggestion, | ||
} from "./effects.js"; | ||
import { completionDecoration } from "./completionDecoration.js"; | ||
import { copilotEvent } from "./annotations.js"; | ||
|
||
// milliseconds before cancelling request | ||
// against codeium | ||
const TIMEOUT = 150; | ||
|
||
/** | ||
* To request a completion, the document needs to have been | ||
* updated and the update should not have been because | ||
* of accepting or clearing a suggestion. | ||
*/ | ||
function shouldRequestCompletion(update: ViewUpdate) { | ||
return ( | ||
update.docChanged && | ||
!update.transactions.some((tr) => | ||
tr.effects.some((e) => e.is(acceptSuggestion) || e.is(clearSuggestion)), | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* Don't request a completion if we've already | ||
* done so, or it's a copilot event we're responding | ||
* to, or if the view is not focused. | ||
*/ | ||
function shouldIgnoreUpdate(update: ViewUpdate) { | ||
// not focused | ||
if (!update.view.hasFocus) return true; | ||
|
||
// contains ghost text | ||
if (update.state.field(completionDecoration).ghostTexts != null) return true; | ||
|
||
// is autocompleting | ||
if (completionStatus(update.state) === "active") return true; | ||
|
||
// bad update | ||
for (const tr of update.transactions) { | ||
if (tr.annotation(copilotEvent) !== undefined) { | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A view plugin that requests completions from the server after a delay | ||
*/ | ||
export function completionRequester() { | ||
let timeout: any = null; | ||
let lastPos = 0; | ||
|
||
return EditorView.updateListener.of((update: ViewUpdate) => { | ||
if (!shouldRequestCompletion(update)) return; | ||
|
||
// Cancel the previous timeout | ||
if (timeout) { | ||
clearTimeout(timeout); | ||
} | ||
|
||
if (shouldIgnoreUpdate(update)) { | ||
return; | ||
} | ||
|
||
// Get the current position and source | ||
const state = update.state; | ||
const pos = state.selection.main.head; | ||
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; | ||
|
||
// Request completion from the server | ||
try { | ||
const completionResult = await getCodeiumCompletions({ | ||
text: source, | ||
cursorOffset: pos, | ||
}); | ||
|
||
if (!completionResult || completionResult.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 insertChangeSet = ChangeSet.of( | ||
completionResult.map((part) => ({ | ||
from: Number(part.offset), | ||
to: Number(part.offset), | ||
insert: part.text, | ||
})), | ||
state.doc.length, | ||
); | ||
|
||
const reverseChangeSet = insertChangeSet.invert(state.doc); | ||
|
||
update.view.dispatch({ | ||
changes: insertChangeSet, | ||
effects: addSuggestions.of({ | ||
reverseChangeSet, | ||
suggestions: completionResult.map((part) => ({ | ||
displayText: part.text, | ||
endReplacement: 0, // "", | ||
text: part.text, | ||
cursorPos: pos, | ||
startPos: Number(part.offset), | ||
endPos: Number(part.offset) + part.text.length, | ||
})), | ||
}), | ||
annotations: [ | ||
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)); | ||
} | ||
}, TIMEOUT); | ||
// Update the last position | ||
lastPos = pos; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { ChangeSet, StateEffect } from "@codemirror/state"; | ||
import { Suggestion } from "./types.js"; | ||
|
||
// Effects to tell StateEffect what to do with GhostText | ||
export const addSuggestions = StateEffect.define<{ | ||
reverseChangeSet: ChangeSet; | ||
suggestions: Suggestion[]; | ||
}>(); | ||
export const acceptSuggestion = StateEffect.define<null>(); | ||
export const clearSuggestion = StateEffect.define<null>(); |
Oops, something went wrong.