This commit is contained in:
duanfuxiang
2025-01-05 11:51:39 +08:00
commit 0c7ee142cb
215 changed files with 20611 additions and 0 deletions

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}