mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-09 08:30:09 +00:00
添加对 CodeMirror 的支持,更新相关依赖版本,并在样式中增加 JSON 视图的样式。新增 JSON 视图类型并实现打开配置文件的功能,更新国际化文本以支持配置文件操作。
This commit is contained in:
168
src/BaseFileView.tsx
Normal file
168
src/BaseFileView.tsx
Normal file
@@ -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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
return super.onClose();
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
32
src/JsonFileView.tsx
Normal file
32
src/JsonFileView.tsx
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = () => {
|
||||
<div className="infio-mcp-hub-container">
|
||||
{/* Header Section */}
|
||||
<div className="infio-mcp-hub-header">
|
||||
<h2 className="infio-mcp-hub-title">{t('mcpHub.title')}</h2>
|
||||
<h3 className="infio-mcp-hub-title">{t('mcpHub.title')}</h3>
|
||||
<div className="infio-mcp-hub-actions">
|
||||
<button
|
||||
onClick={fetchServers}
|
||||
className="obsidian-insight-refresh-btn"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MCP Settings */}
|
||||
@@ -218,6 +237,15 @@ const McpHubView = () => {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Configuration File Access */}
|
||||
<button
|
||||
onClick={handleOpenConfigFile}
|
||||
className="infio-mcp-config-button"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
<span>{t('mcpHub.openConfigFile')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
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<string> {
|
||||
@@ -363,6 +353,61 @@ export class McpHub {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the MCP settings file in Obsidian
|
||||
*/
|
||||
async openMcpSettingsFile(): Promise<void> {
|
||||
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<void> {
|
||||
try {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
26
src/utils/indentation-provider.ts
Normal file
26
src/utils/indentation-provider.ts
Normal file
@@ -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,
|
||||
},
|
||||
])
|
||||
];
|
||||
Reference in New Issue
Block a user