();
+
+ const { settings } = useSettings(plugin);
+
+ // 只在挂载时初始化一次编辑器,settings 变更不应导致重建
+ useEffect(() => {
+ if (!editorRef.current) return;
+
+ const initialize = async () => {
+ aceServiceRef.current = new AceService();
+ if (!editorRef.current) return;
+ const editor = aceServiceRef.current.createEditor(
+ editorRef.current,
+ );
+ editor.setReadOnly(true);
+ aceServiceRef.current.configureEditor(settings, extension);
+
+ if (range) {
+ aceServiceRef.current.setValueWithLineRange(content, range);
+ } else {
+ aceServiceRef.current.setValue(content);
+ }
+
+ const contentLines = range
+ ? range.endLine - range.startLine + 1
+ : content.split("\n").length;
+
+ const editorHeight = Math.min(
+ contentLines * (settings.fontSize + 4) + 20,
+ settings.embedMaxHeight,
+ );
+ editorRef.current.style.height = `${editorHeight}px`;
+
+ const languageMode = await getLanguageMode(extension);
+ setLang(languageMode);
+ };
+
+ initialize();
+
+ return () => {
+ aceServiceRef.current?.destroy();
+ aceServiceRef.current = null;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const displayLabel = useMemo(() => {
+ if (range) {
+ return range.startLine === range.endLine
+ ? `${lang} (Line ${range.startLine})`
+ : `${lang} (Lines ${range.startLine}-${range.endLine})`;
+ }
+ return lang;
+ }, [lang, range]);
+
+ const handleOpenUrl = () => {
+ window.open(sourceUrl, "_blank");
+ };
+
+ const fileName = useMemo(() => {
+ return sourceUrl.split("/").pop() || sourceUrl;
+ }, [sourceUrl]);
+
+ return (
+ <>
+
+
+ {fileName}
+
+
+
+ {displayLabel}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+// ─── 加载状态组件 ───
+
+const LoadingView: React.FC = () => {
+ return (
+
+ Loading remote code...
+
+ );
+};
+
+// ─── 错误状态组件 ───
+
+const ErrorView: React.FC<{ error: string }> = ({ error }) => {
+ return (
+
+ Failed to load remote code
+ {error}
+
+ );
+};
+
+// ─── RemoteEmbedView 类(Component 子类) ───
+
+export interface RemoteEmbedLoadResult {
+ content: string;
+ extension: string;
+ sourceUrl: string;
+}
+
+export class RemoteEmbedView extends MarkdownRenderChild {
+ plugin: AceCodeEditorPlugin;
+ private root: Root | null = null;
+ private range: LineRange | null = null;
+ private loadResult: RemoteEmbedLoadResult | null = null;
+ private subpath: string;
+
+ constructor(
+ plugin: AceCodeEditorPlugin,
+ containerEl: HTMLElement,
+ private loadFn: () => Promise,
+ subpath: string,
+ ) {
+ super(containerEl);
+ this.plugin = plugin;
+ this.subpath = subpath;
+
+ if (subpath) {
+ const parsed = parseLineRange(subpath);
+ if (parsed) this.range = parsed;
+ }
+ }
+
+ async onload() {
+ super.onload();
+ this.containerEl.addClass("ace-embed-view");
+ this.containerEl.addClass("ace-embed-remote-view");
+
+ this.root = createRoot(this.containerEl);
+
+ // 先渲染 loading
+ this.root.render(createElement(LoadingView, {}));
+
+ // 异步加载远程内容
+ try {
+ this.loadResult = await this.loadFn();
+ // 异步操作后检查组件是否仍然存活,避免在已卸载的 root 上渲染
+ if (!this.root) return;
+ this.renderContent();
+ } catch (err) {
+ if (!this.root) return;
+ const errorMsg = err instanceof Error ? err.message : String(err);
+ this.root.render(createElement(ErrorView, { error: errorMsg }));
+ }
+ }
+
+ private renderContent(): void {
+ if (!this.root || !this.loadResult) return;
+
+ this.root.render(
+ createElement(RemoteEmbedContainer, {
+ plugin: this.plugin,
+ content: this.loadResult.content,
+ extension: this.loadResult.extension,
+ range: this.range || undefined,
+ sourceUrl: this.loadResult.sourceUrl,
+ }),
+ );
+ }
+
+ onunload(): void {
+ if (this.root) {
+ this.root.unmount();
+ this.root = null;
+ }
+ super.onunload();
+ }
+}
diff --git a/styles/AceEmbedView.css b/styles/AceEmbedView.css
index 144c883..d5c1711 100644
--- a/styles/AceEmbedView.css
+++ b/styles/AceEmbedView.css
@@ -66,3 +66,71 @@
display: block;
margin: 1rem 0;
}
+
+/* ─── 远程代码嵌入样式 ─── */
+
+/* 远程嵌入视图容器:使用 accent 色边框区分本地嵌入 */
+.ace-embed-remote-view {
+ border-color: var(--interactive-accent);
+
+ &.mod-empty-attachment {
+ padding: 0rem;
+ }
+}
+
+/* 远程嵌入标题修饰:显示外链图标和 accent 色 */
+.ace-embed-remote-title {
+ color: var(--text-accent);
+ font-style: italic;
+
+ &::before {
+ content: "\2197\00a0"; /* ↗ + nbsp */
+ opacity: 0.6;
+ }
+}
+
+/* ─── 加载状态 ─── */
+.ace-embed-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ gap: 8px;
+
+ &::before {
+ content: "";
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--background-modifier-border);
+ border-top-color: var(--interactive-accent);
+ border-radius: 50%;
+ animation: ace-embed-spin 0.8s linear infinite;
+ }
+}
+
+@keyframes ace-embed-spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* ─── 错误状态 ─── */
+.ace-embed-error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 1.5rem;
+ color: var(--text-error);
+ font-size: 0.85rem;
+ gap: 4px;
+
+ .ace-embed-error-msg {
+ font-family: var(--font-monospace);
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ word-break: break-all;
+ }
+}