init
This commit is contained in:
95
src/core/autocomplete/context-detection.ts
Normal file
95
src/core/autocomplete/context-detection.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { generateRandomString } from "./utils";
|
||||
|
||||
const UNIQUE_CURSOR = `${generateRandomString(16)}`;
|
||||
const HEADER_REGEX = `^#+\\s.*${UNIQUE_CURSOR}.*$`;
|
||||
const UNORDERED_LIST_REGEX = `^\\s*(-|\\*)\\s.*${UNIQUE_CURSOR}.*$`;
|
||||
const TASK_LIST_REGEX = `^\\s*(-|[0-9]+\\.) +\\[.\\]\\s.*${UNIQUE_CURSOR}.*$`;
|
||||
const BLOCK_QUOTES_REGEX = `^\\s*>.*${UNIQUE_CURSOR}.*$`;
|
||||
const NUMBERED_LIST_REGEX = `^\\s*\\d+\\.\\s.*${UNIQUE_CURSOR}.*$`
|
||||
const MATH_BLOCK_REGEX = /\$\$[\s\S]*?\$\$/g;
|
||||
const INLINE_MATH_BLOCK_REGEX = /\$[\s\S]*?\$/g;
|
||||
const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;
|
||||
const INLINE_CODE_BLOCK_REGEX = /`.*`/g;
|
||||
|
||||
enum Context {
|
||||
Text = "Text",
|
||||
Heading = "Heading",
|
||||
BlockQuotes = "BlockQuotes",
|
||||
UnorderedList = "UnorderedList",
|
||||
NumberedList = "NumberedList",
|
||||
CodeBlock = "CodeBlock",
|
||||
MathBlock = "MathBlock",
|
||||
TaskList = "TaskList",
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Context {
|
||||
export function values(): Array<Context> {
|
||||
return Object.values(Context).filter(
|
||||
(value) => typeof value === "string"
|
||||
) as Array<Context>;
|
||||
}
|
||||
|
||||
export function getContext(prefix: string, suffix: string): Context {
|
||||
if (new RegExp(HEADER_REGEX, "gm").test(prefix + UNIQUE_CURSOR + suffix)) {
|
||||
return Context.Heading;
|
||||
}
|
||||
if (new RegExp(BLOCK_QUOTES_REGEX, "gm").test(prefix + UNIQUE_CURSOR + suffix)) {
|
||||
return Context.BlockQuotes;
|
||||
}
|
||||
|
||||
if (new RegExp(TASK_LIST_REGEX, "gm").test(prefix + UNIQUE_CURSOR + suffix)) {
|
||||
return Context.TaskList;
|
||||
}
|
||||
|
||||
if (
|
||||
isCursorInRegexBlock(prefix, suffix, MATH_BLOCK_REGEX) ||
|
||||
isCursorInRegexBlock(prefix, suffix, INLINE_MATH_BLOCK_REGEX)
|
||||
) {
|
||||
return Context.MathBlock;
|
||||
}
|
||||
|
||||
if (isCursorInRegexBlock(prefix, suffix, CODE_BLOCK_REGEX) || isCursorInRegexBlock(prefix, suffix, INLINE_CODE_BLOCK_REGEX)) {
|
||||
return Context.CodeBlock;
|
||||
}
|
||||
if (new RegExp(NUMBERED_LIST_REGEX, "gm").test(prefix + UNIQUE_CURSOR + suffix)) {
|
||||
return Context.NumberedList;
|
||||
}
|
||||
if (new RegExp(UNORDERED_LIST_REGEX, "gm").test(prefix + UNIQUE_CURSOR + suffix)) {
|
||||
return Context.UnorderedList;
|
||||
}
|
||||
|
||||
return Context.Text;
|
||||
}
|
||||
|
||||
export function get(value: string) {
|
||||
for (const context of Context.values()) {
|
||||
if (value === context) {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isCursorInRegexBlock(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
regex: RegExp
|
||||
): boolean {
|
||||
const text = prefix + UNIQUE_CURSOR + suffix;
|
||||
const codeBlocks = extractBlocks(text, regex);
|
||||
for (const block of codeBlocks) {
|
||||
if (block.includes(UNIQUE_CURSOR)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractBlocks(text: string, regex: RegExp) {
|
||||
const codeBlocks = text.match(regex);
|
||||
return codeBlocks ? codeBlocks.map((block) => block.trim()) : [];
|
||||
}
|
||||
|
||||
export default Context;
|
||||
273
src/core/autocomplete/index.ts
Normal file
273
src/core/autocomplete/index.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import * as Handlebars from "handlebars";
|
||||
import { err, ok, Result } from "neverthrow";
|
||||
|
||||
import { FewShotExample } from "../../settings/versions";
|
||||
import { CustomLLMModel } from "../../types/llm/model";
|
||||
import { RequestMessage } from '../../types/llm/request';
|
||||
import { InfioSettings } from "../../types/settings";
|
||||
import LLMManager from '../llm/manager';
|
||||
|
||||
import Context from "./context-detection";
|
||||
import RemoveCodeIndicators from "./post-processors/remove-code-indicators";
|
||||
import RemoveMathIndicators from "./post-processors/remove-math-indicators";
|
||||
import RemoveOverlap from "./post-processors/remove-overlap";
|
||||
import RemoveWhitespace from "./post-processors/remove-whitespace";
|
||||
import DataViewRemover from "./pre-processors/data-view-remover";
|
||||
import LengthLimiter from "./pre-processors/length-limiter";
|
||||
import {
|
||||
AutocompleteService,
|
||||
ChatMessage,
|
||||
PostProcessor,
|
||||
PreProcessor,
|
||||
UserMessageFormatter,
|
||||
UserMessageFormattingInputs
|
||||
} from "./types";
|
||||
|
||||
class LLMClient {
|
||||
private llm: LLMManager;
|
||||
private model: CustomLLMModel;
|
||||
|
||||
constructor(llm: LLMManager, model: CustomLLMModel) {
|
||||
this.llm = llm;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
async queryChatModel(messages: RequestMessage[]): Promise<Result<string, Error>> {
|
||||
const data = await this.llm.generateResponse(this.model, {
|
||||
model: this.model.name,
|
||||
messages: messages,
|
||||
stream: false,
|
||||
})
|
||||
return ok(data.choices[0].message.content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AutoComplete implements AutocompleteService {
|
||||
private readonly client: LLMClient;
|
||||
private readonly systemMessage: string;
|
||||
private readonly userMessageFormatter: UserMessageFormatter;
|
||||
private readonly removePreAnswerGenerationRegex: string;
|
||||
private readonly preProcessors: PreProcessor[];
|
||||
private readonly postProcessors: PostProcessor[];
|
||||
private readonly fewShotExamples: FewShotExample[];
|
||||
private debugMode: boolean;
|
||||
|
||||
private constructor(
|
||||
client: LLMClient,
|
||||
systemMessage: string,
|
||||
userMessageFormatter: UserMessageFormatter,
|
||||
removePreAnswerGenerationRegex: string,
|
||||
preProcessors: PreProcessor[],
|
||||
postProcessors: PostProcessor[],
|
||||
fewShotExamples: FewShotExample[],
|
||||
debugMode: boolean,
|
||||
) {
|
||||
this.client = client;
|
||||
this.systemMessage = systemMessage;
|
||||
this.userMessageFormatter = userMessageFormatter;
|
||||
this.removePreAnswerGenerationRegex = removePreAnswerGenerationRegex;
|
||||
this.preProcessors = preProcessors;
|
||||
this.postProcessors = postProcessors;
|
||||
this.fewShotExamples = fewShotExamples;
|
||||
this.debugMode = debugMode;
|
||||
}
|
||||
|
||||
public static fromSettings(settings: InfioSettings): AutocompleteService {
|
||||
const formatter = Handlebars.compile<UserMessageFormattingInputs>(
|
||||
settings.userMessageTemplate,
|
||||
{ noEscape: true, strict: true }
|
||||
);
|
||||
const preProcessors: PreProcessor[] = [];
|
||||
if (settings.dontIncludeDataviews) {
|
||||
preProcessors.push(new DataViewRemover());
|
||||
}
|
||||
preProcessors.push(
|
||||
new LengthLimiter(
|
||||
settings.maxPrefixCharLimit,
|
||||
settings.maxSuffixCharLimit
|
||||
)
|
||||
);
|
||||
|
||||
const postProcessors: PostProcessor[] = [];
|
||||
if (settings.removeDuplicateMathBlockIndicator) {
|
||||
postProcessors.push(new RemoveMathIndicators());
|
||||
}
|
||||
if (settings.removeDuplicateCodeBlockIndicator) {
|
||||
postProcessors.push(new RemoveCodeIndicators());
|
||||
}
|
||||
|
||||
postProcessors.push(new RemoveOverlap());
|
||||
postProcessors.push(new RemoveWhitespace());
|
||||
|
||||
const llm_manager = new LLMManager({
|
||||
deepseek: settings.deepseekApiKey,
|
||||
openai: settings.openAIApiKey,
|
||||
anthropic: settings.anthropicApiKey,
|
||||
gemini: settings.geminiApiKey,
|
||||
groq: settings.groqApiKey,
|
||||
infio: settings.infioApiKey,
|
||||
})
|
||||
const model: CustomLLMModel = settings.activeModels.find(
|
||||
(option) => option.name === settings.chatModelId,
|
||||
)
|
||||
const llm = new LLMClient(llm_manager, model);
|
||||
|
||||
return new AutoComplete(
|
||||
llm,
|
||||
settings.systemMessage,
|
||||
formatter,
|
||||
settings.chainOfThoughRemovalRegex,
|
||||
preProcessors,
|
||||
postProcessors,
|
||||
settings.fewShotExamples,
|
||||
settings.debugMode,
|
||||
);
|
||||
}
|
||||
|
||||
async fetchPredictions(
|
||||
prefix: string,
|
||||
suffix: string
|
||||
): Promise<Result<string, Error>> {
|
||||
const context: Context = Context.getContext(prefix, suffix);
|
||||
|
||||
for (const preProcessor of this.preProcessors) {
|
||||
if (preProcessor.removesCursor(prefix, suffix)) {
|
||||
return ok("");
|
||||
}
|
||||
|
||||
({ prefix, suffix } = preProcessor.process(
|
||||
prefix,
|
||||
suffix,
|
||||
context
|
||||
));
|
||||
}
|
||||
|
||||
const examples = this.fewShotExamples.filter(
|
||||
(example) => example.context === context
|
||||
);
|
||||
const fewShotExamplesChatMessages =
|
||||
fewShotExamplesToChatMessages(examples);
|
||||
|
||||
const messages: RequestMessage[] = [
|
||||
{
|
||||
content: this.getSystemMessageFor(context),
|
||||
role: "system"
|
||||
},
|
||||
...fewShotExamplesChatMessages,
|
||||
{
|
||||
role: "user",
|
||||
content: this.userMessageFormatter({
|
||||
suffix,
|
||||
prefix,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.debugMode) {
|
||||
console.log("Copilot messages send:\n", messages);
|
||||
}
|
||||
|
||||
let result = await this.client.queryChatModel(messages);
|
||||
if (this.debugMode && result.isOk()) {
|
||||
console.log("Copilot response:\n", result.value);
|
||||
}
|
||||
|
||||
result = this.extractAnswerFromChainOfThoughts(result);
|
||||
|
||||
for (const postProcessor of this.postProcessors) {
|
||||
result = result.map((r) => postProcessor.process(prefix, suffix, r, context));
|
||||
}
|
||||
|
||||
result = this.checkAgainstGuardRails(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getSystemMessageFor(context: Context): string {
|
||||
if (context === Context.Text) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in a paragraph. Your answer must complete this paragraph or sentence in a way that fits the surrounding text without overlapping with it. It must be in the same language as the paragraph.";
|
||||
}
|
||||
if (context === Context.Heading) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in the Markdown heading. Your answer must complete this title in a way that fits the content of this paragraph and be in the same language as the paragraph.";
|
||||
}
|
||||
|
||||
if (context === Context.BlockQuotes) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located within a quote. Your answer must complete this quote in a way that fits the context of the paragraph.";
|
||||
}
|
||||
if (context === Context.UnorderedList) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in an unordered list. Your answer must include one or more list items that fit with the surrounding list without overlapping with it.";
|
||||
}
|
||||
|
||||
if (context === Context.NumberedList) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in a numbered list. Your answer must include one or more list items that fit the sequence and context of the surrounding list without overlapping with it.";
|
||||
}
|
||||
|
||||
if (context === Context.CodeBlock) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in a code block. Your answer must complete this code block in the same programming language and support the surrounding code and text outside of the code block.";
|
||||
}
|
||||
if (context === Context.MathBlock) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in a math block. Your answer must only contain LaTeX code that captures the math discussed in the surrounding text. No text or explaination only LaTex math code.";
|
||||
}
|
||||
if (context === Context.TaskList) {
|
||||
return this.systemMessage + "\n\n" + "The <mask/> is located in a task list. Your answer must include one or more (sub)tasks that are logical given the other tasks and the surrounding text.";
|
||||
}
|
||||
|
||||
|
||||
return this.systemMessage;
|
||||
|
||||
}
|
||||
|
||||
private extractAnswerFromChainOfThoughts(
|
||||
result: Result<string, Error>
|
||||
): Result<string, Error> {
|
||||
if (result.isErr()) {
|
||||
return result;
|
||||
}
|
||||
const chainOfThoughts = result.value;
|
||||
|
||||
const regex = new RegExp(this.removePreAnswerGenerationRegex, "gm");
|
||||
const match = regex.exec(chainOfThoughts);
|
||||
if (match === null) {
|
||||
return err(new Error("No match found"));
|
||||
}
|
||||
return ok(chainOfThoughts.replace(regex, ""));
|
||||
}
|
||||
|
||||
private checkAgainstGuardRails(
|
||||
result: Result<string, Error>
|
||||
): Result<string, Error> {
|
||||
if (result.isErr()) {
|
||||
return result;
|
||||
}
|
||||
if (result.value.length === 0) {
|
||||
return err(new Error("Empty result"));
|
||||
}
|
||||
if (result.value.contains("<mask/>")) {
|
||||
return err(new Error("Mask in result"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function fewShotExamplesToChatMessages(
|
||||
examples: FewShotExample[]
|
||||
): ChatMessage[] {
|
||||
return examples
|
||||
.map((example): ChatMessage[] => {
|
||||
return [
|
||||
{
|
||||
role: "user",
|
||||
content: example.input,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: example.answer,
|
||||
},
|
||||
];
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
export default AutoComplete;
|
||||
@@ -0,0 +1,22 @@
|
||||
import Context from "../context-detection";
|
||||
import { PostProcessor } from "../types";
|
||||
|
||||
class RemoveCodeIndicators implements PostProcessor {
|
||||
process(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
completion: string,
|
||||
context: Context
|
||||
): string {
|
||||
if (context === Context.CodeBlock) {
|
||||
completion = completion.replace(/```[a-zA-z]+[ \t]*\n?/g, "");
|
||||
completion = completion.replace(/\n?```[ \t]*\n?/g, "");
|
||||
completion = completion.replace(/`/g, "");
|
||||
}
|
||||
|
||||
|
||||
return completion;
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveCodeIndicators;
|
||||
@@ -0,0 +1,20 @@
|
||||
import Context from "../context-detection";
|
||||
import { PostProcessor } from "../types";
|
||||
|
||||
class RemoveMathIndicators implements PostProcessor {
|
||||
process(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
completion: string,
|
||||
context: Context
|
||||
): string {
|
||||
if (context === Context.MathBlock) {
|
||||
completion = completion.replace(/\n?\$\$\n?/g, "");
|
||||
completion = completion.replace(/\$/g, "");
|
||||
}
|
||||
|
||||
return completion;
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveMathIndicators;
|
||||
96
src/core/autocomplete/post-processors/remove-overlap.ts
Normal file
96
src/core/autocomplete/post-processors/remove-overlap.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import Context from "../context-detection";
|
||||
import { PostProcessor } from "../types";
|
||||
|
||||
|
||||
class RemoveOverlap implements PostProcessor {
|
||||
process(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
completion: string,
|
||||
context: Context
|
||||
): string {
|
||||
completion = removeWordOverlapPrefix(prefix, completion);
|
||||
completion = removeWordOverlapSuffix(completion, suffix);
|
||||
completion = removeWhiteSpaceOverlapPrefix(suffix, completion);
|
||||
completion = removeWhiteSpaceOverlapSuffix(completion, suffix);
|
||||
|
||||
return completion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeWhiteSpaceOverlapPrefix(prefix: string, completion: string): string {
|
||||
let prefixIdx = prefix.length - 1;
|
||||
while (completion.length > 0 && completion[0] === prefix[prefixIdx]) {
|
||||
completion = completion.slice(1);
|
||||
prefixIdx--;
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
|
||||
function removeWhiteSpaceOverlapSuffix(completion: string, suffix: string): string {
|
||||
let suffixIdx = 0;
|
||||
while (completion.length > 0 && completion[completion.length - 1] === suffix[suffixIdx]) {
|
||||
completion = completion.slice(0, -1);
|
||||
suffixIdx++;
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
function removeWordOverlapPrefix(prefix: string, completion: string): string {
|
||||
const rightTrimmed = completion.trimStart();
|
||||
|
||||
const startIdxOfEachWord = startLocationOfEachWord(prefix);
|
||||
|
||||
while (startIdxOfEachWord.length > 0) {
|
||||
const idx = startIdxOfEachWord.pop();
|
||||
const leftSubstring = prefix.slice(idx);
|
||||
if (rightTrimmed.startsWith(leftSubstring)) {
|
||||
return rightTrimmed.replace(leftSubstring, "");
|
||||
}
|
||||
}
|
||||
|
||||
return completion;
|
||||
}
|
||||
|
||||
function removeWordOverlapSuffix(completion: string, suffix: string): string {
|
||||
const suffixTrimmed = removeLeadingWhiteSpace(suffix);
|
||||
|
||||
const startIdxOfEachWord = startLocationOfEachWord(completion);
|
||||
|
||||
while (startIdxOfEachWord.length > 0) {
|
||||
const idx = startIdxOfEachWord.pop();
|
||||
const suffixSubstring = completion.slice(idx);
|
||||
if (suffixTrimmed.startsWith(suffixSubstring)) {
|
||||
return completion.replace(suffixSubstring, "");
|
||||
}
|
||||
}
|
||||
|
||||
return completion;
|
||||
}
|
||||
|
||||
function removeLeadingWhiteSpace(completion: string): string {
|
||||
return completion.replace(/^[ \t\f\r\v]+/, "");
|
||||
}
|
||||
|
||||
function startLocationOfEachWord(text: string): number[] {
|
||||
const locations: number[] = [];
|
||||
if (text.length > 0 && !isWhiteSpaceChar(text[0])) {
|
||||
locations.push(0);
|
||||
}
|
||||
|
||||
for (let i = 1; i < text.length; i++) {
|
||||
if (isWhiteSpaceChar(text[i - 1]) && !isWhiteSpaceChar(text[i])) {
|
||||
locations.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
}
|
||||
|
||||
function isWhiteSpaceChar(char: string | undefined): boolean {
|
||||
return char !== undefined && char.match(/\s/) !== null;
|
||||
}
|
||||
|
||||
export default RemoveOverlap;
|
||||
24
src/core/autocomplete/post-processors/remove-whitespace.ts
Normal file
24
src/core/autocomplete/post-processors/remove-whitespace.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Context from "../context-detection";
|
||||
import { PostProcessor } from "../types";
|
||||
|
||||
class RemoveWhitespace implements PostProcessor {
|
||||
process(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
completion: string,
|
||||
context: Context
|
||||
): string {
|
||||
if (context === Context.Text || context === Context.Heading || context === Context.MathBlock || context === Context.TaskList || context === Context.NumberedList || context === Context.UnorderedList) {
|
||||
if (prefix.endsWith(" ") || suffix.endsWith("\n")) {
|
||||
completion = completion.trimStart();
|
||||
}
|
||||
if (suffix.startsWith(" ")) {
|
||||
completion = completion.trimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return completion;
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveWhitespace;
|
||||
34
src/core/autocomplete/pre-processors/data-view-remover.ts
Normal file
34
src/core/autocomplete/pre-processors/data-view-remover.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { generateRandomString } from "../utils";
|
||||
import Context from "../context-detection";
|
||||
import { PrefixAndSuffix, PreProcessor } from "../types";
|
||||
|
||||
const DATA_VIEW_REGEX = /```dataview(js){0,1}(.|\n)*?```/gm;
|
||||
const UNIQUE_CURSOR = `${generateRandomString(16)}`;
|
||||
|
||||
class DataViewRemover implements PreProcessor {
|
||||
process(prefix: string, suffix: string, context: Context): PrefixAndSuffix {
|
||||
let text = prefix + UNIQUE_CURSOR + suffix;
|
||||
text = text.replace(DATA_VIEW_REGEX, "");
|
||||
const [prefixNew, suffixNew] = text.split(UNIQUE_CURSOR);
|
||||
|
||||
return { prefix: prefixNew, suffix: suffixNew };
|
||||
}
|
||||
|
||||
removesCursor(prefix: string, suffix: string): boolean {
|
||||
const text = prefix + UNIQUE_CURSOR + suffix;
|
||||
const dataviewAreasWithCursor = text
|
||||
.match(DATA_VIEW_REGEX)
|
||||
?.filter((dataviewArea) => dataviewArea.includes(UNIQUE_CURSOR));
|
||||
|
||||
if (
|
||||
dataviewAreasWithCursor !== undefined &&
|
||||
dataviewAreasWithCursor.length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default DataViewRemover;
|
||||
24
src/core/autocomplete/pre-processors/length-limiter.ts
Normal file
24
src/core/autocomplete/pre-processors/length-limiter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Context from "../context-detection";
|
||||
import { PrefixAndSuffix, PreProcessor } from "../types";
|
||||
|
||||
class LengthLimiter implements PreProcessor {
|
||||
private readonly maxPrefixChars: number;
|
||||
private readonly maxSuffixChars: number;
|
||||
|
||||
constructor(maxPrefixChars: number, maxSuffixChars: number) {
|
||||
this.maxPrefixChars = maxPrefixChars;
|
||||
this.maxSuffixChars = maxSuffixChars;
|
||||
}
|
||||
|
||||
process(prefix: string, suffix: string, context: Context): PrefixAndSuffix {
|
||||
prefix = prefix.slice(-this.maxPrefixChars);
|
||||
suffix = suffix.slice(0, this.maxSuffixChars);
|
||||
return { prefix, suffix };
|
||||
}
|
||||
|
||||
removesCursor(prefix: string, suffix: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default LengthLimiter;
|
||||
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;
|
||||
|
||||
}
|
||||
57
src/core/autocomplete/types.ts
Normal file
57
src/core/autocomplete/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Result } from "neverthrow";
|
||||
|
||||
import Context from "./context-detection";
|
||||
|
||||
export interface AutocompleteService {
|
||||
fetchPredictions(
|
||||
prefix: string,
|
||||
suffix: string
|
||||
): Promise<Result<string, Error>>;
|
||||
}
|
||||
|
||||
export interface PostProcessor {
|
||||
process(
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
completion: string,
|
||||
context: Context
|
||||
): string;
|
||||
}
|
||||
|
||||
export interface PreProcessor {
|
||||
process(prefix: string, suffix: string, context: Context): PrefixAndSuffix;
|
||||
removesCursor(prefix: string, suffix: string): boolean;
|
||||
}
|
||||
|
||||
export interface PrefixAndSuffix {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
content: string;
|
||||
role: "user" | "assistant" | "system";
|
||||
}
|
||||
|
||||
|
||||
export interface UserMessageFormattingInputs {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
export type UserMessageFormatter = (
|
||||
inputs: UserMessageFormattingInputs
|
||||
) => string;
|
||||
|
||||
export interface ApiClient {
|
||||
queryChatModel(messages: ChatMessage[]): Promise<Result<string, Error>>;
|
||||
checkIfConfiguredCorrectly?(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface ModelOptions {
|
||||
temperature: number;
|
||||
top_p: number;
|
||||
frequency_penalty: number;
|
||||
presence_penalty: number;
|
||||
max_tokens: number;
|
||||
}
|
||||
68
src/core/autocomplete/utils.ts
Normal file
68
src/core/autocomplete/utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as mm from "micromatch";
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function enumKeys<O extends object, K extends keyof O = keyof O>(
|
||||
obj: O
|
||||
): K[] {
|
||||
return Object.keys(obj).filter((k) => Number.isNaN(+k)) as K[];
|
||||
}
|
||||
|
||||
export function generateRandomString(n: number): string {
|
||||
let result = '';
|
||||
const characters = '0123456789abcdef';
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters[randomIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isMatchBetweenPathAndPatterns(
|
||||
path: string,
|
||||
patterns: string[],
|
||||
): boolean {
|
||||
patterns = patterns
|
||||
.map(p => p.trim())
|
||||
.filter((p) => p.length > 0);
|
||||
if (patterns.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const exclusionPatterns = patterns.filter((p) => p.startsWith('!')).map(p => p.slice(1));
|
||||
const inclusionPatterns = patterns.filter((p) => !p.startsWith('!'));
|
||||
|
||||
return mm.some(path, inclusionPatterns) && !mm.some(path, exclusionPatterns);
|
||||
}
|
||||
|
||||
export function extractNextWordAndRemaining(suggestion: string): [string | undefined, string | undefined] {
|
||||
const leadingWhitespacesMatch = suggestion.match(/^(\s*)/);
|
||||
const leadingWhitespaces = leadingWhitespacesMatch ? leadingWhitespacesMatch[0] : '';
|
||||
const trimmedSuggestion = suggestion.slice(leadingWhitespaces.length);
|
||||
|
||||
|
||||
let nextWord: string | undefined;
|
||||
let remaining: string | undefined = undefined;
|
||||
|
||||
const whitespaceAfterNextWordMatch = trimmedSuggestion.match(/\s+/);
|
||||
if (!whitespaceAfterNextWordMatch) {
|
||||
nextWord = trimmedSuggestion || undefined;
|
||||
} else {
|
||||
const whitespaceAfterNextWordStartingIndex = whitespaceAfterNextWordMatch.index!;
|
||||
const whitespaceAfterNextWord = whitespaceAfterNextWordMatch[0];
|
||||
const whitespaceLength = whitespaceAfterNextWord.length;
|
||||
const startOfWhitespaceAfterNextWordIndex = whitespaceAfterNextWordStartingIndex + whitespaceLength;
|
||||
|
||||
nextWord = trimmedSuggestion.substring(0, whitespaceAfterNextWordStartingIndex);
|
||||
if (startOfWhitespaceAfterNextWordIndex < trimmedSuggestion.length) {
|
||||
remaining = trimmedSuggestion.slice(startOfWhitespaceAfterNextWordIndex);
|
||||
nextWord += whitespaceAfterNextWord;
|
||||
}
|
||||
}
|
||||
|
||||
return [nextWord ? leadingWhitespaces + nextWord : undefined, remaining];
|
||||
}
|
||||
Reference in New Issue
Block a user