From ad60944974b525ecdc2ba0005df73c402b2e195d Mon Sep 17 00:00:00 2001 From: Barrca <60709314+FBarrca@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:47:27 +0200 Subject: [PATCH] Reformat the code with the new prettier config --- .github/workflows/format.yml | 35 +- .github/workflows/release.yml | 88 +-- .prettierrc | 1 - README.md | 11 +- manifest.json | 2 +- src/api.ts | 366 ++++++------ src/default_prompts.ts | 2 +- src/main.ts | 79 ++- src/modules/AIExtension.ts | 46 +- src/modules/SelectionState.ts | 75 +-- src/modules/WidgetExtension.ts | 853 +++++++++++++++------------- src/modules/commands/parser.ts | 23 +- src/modules/commands/source.ts | 163 +++--- src/modules/diffExtension.ts | 255 +++++---- src/modules/messageHistory/queue.ts | 80 +-- src/settings.ts | 89 ++- styles.css | 21 +- 17 files changed, 1191 insertions(+), 998 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f7e15f9..1475e1b 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,26 +1,25 @@ name: Format Check on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] + push: + branches: [main, master] + pull_request: + branches: [main, master] jobs: - prettier: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 + prettier: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 - - name: Install dependencies - run: npm ci - - - name: Prettier check - run: npx prettier --check . + - name: Install dependencies + run: npm ci + - name: Prettier check + run: npx prettier --check . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab82e6f..b8237db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,57 +1,57 @@ name: Build and Release Obsidian Plugin on: - push: - tags: - - '*' # Triggers on any tag + push: + tags: + - "*" # Triggers on any tag env: - PLUGIN_NAME: obsidian-inlineAI + PLUGIN_NAME: obsidian-inlineAI jobs: - build-and-release: - runs-on: ubuntu-latest + build-and-release: + runs-on: ubuntu-latest - steps: - # 1. Checkout the repository - - name: Checkout Repository - uses: actions/checkout@v3 + steps: + # 1. Checkout the repository + - name: Checkout Repository + uses: actions/checkout@v3 - # 2. Set up Node.js environment with caching - - name: Set Up Node.js - uses: actions/setup-node@v3 - with: - node-version: 'lts/*' # Use the latest LTS version - cache: 'npm' + # 2. Set up Node.js environment with caching + - name: Set Up Node.js + uses: actions/setup-node@v3 + with: + node-version: "lts/*" # Use the latest LTS version + cache: "npm" - # 3. Install dependencies and build the project - - name: Install and Build - run: | - npm ci - npm run build --if-present + # 3. Install dependencies and build the project + - name: Install and Build + run: | + npm ci + npm run build --if-present - # 4. Prepare release assets - - name: Prepare Release Assets - run: | - mkdir -p $PLUGIN_NAME - cp main.js manifest.json styles.css $PLUGIN_NAME - zip -r ${PLUGIN_NAME}.zip $PLUGIN_NAME + # 4. Prepare release assets + - name: Prepare Release Assets + run: | + mkdir -p $PLUGIN_NAME + cp main.js manifest.json styles.css $PLUGIN_NAME + zip -r ${PLUGIN_NAME}.zip $PLUGIN_NAME - # 5. Retrieve the tag name - - name: Get Tag Name - id: get_tag - run: echo "tag_name=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT + # 5. Retrieve the tag name + - name: Get Tag Name + id: get_tag + run: echo "tag_name=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT - # 6. Create a new release and upload all assets in one step - - name: Create Release and Upload Assets - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.get_tag.outputs.tag_name }} - name: ${{ steps.get_tag.outputs.tag_name }} - artifacts: | - ${{ env.PLUGIN_NAME }}.zip - main.js - manifest.json - styles.css - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # 6. Create a new release and upload all assets in one step + - name: Create Release and Upload Assets + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.get_tag.outputs.tag_name }} + name: ${{ steps.get_tag.outputs.tag_name }} + artifacts: | + ${{ env.PLUGIN_NAME }}.zip + main.js + manifest.json + styles.css + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierrc b/.prettierrc index 311847d..0967ef4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,2 +1 @@ {} - diff --git a/README.md b/README.md index 2a00a36..0053f67 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,14 @@ ### Initial Setup 1. **Set up your API key**: + - Open the plugin settings in Obsidian. + - Enter your API key for OpenAI, Ollama, Gemini or any supported model provider. - - Open the plugin settings in Obsidian. - - Enter your API key for OpenAI, Ollama, Gemini or any supported model provider. 2. **Choose a model**: + - Supported models include `gpt-4`, `llama3.2`, and others. - - Supported models include `gpt-4`, `llama3.2`, and others. 3. **Configure prompts**: - - - Define system and transformation prompts in settings for customized interactions. + - Define system and transformation prompts in settings for customized interactions. ### How to Use @@ -97,6 +96,7 @@ Want to contribute? Here’s how: git clone https://github.com/FBarrca/obsidian-inlineai.git ``` + 2. Install dependencies: ```bash @@ -104,6 +104,7 @@ Want to contribute? Here’s how: npm install ``` + 3. Build the plugin: ```bash diff --git a/manifest.json b/manifest.json index dcb9178..d893a54 100644 --- a/manifest.json +++ b/manifest.json @@ -8,4 +8,4 @@ "authorUrl": "https://github.com/FBarrca/", "fundingUrl": "https://www.buymeacoffee.com/FBarrCa", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/src/api.ts b/src/api.ts index 5e1ac67..38da45d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,7 +2,11 @@ import { ChatOpenAI } from "@langchain/openai"; import { ChatOllama } from "@langchain/ollama"; import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; -import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; +import { + SystemMessage, + HumanMessage, + AIMessage, +} from "@langchain/core/messages"; import { InlineAISettings } from "./settings"; import { App, MarkdownView, Notice } from "obsidian"; import { EditorView } from "@codemirror/view"; @@ -13,196 +17,228 @@ import { MessageQueue } from "./modules/messageHistory/queue"; const MESSAGE_HISTORY_LIMIT = 20; export type HistoryMessage = { - mode: string; - userPrompt: string; + mode: string; + userPrompt: string; }; /** * Class to manage interactions with different chat APIs. */ export class ChatApiManager { - private chatClient: ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null; - private app: App; - private settings: InlineAISettings; - private messageHistory: MessageQueue; - /** - * Initializes the ChatApiManager with the given settings. - * @param settings - Configuration settings for the chat API. - * @param app - The Obsidian App instance. - */ - constructor(settings: InlineAISettings, app: App) { - this.app = app; - this.chatClient = this.initializeChatClient(settings); - this.settings = settings; - this.messageHistory = new MessageQueue(MESSAGE_HISTORY_LIMIT); - } + private chatClient: ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null; + private app: App; + private settings: InlineAISettings; + private messageHistory: MessageQueue; + /** + * Initializes the ChatApiManager with the given settings. + * @param settings - Configuration settings for the chat API. + * @param app - The Obsidian App instance. + */ + constructor(settings: InlineAISettings, app: App) { + this.app = app; + this.chatClient = this.initializeChatClient(settings); + this.settings = settings; + this.messageHistory = new MessageQueue( + MESSAGE_HISTORY_LIMIT, + ); + } - /** - * Initializes the appropriate chat client based on the provider specified in settings. - * @param settings - Configuration settings for the chat API. - * @returns An instance of ChatOpenAI, ChatOllama, or null if initialization fails. - */ - private initializeChatClient(settings: InlineAISettings): ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null { - try { - if (settings.messageHistory) { - this.messageHistory = new MessageQueue(MESSAGE_HISTORY_LIMIT); - } else { - this.messageHistory = new MessageQueue(0); - } - - switch (settings.provider) { - case "openai": - if (!settings.apiKey) { - new Notice("⚠️ OpenAI API key is required. Please check your settings."); - return null; - } - return new ChatOpenAI({ - modelName: settings.model, - temperature: 0, - apiKey: settings.apiKey, - }); + /** + * Initializes the appropriate chat client based on the provider specified in settings. + * @param settings - Configuration settings for the chat API. + * @returns An instance of ChatOpenAI, ChatOllama, or null if initialization fails. + */ + private initializeChatClient( + settings: InlineAISettings, + ): ChatOpenAI | ChatOllama | ChatGoogleGenerativeAI | null { + try { + if (settings.messageHistory) { + this.messageHistory = new MessageQueue( + MESSAGE_HISTORY_LIMIT, + ); + } else { + this.messageHistory = new MessageQueue(0); + } - case "ollama": - return new ChatOllama({ - model: settings.model, - }); - case "gemini": - return new ChatGoogleGenerativeAI({ - model: settings.model, - apiKey: settings.apiKey, - }); - case "custom": - if (!settings.apiKey || !settings.customURL) { - new Notice("⚠️ API key and custom base URL are required for custom providers."); - return null; - } - return new ChatOpenAI({ - modelName: settings.model, - temperature: 0, - openAIApiKey: settings.apiKey, - // 'configuration.basePath' is the recognized property - configuration: { - baseURL: settings.customURL.trim(), - }, - }); + switch (settings.provider) { + case "openai": + if (!settings.apiKey) { + new Notice( + "⚠️ OpenAI API key is required. Please check your settings.", + ); + return null; + } + return new ChatOpenAI({ + modelName: settings.model, + temperature: 0, + apiKey: settings.apiKey, + }); - default: - new Notice(`⚠️ Unsupported provider: ${settings.provider}`); - return null; - } - } catch (error: any) { - console.error("Error initializing chat client:", error); - new Notice(`❌ Error initializing chat client: ${error.message}`); - return null; - } - } + case "ollama": + return new ChatOllama({ + model: settings.model, + }); + case "gemini": + return new ChatGoogleGenerativeAI({ + model: settings.model, + apiKey: settings.apiKey, + }); + case "custom": + if (!settings.apiKey || !settings.customURL) { + new Notice( + "⚠️ API key and custom base URL are required for custom providers.", + ); + return null; + } + return new ChatOpenAI({ + modelName: settings.model, + temperature: 0, + openAIApiKey: settings.apiKey, + // 'configuration.basePath' is the recognized property + configuration: { + baseURL: settings.customURL.trim(), + }, + }); - /** - * Calls the chat API with the provided content and context. - * @param systemMessage - The system message to send to the chat API. - * @param message - The user's message to send to the chat API. - * @returns A promise that resolves with the generated content or an error message. - */ - public async callApi(systemMessage: string, message: string): Promise { - if (!this.chatClient) { - new Notice("⚠️ Chat client is not initialized. Please check your settings."); - return "⚠️ Chat client is not available."; - } + default: + new Notice(`⚠️ Unsupported provider: ${settings.provider}`); + return null; + } + } catch (error: any) { + console.error("Error initializing chat client:", error); + new Notice(`❌ Error initializing chat client: ${error.message}`); + return null; + } + } - const messages = [ - new SystemMessage(systemMessage), - new HumanMessage(message), - ]; + /** + * Calls the chat API with the provided content and context. + * @param systemMessage - The system message to send to the chat API. + * @param message - The user's message to send to the chat API. + * @returns A promise that resolves with the generated content or an error message. + */ + public async callApi( + systemMessage: string, + message: string, + ): Promise { + if (!this.chatClient) { + new Notice( + "⚠️ Chat client is not initialized. Please check your settings.", + ); + return "⚠️ Chat client is not available."; + } - try { - const aiMessage: AIMessage = await this.chatClient.invoke(messages); - return aiMessage.content.toString(); - } catch (error: any) { - console.error("Error calling the chat model:", error); - new Notice(`❌ Error calling the chat model: ${error.message}`); - return "⚠️ Failed to generate a response. Please try again later."; - } - } + const messages = [ + new SystemMessage(systemMessage), + new HumanMessage(message), + ]; - /** - * Handles user input and updates the editor with the response. - * @param systemPrompt - The system prompt to send to the chat API. - * @param userRequest - The user's request to process. - * @returns The AI-generated response or an error message. - */ - private async handleEditorUpdate(systemPrompt: string, userRequest: string): Promise { - try { - const response = await this.callApi(systemPrompt, userRequest); - if (!response) return "⚠️ No response generated."; + try { + const aiMessage: AIMessage = await this.chatClient.invoke(messages); + return aiMessage.content.toString(); + } catch (error: any) { + console.error("Error calling the chat model:", error); + new Notice(`❌ Error calling the chat model: ${error.message}`); + return "⚠️ Failed to generate a response. Please try again later."; + } + } - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!markdownView) { - new Notice("⚠️ No active Markdown editor found."); - return ""; - } + /** + * Handles user input and updates the editor with the response. + * @param systemPrompt - The system prompt to send to the chat API. + * @param userRequest - The user's request to process. + * @returns The AI-generated response or an error message. + */ + private async handleEditorUpdate( + systemPrompt: string, + userRequest: string, + ): Promise { + try { + const response = await this.callApi(systemPrompt, userRequest); + if (!response) return "⚠️ No response generated."; - const mainEditorView = (markdownView.editor as any).cm as EditorView; - mainEditorView?.dispatch({ - effects: setGeneratedResponseEffect.of({ airesponse: response, prompt: userRequest }), - }); + const markdownView = + this.app.workspace.getActiveViewOfType(MarkdownView); + if (!markdownView) { + new Notice("⚠️ No active Markdown editor found."); + return ""; + } - return response; - } catch (error: any) { - console.error("Error processing request:", error); - new Notice(`❌ Error processing request: ${error.message}`); - return "⚠️ Failed to process request."; - } - } - /** - * Processes selected text using the specified prompt and transformation. - * @param userPrompt - The transformation prompt (e.g., "Add Emojis"). - * @param selectedText - The selected text to transform. - * @returns The transformed text or an error message. - */ - public async callSelection(userPrompt: string, selectedText: string): Promise { - userPrompt = parseCommand(userPrompt, this.settings.commandPrefix, this.settings.customCommands); + const mainEditorView = (markdownView.editor as any) + .cm as EditorView; + mainEditorView?.dispatch({ + effects: setGeneratedResponseEffect.of({ + airesponse: response, + prompt: userRequest, + }), + }); - let isCursor = false; - if (selectedText.trim().length === 0) { - isCursor = true; - } + return response; + } catch (error: any) { + console.error("Error processing request:", error); + new Notice(`❌ Error processing request: ${error.message}`); + return "⚠️ Failed to process request."; + } + } + /** + * Processes selected text using the specified prompt and transformation. + * @param userPrompt - The transformation prompt (e.g., "Add Emojis"). + * @param selectedText - The selected text to transform. + * @returns The transformed text or an error message. + */ + public async callSelection( + userPrompt: string, + selectedText: string, + ): Promise { + userPrompt = parseCommand( + userPrompt, + this.settings.commandPrefix, + this.settings.customCommands, + ); - const systemPrompt = isCursor ? this.settings.cursorPrompt : this.settings.selectionPrompt; - let finalUserPrompt = ``; - const mode = isCursor ? "cursor" : "selection"; - if (this.settings.messageHistory) { - this.messageHistory.enqueue({ mode, userPrompt }); - } + let isCursor = false; + if (selectedText.trim().length === 0) { + isCursor = true; + } - if (isCursor) { - finalUserPrompt = ` + const systemPrompt = isCursor + ? this.settings.cursorPrompt + : this.settings.selectionPrompt; + let finalUserPrompt = ``; + const mode = isCursor ? "cursor" : "selection"; + if (this.settings.messageHistory) { + this.messageHistory.enqueue({ mode, userPrompt }); + } + + if (isCursor) { + finalUserPrompt = ` **Task:** ${userPrompt} **Output:**`; - } else { - finalUserPrompt = ` + } else { + finalUserPrompt = ` **Task:** ${userPrompt} **Input:** ${selectedText} **Output:**`; - } - return this.handleEditorUpdate(systemPrompt, finalUserPrompt); - } + } + return this.handleEditorUpdate(systemPrompt, finalUserPrompt); + } - /** - * Updates the manager's settings and reinitializes the chat client. - * @param settings - New configuration settings for the chat API. - */ - public updateSettings(settings: InlineAISettings): void { - this.settings = settings; - const newChatClient = this.initializeChatClient(settings); - if (!newChatClient) { - return; - } - this.chatClient = newChatClient; - } + /** + * Updates the manager's settings and reinitializes the chat client. + * @param settings - New configuration settings for the chat API. + */ + public updateSettings(settings: InlineAISettings): void { + this.settings = settings; + const newChatClient = this.initializeChatClient(settings); + if (!newChatClient) { + return; + } + this.chatClient = newChatClient; + } - public getMessageHistory(): HistoryMessage[] { - return this.messageHistory.getItems(); - } + public getMessageHistory(): HistoryMessage[] { + return this.messageHistory.getItems(); + } } diff --git a/src/default_prompts.ts b/src/default_prompts.ts index 1d8e0c0..51fc2e6 100644 --- a/src/default_prompts.ts +++ b/src/default_prompts.ts @@ -30,7 +30,7 @@ It is **very important** that you follow the examples. Do not add anything at th | John | 30 | Engineer| --- -` +`; export const cursorPrompt = ` You are an advanced language model specialized in following specific instructions to create and process markdown documents. Always use **Obsidian-flavored Markdown** syntax in your responses whenever applicable. diff --git a/src/main.ts b/src/main.ts index 17c6ed2..a125996 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,24 @@ // main.ts import { Plugin, MarkdownView, App } from "obsidian"; import { EditorView } from "@codemirror/view"; -import { InlineAISettings, DEFAULT_SETTINGS, InlineAISettingsTab } from "./settings"; -import { acceptTooltipEffect, commandEffect, dismissTooltipEffect, FloatingTooltipExtension } from "./modules/WidgetExtension"; +import { + InlineAISettings, + DEFAULT_SETTINGS, + InlineAISettingsTab, +} from "./settings"; +import { + acceptTooltipEffect, + commandEffect, + dismissTooltipEffect, + FloatingTooltipExtension, +} from "./modules/WidgetExtension"; import { ChatApiManager } from "./api"; import { generatedResponseState } from "./modules/AIExtension"; -import { buildSelectionHiglightState, currentSelectionState, setSelectionInfoEffect } from "./modules/SelectionState"; +import { + buildSelectionHiglightState, + currentSelectionState, + setSelectionInfoEffect, +} from "./modules/SelectionState"; import { diffExtension } from "./modules/diffExtension"; export default class InlineAIChatPlugin extends Plugin { @@ -21,7 +34,7 @@ export default class InlineAIChatPlugin extends Plugin { generatedResponseState, currentSelectionState, buildSelectionHiglightState, - diffExtension + diffExtension, ]); // Add command to show tooltip @@ -29,9 +42,11 @@ export default class InlineAIChatPlugin extends Plugin { id: "show-cursor-tooltip", name: "Show cursor tooltip", callback: () => { - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); + const markdownView = + this.app.workspace.getActiveViewOfType(MarkdownView); if (markdownView) { - const cmEditor = (markdownView.editor as any).cm as EditorView; + const cmEditor = (markdownView.editor as any) + .cm as EditorView; // Grab the main selection range const { from, to } = cmEditor.state.selection.main; @@ -39,13 +54,22 @@ export default class InlineAIChatPlugin extends Plugin { if (from !== to) { // If there is a real selection, store it - const selectedText = cmEditor.state.doc.sliceString(from, to); + const selectedText = cmEditor.state.doc.sliceString( + from, + to, + ); effects.push( - setSelectionInfoEffect.of({ from, to, text: selectedText }) + setSelectionInfoEffect.of({ + from, + to, + text: selectedText, + }), ); } else { // If no selection, store cursor position instead of null - effects.push(setSelectionInfoEffect.of({ from, to, text: "" })); + effects.push( + setSelectionInfoEffect.of({ from, to, text: "" }), + ); } // Also trigger the overlay @@ -55,18 +79,22 @@ export default class InlineAIChatPlugin extends Plugin { cmEditor.dispatch({ effects }); } }, - hotkeys: [ - ], + hotkeys: [], }); this.addCommand({ id: "accept-tooltip", name: "Accept tooltip suggestion", callback: () => { - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); + const markdownView = + this.app.workspace.getActiveViewOfType(MarkdownView); if (markdownView) { - const cmEditor = (markdownView.editor as any).cm as EditorView; + const cmEditor = (markdownView.editor as any) + .cm as EditorView; - const response = cmEditor.state.field(generatedResponseState, false); + const response = cmEditor.state.field( + generatedResponseState, + false, + ); if (response) { cmEditor.dispatch({ effects: acceptTooltipEffect.of(null), @@ -76,25 +104,28 @@ export default class InlineAIChatPlugin extends Plugin { }); } } - } - + }, }); this.addCommand({ id: "discard-tooltip", name: "Discard tooltip suggestion", callback: () => { - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); + const markdownView = + this.app.workspace.getActiveViewOfType(MarkdownView); if (markdownView) { - const cmEditor = (markdownView.editor as any).cm as EditorView; - const response = cmEditor.state.field(generatedResponseState, false); + const cmEditor = (markdownView.editor as any) + .cm as EditorView; + const response = cmEditor.state.field( + generatedResponseState, + false, + ); if (response) { cmEditor.dispatch({ effects: dismissTooltipEffect.of(null), }); } } - } - + }, }); // Add settings tab @@ -106,7 +137,11 @@ export default class InlineAIChatPlugin extends Plugin { } async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ); } async saveSettings() { diff --git a/src/modules/AIExtension.ts b/src/modules/AIExtension.ts index 0734970..ba188a6 100644 --- a/src/modules/AIExtension.ts +++ b/src/modules/AIExtension.ts @@ -1,38 +1,36 @@ -import { - StateEffect, - StateField, -} from "@codemirror/state"; +import { StateEffect, StateField } from "@codemirror/state"; import { dismissTooltipEffect } from "./WidgetExtension"; // Custom structure that has a airesponse and context fields export interface AIResponse { - airesponse: string; - prompt: string; + airesponse: string; + prompt: string; } /** * State Effect to set the AI response. */ -export const setGeneratedResponseEffect = StateEffect.define(); +export const setGeneratedResponseEffect = + StateEffect.define(); // State field of type text to store the response from the API export const generatedResponseState = StateField.define({ - create() { - return null; - }, - update(value, tr) { - // Check if the transaction contains an effect to set the response - if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) { - const effect = tr.effects.find((e) => e.is(setGeneratedResponseEffect)); - return effect ? effect.value : value; - } - // if we geta dismmisTooltipEffect we should clear the response - if (tr.effects.some((e) => e.is(dismissTooltipEffect))) { - return null; - } - return value; - }, + create() { + return null; + }, + update(value, tr) { + // Check if the transaction contains an effect to set the response + if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) { + const effect = tr.effects.find((e) => + e.is(setGeneratedResponseEffect), + ); + return effect ? effect.value : value; + } + // if we geta dismmisTooltipEffect we should clear the response + if (tr.effects.some((e) => e.is(dismissTooltipEffect))) { + return null; + } + return value; + }, }); - - diff --git a/src/modules/SelectionState.ts b/src/modules/SelectionState.ts index e5abc58..5f40bc1 100644 --- a/src/modules/SelectionState.ts +++ b/src/modules/SelectionState.ts @@ -8,62 +8,67 @@ import { dismissTooltipEffect } from "./WidgetExtension"; * Interface describing the selection range and text. */ export interface SelectionInfo { - from: number; - to: number; - text: string; + from: number; + to: number; + text: string; } /** * Effect used to set or clear the selection info. */ -export const setSelectionInfoEffect = StateEffect.define(); +export const setSelectionInfoEffect = + StateEffect.define(); /** * Field that holds the most recently preserved selection info. */ export const currentSelectionState = StateField.define({ - create() { - return null; - }, - update(value, tr) { - // Look for a setSelectionInfoEffect in this transaction - const effect = tr.effects.find(e => e.is(setSelectionInfoEffect)); - if (effect) { - return effect.value; - } else if (tr.effects.some(e => e.is(dismissTooltipEffect))) { - return null; - } + create() { + return null; + }, + update(value, tr) { + // Look for a setSelectionInfoEffect in this transaction + const effect = tr.effects.find((e) => e.is(setSelectionInfoEffect)); + if (effect) { + return effect.value; + } else if (tr.effects.some((e) => e.is(dismissTooltipEffect))) { + return null; + } - return value; - }, + return value; + }, }); /** * Decoration to highlight the selected text. */ const highlightDecoration = Decoration.mark({ - class: "cm-selectionBackground", // CSS class for highlighting + class: "cm-selectionBackground", // CSS class for highlighting }); /** * StateField that manages the decoration set for highlighting. */ export const buildSelectionHiglightState = StateField.define({ - create(state) { - const info = state.field(currentSelectionState); - if (info && info.from !== info.to) { - console.log("info", info); - return Decoration.set([highlightDecoration.range(info.from, info.to)]); - } - return Decoration.none; - }, - update(decos, tr) { - // Check if selectionInfoField has changed - const info = tr.state.field(currentSelectionState); - if (info && info.from !== info.to) { - return Decoration.set([highlightDecoration.range(info.from, info.to)]); - } - return Decoration.none; - }, - provide: f => EditorView.decorations.from(f), + create(state) { + const info = state.field(currentSelectionState); + if (info && info.from !== info.to) { + console.log("info", info); + return Decoration.set([ + highlightDecoration.range(info.from, info.to), + ]); + } + return Decoration.none; + }, + update(decos, tr) { + // Check if selectionInfoField has changed + const info = tr.state.field(currentSelectionState); + if (info && info.from !== info.to) { + return Decoration.set([ + highlightDecoration.range(info.from, info.to), + ]); + } + return Decoration.none; + }, + provide: (f) => EditorView.decorations.from(f), }); diff --git a/src/modules/WidgetExtension.ts b/src/modules/WidgetExtension.ts index 92e8b6b..012d989 100644 --- a/src/modules/WidgetExtension.ts +++ b/src/modules/WidgetExtension.ts @@ -1,460 +1,501 @@ // modules/WidgetExtension.ts -import { - EditorState, - StateEffect, - StateField, -} from "@codemirror/state"; +import { EditorState, StateEffect, StateField } from "@codemirror/state"; import { - EditorView, - Decoration, - DecorationSet, - WidgetType, - placeholder, - keymap, + EditorView, + Decoration, + DecorationSet, + WidgetType, + placeholder, + keymap, } from "@codemirror/view"; import { setIcon } from "obsidian"; import { ChatApiManager } from "../api"; import { currentSelectionState, SelectionInfo } from "./SelectionState"; -import { createSlashCommandHighlighter, slashCommandAutocompletion } from "./commands/source"; +import { + createSlashCommandHighlighter, + slashCommandAutocompletion, +} from "./commands/source"; import InlineAIChatPlugin from "src/main"; - - // Some existing exports export const commandEffect = StateEffect.define(); export const dismissTooltipEffect = StateEffect.define(); export const acceptTooltipEffect = StateEffect.define(); class FloatingWidget extends WidgetType { - private chatApiManager: ChatApiManager; - private selectionInfo: SelectionInfo | null; - private plugin: InlineAIChatPlugin; + private chatApiManager: ChatApiManager; + private selectionInfo: SelectionInfo | null; + private plugin: InlineAIChatPlugin; - private outerEditorView: EditorView | null = null; + private outerEditorView: EditorView | null = null; - private dom: HTMLElement; - private innerDom: HTMLElement; + private dom: HTMLElement; + private innerDom: HTMLElement; - private textFieldView?: EditorView; - // Primary Action Buttons - private submitButton!: HTMLButtonElement; - private loaderElement!: HTMLElement; + private textFieldView?: EditorView; + // Primary Action Buttons + private submitButton!: HTMLButtonElement; + private loaderElement!: HTMLElement; - //Secondary Action Buttons - private acceptButton!: HTMLButtonElement; - private discardButton!: HTMLButtonElement; + //Secondary Action Buttons + private acceptButton!: HTMLButtonElement; + private discardButton!: HTMLButtonElement; - // Track the current index in message history for up/down navigation - private messageHistoryIndex: number | null = null; + // Track the current index in message history for up/down navigation + private messageHistoryIndex: number | null = null; - // Store references to event listeners for cleanup - private onClickOutside: ((event: MouseEvent) => void) | null = null; - private onEscape: ((event: KeyboardEvent) => void) | null = null; - private escapeWindows: Window[] = []; + // Store references to event listeners for cleanup + private onClickOutside: ((event: MouseEvent) => void) | null = null; + private onEscape: ((event: KeyboardEvent) => void) | null = null; + private escapeWindows: Window[] = []; - constructor(chatApiManager: ChatApiManager, selectionInfo: SelectionInfo | null, plugin: InlineAIChatPlugin) { - super(); - this.chatApiManager = chatApiManager; - this.selectionInfo = selectionInfo; - this.plugin = plugin; + constructor( + chatApiManager: ChatApiManager, + selectionInfo: SelectionInfo | null, + plugin: InlineAIChatPlugin, + ) { + super(); + this.chatApiManager = chatApiManager; + this.selectionInfo = selectionInfo; + this.plugin = plugin; - // Create main DOM structure using createEl - this.dom = createEl("div", { cls: "cm-cursor-overlay", attr: { style: "user-select: none;" } }); - this.innerDom = this.dom.createEl("div", { cls: "cm-cursor-overlay-inner" }); - } + // Create main DOM structure using createEl + this.dom = createEl("div", { + cls: "cm-cursor-overlay", + attr: { style: "user-select: none;" }, + }); + this.innerDom = this.dom.createEl("div", { + cls: "cm-cursor-overlay-inner", + }); + } - /** - * Overriding toDOM(view: EditorView) instead of just toDOM(). - */ - public override toDOM(view: EditorView): HTMLElement { - // Capture the outer EditorView - this.outerEditorView = view; + /** + * Overriding toDOM(view: EditorView) instead of just toDOM(). + */ + public override toDOM(view: EditorView): HTMLElement { + // Capture the outer EditorView + this.outerEditorView = view; - this.createPencilIcon(); - this.createInputField(); - this.createSubmitButton(); - this.createLoader(); + this.createPencilIcon(); + this.createInputField(); + this.createSubmitButton(); + this.createLoader(); - setTimeout(() => { - this.textFieldView?.focus(); - }, 0); + setTimeout(() => { + this.textFieldView?.focus(); + }, 0); - // Setup "click outside" and "Escape" dismissal - this.onClickOutside = (event: MouseEvent) => { - if (!this.dom.contains(event.target as Node)) { - this.dismissTooltip(); - } - }; + // Setup "click outside" and "Escape" dismissal + this.onClickOutside = (event: MouseEvent) => { + if (!this.dom.contains(event.target as Node)) { + this.dismissTooltip(); + } + }; - // Escape key: listen on all windows - this.onEscape = (event: KeyboardEvent) => { - if (event.key === "Escape") { - this.dismissTooltip(); - } - }; + // Escape key: listen on all windows + this.onEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + this.dismissTooltip(); + } + }; - // Always add to main window - window.addEventListener("mousedown", this.onClickOutside); - window.addEventListener("keydown", this.onEscape); + // Always add to main window + window.addEventListener("mousedown", this.onClickOutside); + window.addEventListener("keydown", this.onEscape); - // Try to add to all popout windows (if any) - this.escapeWindows = [window]; - // @ts-ignore - if (window.app && window.app.openPopouts instanceof Function) { - // Obsidian 1.4+ openPopouts returns array of popout windows - // @ts-ignore - const popouts: Window[] = window.app.openPopouts(); - for (const popout of popouts) { - try { - popout.addEventListener("keydown", this.onEscape!); - this.escapeWindows.push(popout); - } catch (e) { - // ignore - } - } - } else if ((window as any).app && Array.isArray((window as any).app?.workspace?.getWindows?.())) { - // Legacy: app.workspace.getWindows() returns array of windows - const popouts: Window[] = (window as any).app.workspace.getWindows(); - for (const popout of popouts) { - try { - popout.addEventListener("keydown", this.onEscape!); - this.escapeWindows.push(popout); - } catch (e) { - // ignore - } - } - } + // Try to add to all popout windows (if any) + this.escapeWindows = [window]; + // @ts-ignore + if (window.app && window.app.openPopouts instanceof Function) { + // Obsidian 1.4+ openPopouts returns array of popout windows + // @ts-ignore + const popouts: Window[] = window.app.openPopouts(); + for (const popout of popouts) { + try { + popout.addEventListener("keydown", this.onEscape!); + this.escapeWindows.push(popout); + } catch (e) { + // ignore + } + } + } else if ( + (window as any).app && + Array.isArray((window as any).app?.workspace?.getWindows?.()) + ) { + // Legacy: app.workspace.getWindows() returns array of windows + const popouts: Window[] = ( + window as any + ).app.workspace.getWindows(); + for (const popout of popouts) { + try { + popout.addEventListener("keydown", this.onEscape!); + this.escapeWindows.push(popout); + } catch (e) { + // ignore + } + } + } - // Cleanup - this.dom.addEventListener("destroy", () => { - document.removeEventListener("mousedown", this.onClickOutside!); - for (const win of this.escapeWindows) { - try { - win.removeEventListener("keydown", this.onEscape!); - } catch (e) { - // ignore - } - } - this.escapeWindows = []; - }); + // Cleanup + this.dom.addEventListener("destroy", () => { + document.removeEventListener("mousedown", this.onClickOutside!); + for (const win of this.escapeWindows) { + try { + win.removeEventListener("keydown", this.onEscape!); + } catch (e) { + // ignore + } + } + this.escapeWindows = []; + }); - return this.dom; - } + return this.dom; + } - public override destroy(): void { - this.textFieldView?.destroy(); - this.innerDom.empty(); + public override destroy(): void { + this.textFieldView?.destroy(); + this.innerDom.empty(); - this.submitButton.remove(); - this.loaderElement.remove(); + this.submitButton.remove(); + this.loaderElement.remove(); - if (this.acceptButton) this.acceptButton.remove(); - if (this.discardButton) this.discardButton.remove(); + if (this.acceptButton) this.acceptButton.remove(); + if (this.discardButton) this.discardButton.remove(); - this.textFieldView = undefined; - this.outerEditorView = null; - this.messageHistoryIndex = null; - } + this.textFieldView = undefined; + this.outerEditorView = null; + this.messageHistoryIndex = null; + } - private dismissTooltip() { - if (this.outerEditorView) { - this.outerEditorView.dispatch({ - effects: dismissTooltipEffect.of(null), - }); - } - } + private dismissTooltip() { + if (this.outerEditorView) { + this.outerEditorView.dispatch({ + effects: dismissTooltipEffect.of(null), + }); + } + } + private createPencilIcon() { + if (!this.innerDom.querySelector(".cm-pencil-icon")) { + const icon = this.innerDom.createEl("div", { + cls: "cm-pencil-icon", + }); + setIcon(icon, "pencil"); + } + } - private createPencilIcon() { - if (!this.innerDom.querySelector(".cm-pencil-icon")) { - const icon = this.innerDom.createEl("div", { cls: "cm-pencil-icon" }); - setIcon(icon, "pencil"); - } - } + private createInputField() { + const editorDom = this.innerDom.createEl("div", { + cls: "cm-tooltip-editor", + attr: { style: "user-select: text;" }, + }); - private createInputField() { - const editorDom = this.innerDom.createEl("div", { cls: "cm-tooltip-editor", attr: { style: "user-select: text;" } }); + this.textFieldView = new EditorView({ + state: EditorState.create({ + doc: "", + extensions: [ + // 1) Show a placeholder in the input field + placeholder("Ask copilot"), + // 2) Add key bindings (including default ones for typical editor commands) + keymap.of([ + { + key: "Enter", + run: () => { + this.submitAction(); + return true; + }, + preventDefault: true, + }, + { + key: "Mod-a", + run: (view) => { + const doc = view.state.doc; + view.dispatch({ + selection: { anchor: 0, head: doc.length }, + }); + return true; + }, + preventDefault: true, + }, + { + key: "ArrowUp", + run: () => { + const messageHistory = + this.chatApiManager.getMessageHistory(); + if ( + messageHistory.length === 0 || + !this.textFieldView + ) { + return true; + } - this.textFieldView = new EditorView({ - state: EditorState.create({ - doc: "", - extensions: [ - // 1) Show a placeholder in the input field - placeholder("Ask copilot"), - // 2) Add key bindings (including default ones for typical editor commands) - keymap.of([ - { - key: "Enter", - run: () => { - this.submitAction() - return true - }, - preventDefault: true, - }, - { - key: "Mod-a", - run: (view) => { - const doc = view.state.doc; - view.dispatch({ - selection: { anchor: 0, head: doc.length }, - }); - return true; - }, - preventDefault: true, - }, - { - key: "ArrowUp", - run: () => { - const messageHistory = this.chatApiManager.getMessageHistory(); - if (messageHistory.length === 0 || !this.textFieldView) { - return true; - } + // If not started, set to last index + if (this.messageHistoryIndex === null) { + this.messageHistoryIndex = + messageHistory.length - 1; + } else if (this.messageHistoryIndex > 0) { + this.messageHistoryIndex--; + } + // Clamp to 0 + if (this.messageHistoryIndex < 0) + this.messageHistoryIndex = 0; - // If not started, set to last index - if (this.messageHistoryIndex === null) { - this.messageHistoryIndex = messageHistory.length - 1; - } else if (this.messageHistoryIndex > 0) { - this.messageHistoryIndex--; - } - // Clamp to 0 - if (this.messageHistoryIndex < 0) this.messageHistoryIndex = 0; + const msg = + messageHistory[this.messageHistoryIndex] + .userPrompt; + this.textFieldView.dispatch({ + changes: { + from: 0, + to: this.textFieldView.state.doc.length, + insert: msg, + }, + }); + // Move cursor to end + this.textFieldView.dispatch({ + selection: { + anchor: msg.length, + head: msg.length, + }, + }); + return true; + }, + preventDefault: true, + }, + { + key: "ArrowDown", + run: () => { + const messageHistory = + this.chatApiManager.getMessageHistory(); + if ( + messageHistory.length === 0 || + !this.textFieldView + ) { + return true; + } + if (this.messageHistoryIndex === null) { + // Do nothing if not in history navigation + return true; + } + if ( + this.messageHistoryIndex < + messageHistory.length - 1 + ) { + this.messageHistoryIndex++; + const msg = + messageHistory[this.messageHistoryIndex] + .userPrompt; + this.textFieldView.dispatch({ + changes: { + from: 0, + to: this.textFieldView.state.doc + .length, + insert: msg, + }, + }); + // Move cursor to end + this.textFieldView.dispatch({ + selection: { + anchor: msg.length, + head: msg.length, + }, + }); + } else { + // If at the end, clear the field and exit history navigation + this.messageHistoryIndex = null; + this.textFieldView.dispatch({ + changes: { + from: 0, + to: this.textFieldView.state.doc + .length, + insert: "", + }, + }); + } + return true; + }, + preventDefault: true, + }, + ]), - const msg = messageHistory[this.messageHistoryIndex].userPrompt; - this.textFieldView.dispatch({ - changes: { - from: 0, - to: this.textFieldView.state.doc.length, - insert: msg, - } - }); - // Move cursor to end - this.textFieldView.dispatch({ - selection: { anchor: msg.length, head: msg.length } - }); - return true; - }, - preventDefault: true, - }, - { - key: "ArrowDown", - run: () => { - const messageHistory = this.chatApiManager.getMessageHistory(); - if (messageHistory.length === 0 || !this.textFieldView) { - return true; - } - if (this.messageHistoryIndex === null) { - // Do nothing if not in history navigation - return true; - } - if (this.messageHistoryIndex < messageHistory.length - 1) { - this.messageHistoryIndex++; - const msg = messageHistory[this.messageHistoryIndex].userPrompt; - this.textFieldView.dispatch({ - changes: { - from: 0, - to: this.textFieldView.state.doc.length, - insert: msg, - } - }); - // Move cursor to end - this.textFieldView.dispatch({ - selection: { anchor: msg.length, head: msg.length } - }); - } else { - // If at the end, clear the field and exit history navigation - this.messageHistoryIndex = null; - this.textFieldView.dispatch({ - changes: { - from: 0, - to: this.textFieldView.state.doc.length, - insert: "", - } - }); - } - return true; - }, - preventDefault: true, - }, - ]), + // 3) Enable slash-command autocompletion + slashCommandAutocompletion({ + prefix: this.plugin.settings.commandPrefix, + customCommands: this.plugin.settings.customCommands, + }), + createSlashCommandHighlighter({ + prefix: this.plugin.settings.commandPrefix, + customCommands: this.plugin.settings.customCommands, + }), + ], + }), + parent: editorDom, + }); - // 3) Enable slash-command autocompletion - slashCommandAutocompletion({ - prefix: this.plugin.settings.commandPrefix, - customCommands: this.plugin.settings.customCommands - }), - createSlashCommandHighlighter({ - prefix: this.plugin.settings.commandPrefix, - customCommands: this.plugin.settings.customCommands - }) - ], - }), - parent: editorDom, - }) + // Reset messageHistoryIndex when user types or changes input + this.textFieldView.dom.addEventListener("input", () => { + // Only reset if the input is not the result of an up/down navigation + // (i.e., if the value doesn't match the current history entry) + // But for simplicity, always reset if user types + this.messageHistoryIndex = null; + }); + } - // Reset messageHistoryIndex when user types or changes input - this.textFieldView.dom.addEventListener("input", () => { - // Only reset if the input is not the result of an up/down navigation - // (i.e., if the value doesn't match the current history entry) - // But for simplicity, always reset if user types - this.messageHistoryIndex = null; - }); - } + private createSubmitButton() { + this.submitButton = this.innerDom.createEl("button", { + cls: "submit-button tooltip-button", + text: "Submit", + }); + setIcon(this.submitButton, "send-horizontal"); - private createSubmitButton() { - this.submitButton = this.innerDom.createEl("button", { - cls: "submit-button tooltip-button", - text: "Submit", - }); - setIcon(this.submitButton, "send-horizontal"); + this.submitButton.onclick = () => { + this.submitAction(); + }; + } - this.submitButton.onclick = () => { - this.submitAction(); - }; - } + private createLoader() { + this.loaderElement = this.innerDom.createEl("div", { cls: "loader" }); + this.toggleLoading(false); + } - private createLoader() { - this.loaderElement = this.innerDom.createEl("div", { cls: "loader" }); - this.toggleLoading(false); - } + /** + * Handles the submit action by calling the AI with the user input and the selected text. + */ + private submitAction() { + const userPrompt = this.textFieldView?.state.doc.toString() ?? ""; - /** - * Handles the submit action by calling the AI with the user input and the selected text. - */ - private submitAction() { - const userPrompt = this.textFieldView?.state.doc.toString() ?? ""; + if (!userPrompt.trim()) { + console.warn("Empty input. Submission aborted."); + return; + } - if (!userPrompt.trim()) { - console.warn("Empty input. Submission aborted."); - return; - } + // Grab the selected text from the stored selection info + const selectedText = this.selectionInfo?.text ?? ""; - // Grab the selected text from the stored selection info - const selectedText = this.selectionInfo?.text ?? ""; + // Show loader + this.toggleLoading(true); - // Show loader - this.toggleLoading(true); + this.chatApiManager + .callSelection(userPrompt, selectedText) + .then((aiResponse) => { + this.showActionButtons(); + }) + .catch((error) => { + console.error("Error calling AI:", error); + }) + .finally(() => { + // Hide loader + this.toggleLoading(false); + }); - this.chatApiManager - .callSelection(userPrompt, selectedText) - .then((aiResponse) => { - this.showActionButtons(); - }) - .catch((error) => { - console.error("Error calling AI:", error); - }) - .finally(() => { - // Hide loader - this.toggleLoading(false); - }); + // Reset message history navigation after submit + this.messageHistoryIndex = null; + } - // Reset message history navigation after submit - this.messageHistoryIndex = null; - } + /** + * Toggles the visibility of the submit button and loader. + * @param isLoading - Whether to show the loader. + */ + private toggleLoading(isLoading: boolean) { + if (isLoading) { + this.submitButton.classList.add("hidden"); + this.loaderElement.classList.remove("hidden"); + } else { + this.submitButton.classList.remove("hidden"); + this.loaderElement.classList.add("hidden"); + } + } - /** - * Toggles the visibility of the submit button and loader. - * @param isLoading - Whether to show the loader. - */ - private toggleLoading(isLoading: boolean) { - if (isLoading) { - this.submitButton.classList.add("hidden"); - this.loaderElement.classList.remove("hidden"); - } else { - this.submitButton.classList.remove("hidden"); - this.loaderElement.classList.add("hidden"); - } - } + /** + * Transitions the widget to show Accept, Discard, and Reload buttons. + */ + private showActionButtons() { + this.submitButton.classList.add("hidden"); + this.createAcceptButton(); + this.createDiscardButton(); + } + /** + * Creates the Accept button. + */ + private createAcceptButton() { + if (!this.acceptButton) { + this.acceptButton = this.innerDom.createEl("button", { + cls: "accept-button tooltip-button primary-action", + text: "Accept", + }); + setIcon(this.acceptButton, "check"); - /** - * Transitions the widget to show Accept, Discard, and Reload buttons. - */ - private showActionButtons() { - this.submitButton.classList.add("hidden"); - this.createAcceptButton(); - this.createDiscardButton(); - } + this.acceptButton.onclick = () => { + this.acceptAction(); + }; - /** - * Creates the Accept button. - */ - private createAcceptButton() { - if (!this.acceptButton) { - this.acceptButton = this.innerDom.createEl("button", { - cls: "accept-button tooltip-button primary-action", - text: "Accept", - }); - setIcon(this.acceptButton, "check"); + this.innerDom.appendChild(this.acceptButton); + } + } - this.acceptButton.onclick = () => { - this.acceptAction(); - }; + /** + * Creates the Discard button. + */ + private createDiscardButton() { + if (!this.discardButton) { + this.discardButton = this.innerDom.createEl("button", { + cls: "discard-button tooltip-button", + text: "Discard", + }); + setIcon(this.discardButton, "cross"); - this.innerDom.appendChild(this.acceptButton); - } - } + this.discardButton.onclick = () => { + this.discardAction(); + }; - /** - * Creates the Discard button. - */ - private createDiscardButton() { - if (!this.discardButton) { - this.discardButton = this.innerDom.createEl("button", { - cls: "discard-button tooltip-button", - text: "Discard", - }); - setIcon(this.discardButton, "cross"); + this.innerDom.appendChild(this.discardButton); + } + } - this.discardButton.onclick = () => { - this.discardAction(); - }; + /** + * Handles the Accept action. + * Confirms the result, applies changes, and closes the tooltip. + */ + private acceptAction() { + if (this.outerEditorView) { + this.outerEditorView.dispatch({ + effects: acceptTooltipEffect.of(null), + }); + } + this.dismissTooltip(); + } - this.innerDom.appendChild(this.discardButton); - } - } - - - /** - * Handles the Accept action. - * Confirms the result, applies changes, and closes the tooltip. - */ - private acceptAction() { - if (this.outerEditorView) { - this.outerEditorView.dispatch({ - effects: acceptTooltipEffect.of(null), - }); - } - this.dismissTooltip(); - } - - private discardAction() { - this.dismissTooltip(); - } + private discardAction() { + this.dismissTooltip(); + } } /** * Build decorations for the first non-empty selection range. */ function renderFloatingWidget( - state: EditorState, - chatApiManager: ChatApiManager, - plugin: InlineAIChatPlugin + state: EditorState, + chatApiManager: ChatApiManager, + plugin: InlineAIChatPlugin, ): DecorationSet { - const firstSelectedRange = state.selection.ranges.find((range) => !range.empty) ?? state.selection.main; + const firstSelectedRange = + state.selection.ranges.find((range) => !range.empty) ?? + state.selection.main; - const selectionInfo = state.field(currentSelectionState, false) ?? null; + const selectionInfo = state.field(currentSelectionState, false) ?? null; - const deco = Decoration.widget({ - widget: new FloatingWidget(chatApiManager, selectionInfo, plugin), - above: true, - inline: true, - side: -9999, - }).range(firstSelectedRange.from); + const deco = Decoration.widget({ + widget: new FloatingWidget(chatApiManager, selectionInfo, plugin), + above: true, + inline: true, - return Decoration.set([deco]); + side: -1, + }).range(firstSelectedRange.from); + + return Decoration.set([deco]); } /** @@ -467,30 +508,36 @@ function renderFloatingWidget( * When the user dismisses the tooltip, it clears the decoration set. * Otherwise, it returns the existing decoration set. */ -function FloatingTooltipState(chatApiManager: ChatApiManager, plugin: InlineAIChatPlugin) { - return StateField.define({ - create(state) { - return Decoration.none; - }, - update(decorations, tr) { - // Recompute if the user triggers the command - if (tr.effects.some((e) => e.is(commandEffect))) { - return renderFloatingWidget(tr.state, chatApiManager, plugin); - } - // Or dismiss it - if (tr.effects.some((e) => e.is(dismissTooltipEffect))) { - return Decoration.none; - } - // Otherwise, return the existing overlay - return decorations; - }, - provide: (field) => EditorView.decorations.from(field), - }); +function FloatingTooltipState( + chatApiManager: ChatApiManager, + plugin: InlineAIChatPlugin, +) { + return StateField.define({ + create(state) { + return Decoration.none; + }, + update(decorations, tr) { + // Recompute if the user triggers the command + if (tr.effects.some((e) => e.is(commandEffect))) { + return renderFloatingWidget(tr.state, chatApiManager, plugin); + } + // Or dismiss it + if (tr.effects.some((e) => e.is(dismissTooltipEffect))) { + return Decoration.none; + } + // Otherwise, return the existing overlay + return decorations; + }, + provide: (field) => EditorView.decorations.from(field), + }); } /** * Extension enabling selection overlay widgets. */ -export function FloatingTooltipExtension(chatApiManager: ChatApiManager, plugin: InlineAIChatPlugin) { - return [FloatingTooltipState(chatApiManager, plugin)]; +export function FloatingTooltipExtension( + chatApiManager: ChatApiManager, + plugin: InlineAIChatPlugin, +) { + return [FloatingTooltipState(chatApiManager, plugin)]; } diff --git a/src/modules/commands/parser.ts b/src/modules/commands/parser.ts index ad4d79d..5057279 100644 --- a/src/modules/commands/parser.ts +++ b/src/modules/commands/parser.ts @@ -1,12 +1,15 @@ import { SlashCommand } from "./source"; - - -export function parseCommand(userInput: string, prefix: string, customCommands: SlashCommand[]): string { - for (const command of customCommands) { - const commandPattern = `${prefix}${command.keyword}`; - if (userInput.includes(commandPattern)) { - return userInput.replace(commandPattern, command.prompt); - } - } - return userInput; // Return original text if no command matches + +export function parseCommand( + userInput: string, + prefix: string, + customCommands: SlashCommand[], +): string { + for (const command of customCommands) { + const commandPattern = `${prefix}${command.keyword}`; + if (userInput.includes(commandPattern)) { + return userInput.replace(commandPattern, command.prompt); + } + } + return userInput; // Return original text if no command matches } diff --git a/src/modules/commands/source.ts b/src/modules/commands/source.ts index 5f70cc8..99410fa 100644 --- a/src/modules/commands/source.ts +++ b/src/modules/commands/source.ts @@ -1,87 +1,118 @@ -import { autocompletion, CompletionContext } from "@codemirror/autocomplete" -import { Extension, Range } from "@codemirror/state" -import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view" +import { autocompletion, CompletionContext } from "@codemirror/autocomplete"; +import { Extension, Range } from "@codemirror/state"; +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, +} from "@codemirror/view"; /** - * + * * Interface describing a slash command. */ export interface SlashCommand { - keyword: string; - prompt: string; + keyword: string; + prompt: string; } // Factory function that creates a completion source with custom parameters -function createSlashCommandSource(options: { - prefix: string, - customCommands: SlashCommand[] -} = { - prefix: '/', - customCommands: [] -}) { - const { prefix, customCommands } = options; - return (context: CompletionContext) => { - let word = context.matchBefore(new RegExp(`^\\${prefix}\\w*`)) - if (!word || (word.from == word.to && !context.explicit)) - return null - return { - from: word.from+1, - options: (customCommands).map(cmd => ({ - label: cmd.keyword, - type: undefined, - detail: cmd.prompt, - })) - } - } +function createSlashCommandSource( + options: { + prefix: string; + customCommands: SlashCommand[]; + } = { + prefix: "/", + customCommands: [], + }, +) { + const { prefix, customCommands } = options; + return (context: CompletionContext) => { + let word = context.matchBefore(new RegExp(`^\\${prefix}\\w*`)); + if (!word || (word.from == word.to && !context.explicit)) return null; + return { + from: word.from + 1, + options: customCommands.map((cmd) => ({ + label: cmd.keyword, + type: undefined, + detail: cmd.prompt, + })), + }; + }; } // Create the extension that uses our custom completion source. -export function slashCommandAutocompletion(options: { prefix: string, customCommands: SlashCommand[] } = { prefix: '/', customCommands: [] }) { - return autocompletion({ - override: [createSlashCommandSource(options)], - tooltipClass: () => "tooltip-autocomplete", - optionClass: () => "completion-label", - icons: false, - } - ) +export function slashCommandAutocompletion( + options: { prefix: string; customCommands: SlashCommand[] } = { + prefix: "/", + customCommands: [], + }, +) { + return autocompletion({ + override: [createSlashCommandSource(options)], + tooltipClass: () => "tooltip-autocomplete", + optionClass: () => "completion-label", + icons: false, + }); } // Create a decoration for highlighting slash commands -const slashCommandMark = Decoration.mark({ class: "cm-slash-command" }) +const slashCommandMark = Decoration.mark({ class: "cm-slash-command" }); -export function createSlashCommandHighlighter({ prefix, customCommands }: { prefix: string, customCommands: SlashCommand[] }) { - return ViewPlugin.fromClass(class { - decorations: DecorationSet +export function createSlashCommandHighlighter({ + prefix, + customCommands, +}: { + prefix: string; + customCommands: SlashCommand[]; +}) { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; - constructor(view: EditorView) { - this.decorations = this.buildDecorations(view) - } + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + } - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.decorations = this.buildDecorations(update.view) - } - } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); + } + } - buildDecorations(view: EditorView) { - const keywords = customCommands.map(cmd => cmd.keyword).join('|') - const regexp = new RegExp(`\\${prefix}(${keywords})\\b`, 'g') + buildDecorations(view: EditorView) { + const keywords = customCommands + .map((cmd) => cmd.keyword) + .join("|"); + const regexp = new RegExp(`\\${prefix}(${keywords})\\b`, "g"); - const decorations = view.visibleRanges.flatMap(({ from, to }) => { - const text = view.state.doc.sliceString(from, to) - return [...text.matchAll(regexp)] - .map(match => - match.index !== undefined - ? slashCommandMark.range(from + match.index, from + match.index + 1 + match[1].length) - : null - ) - .filter((dec): dec is Range => dec !== null) - }) - + const decorations = view.visibleRanges.flatMap( + ({ from, to }) => { + const text = view.state.doc.sliceString(from, to); + return [...text.matchAll(regexp)] + .map((match) => + match.index !== undefined + ? slashCommandMark.range( + from + match.index, + from + + match.index + + 1 + + match[1].length, + ) + : null, + ) + .filter( + (dec): dec is Range => dec !== null, + ); + }, + ); - return Decoration.set(decorations) - } - }, { - decorations: v => v.decorations - }) + return Decoration.set(decorations); + } + }, + { + decorations: (v) => v.decorations, + }, + ); } diff --git a/src/modules/diffExtension.ts b/src/modules/diffExtension.ts index 6348964..82b91eb 100644 --- a/src/modules/diffExtension.ts +++ b/src/modules/diffExtension.ts @@ -1,21 +1,20 @@ // modules/diffExtension.ts +import { EditorState, StateField, RangeSetBuilder } from "@codemirror/state"; import { - EditorState, - StateField, - RangeSetBuilder, -} from "@codemirror/state"; -import { - Decoration, - DecorationSet, - EditorView, - WidgetType, - ViewPlugin, - ViewUpdate, + Decoration, + DecorationSet, + EditorView, + WidgetType, + ViewPlugin, + ViewUpdate, } from "@codemirror/view"; import DiffMatchPatch from "diff-match-patch"; import { acceptTooltipEffect, dismissTooltipEffect } from "./WidgetExtension"; -import { generatedResponseState, setGeneratedResponseEffect } from "./AIExtension"; +import { + generatedResponseState, + setGeneratedResponseEffect, +} from "./AIExtension"; import { currentSelectionState } from "./SelectionState"; /** @@ -23,30 +22,30 @@ import { currentSelectionState } from "./SelectionState"; * Improves accessibility by using appropriate ARIA attributes. */ class ChangeContentWidget extends WidgetType { - constructor( - private readonly content: string, - private readonly type: 'added' | 'removed' - ) { - super(); - } + constructor( + private readonly content: string, + private readonly type: "added" | "removed", + ) { + super(); + } - toDOM(): HTMLElement { - const wrapper = document.createElement("span"); - wrapper.className = `cm-change-widget cm-change-${this.type}`; - wrapper.textContent = this.content; + toDOM(): HTMLElement { + const wrapper = document.createElement("span"); + wrapper.className = `cm-change-widget cm-change-${this.type}`; + wrapper.textContent = this.content; - // Accessibility: Provide ARIA label - wrapper.setAttribute( - "aria-label", - this.type === 'added' ? "Added content" : "Removed content" - ); - return wrapper; - } + // Accessibility: Provide ARIA label + wrapper.setAttribute( + "aria-label", + this.type === "added" ? "Added content" : "Removed content", + ); + return wrapper; + } - ignoreEvent(): boolean { - // Decide whether to ignore events on the widget - return false; - } + ignoreEvent(): boolean { + // Decide whether to ignore events on the widget + return false; + } } /** @@ -56,58 +55,57 @@ class ChangeContentWidget extends WidgetType { * @returns A DecorationSet with the appropriate widgets. */ function generateDiffView(state: EditorState): DecorationSet { - try { - // Retrieve the AI response and the current context text from the state - const response = state.field(generatedResponseState); - const context = state.field(currentSelectionState); + try { + // Retrieve the AI response and the current context text from the state + const response = state.field(generatedResponseState); + const context = state.field(currentSelectionState); - const aiText: string = response?.airesponse ?? ""; - const contextText: string = context?.text ?? ""; + const aiText: string = response?.airesponse ?? ""; + const contextText: string = context?.text ?? ""; - // Use diff_match_patch instead of diffWords - const dmp = new DiffMatchPatch(); - let diffs = dmp.diff_main(contextText, aiText); + // Use diff_match_patch instead of diffWords + const dmp = new DiffMatchPatch(); + let diffs = dmp.diff_main(contextText, aiText); - // Perform semantic cleanup - dmp.diff_cleanupSemantic(diffs); + // Perform semantic cleanup + dmp.diff_cleanupSemantic(diffs); - // Initialize RangeSetBuilder for efficient decoration construction - const builder = new RangeSetBuilder(); - let currentPos = context?.from ?? 0; + // Initialize RangeSetBuilder for efficient decoration construction + const builder = new RangeSetBuilder(); + let currentPos = context?.from ?? 0; + diffs.forEach(([op, text]) => { + const length = text.length; - diffs.forEach(([op, text]) => { - const length = text.length; + if (op === DiffMatchPatch.DIFF_INSERT) { + // AI text added + const widget = new ChangeContentWidget(text, "added"); + builder.add( + currentPos, + currentPos, + Decoration.widget({ widget, side: 1 }), + ); + } else if (op === DiffMatchPatch.DIFF_DELETE) { + // Context text removed + const widget = new ChangeContentWidget(text, "removed"); + // Attach the widget over the removed range + builder.add( + currentPos, + currentPos + length, + Decoration.widget({ widget, side: -1 }), + ); + currentPos += length; + } else { + // No change, move currentPos forward + currentPos += length; + } + }); - if (op === DiffMatchPatch.DIFF_INSERT) { - // AI text added - const widget = new ChangeContentWidget(text, "added"); - builder.add( - currentPos, - currentPos, - Decoration.widget({ widget, side: 1 }) - ); - } else if (op === DiffMatchPatch.DIFF_DELETE) { - // Context text removed - const widget = new ChangeContentWidget(text, "removed"); - // Attach the widget over the removed range - builder.add( - currentPos, - currentPos + length, - Decoration.widget({ widget, side: -1 }) - ); - currentPos += length; - } else { - // No change, move currentPos forward - currentPos += length; - } - }); - - return builder.finish(); - } catch (error) { - console.error("Error generating diff view:", error); - return Decoration.none; - } + return builder.finish(); + } catch (error) { + console.error("Error generating diff view:", error); + return Decoration.none; + } } /** @@ -117,69 +115,70 @@ function generateDiffView(state: EditorState): DecorationSet { * @param view - The EditorView instance. */ function dispatchAIChanges(state: EditorState, view: EditorView): void { - try { - // Grab the AI text and selection info (original context) - const response = state.field(generatedResponseState); - const context = state.field(currentSelectionState); + try { + // Grab the AI text and selection info (original context) + const response = state.field(generatedResponseState); + const context = state.field(currentSelectionState); - const aiText: string = response?.airesponse ?? ""; - const selectionFrom = context?.from ?? 0; - const selectionTo = context?.to ?? 0; + const aiText: string = response?.airesponse ?? ""; + const selectionFrom = context?.from ?? 0; + const selectionTo = context?.to ?? 0; - // Dispatch the transaction to apply the AI changes - view.dispatch({ - changes: { from: selectionFrom, to: selectionTo, insert: aiText }, - }); - } catch (error) { - console.error("Error applying diff changes:", error); - } + // Dispatch the transaction to apply the AI changes + view.dispatch({ + changes: { from: selectionFrom, to: selectionTo, insert: aiText }, + }); + } catch (error) { + console.error("Error applying diff changes:", error); + } } export const diffDecorationState = StateField.define({ - create(): DecorationSet { - return Decoration.none; - }, - update(decorations: DecorationSet, tr) { - // Check if we got the AI response effect - if (tr.effects.some(e => e.is(setGeneratedResponseEffect))) { - return generateDiffView(tr.state); - } + create(): DecorationSet { + return Decoration.none; + }, + update(decorations: DecorationSet, tr) { + // Check if we got the AI response effect + if (tr.effects.some((e) => e.is(setGeneratedResponseEffect))) { + return generateDiffView(tr.state); + } - // Check if the dismiss tooltip effect is present - const hasDismissEffect = tr.effects.some(e => e.is(dismissTooltipEffect)); - if (hasDismissEffect) { - return Decoration.none; - } + // Check if the dismiss tooltip effect is present + const hasDismissEffect = tr.effects.some((e) => + e.is(dismissTooltipEffect), + ); + if (hasDismissEffect) { + return Decoration.none; + } - // Retain the existing decorations if no relevant changes - return decorations; - }, - provide: (field) => EditorView.decorations.from(field), + // Retain the existing decorations if no relevant changes + return decorations; + }, + provide: (field) => EditorView.decorations.from(field), }); /** * Plugin to handle applying diff changes when the accept tooltip effect is triggered. */ -const applyDiffPlugin = ViewPlugin.fromClass(class { - update(update: ViewUpdate) { - // Iterate through all transactions in the update - for (const transaction of update.transactions) { - for (const effect of transaction.effects) { - if (effect.is(acceptTooltipEffect)) { - // Apply the diff changes by dispatching the transaction - setTimeout(() => { - dispatchAIChanges(update.state, update.view); - }, 0); - } - } - } - } -}); +const applyDiffPlugin = ViewPlugin.fromClass( + class { + update(update: ViewUpdate) { + // Iterate through all transactions in the update + for (const transaction of update.transactions) { + for (const effect of transaction.effects) { + if (effect.is(acceptTooltipEffect)) { + // Apply the diff changes by dispatching the transaction + setTimeout(() => { + dispatchAIChanges(update.state, update.view); + }, 0); + } + } + } + } + }, +); /** * Exported extension to be included in the EditorView. */ -export const diffExtension = [ - diffDecorationState, - applyDiffPlugin, -]; +export const diffExtension = [diffDecorationState, applyDiffPlugin]; diff --git a/src/modules/messageHistory/queue.ts b/src/modules/messageHistory/queue.ts index ddbd63d..43ad93c 100644 --- a/src/modules/messageHistory/queue.ts +++ b/src/modules/messageHistory/queue.ts @@ -5,51 +5,51 @@ * - Provides standard queue operations: enqueue, dequeue, peek, size, isEmpty, and getItems. */ export class MessageQueue { - private items: T[] = []; - private maxLength: number; + private items: T[] = []; + private maxLength: number; - constructor(maxLength: number) { - this.maxLength = maxLength; - } + constructor(maxLength: number) { + this.maxLength = maxLength; + } - // Adds an item to the queue - enqueue(item: T): void { - // If the new item is exactly the same as the previous one, do not add it - if (this.items.length > 0) { - const lastItem = this.items[this.items.length - 1]; - if (lastItem === item) { - return; - } - } - if (this.items.length >= this.maxLength) { - // Remove the oldest item if the queue is full - this.items.shift(); - } - this.items.push(item); - } + // Adds an item to the queue + enqueue(item: T): void { + // If the new item is exactly the same as the previous one, do not add it + if (this.items.length > 0) { + const lastItem = this.items[this.items.length - 1]; + if (lastItem === item) { + return; + } + } + if (this.items.length >= this.maxLength) { + // Remove the oldest item if the queue is full + this.items.shift(); + } + this.items.push(item); + } - // Removes and returns the item at the front of the queue - dequeue(): T | undefined { - return this.items.shift(); - } + // Removes and returns the item at the front of the queue + dequeue(): T | undefined { + return this.items.shift(); + } - // Returns the item at the front of the queue without removing it - peek(): T | undefined { - return this.items[0]; - } + // Returns the item at the front of the queue without removing it + peek(): T | undefined { + return this.items[0]; + } - // Returns the current size of the queue - size(): number { - return this.items.length; - } + // Returns the current size of the queue + size(): number { + return this.items.length; + } - // Checks if the queue is empty - isEmpty(): boolean { - return this.items.length === 0; - } + // Checks if the queue is empty + isEmpty(): boolean { + return this.items.length === 0; + } - // Returns the items in the queue - getItems(): T[] { - return [...this.items]; - } + // Returns the items in the queue + getItems(): T[] { + return [...this.items]; + } } diff --git a/src/settings.ts b/src/settings.ts index 8e04c4c..d53d7be 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -26,7 +26,7 @@ export const DEFAULT_SETTINGS: InlineAISettings = { cursorPrompt: cursorPrompt, customCommands: [], commandPrefix: "/", - messageHistory: false + messageHistory: false, }; export class InlineAISettingsTab extends PluginSettingTab { @@ -50,7 +50,9 @@ export class InlineAISettingsTab extends PluginSettingTab { // Provider setting new Setting(containerEl) .setName("Provider") - .setDesc("Choose between OpenAI, Ollama, or a custom OpenAI-compatible endpoint.") + .setDesc( + "Choose between OpenAI, Ollama, or a custom OpenAI-compatible endpoint.", + ) .addDropdown((dropdown) => dropdown .addOption("openai", "OpenAI") @@ -59,10 +61,14 @@ export class InlineAISettingsTab extends PluginSettingTab { .addOption("custom", "Custom/OpenAI-compatible") .setValue(this.plugin.settings.provider) .onChange(async (value) => { - this.plugin.settings.provider = value as "openai" | "ollama" | "custom" | "gemini"; + this.plugin.settings.provider = value as + | "openai" + | "ollama" + | "custom" + | "gemini"; await this.saveSettings(); this.display(); // Refresh UI to show/hide API key field - }) + }), ); // Model setting @@ -79,7 +85,11 @@ export class InlineAISettingsTab extends PluginSettingTab { }); // API Key setting (conditionally displayed for OpenAI-supported endpoints) - if (this.plugin.settings.provider === "openai" || this.plugin.settings.provider === "custom" || this.plugin.settings.provider === "gemini") { + if ( + this.plugin.settings.provider === "openai" || + this.plugin.settings.provider === "custom" || + this.plugin.settings.provider === "gemini" + ) { new Setting(containerEl) .setName("API key") .setDesc("Enter your API key.") @@ -97,7 +107,9 @@ export class InlineAISettingsTab extends PluginSettingTab { if (this.plugin.settings.provider === "custom") { new Setting(containerEl) .setName("Custom endpoint") - .setDesc("Enter your OpenAI-compatible base URL (e.g. https://api.groq.com/openai/v1).") + .setDesc( + "Enter your OpenAI-compatible base URL (e.g. https://api.groq.com/openai/v1).", + ) .addText((text) => { text.setPlaceholder("https://api.mycustomhost.com/v1") .setValue(this.plugin.settings.customURL || "") @@ -113,12 +125,16 @@ export class InlineAISettingsTab extends PluginSettingTab { // Selection Prompt setting new Setting(containerEl) .setName("Selection prompt") - .setDesc("System Prompt used when the tooltip is triggered with selected text.") + .setDesc( + "System Prompt used when the tooltip is triggered with selected text.", + ) .addTextArea((textarea) => { - textarea.setPlaceholder("e.g., Summarize the selected text.") + textarea + .setPlaceholder("e.g., Summarize the selected text.") .setValue(this.plugin.settings.selectionPrompt) .inputEl.addEventListener("blur", async () => { - this.plugin.settings.selectionPrompt = textarea.getValue(); + this.plugin.settings.selectionPrompt = + textarea.getValue(); await this.saveSettings(); }); textarea.inputEl.classList.add("wide-text-settings"); @@ -127,9 +143,14 @@ export class InlineAISettingsTab extends PluginSettingTab { // Cursor Prompt setting new Setting(containerEl) .setName("Cursor prompt") - .setDesc("System Prompt used when the tooltip is triggered with selected text.") + .setDesc( + "System Prompt used when the tooltip is triggered with selected text.", + ) .addTextArea((textarea) => { - textarea.setPlaceholder("e.g., Generate text based on cursor position.") + textarea + .setPlaceholder( + "e.g., Generate text based on cursor position.", + ) .setValue(this.plugin.settings.cursorPrompt) .inputEl.addEventListener("blur", async () => { this.plugin.settings.cursorPrompt = textarea.getValue(); @@ -141,28 +162,37 @@ export class InlineAISettingsTab extends PluginSettingTab { // Message History setting new Setting(containerEl) .setName("Message History") - .setDesc("Enable message history, you can navigate through the history using the up/down arrow keys.") + .setDesc( + "Enable message history, you can navigate through the history using the up/down arrow keys.", + ) .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.messageHistory) + toggle + .setValue(this.plugin.settings.messageHistory) .onChange(async (value) => { this.plugin.settings.messageHistory = value; await this.saveSettings(); }); }); - + // Custom Commands Section containerEl.createEl("h3", { text: "Custom Commands" }); - containerEl.createEl("p", { text: "Add your own custom commands. Triggered with the prefix defined in the Command Prefix setting." }); + containerEl.createEl("p", { + text: "Add your own custom commands. Triggered with the prefix defined in the Command Prefix setting.", + }); // Command Prefix setting new Setting(containerEl) .setName("Command Prefix") - .setDesc("The prefix used to trigger custom commands (e.g., /, !, #)") + .setDesc( + "The prefix used to trigger custom commands (e.g., /, !, #)", + ) .addText((text) => { text.setPlaceholder("/") .setValue(this.plugin.settings.commandPrefix) .inputEl.addEventListener("blur", async () => { - this.plugin.settings.commandPrefix = text.getValue().charAt(0); + this.plugin.settings.commandPrefix = text + .getValue() + .charAt(0); await this.saveSettings(); this.display(); }); @@ -177,15 +207,18 @@ export class InlineAISettingsTab extends PluginSettingTab { text.setValue(command.keyword) .setPlaceholder("Command name") .inputEl.addEventListener("blur", async () => { - this.plugin.settings.customCommands[index].keyword = text.getValue(); + this.plugin.settings.customCommands[index].keyword = + text.getValue(); await this.saveSettings(); }); }) .addTextArea((textarea) => { - textarea.setValue(command.prompt) + textarea + .setValue(command.prompt) .setPlaceholder("Command prompt") .inputEl.addEventListener("blur", async () => { - this.plugin.settings.customCommands[index].prompt = textarea.getValue(); + this.plugin.settings.customCommands[index].prompt = + textarea.getValue(); await this.saveSettings(); }); }) @@ -194,25 +227,29 @@ export class InlineAISettingsTab extends PluginSettingTab { .setIcon("trash") .setTooltip("Delete this command") .onClick(async () => { - this.plugin.settings.customCommands.splice(index, 1); + this.plugin.settings.customCommands.splice( + index, + 1, + ); await this.saveSettings(); this.display(); - }) + }), ); }); - - // Add new command button new Setting(containerEl).addButton((btn) => btn .setButtonText("Add Command") .setCta() .onClick(async () => { - this.plugin.settings.customCommands.push({ keyword: "new_command", prompt: "" }); + this.plugin.settings.customCommands.push({ + keyword: "new_command", + prompt: "", + }); await this.saveSettings(); this.display(); - }) + }), ); } } diff --git a/styles.css b/styles.css index 29da3b8..9671914 100644 --- a/styles.css +++ b/styles.css @@ -79,12 +79,16 @@ @keyframes l5 { 0%, 33% { - box-shadow: 10px 0 var(--text-accent), -10px 0 var(--text-faint); + box-shadow: + 10px 0 var(--text-accent), + -10px 0 var(--text-faint); background: var(--text-accent); } 66%, 100% { - box-shadow: 10px 0 var(--text-faint), -10px 0 var(--text-accent); + box-shadow: + 10px 0 var(--text-faint), + -10px 0 var(--text-accent); background: var(--text-faint); } } @@ -118,7 +122,7 @@ height: 10em; } -.tooltip-autocomplete{ +.tooltip-autocomplete { background-color: var(--background-primary-alt) !important; color: var(--text-normal) !important; width: 101.9% !important; @@ -130,11 +134,11 @@ } .tooltip-autocomplete li[aria-selected] { - background-color: var(--text-selection)!important; + background-color: var(--text-selection) !important; color: var(--text-accent) !important; } -.completion-label{ +.completion-label { font-size: var(--font-ui-small); font-weight: var(--font-normal); font-family: Arial, Helvetica, sans-serif !important; @@ -144,12 +148,11 @@ .cm-tooltip.cm-tooltip-below { top: 100%; - margin-top: 0.8em; + margin-top: 0.8em; bottom: auto; } /* Slash command highlighting */ .cm-slash-command { - color: var(--text-accent); - -} \ No newline at end of file + color: var(--text-accent); +}