diff --git a/src/QuartzEditorProvider.ts b/src/QuartzEditorProvider.ts index ff1b4b5..465cb45 100644 --- a/src/QuartzEditorProvider.ts +++ b/src/QuartzEditorProvider.ts @@ -115,6 +115,9 @@ export class QuartzEditorProvider implements vscode.CustomTextEditorProvider { case 'requestGitDiff': await this.openGitDiff(webviewPanel.webview, document); return; + case 'openLink': + await this.openLink(webviewPanel.webview, document, message.href); + return; case 'diffNoChanges': vscode.window.showInformationMessage('No changes compared to HEAD.'); return; @@ -269,6 +272,42 @@ export class QuartzEditorProvider implements vscode.CustomTextEditorProvider { await vscode.workspace.applyEdit(edit); } + private async openLink( + webview: vscode.Webview, + document: vscode.TextDocument, + href: unknown, + ): Promise { + if (typeof href !== 'string' || !href.trim()) return; + + const trimmed = href.trim(); + if (trimmed.startsWith('#')) return; + + if (/^https?:\/\//i.test(trimmed)) { + await vscode.env.openExternal(vscode.Uri.parse(trimmed)); + return; + } + + const target = this.resolveLocalLink(document, trimmed); + + try { + await vscode.workspace.fs.stat(target); + } catch { + vscode.window.showWarningMessage(`Linked file does not exist: ${target.fsPath}`); + return; + } + + await vscode.commands.executeCommand('vscode.open', target); + } + + private resolveLocalLink(document: vscode.TextDocument, href: string): vscode.Uri { + const pathOnly = href.split('#', 1)[0]; + const resolvedPath = path.isAbsolute(pathOnly) + ? pathOnly + : path.resolve(path.dirname(document.uri.fsPath), pathOnly); + + return vscode.Uri.file(resolvedPath); + } + private getHtmlForWebview(webview: vscode.Webview): string { const scriptUri = webview.asWebviewUri( vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'webview', 'index.js'), diff --git a/src/webview/App.tsx b/src/webview/App.tsx index dbde365..2dc8de4 100644 --- a/src/webview/App.tsx +++ b/src/webview/App.tsx @@ -7,13 +7,9 @@ import { computeDiff } from './diff/diffEngine'; import { computeAlignment } from './diff/alignment'; import type { DiffResult, AlignedRow } from './diff/types'; import { EDITOR_THEMES } from './types'; -import type { EditorConfig } from './types'; +import type { EditorConfig, VsCodeApi } from './types'; -declare function acquireVsCodeApi(): { - postMessage(message: unknown): void; - getState(): unknown; - setState(state: unknown): void; -}; +declare function acquireVsCodeApi(): VsCodeApi; const vscode = acquireVsCodeApi(); diff --git a/src/webview/Editor.tsx b/src/webview/Editor.tsx index 8be813e..3ad201d 100644 --- a/src/webview/Editor.tsx +++ b/src/webview/Editor.tsx @@ -49,7 +49,7 @@ import { CalloutExtension } from './extensions/calloutExtension'; import { SearchHighlightExtension } from './extensions/searchHighlightExtension'; import { SearchBar } from './components/SearchBar'; import { LinkDialog } from './components/LinkDialog'; -import type { EditorConfig } from './types'; +import type { EditorConfig, VsCodeApi } from './types'; import './styles/callout.css'; import './styles/codeBlock.css'; @@ -249,6 +249,20 @@ export function Editor({ initialContent, config: _config, onUpdate }: EditorProp class: 'quartz-editor-content', spellcheck: 'true', }, + handleClick: (_view, _pos, event) => { + if (!event.metaKey && !event.ctrlKey) return false; + + const target = event.target; + if (!(target instanceof Element)) return false; + + const link = target.closest('a[href]'); + if (!(link instanceof HTMLAnchorElement)) return false; + + event.preventDefault(); + const vscode = (window as unknown as { vscodeApi?: VsCodeApi }).vscodeApi; + vscode?.postMessage({ type: 'openLink', href: link.getAttribute('href') || '' }); + return true; + }, }, }); diff --git a/src/webview/components/FrontmatterBanner.tsx b/src/webview/components/FrontmatterBanner.tsx index 71ec9c3..884016d 100644 --- a/src/webview/components/FrontmatterBanner.tsx +++ b/src/webview/components/FrontmatterBanner.tsx @@ -1,12 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type { VsCodeApi } from '../types'; import '../styles/frontmatter.css'; -interface VsCodeApi { - postMessage(message: unknown): void; - getState(): Record | undefined; - setState(state: unknown): void; -} - interface FrontmatterBannerProps { frontmatter: string | null; onChange: (yaml: string) => void; diff --git a/src/webview/types.ts b/src/webview/types.ts index a3310eb..918e73c 100644 --- a/src/webview/types.ts +++ b/src/webview/types.ts @@ -7,3 +7,9 @@ export interface EditorConfig { preserveFormatting: boolean; showBlockHandles: boolean; } + +export interface VsCodeApi { + postMessage(message: unknown): void; + getState(): unknown; + setState(state: unknown): void; +}