diff --git a/media/USER_MANUAL.md b/media/USER_MANUAL.md index 47cc091..4b1eeed 100644 --- a/media/USER_MANUAL.md +++ b/media/USER_MANUAL.md @@ -104,7 +104,7 @@ **症状**:安装 VSIX 文件时报错 -#### **解决方案**: +#### 解决方案: - 确认 VS Code 版本 >= 1.60.0 diff --git a/src/extension.ts b/src/extension.ts index 08018fc..6e013b6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from "vscode"; import { ICViewProvider } from "./views/ICViewProvider"; import { showICHelperPanel } from "./panels/ICHelperPanel"; import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel"; +import { UserManualPanel } from "./panels/UserManualPanel"; import { ChatHistoryManager } from "./utils/chatHistoryManager"; import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider"; import { VCDFileServer } from "./services/vcdFileServer"; @@ -184,9 +185,8 @@ export async function activate(context: vscode.ExtensionContext) { // 注册命令:打开用户手册 const openUserManualCommand = vscode.commands.registerCommand( "ic-coder.openUserManual", - async () => { - const manualPath = vscode.Uri.joinPath(context.extensionUri, "media", "USER_MANUAL.md"); - await vscode.commands.executeCommand("markdown.showPreview", manualPath); + () => { + UserManualPanel.render(context.extensionUri); } ); diff --git a/src/panels/UserManualPanel.ts b/src/panels/UserManualPanel.ts new file mode 100644 index 0000000..e3f35e1 --- /dev/null +++ b/src/panels/UserManualPanel.ts @@ -0,0 +1,181 @@ +/** + * 用户手册只读预览面板 + */ + +import * as vscode from "vscode"; + +export class UserManualPanel { + public static currentPanel: UserManualPanel | undefined; + private readonly _panel: vscode.WebviewPanel; + private _disposables: vscode.Disposable[] = []; + + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + this._panel = panel; + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._update(extensionUri); + } + + public static render(extensionUri: vscode.Uri) { + if (UserManualPanel.currentPanel) { + UserManualPanel.currentPanel._panel.reveal(vscode.ViewColumn.One); + } else { + const panel = vscode.window.createWebviewPanel( + "userManual", + "IC Coder 用户手册", + vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")], + }, + ); + UserManualPanel.currentPanel = new UserManualPanel(panel, extensionUri); + } + } + + private async _update(extensionUri: vscode.Uri) { + const manualPath = vscode.Uri.joinPath( + extensionUri, + "media", + "USER_MANUAL.md", + ); + const markdown = await vscode.workspace.fs.readFile(manualPath); + const content = Buffer.from(markdown).toString("utf-8"); + this._panel.webview.html = await this._getHtmlContent( + content, + extensionUri, + ); + } + + private async _getHtmlContent( + markdown: string, + extensionUri: vscode.Uri, + ): Promise { + let inCodeBlock = false; + let inTable = false; + let tableRows: string[] = []; + const lines: string[] = []; + + // 先处理图片 + markdown = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => { + const imgUri = this._panel.webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "media", src), + ); + return `${alt}`; + }); + + markdown.split("\n").forEach((line) => { + // 代码块 + if (line.startsWith("```")) { + if (inCodeBlock) { + lines.push(""); + inCodeBlock = false; + } else { + lines.push("
");
+          inCodeBlock = true;
+        }
+        return;
+      }
+      if (inCodeBlock) {
+        lines.push(line);
+        return;
+      }
+
+      // 表格
+      if (line.startsWith("|")) {
+        if (!inTable) inTable = true;
+        tableRows.push(line);
+        return;
+      } else if (inTable) {
+        // 表格结束
+        const headers = tableRows[0]
+          .split("|")
+          .filter((c) => c.trim())
+          .map((h) => `${h.trim()}`)
+          .join("");
+        const body = tableRows
+          .slice(2)
+          .map(
+            (r) =>
+              "" +
+              r
+                .split("|")
+                .filter((c) => c.trim())
+                .map((c) => `${c.trim()}`)
+                .join("") +
+              "",
+          )
+          .join("");
+        lines.push(
+          `${headers}${body}
`, + ); + tableRows = []; + inTable = false; + } + + // 其他行 + if (line === "---") lines.push("
"); + else if (line.startsWith("#### ")) + lines.push(`

${line.slice(5)}

`); + else if (line.startsWith("### ")) lines.push(`

${line.slice(4)}

`); + else if (line.startsWith("## ")) lines.push(`

${line.slice(3)}

`); + else if (line.startsWith("# ")) lines.push(`

${line.slice(2)}

`); + else if (line.startsWith("- ")) + lines.push( + `
  • ${line.slice(2).replace(/\*\*(.+?)\*\*/g, "$1")}
  • `, + ); + else if (line.trim() === "") lines.push("

    "); + else + lines.push( + `

    ${line.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\((.+?)\)/g, '$1')}

    `, + ); + }); + + const html = lines + .join("\n") + .replace(/((?:
  • .*<\/li>\n?)+)/g, "
      $1
    "); + + return ` + + + + + +${html} +`; + } + + public dispose() { + UserManualPanel.currentPanel = undefined; + this._panel.dispose(); + while (this._disposables.length) { + this._disposables.pop()?.dispose(); + } + } +}