mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-16 04:31:38 +00:00
init
This commit is contained in:
35
src/core/autocomplete/states/disabled-file-specific-state.ts
Normal file
35
src/core/autocomplete/states/disabled-file-specific-state.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
|
||||
class DisabledFileSpecificState extends State {
|
||||
getStatusBarText(): string {
|
||||
return "Disabled for this file";
|
||||
}
|
||||
|
||||
handleSettingChanged(settings: InfioSettings) {
|
||||
if (!this.context.settings.autocompleteEnabled) {
|
||||
this.context.transitionToDisabledManualState();
|
||||
}
|
||||
if (!this.context.isCurrentFilePathIgnored() || !this.context.currentFileContainsIgnoredTag()) {
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChange(file: TFile): void {
|
||||
if (this.context.isCurrentFilePathIgnored() || this.context.currentFileContainsIgnoredTag()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.context.settings.autocompleteEnabled) {
|
||||
this.context.transitionToIdleState();
|
||||
} else {
|
||||
this.context.transitionToDisabledManualState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DisabledFileSpecificState;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
import { checkForErrors } from "../../../utils/auto-complete";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
class DisabledInvalidSettingsState extends State {
|
||||
getStatusBarText(): string {
|
||||
return "Disabled invalid settings";
|
||||
}
|
||||
|
||||
handleSettingChanged(settings: InfioSettings) {
|
||||
const settingErrors = checkForErrors(settings);
|
||||
if (settingErrors.size > 0) {
|
||||
return
|
||||
}
|
||||
if (this.context.settings.autocompleteEnabled) {
|
||||
this.context.transitionToIdleState();
|
||||
} else {
|
||||
this.context.transitionToDisabledManualState();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DisabledInvalidSettingsState;
|
||||
21
src/core/autocomplete/states/disabled-manual-state.ts
Normal file
21
src/core/autocomplete/states/disabled-manual-state.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
class DisabledManualState extends State {
|
||||
getStatusBarText(): string {
|
||||
return "Disabled";
|
||||
}
|
||||
|
||||
handleSettingChanged(settings: InfioSettings): void {
|
||||
if (this.context.settings.autocompleteEnabled) {
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChange(file: TFile): void { }
|
||||
}
|
||||
|
||||
export default DisabledManualState;
|
||||
46
src/core/autocomplete/states/idle-state.ts
Normal file
46
src/core/autocomplete/states/idle-state.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
|
||||
class IdleState extends State {
|
||||
|
||||
async handleDocumentChange(
|
||||
documentChanges: DocumentChanges
|
||||
): Promise<void> {
|
||||
if (
|
||||
!documentChanges.isDocInFocus()
|
||||
|| !documentChanges.hasDocChanged()
|
||||
|| documentChanges.hasUserDeleted()
|
||||
|| documentChanges.hasMultipleCursors()
|
||||
|| documentChanges.hasSelection()
|
||||
|| documentChanges.hasUserUndone()
|
||||
|| documentChanges.hasUserRedone()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedSuggestion = this.context.getCachedSuggestionFor(documentChanges.getPrefix(), documentChanges.getSuffix());
|
||||
const isThereCachedSuggestion = cachedSuggestion !== undefined && cachedSuggestion.trim().length > 0;
|
||||
|
||||
if (this.context.settings.cacheSuggestions && isThereCachedSuggestion) {
|
||||
this.context.transitionToSuggestingState(cachedSuggestion, documentChanges.getPrefix(), documentChanges.getSuffix());
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (this.context.containsTriggerCharacters(documentChanges)) {
|
||||
this.context.transitionToQueuedState(documentChanges.getPrefix(), documentChanges.getSuffix());
|
||||
}
|
||||
}
|
||||
|
||||
handlePredictCommand(prefix: string, suffix: string): void {
|
||||
this.context.transitionToPredictingState(prefix, suffix);
|
||||
}
|
||||
|
||||
getStatusBarText(): string {
|
||||
return "Idle";
|
||||
}
|
||||
}
|
||||
|
||||
export default IdleState;
|
||||
38
src/core/autocomplete/states/init-state.ts
Normal file
38
src/core/autocomplete/states/init-state.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
import { EventHandler } from "./types";
|
||||
|
||||
class InitState implements EventHandler {
|
||||
async handleDocumentChange(documentChanges: DocumentChanges): Promise<void> { }
|
||||
|
||||
handleSettingChanged(settings: InfioSettings): void { }
|
||||
|
||||
handleAcceptKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePartialAcceptKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleCancelKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePredictCommand(): void { }
|
||||
|
||||
handleAcceptCommand(): void { }
|
||||
|
||||
getStatusBarText(): string {
|
||||
return "Initializing...";
|
||||
}
|
||||
|
||||
handleFileChange(file: TFile): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default InitState;
|
||||
94
src/core/autocomplete/states/predicting-state.ts
Normal file
94
src/core/autocomplete/states/predicting-state.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
import Context from "../context-detection";
|
||||
import EventListener from "../../../event-listener";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
class PredictingState extends State {
|
||||
private predictionPromise: Promise<void> | null = null;
|
||||
private isStillNeeded = true;
|
||||
private readonly prefix: string;
|
||||
private readonly suffix: string;
|
||||
|
||||
constructor(context: EventListener, prefix: string, suffix: string) {
|
||||
super(context);
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
static createAndStartPredicting(
|
||||
context: EventListener,
|
||||
prefix: string,
|
||||
suffix: string
|
||||
): PredictingState {
|
||||
const predictingState = new PredictingState(context, prefix, suffix);
|
||||
predictingState.startPredicting();
|
||||
context.setContext(Context.getContext(prefix, suffix));
|
||||
return predictingState;
|
||||
}
|
||||
|
||||
handleCancelKeyPressed(): boolean {
|
||||
this.cancelPrediction();
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleDocumentChange(
|
||||
documentChanges: DocumentChanges
|
||||
): Promise<void> {
|
||||
if (
|
||||
documentChanges.hasCursorMoved() ||
|
||||
documentChanges.hasUserTyped() ||
|
||||
documentChanges.hasUserDeleted() ||
|
||||
documentChanges.isTextAdded()
|
||||
) {
|
||||
this.cancelPrediction();
|
||||
}
|
||||
}
|
||||
|
||||
private cancelPrediction(): void {
|
||||
this.isStillNeeded = false;
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
|
||||
startPredicting(): void {
|
||||
this.predictionPromise = this.predict();
|
||||
}
|
||||
|
||||
private async predict(): Promise<void> {
|
||||
|
||||
const result =
|
||||
await this.context.autocomplete?.fetchPredictions(
|
||||
this.prefix,
|
||||
this.suffix
|
||||
);
|
||||
|
||||
if (!this.isStillNeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isErr()) {
|
||||
new Notice(
|
||||
`Copilot: Something went wrong cannot make a prediction. Full error is available in the dev console. Please check your settings. `
|
||||
);
|
||||
console.error(result.error);
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
|
||||
const prediction = result.unwrapOr("");
|
||||
|
||||
if (prediction === "") {
|
||||
this.context.transitionToIdleState();
|
||||
return;
|
||||
}
|
||||
this.context.transitionToSuggestingState(prediction, this.prefix, this.suffix);
|
||||
}
|
||||
|
||||
|
||||
getStatusBarText(): string {
|
||||
return `Predicting for ${this.context.context}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default PredictingState;
|
||||
84
src/core/autocomplete/states/queued-state.ts
Normal file
84
src/core/autocomplete/states/queued-state.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import Context from "../context-detection";
|
||||
import EventListener from "../../../event-listener";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
|
||||
class QueuedState extends State {
|
||||
private timer: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly prefix: string;
|
||||
private readonly suffix: string;
|
||||
|
||||
|
||||
private constructor(
|
||||
context: EventListener,
|
||||
prefix: string,
|
||||
suffix: string
|
||||
) {
|
||||
super(context);
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
static createAndStartTimer(
|
||||
context: EventListener,
|
||||
prefix: string,
|
||||
suffix: string
|
||||
): QueuedState {
|
||||
const state = new QueuedState(context, prefix, suffix);
|
||||
state.startTimer();
|
||||
context.setContext(Context.getContext(prefix, suffix));
|
||||
return state;
|
||||
}
|
||||
|
||||
handleCancelKeyPressed(): boolean {
|
||||
this.cancelTimer();
|
||||
this.context.transitionToIdleState();
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleDocumentChange(
|
||||
documentChanges: DocumentChanges
|
||||
): Promise<void> {
|
||||
if (
|
||||
documentChanges.isDocInFocus() &&
|
||||
documentChanges.isTextAdded() &&
|
||||
this.context.containsTriggerCharacters(documentChanges)
|
||||
) {
|
||||
this.cancelTimer();
|
||||
this.context.transitionToQueuedState(documentChanges.getPrefix(), documentChanges.getSuffix());
|
||||
return
|
||||
}
|
||||
if (
|
||||
(documentChanges.hasCursorMoved() ||
|
||||
documentChanges.hasUserTyped() ||
|
||||
documentChanges.hasUserDeleted() ||
|
||||
documentChanges.isTextAdded() ||
|
||||
!documentChanges.isDocInFocus())
|
||||
) {
|
||||
this.cancelTimer();
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
}
|
||||
|
||||
startTimer(): void {
|
||||
this.cancelTimer();
|
||||
this.timer = setTimeout(() => {
|
||||
this.context.transitionToPredictingState(this.prefix, this.suffix);
|
||||
}, this.context.settings.delay);
|
||||
}
|
||||
|
||||
private cancelTimer(): void {
|
||||
if (this.timer !== null) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusBarText(): string {
|
||||
return `Queued (${this.context.settings.delay} ms)`;
|
||||
}
|
||||
}
|
||||
|
||||
export default QueuedState;
|
||||
67
src/core/autocomplete/states/state.ts
Normal file
67
src/core/autocomplete/states/state.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Notice, TFile } from "obsidian";
|
||||
|
||||
import EventListener from "../../../event-listener";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
// import { Settings } from "../settings/versions";
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
import { checkForErrors } from "../../../utils/auto-complete";
|
||||
|
||||
import { EventHandler } from "./types";
|
||||
|
||||
abstract class State implements EventHandler {
|
||||
protected readonly context: EventListener;
|
||||
|
||||
constructor(context: EventListener) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
handleSettingChanged(settings: InfioSettings): void {
|
||||
const settingErrors = checkForErrors(settings);
|
||||
if (!settings.autocompleteEnabled) {
|
||||
new Notice("Copilot is now disabled.");
|
||||
this.context.transitionToDisabledManualState()
|
||||
} else if (settingErrors.size > 0) {
|
||||
new Notice(
|
||||
`Copilot: There are ${settingErrors.size} errors in your settings. The plugin will be disabled until they are fixed.`
|
||||
);
|
||||
this.context.transitionToDisabledInvalidSettingsState();
|
||||
} else if (this.context.isCurrentFilePathIgnored() || this.context.currentFileContainsIgnoredTag()) {
|
||||
this.context.transitionToDisabledFileSpecificState();
|
||||
}
|
||||
}
|
||||
|
||||
async handleDocumentChange(
|
||||
documentChanges: DocumentChanges
|
||||
): Promise<void> {
|
||||
}
|
||||
|
||||
handleAcceptKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePartialAcceptKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleCancelKeyPressed(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handlePredictCommand(prefix: string, suffix: string): void {
|
||||
}
|
||||
|
||||
handleAcceptCommand(): void {
|
||||
}
|
||||
|
||||
abstract getStatusBarText(): string;
|
||||
|
||||
handleFileChange(file: TFile): void {
|
||||
if (this.context.isCurrentFilePathIgnored() || this.context.currentFileContainsIgnoredTag()) {
|
||||
this.context.transitionToDisabledFileSpecificState();
|
||||
} else if (this.context.isDisabled()) {
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default State;
|
||||
177
src/core/autocomplete/states/suggesting-state.ts
Normal file
177
src/core/autocomplete/states/suggesting-state.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
import { Settings } from "../../../settings/versions";
|
||||
import { extractNextWordAndRemaining } from "../utils";
|
||||
import EventListener from "../../../event-listener";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
import State from "./state";
|
||||
|
||||
class SuggestingState extends State {
|
||||
private readonly suggestion: string;
|
||||
private readonly prefix: string;
|
||||
private readonly suffix: string;
|
||||
|
||||
|
||||
constructor(context: EventListener, suggestion: string, prefix: string, suffix: string) {
|
||||
super(context);
|
||||
this.suggestion = suggestion;
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
|
||||
async handleDocumentChange(
|
||||
documentChanges: DocumentChanges
|
||||
): Promise<void> {
|
||||
|
||||
if (
|
||||
documentChanges.hasCursorMoved()
|
||||
|| documentChanges.hasUserUndone()
|
||||
|| documentChanges.hasUserDeleted()
|
||||
|| documentChanges.hasUserRedone()
|
||||
|| !documentChanges.isDocInFocus()
|
||||
|| documentChanges.hasSelection()
|
||||
|| documentChanges.hasMultipleCursors()
|
||||
) {
|
||||
this.clearPrediction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
documentChanges.noUserEvents()
|
||||
|| !documentChanges.hasDocChanged()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasUserAddedPartOfSuggestion(documentChanges)) {
|
||||
this.acceptPartialAddedText(documentChanges);
|
||||
return
|
||||
}
|
||||
|
||||
const currentPrefix = documentChanges.getPrefix();
|
||||
const currentSuffix = documentChanges.getSuffix();
|
||||
const suggestion = this.context.getCachedSuggestionFor(currentPrefix, currentSuffix);
|
||||
const isThereCachedSuggestion = suggestion !== undefined;
|
||||
const isCachedSuggestionDifferent = suggestion !== this.suggestion;
|
||||
|
||||
if (!isCachedSuggestionDifferent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThereCachedSuggestion) {
|
||||
this.context.transitionToSuggestingState(suggestion, currentPrefix, currentSuffix);
|
||||
return;
|
||||
}
|
||||
this.clearPrediction();
|
||||
}
|
||||
|
||||
|
||||
hasUserAddedPartOfSuggestion(documentChanges: DocumentChanges): boolean {
|
||||
const addedPrefixText = documentChanges.getAddedPrefixText();
|
||||
const addedSuffixText = documentChanges.getAddedSuffixText();
|
||||
|
||||
return addedPrefixText !== undefined
|
||||
&& addedSuffixText !== undefined
|
||||
&& this.suggestion.toLowerCase().startsWith(addedPrefixText.toLowerCase())
|
||||
&& this.suggestion.toLowerCase().endsWith(addedSuffixText.toLowerCase());
|
||||
}
|
||||
|
||||
acceptPartialAddedText(documentChanges: DocumentChanges): void {
|
||||
const addedPrefixText = documentChanges.getAddedPrefixText();
|
||||
const addedSuffixText = documentChanges.getAddedSuffixText();
|
||||
if (addedSuffixText === undefined || addedPrefixText === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIdx = addedPrefixText.length;
|
||||
const endIdx = this.suggestion.length - addedSuffixText.length
|
||||
const remainingSuggestion = this.suggestion.substring(startIdx, endIdx);
|
||||
|
||||
if (remainingSuggestion.trim() === "") {
|
||||
this.clearPrediction();
|
||||
} else {
|
||||
this.context.transitionToSuggestingState(remainingSuggestion, documentChanges.getPrefix(), documentChanges.getSuffix());
|
||||
}
|
||||
}
|
||||
|
||||
private clearPrediction(): void {
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
|
||||
handleAcceptKeyPressed(): boolean {
|
||||
this.accept();
|
||||
return true;
|
||||
}
|
||||
|
||||
private accept() {
|
||||
this.addPartialSuggestionCaches(this.suggestion);
|
||||
this.context.insertCurrentSuggestion(this.suggestion);
|
||||
this.context.transitionToIdleState();
|
||||
}
|
||||
|
||||
handlePartialAcceptKeyPressed(): boolean {
|
||||
this.acceptNextWord();
|
||||
return true;
|
||||
}
|
||||
|
||||
private acceptNextWord() {
|
||||
const [nextWord, remaining] = extractNextWordAndRemaining(this.suggestion);
|
||||
|
||||
if (nextWord !== undefined && remaining !== undefined) {
|
||||
const updatedPrefix = this.prefix + nextWord;
|
||||
|
||||
this.addPartialSuggestionCaches(nextWord, remaining);
|
||||
this.context.insertCurrentSuggestion(nextWord);
|
||||
this.context.transitionToSuggestingState(remaining, updatedPrefix, this.suffix, false);
|
||||
} else {
|
||||
this.accept();
|
||||
}
|
||||
}
|
||||
|
||||
private addPartialSuggestionCaches(acceptSuggestion: string, remainingSuggestion = "") {
|
||||
// store the sub-suggestions in the cache
|
||||
// so that we can have partial suggestions if the user edits a part
|
||||
for (let i = 0; i < acceptSuggestion.length; i++) {
|
||||
const prefix = this.prefix + acceptSuggestion.substring(0, i);
|
||||
const suggestion = acceptSuggestion.substring(i) + remainingSuggestion;
|
||||
this.context.addSuggestionToCache(prefix, this.suffix, suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
private getNextWordAndRemaining(): [string | undefined, string | undefined] {
|
||||
const words = this.suggestion.split(" ");
|
||||
if (words.length === 0) {
|
||||
return ["", ""];
|
||||
}
|
||||
|
||||
if (words.length === 1) {
|
||||
return [words[0] + " ", ""];
|
||||
}
|
||||
|
||||
return [words[0] + " ", words.slice(1).join(" ")];
|
||||
}
|
||||
|
||||
handleCancelKeyPressed(): boolean {
|
||||
this.context.clearSuggestionsCache();
|
||||
this.clearPrediction();
|
||||
return true;
|
||||
}
|
||||
|
||||
handleAcceptCommand() {
|
||||
this.accept();
|
||||
}
|
||||
|
||||
getStatusBarText(): string {
|
||||
return `Suggesting for ${this.context.context}`;
|
||||
}
|
||||
|
||||
handleSettingChanged(settings: Settings): void {
|
||||
if (!settings.cacheSuggestions) {
|
||||
this.clearPrediction();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SuggestingState;
|
||||
25
src/core/autocomplete/states/types.ts
Normal file
25
src/core/autocomplete/states/types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import { TFile } from "obsidian";
|
||||
|
||||
import { InfioSettings } from "../../../types/settings";
|
||||
import { DocumentChanges } from "../../../render-plugin/document-changes-listener";
|
||||
|
||||
export interface EventHandler {
|
||||
handleSettingChanged(settings: InfioSettings): void;
|
||||
|
||||
handleDocumentChange(documentChanges: DocumentChanges): Promise<void>;
|
||||
|
||||
handleAcceptKeyPressed(): boolean;
|
||||
|
||||
handlePartialAcceptKeyPressed(): boolean;
|
||||
|
||||
handleCancelKeyPressed(): boolean;
|
||||
|
||||
handlePredictCommand(prefix: string, suffix: string): void;
|
||||
handleAcceptCommand(): void;
|
||||
|
||||
getStatusBarText(): string;
|
||||
|
||||
handleFileChange(file: TFile): void;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user