diff --git a/package.json b/package.json index 5d6bca7..fecc3a4 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "chart.js": "^4.4.2", "js-tiktoken": "^1.0.10", "lucide-react": "^0.363.0", + "minimatch": "^9.0.4", "openai": "^4.30.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/api/client.ts b/src/api/client.ts index 6c97ea8..4cb42cd 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -113,7 +113,7 @@ export class BaseAPIClient implements APIClient { async fetchCompletions(language: string, prefix: string, suffix: string) { const { settings } = this.plugin; - const instance = this.getInstance(settings.chat.provider); + const instance = this.getInstance(settings.completions.provider); if (instance === undefined) { return; } diff --git a/src/api/provider.ts b/src/api/provider.ts index 6927bef..5f6af4e 100644 --- a/src/api/provider.ts +++ b/src/api/provider.ts @@ -33,11 +33,13 @@ export const OPENAI_MODELS = [ 'gpt-3.5-turbo-16k-0613', ] as const; -// TODO -export const OPENROUTER_MODELS = ['gpt-4']; +// TODO: +// This is a placeholder. +export const OPENROUTER_MODELS = ['gpt-4'] as const; -// TODO -export const OLLAMA_MODELS = ['gpt-4']; +// TODO: +// This is a placeholder. +export const OLLAMA_MODELS = ['gpt-4'] as const; export const MODELS = { openai: OPENAI_MODELS, diff --git a/src/api/usage.ts b/src/api/usage.ts index 783f41d..f6481fe 100644 --- a/src/api/usage.ts +++ b/src/api/usage.ts @@ -57,33 +57,41 @@ const OPENAI_MODEL_OUTPUT_COSTS: Record = { 'gpt-3.5-turbo-16k-0613': 1.5, }; -// TODO +// TODO: +// This is a placeholder. const OPENROUTER_INPUT_COSTS: Record = { 'gpt-4': 30, }; -// TODO +// TODO: +// This is a placeholder. const OPENROUTER_OUTPUT_COSTS: Record = { 'gpt-4': 60, }; -// TODO +// TODO: +// This is a placeholder. const OLLAMA_INPUT_COSTS: Record = { 'gpt-4': 0, }; -// TODO +// TODO: +// This is a placeholder. const OLLAMA_OUTPUT_COSTS: Record = { 'gpt-4': 0, }; -const INPUT_COSTS: Record> = { +// TODO: +// Replace `Record` to an appropriate type. +const INPUT_COSTS: Record> = { openai: OPENAI_MODEL_INPUT_COSTS, openrouter: OPENROUTER_INPUT_COSTS, ollama: OLLAMA_INPUT_COSTS, }; -const OUTPUT_COSTS: Record> = { +// TODO: +// Replace `Record` to an appropriate type. +const OUTPUT_COSTS: Record> = { openai: OPENAI_MODEL_OUTPUT_COSTS, openrouter: OPENROUTER_OUTPUT_COSTS, ollama: OLLAMA_OUTPUT_COSTS, diff --git a/src/main.ts b/src/main.ts index 334278e..c58efa5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { Extension } from '@codemirror/state'; -import { addIcon, Notice, Plugin, setIcon } from 'obsidian'; +import { minimatch } from 'minimatch'; +import { addIcon, MarkdownView, Notice, Plugin, setIcon } from 'obsidian'; import { MemoryCacheProxy } from './api/cache'; import { APIClient, BaseAPIClient } from './api/client'; import { UsageMonitorProxy, UsageTracker } from './api/usage'; @@ -191,9 +192,25 @@ export default class Markpilot extends Plugin { getEditorExtension() { return inlineCompletionsExtension(async (...args) => { - if (this.settings.completions.enabled) { - return this.client.fetchCompletions(...args); + // TODO: + // Extract this logic to somewhere appropriate. + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + const file = view?.file; + const content = view?.editor.getValue(); + const isIgnoredFile = this.settings.completions.ignoredFiles.some( + (pattern) => file?.path && minimatch(file?.path, pattern), + ); + const hasIgnoredTags = this.settings.completions.ignoredTags.some((tag) => + content?.includes(tag), + ); + if ( + isIgnoredFile || + hasIgnoredTags || + !this.settings.completions.enabled + ) { + return; } + return this.client.fetchCompletions(...args); }, this); } diff --git a/src/settings.ts b/src/settings.ts index 5a3b496..74de365 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -29,6 +29,8 @@ export interface MarkpilotSettings { windowSize: number; acceptKey: string; rejectKey: string; + ignoredFiles: string[]; + ignoredTags: string[]; }; chat: { enabled: boolean; @@ -64,13 +66,15 @@ export const DEFAULT_SETTINGS: MarkpilotSettings = { completions: { enabled: true, provider: 'openai', - model: 'gpt-3.5-turbo', + model: 'gpt-3.5-turbo-instruct', maxTokens: 64, temperature: 0, waitTime: 500, windowSize: 512, acceptKey: 'Tab', rejectKey: 'Escape', + ignoredFiles: [], + ignoredTags: [], }, chat: { enabled: true, @@ -148,13 +152,8 @@ export class MarkpilotSettingTab extends PluginSettingTab { text .setValue(settings.providers.ollama.apiUrl ?? '') .onChange(async (value) => { - if (validateURL(value)) { - new Notice('Invalid Ollama API URL.'); - return; - } settings.providers.ollama.apiUrl = value; await plugin.saveSettings(); - new Notice('Successfully saved Ollama API URL.'); }), ); @@ -168,11 +167,16 @@ export class MarkpilotSettingTab extends PluginSettingTab { new Notice('Ollama API URL is not set.'); return; } - // TODO - const response = await fetch(apiUrl); - if (response.ok) { + if (!validateURL(apiUrl)) { + new Notice('Invalid Ollama API URL.'); + return; + } + // TODO: + // Properly implement logic for checking Ollama API status. + try { + await fetch(apiUrl); new Notice('Successfully connected to Ollama API.'); - } else { + } catch { new Notice('Failed to connect to Ollama API.'); } }), @@ -198,44 +202,48 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.chat.enabled) .setName('Provider') .setDesc('Select the provider for inline completions.') .addDropdown((dropdown) => { for (const option of PROVIDERS) { dropdown.addOption(option, option); } - dropdown.setValue(settings.completions.provider); - dropdown.onChange(async (value) => { - settings.completions.provider = value as Provider; - await plugin.saveSettings(); - this.display(); // Re-render settings tab - }); + dropdown + .setDisabled(!settings.completions.enabled) + .setValue(settings.completions.provider) + .onChange(async (value) => { + settings.completions.provider = value as Provider; + await plugin.saveSettings(); + this.display(); // Re-render settings tab + }); }); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Model') .setDesc('Select the model for inline completions.') .addDropdown((dropdown) => { for (const option of MODELS[settings.completions.provider]) { dropdown.addOption(option, option); } - dropdown.setValue(settings.completions.model); - dropdown.onChange(async (value) => { - settings.completions.model = value as Model; - await plugin.saveSettings(); - }); + dropdown + .setDisabled(!settings.completions.enabled) + .setValue(settings.completions.model) + .onChange(async (value) => { + settings.completions.model = value as Model; + await plugin.saveSettings(); + }); }); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Max tokens') .setDesc('Set the max tokens for inline completions.') .addSlider((slider) => slider + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.maxTokens) .setLimits(128, 8192, 128) + // TODO: + // Figure out how to add unit to the slider .setDynamicTooltip() .onChange(async (value) => { settings.completions.maxTokens = value; @@ -244,11 +252,11 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Temperature') .setDesc('Set the temperature for inline completions.') .addSlider((slider) => slider + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.temperature) .setLimits(0, 1, 0.01) .setDynamicTooltip() @@ -259,13 +267,13 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Wait time') .setDesc( 'Time in milliseconds which it will wait for before fetching inline completions from the server.', ) .addSlider((slider) => slider + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.waitTime) .setLimits(0, 1000, 100) .setDynamicTooltip() @@ -280,13 +288,13 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Window size') .setDesc( 'Set the window size for inline completions. The window size the number of characters around the cursor used to obtain inline completions', ) .addSlider((slider) => slider + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.windowSize) .setLimits(128, 8192, 128) .setDynamicTooltip() @@ -298,13 +306,13 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Accept key') .setDesc( 'Set the key to accept inline completions. The list of available keys can be found at: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values', ) .addText((text) => text + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.acceptKey) .onChange(async (value) => { settings.completions.acceptKey = value; @@ -314,13 +322,13 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.completions.enabled) .setName('Reject key') .setDesc( 'Set the key to reject inline completions. The list of available keys can be found at: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values', ) .addText((text) => text + .setDisabled(!settings.completions.enabled) .setValue(settings.completions.rejectKey) .onChange(async (value) => { settings.completions.rejectKey = value; @@ -329,6 +337,39 @@ export class MarkpilotSettingTab extends PluginSettingTab { }), ); + new Setting(containerEl) + + .setName('Ignored files') + .setDesc( + 'Set the list of files to ignore inline completions. The completions will not be triggered in these files.', + ) + .addTextArea((text) => + text + .setDisabled(!settings.completions.enabled) + .setValue(settings.completions.ignoredFiles.join('\n')) + .setPlaceholder('myFile.md\nmyDirectory/**/*.md') + .onChange(async (value) => { + settings.completions.ignoredFiles = value.split('\n'); + await plugin.saveSettings(); + }), + ); + + new Setting(containerEl) + .setName('Ignored tags') + .setDesc( + 'Set the list of tags to ignore inline completions. The completions will not be triggered in these tags.', + ) + .addTextArea((text) => + text + .setDisabled(!settings.completions.enabled) + .setValue(settings.completions.ignoredTags.join('\n')) + .setPlaceholder('#myTag\n#myTag2') + .onChange(async (value) => { + settings.completions.ignoredTags = value.split('\n'); + await plugin.saveSettings(); + }), + ); + /************************************************************/ /* Chat View */ /************************************************************/ @@ -352,42 +393,44 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.chat.enabled) .setName('Provider') .setDesc('Select the provider for chat view.') .addDropdown((dropdown) => { for (const option of PROVIDERS) { dropdown.addOption(option, option); } - dropdown.setValue(settings.chat.provider); - dropdown.onChange(async (value) => { - settings.chat.provider = value as Provider; - await plugin.saveSettings(); - this.display(); // Re-render settings tab - }); + dropdown + .setDisabled(!settings.chat.enabled) + .setValue(settings.chat.provider) + .onChange(async (value) => { + settings.chat.provider = value as Provider; + await plugin.saveSettings(); + this.display(); // Re-render settings tab + }); }); new Setting(containerEl) - .setDisabled(!settings.chat.enabled) .setName('Model') .setDesc('Select the model for GPT.') .addDropdown((dropdown) => { for (const option of MODELS[settings.chat.provider]) { dropdown.addOption(option, option); } - dropdown.setValue(settings.chat.model); - dropdown.onChange(async (value) => { - settings.chat.model = value as Model; - await plugin.saveSettings(); - }); + dropdown + .setDisabled(!settings.chat.enabled) + .setValue(settings.chat.model) + .onChange(async (value) => { + settings.chat.model = value as Model; + await plugin.saveSettings(); + }); }); new Setting(containerEl) - .setDisabled(!settings.chat.enabled) .setName('Max tokens') .setDesc('Set the max tokens for chat view.') .addSlider((slider) => slider + .setDisabled(!settings.chat.enabled) .setValue(settings.chat.maxTokens) .setLimits(128, 8192, 128) .setDynamicTooltip() @@ -398,11 +441,11 @@ export class MarkpilotSettingTab extends PluginSettingTab { ); new Setting(containerEl) - .setDisabled(!settings.chat.enabled) .setName('Temperature') .setDesc('Set the temperature for chat view.') .addSlider((slider) => slider + .setDisabled(!settings.chat.enabled) .setValue(settings.chat.temperature) .setLimits(0, 1, 0.01) .setDynamicTooltip() @@ -424,11 +467,14 @@ export class MarkpilotSettingTab extends PluginSettingTab { 'Turn this on to enable memory caching. The cached data will be invalided on startup.', ) .addToggle((toggle) => - toggle.setValue(settings.cache.enabled).onChange(async (value) => { - settings.cache.enabled = value; - await plugin.saveSettings(); - this.display(); // Re-render settings tab - }), + toggle + .setDisabled(!settings.completions.enabled) + .setValue(settings.cache.enabled) + .onChange(async (value) => { + settings.cache.enabled = value; + await plugin.saveSettings(); + this.display(); // Re-render settings tab + }), ); /************************************************************/ diff --git a/yarn.lock b/yarn.lock index 74f5900..4a12b2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -501,6 +501,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1672,6 +1679,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + moment@2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"