diff --git a/package.json b/package.json index 04e4cfb..f0a979d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,12 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.27.3", + "codemirror": "^6.0.1", + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/state": "^6.5.2", + "@codemirror/theme-one-dark": "^6.1.2", + "@codemirror/view": "^6.35.0", "@codemirror/basic-setup": "^0.20.0", "@codemirror/lang-markdown": "^6.3.2", "@codemirror/merge": "^6.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce71b19..4029e7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,27 @@ importers: '@codemirror/basic-setup': specifier: ^0.20.0 version: 0.20.0 + '@codemirror/commands': + specifier: ^6.7.1 + version: 6.8.1 + '@codemirror/lang-json': + specifier: ^6.0.1 + version: 6.0.2 '@codemirror/lang-markdown': specifier: ^6.3.2 version: 6.3.3 '@codemirror/merge': specifier: ^6.10.0 version: 6.10.2 + '@codemirror/state': + specifier: ^6.5.2 + version: 6.5.2 + '@codemirror/theme-one-dark': + specifier: ^6.1.2 + version: 6.1.3 + '@codemirror/view': + specifier: ^6.35.0 + version: 6.38.0 '@electric-sql/pglite': specifier: 0.2.14 version: 0.2.14 @@ -87,6 +102,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + codemirror: + specifier: ^6.0.1 + version: 6.0.2 delay: specifier: ^6.0.0 version: 6.0.0 @@ -565,6 +583,9 @@ packages: '@codemirror/commands@0.20.0': resolution: {integrity: sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==} + '@codemirror/commands@6.8.1': + resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} + '@codemirror/lang-css@6.3.1': resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} @@ -574,6 +595,9 @@ packages: '@codemirror/lang-javascript@6.2.4': resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + '@codemirror/lang-markdown@6.3.3': resolution: {integrity: sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==} @@ -599,12 +623,18 @@ packages: '@codemirror/search@0.20.1': resolution: {integrity: sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==} + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + '@codemirror/state@0.20.1': resolution: {integrity: sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==} '@codemirror/state@6.5.2': resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + '@codemirror/view@0.20.7': resolution: {integrity: sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==} @@ -1650,6 +1680,9 @@ packages: '@lezer/javascript@1.5.1': resolution: {integrity: sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==} + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + '@lezer/lr@0.16.3': resolution: {integrity: sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==} @@ -3107,6 +3140,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} @@ -7037,6 +7073,13 @@ snapshots: '@codemirror/view': 0.20.7 '@lezer/common': 0.16.1 + '@codemirror/commands@6.8.1': + dependencies: + '@codemirror/language': 6.11.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.0 + '@lezer/common': 1.2.3 + '@codemirror/lang-css@6.3.1': dependencies: '@codemirror/autocomplete': 6.18.6 @@ -7067,6 +7110,11 @@ snapshots: '@lezer/common': 1.2.3 '@lezer/javascript': 1.5.1 + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.11.2 + '@lezer/json': 1.0.3 + '@codemirror/lang-markdown@6.3.3': dependencies: '@codemirror/autocomplete': 6.18.6 @@ -7130,12 +7178,25 @@ snapshots: '@codemirror/view': 0.20.7 crelt: 1.0.6 + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.0 + crelt: 1.0.6 + '@codemirror/state@0.20.1': {} '@codemirror/state@6.5.2': dependencies: '@marijn/find-cluster-break': 1.0.2 + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.11.2 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.0 + '@lezer/highlight': 1.2.1 + '@codemirror/view@0.20.7': dependencies: '@codemirror/state': 0.20.1 @@ -8107,6 +8168,12 @@ snapshots: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + '@lezer/lr@0.16.3': dependencies: '@lezer/common': 0.16.1 @@ -9758,6 +9825,16 @@ snapshots: co@4.6.0: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.18.6 + '@codemirror/commands': 6.8.1 + '@codemirror/language': 6.11.2 + '@codemirror/lint': 6.8.5 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.38.0 + collect-v8-coverage@1.0.2: {} color-convert@2.0.1: diff --git a/src/BaseFileView.tsx b/src/BaseFileView.tsx new file mode 100644 index 0000000..79fafe0 --- /dev/null +++ b/src/BaseFileView.tsx @@ -0,0 +1,168 @@ +import { EditorState, Extension } from "@codemirror/state"; +import { EditorView, ViewUpdate } from "@codemirror/view"; +import { TFile, TextFileView, WorkspaceLeaf } from "obsidian"; + +import InfioPlugin from './main'; + +export default abstract class BaseView extends TextFileView { + public plugin: InfioPlugin; + protected cmEditor: EditorView; + protected editorEl: HTMLElement; + protected state: { filePath?: string } | null = null; + protected isEditorLoaded: boolean = false; + protected currentFilePath: string | null = null; + + protected constructor(leaf: WorkspaceLeaf, plugin: InfioPlugin) { + super(leaf); + this.plugin = plugin; + } + + onload(): void { + super.onload(); + this.editorEl = this.contentEl.createDiv("datafile-source-view mod-cm6"); + + this.cmEditor = new EditorView({ + state: this.createDefaultEditorState(), + parent: this.editorEl, + }); + + this.app.workspace.trigger("codemirror", this.cmEditor); + this.isEditorLoaded = true; + + // Load file content if state contains filePath and editor is now loaded + if (this.state?.filePath) { + this.loadFileFromPath(this.state.filePath); + } + } + + async setState(state: { filePath?: string }): Promise { + this.state = state; + // If filePath is provided and editor is loaded, load the file immediately + if (state.filePath && this.isEditorLoaded) { + await this.loadFileFromPath(state.filePath); + } + } + + getState(): { filePath?: string } { + return { filePath: this.currentFilePath }; + } + + private async loadFileFromPath(filePath: string): Promise { + // Store the current file path for saving + this.currentFilePath = filePath; + + // Try to get the file from vault first (for regular files) + const file = this.app.vault.getAbstractFileByPath(filePath); + + if (file && file instanceof TFile) { + // Regular file in vault + this.file = file; + await this.onLoadFile(file); + } else { + // File not in vault (hidden directory), read directly from filesystem + console.log('File not in vault, reading directly from filesystem'); + await this.loadFileFromFilesystem(filePath); + } + } + + private async loadFileFromFilesystem(filePath: string): Promise { + try { + // Use vault adapter to read file directly from filesystem + const content = await this.app.vault.adapter.read(filePath); + this.setViewData(content, true); + } catch (error) { + console.error('Failed to load file from filesystem:', error); + // If file doesn't exist, create it with empty content + this.setViewData('{}', true); + } + } + + async onLoadFile(file: TFile): Promise { + try { + const content = await this.app.vault.read(file); + this.setViewData(content, true); + } catch (error) { + console.error('Failed to load file content:', error); + } + } + + getViewData(): string { + return this.cmEditor.state.doc.toString(); + } + + setViewData(data: string, clear: boolean): void { + if (clear) { + this.cmEditor.dispatch({ changes: { from: 0, to: this.cmEditor.state.doc.length, insert: data } }); + } else { + this.cmEditor.dispatch({ changes: { from: 0, to: this.cmEditor.state.doc.length, insert: data } }); + } + } + + clear(): void { + this.setViewData('', true); + } + + async save(clear?: boolean): Promise { + const content = this.getViewData(); + + if (this.file) { + // Regular file in vault + await this.app.vault.modify(this.file, content); + } else if (this.currentFilePath) { + // File in hidden directory, save directly to filesystem + await this.app.vault.adapter.write(this.currentFilePath, content); + } + + if (clear) { + this.clear(); + } + } + + // gets the title of the document + getDisplayText(): string { + if (this.file) { + return this.file.basename; + } + if (this.currentFilePath) { + return this.currentFilePath.split('/').pop() || "JSON File"; + } + if (this.state?.filePath) { + return this.state.filePath.split('/').pop() || "JSON File"; + } + return "NOFILE"; + } + + onClose(): Promise { + return super.onClose(); + } + + async reload(): Promise { + await this.save(false); + + const data = this.getViewData(); + this.cmEditor.setState(this.createDefaultEditorState()); + this.setViewData(data, false); + } + + protected onEditorUpdate(update: ViewUpdate): void { + if (update.docChanged) { + this.requestSave(); + } + } + + abstract getViewType(): string; + + protected abstract getEditorExtensions(): Extension[]; + + private createDefaultEditorState(): EditorState { + return EditorState.create({ + extensions: [...this.getCommonEditorExtensions(), ...this.getEditorExtensions()] + }); + } + + private getCommonEditorExtensions(): Extension[] { + const extensions: Extension[] = []; + extensions.push(EditorView.lineWrapping); + return extensions; + } +} diff --git a/src/JsonFileView.tsx b/src/JsonFileView.tsx new file mode 100644 index 0000000..556d692 --- /dev/null +++ b/src/JsonFileView.tsx @@ -0,0 +1,32 @@ + +import { json } from "@codemirror/lang-json"; +import { Extension } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { basicSetup } from "codemirror"; +import { WorkspaceLeaf } from "obsidian"; + +import BaseView from "./BaseFileView"; +import { JSON_VIEW_TYPE } from './constants'; +import InfioPlugin from './main'; +import { getIndentByTabExtension } from "./utils/indentation-provider"; + +export default class JsonView extends BaseView { + constructor(leaf: WorkspaceLeaf, plugin: InfioPlugin) { + super(leaf, plugin); + } + + getViewType(): string { + return JSON_VIEW_TYPE; + } + + protected getEditorExtensions(): Extension[] { + const extensions = [ + basicSetup, + getIndentByTabExtension(), + json(), + EditorView.updateListener.of(this.onEditorUpdate.bind(this)) + ]; + + return extensions; + } +} diff --git a/src/components/chat-view/McpHubView.tsx b/src/components/chat-view/McpHubView.tsx index ecce6c3..77b0a51 100644 --- a/src/components/chat-view/McpHubView.tsx +++ b/src/components/chat-view/McpHubView.tsx @@ -1,4 +1,4 @@ -import { AlertTriangle, ChevronDown, ChevronRight, FileText, Folder, Power, RotateCcw, Trash2, Wrench } from 'lucide-react' +import { AlertTriangle, ChevronDown, ChevronRight, ExternalLink, FileText, Folder, Power, RotateCcw, Trash2, Wrench } from 'lucide-react' import { Notice } from 'obsidian' import React, { useEffect, useState } from 'react' @@ -108,6 +108,17 @@ const McpHubView = () => { } } + const handleOpenConfigFile = async () => { + const hub = await getMcpHub(); + if (hub) { + try { + await hub.openMcpSettingsFile(); + } catch (error) { + console.error('Failed to open config file:', error) + } + } + } + const toggleServerExpansion = (serverKey: string) => { setExpandedServers(prev => ({ ...prev, [serverKey]: !prev[serverKey] })); if (!expandedServers[serverKey] && !activeServerDetailTab[serverKey]) { @@ -196,7 +207,15 @@ const McpHubView = () => {
{/* Header Section */}
-

{t('mcpHub.title')}

+

{t('mcpHub.title')}

+
+ +
{/* MCP Settings */} @@ -218,6 +237,15 @@ const McpHubView = () => {

+ + {/* Configuration File Access */} + {/* Create New Server Section */} @@ -448,6 +476,57 @@ const McpHubView = () => { line-height: 1.4; } + .infio-mcp-hub-actions { + display: flex; + gap: var(--size-2-2); + } + + .obsidian-insight-refresh-btn { + display: flex; + align-items: center; + justify-content: center; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + color: var(--text-muted); + padding: 0 !important; + margin: 0 !important; + width: 24px !important; + height: 24px !important; + + &:hover { + background-color: var(--background-modifier-hover) !important; + } + } + + .obsidian-insight-refresh-btn:hover:not(:disabled) { + background-color: var(--interactive-hover); + } + + .infio-mcp-config-button { + display: flex; + align-items: center; + gap: 8px; + background-color: var(--interactive-normal); + color: var(--text-normal); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + } + + .infio-mcp-config-button:hover { + background-color: var(--interactive-hover); + border-color: var(--interactive-accent); + } + + .infio-mcp-config-button:active { + transform: translateY(1px); + } + /* Search Section */ .infio-mcp-search-section { margin-bottom: 16px; diff --git a/src/constants.ts b/src/constants.ts index 1ca64f4..55dfb60 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,22 +3,10 @@ import { LLMModel } from './types/llm/model' export const CHAT_VIEW_TYPE = 'infio-chat-view' export const APPLY_VIEW_TYPE = 'infio-apply-view' export const PREVIEW_VIEW_TYPE = 'infio-preview-view' +export const JSON_VIEW_TYPE = 'infio-json-view' export const DEFAULT_MODELS: LLMModel[] = [] -// export const PROVIDERS: ApiProvider[] = [ -// 'Infio', -// 'OpenRouter', -// 'SiliconFlow', -// 'Anthropic', -// 'Deepseek', -// 'OpenAI', -// 'Google', -// 'Groq', -// 'Ollama', -// 'OpenAICompatible', -// ] - export const SUPPORT_EMBEDDING_SIMENTION: number[] = [ 384, 512, diff --git a/src/core/mcp/McpHub.ts b/src/core/mcp/McpHub.ts index 37b7911..1e8fa21 100644 --- a/src/core/mcp/McpHub.ts +++ b/src/core/mcp/McpHub.ts @@ -1,7 +1,5 @@ -// Obsidian -import { App, EventRef, Notice, TFile, normalizePath } from 'obsidian'; -// Node built-in +import { App, EventRef, Notice, TFile, normalizePath } from 'obsidian'; import * as path from "path"; // SDK / External Libraries @@ -23,7 +21,7 @@ import { EnvironmentVariables, shellEnvSync } from 'shell-env'; import { z } from "zod"; // Keep zod // Internal/Project imports -import { INFIO_BASE_URL } from '../../constants' +import { INFIO_BASE_URL, JSON_VIEW_TYPE } from '../../constants'; import { t } from "../../lang/helpers"; import InfioPlugin from "../../main"; // Assuming path is correct and will be resolved, if not, this will remain an error. @@ -331,24 +329,16 @@ export class McpHub { async ensureMcpFileExists(): Promise { const mcpFolderPath = ".infio_json_db/mcp" if (!await this.app.vault.adapter.exists(normalizePath(mcpFolderPath))) { - await this.app.vault.createFolder(mcpFolderPath); + await this.app.vault.createFolder(normalizePath(mcpFolderPath)); } this.mcpSettingsFilePath = normalizePath(path.join(mcpFolderPath, "settings.json")) - const fileExists = await this.app.vault.adapter.exists(this.mcpSettingsFilePath); + const fileExists = await this.app.vault.adapter.exists(normalizePath(this.mcpSettingsFilePath)); if (!fileExists) { - await this.app.vault.adapter.write( - this.mcpSettingsFilePath, + await this.app.vault.create( + normalizePath(this.mcpSettingsFilePath), JSON.stringify({ mcpServers: {} }, null, 2) ); } - // this.globalMcpFilePath = normalizePath(path.join(mcpFolderPath, "global.json")) - // const fileExists1 = await this.app.vault.adapter.exists(this.globalMcpFilePath); - // if (!fileExists1) { - // await this.app.vault.adapter.write( - // this.globalMcpFilePath, - // JSON.stringify({ mcpServers: {} }, null, 2) - // ); - // } } async getMcpSettingsFilePath(): Promise { @@ -363,6 +353,61 @@ export class McpHub { })); } + /** + * Opens the MCP settings file in Obsidian + */ + async openMcpSettingsFile(): Promise { + try { + await this.ensureMcpFileExists(); + const filePath = this.mcpSettingsFilePath; + + console.log('Attempting to open MCP settings file:', filePath); + + // 检查文件是否已经打开 + let existingLeaf: any = null; + this.app.workspace.iterateAllLeaves((leaf) => { + if (leaf.view.getViewType() === JSON_VIEW_TYPE) { + // 检查视图状态中的文件路径 + const viewState = leaf.view.getState(); + if (viewState && viewState.filePath === filePath) { + existingLeaf = leaf; + return false; // 停止遍历 + } + } + }); + + if (existingLeaf) { + // 如果文件已经打开,重新加载最新内容并激活 leaf + await existingLeaf.setViewState({ + type: JSON_VIEW_TYPE, + active: true, + state: { filePath } // 重新设置状态以触发重新加载 + }); + this.app.workspace.setActiveLeaf(existingLeaf); + this.app.workspace.revealLeaf(existingLeaf); + console.log('MCP settings file is already open, reloading content and activating existing view:', filePath); + } else { + // 如果文件没有打开,创建新的 leaf + const leaf = this.app.workspace.getLeaf(true); + + if (leaf) { + await leaf.setViewState({ + type: JSON_VIEW_TYPE, + active: true, + state: { filePath } // 传递文件路径到视图 + }); + + this.app.workspace.revealLeaf(leaf); + console.log('Successfully opened MCP settings file in JSON view:', filePath); + } else { + console.error('Failed to get workspace leaf for JSON view'); + } + } + } catch (error) { + console.error('Failed to open MCP settings file:', error); + } + } + // Combined and simplified initializeMcpServers, only for global scope private async initializeGlobalMcpServers(): Promise { try { diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 53f7d96..3ab3f6c 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -529,6 +529,13 @@ export default { parameters: "Parameters", toolNoDescription: "No description", useMcpToolFrom: "Use MCP tool from", + configurationFile: "Configuration File", + configurationFileDescription: "Directly edit the MCP servers configuration file to add, modify, or remove servers.", + openConfigFile: "Open Configuration File", + configFileOpened: "Configuration file opened in Obsidian", + configFileNotFound: "Configuration file not found", + failedToOpenConfig: "Failed to open configuration file", + openedWithSystemApp: "Configuration file opened with system default application", }, semanticSearch: { title: "Semantic Index", diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index db55eab..5458ef3 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -525,10 +525,17 @@ export default { errors: "错误", noTools: "没有可用工具", noResources: "没有可用资源", - noErrors: "没有错误记录", - parameters: "参数", - toolNoDescription: "无描述", - useMcpToolFrom: "使用来自以下的 MCP 工具:", + noErrors: "没有错误记录", + parameters: "参数", + toolNoDescription: "无描述", + useMcpToolFrom: "使用来自以下的 MCP 工具:", + configurationFile: "配置文件", + configurationFileDescription: "直接编辑 MCP 服务器配置文件来添加、修改或删除服务器。", + openConfigFile: "打开配置文件", + configFileOpened: "配置文件已在 Obsidian 中打开", + configFileNotFound: "配置文件未找到", + failedToOpenConfig: "打开配置文件失败", + openedWithSystemApp: "配置文件已使用系统默认应用程序打开", } }, semanticSearch: { diff --git a/src/main.ts b/src/main.ts index 351f69f..fbedf24 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import { Editor, MarkdownView, Modal, Notice, Plugin, TFile } from 'obsidian' import { ApplyView } from './ApplyView' import { ChatView } from './ChatView' import { ChatProps } from './components/chat-view/ChatView' -import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants' +import { APPLY_VIEW_TYPE, CHAT_VIEW_TYPE, JSON_VIEW_TYPE, PREVIEW_VIEW_TYPE } from './constants' import { getDiffStrategy } from "./core/diff/DiffStrategy" import { InlineEdit } from './core/edit/inline-edit-processor' import { McpHub } from './core/mcp/McpHub' @@ -16,6 +16,7 @@ import { DBManager } from './database/database-manager' import { migrateToJsonDatabase } from './database/json/migrateToJsonDatabase' import { EmbeddingManager } from './embedworker/EmbeddingManager' import EventListener from "./event-listener" +import JsonView from './JsonFileView' import { t } from './lang/helpers' import { PreviewView } from './PreviewView' import CompletionKeyWatcher from "./render-plugin/completion-key-watcher" @@ -88,6 +89,7 @@ export default class InfioPlugin extends Plugin { this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this)) this.registerView(APPLY_VIEW_TYPE, (leaf) => new ApplyView(leaf)) this.registerView(PREVIEW_VIEW_TYPE, (leaf) => new PreviewView(leaf)) + this.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, this)) // register markdown processor for Inline Edit this.inlineEdit = new InlineEdit(this, this.settings); diff --git a/src/utils/indentation-provider.ts b/src/utils/indentation-provider.ts new file mode 100644 index 0000000..fde404c --- /dev/null +++ b/src/utils/indentation-provider.ts @@ -0,0 +1,26 @@ +import { indentLess, indentWithTab } from "@codemirror/commands"; +import { indentUnit } from "@codemirror/language"; +import { Extension } from "@codemirror/state"; +import { keymap } from "@codemirror/view"; + +export const getIndentByTabExtension = (): Extension[] => + [ + keymap.of([indentWithTab]), + indentUnit.of(" ") + ]; + +export const getInsertTabsExtension = (): Extension[] => + [ + keymap.of([ + // { + // key: 'Tab', + // preventDefault: true, + // run: insertTab, + // }, + { + key: 'Shift-Tab', + preventDefault: true, + run: indentLess, + }, + ]) + ]; diff --git a/styles.css b/styles.css index 264add3..b905fb1 100644 --- a/styles.css +++ b/styles.css @@ -2598,3 +2598,21 @@ button.infio-chat-input-model-select { max-width: 80vw; max-height: 80vh; } + +/* + * JSON View Styles + */ +.datafile-source-view.mod-cm6 .cm-gutters { + flex: 0 0 auto; + background-color: transparent; + color: var(--text-faint) !important; + border-right: none !important; + margin-inline-end: var(--file-folding-offset); + font-size: var(--font-ui-smaller); + z-index: 1; + font-variant: tabular-nums; +} + +.cm-gutterElement.cm-activeLineGutter { + background-color: transparent; +}