Skip to content

Commit fa92635

Browse files
committed
feat(editor): enhance editor functionality with tab management and file handling
1 parent f6cf661 commit fa92635

7 files changed

Lines changed: 285 additions & 49 deletions

File tree

packages/ui/lib/app.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { CommandPalette } from "@/components/CommandPalette/CommandPalette";
33
import { KeyBar } from "@/components/KeyBar/KeyBar";
44
import { PanelGroup } from "@/components/PanelGroup/PanelGroup";
55
import { DialogHolder, useDialog } from "@/dialogs/dialogContext";
6-
import { useBridge } from "@dotdirfm/ui-bridge";
76
import { CommandLine } from "@/features/command-line/CommandLine/CommandLine";
8-
import { useCommandRegistry } from "@dotdirfm/commands";
97
import { useBuiltInCommands } from "@/features/commands/useBuiltInCommands";
108
import { useCommandRouting } from "@/features/commands/useCommandRouting";
119
import { useExtensionRuntime } from "@/features/extensions/useExtensionRuntime";
@@ -14,12 +12,14 @@ import { useFileOperations } from "@/features/file-ops/useFileOperations";
1412
import { useActivePanelNavigation } from "@/features/panels/panelControllers";
1513
import { Terminal, TerminalToolbar } from "@/features/terminal/Terminal";
1614
import { useSystemTheme } from "@/features/themes/useSystemTheme";
17-
import { useFocusContext } from "@dotdirfm/ui-focus";
1815
import { useViewerEditorState } from "@/hooks/useViewerEditorState";
1916
import { useWorkspacePersistenceProcess, useWorkspaceRestoreProcess } from "@/processes/workspace-session/model/useWorkspaceSessionProcess";
2017
import baseStyles from "@/styles/base.module.css";
2118
import panelsStyles from "@/styles/panels.module.css";
2219
import terminalStyles from "@/styles/terminal.module.css";
20+
import { useCommandRegistry } from "@dotdirfm/commands";
21+
import { useBridge } from "@dotdirfm/ui-bridge";
22+
import { useFocusContext } from "@dotdirfm/ui-focus";
2323
import { cx } from "@dotdirfm/ui-utils";
2424
import { useAtomValue, useSetAtom } from "jotai";
2525
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
@@ -83,10 +83,16 @@ export const App = forwardRef<AppHandle, { widget: React.ReactNode }>(function A
8383
const {
8484
handleViewFile,
8585
handleEditFile,
86+
openFileInEditor,
8687
handleOpenCreateFileConfirm,
8788
requestCloseViewer,
8889
requestCloseEditor,
90+
requestCloseEditorTab,
91+
setActiveEditorTab,
8992
viewerOpen,
93+
editorFiles,
94+
activeEditorFileIndex,
95+
editorDirty,
9096
} = useViewerEditorState();
9197

9298
useWorkspacePersistenceProcess();
@@ -146,9 +152,15 @@ export const App = forwardRef<AppHandle, { widget: React.ReactNode }>(function A
146152
onOpenCreateFileConfirm: handleOpenCreateFileConfirm,
147153
onViewFile: handleViewFile,
148154
onEditFile: handleEditFile,
155+
openFileInEditor,
149156
onRequestCloseViewer: requestCloseViewer,
150157
onRequestCloseEditor: requestCloseEditor,
151158
viewerOpen,
159+
editorFiles,
160+
activeEditorFileIndex,
161+
editorDirty,
162+
requestCloseEditorTab,
163+
setActiveEditorTab,
152164
});
153165

154166
useCommandRouting(rootRef);

packages/ui/lib/dialogs/dialogContext.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ export type DialogSpec =
167167
props: EditorProps;
168168
onClose: () => void;
169169
onDirtyChange?: (dirty: boolean) => void;
170+
tabFiles?: Array<{ path: string; name: string; langId: string; dirty?: boolean }>;
171+
activeTabIndex?: number;
172+
onTabClose?: (index: number) => void;
173+
onTabSelect?: (index: number) => void;
170174
};
171175

172176
export type DialogUpdate =
@@ -585,6 +589,10 @@ function renderDialogContent(dialog: DialogSpec, ctx: DialogContextValue, stackI
585589
stackIndex={stackIndex}
586590
onClose={dialog.onClose}
587591
onDirtyChange={dialog.onDirtyChange}
592+
tabFiles={dialog.tabFiles}
593+
activeTabIndex={dialog.activeTabIndex}
594+
onTabClose={dialog.onTabClose}
595+
onTabSelect={dialog.onTabSelect}
588596
/>
589597
);
590598
default:

packages/ui/lib/features/commands/useBuiltInCommands.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { runCommandSequence, type RunCommandsArgs } from "@dotdirfm/commands";
7373
import { useLoadedExtensions } from "@/features/extensions/useLoadedExtensions";
7474
import { executeMountedExtensionCommand } from "@/features/extensions/extensionCommandHandlers";
7575
import { DOTDIR_MONACO_EXECUTE_ACTION } from "@/features/extensions/builtins/monacoCommandBridge";
76+
import { useExtensionHostClient } from "@/features/extensions/extensionHostClient";
7677
import { useLanguageRegistry } from "@/features/languages/languageRegistry";
7778
import { useActivePanelNavigation } from "@/features/panels/panelControllers";
7879
import { DEFAULT_EDITOR_FILE_SIZE_LIMIT } from "@/features/settings/userSettings";
@@ -90,9 +91,15 @@ export interface BuiltInCommandDeps {
9091
onOpenCreateFileConfirm: (path: string, name: string, langId: string) => Promise<void>;
9192
onViewFile: (filePath: string, fileName: string, fileSize: number) => void;
9293
onEditFile: (filePath: string, fileName: string, fileSize: number, langId: string) => void;
94+
openFileInEditor: (filePath: string, fileName: string, fileSize: number, langId: string) => void;
9395
onRequestCloseViewer: () => void;
9496
onRequestCloseEditor: () => void;
9597
viewerOpen: boolean;
98+
editorFiles: Array<{ path: string; name: string; size: number; langId: string; dirty: boolean }>;
99+
activeEditorFileIndex: number;
100+
editorDirty: boolean;
101+
requestCloseEditorTab: (index: number) => void;
102+
setActiveEditorTab: (index: number) => void;
96103
}
97104

98105
export function useBuiltInCommands(deps: BuiltInCommandDeps): void {
@@ -126,6 +133,18 @@ export function useBuiltInCommands(deps: BuiltInCommandDeps): void {
126133
// Updated every render so command handlers always see the latest callbacks.
127134
const depsRef = useLatestRef(deps);
128135

136+
// Wire the openFileInEditor callback to the extension host so LSP go-to-definition works.
137+
const extensionHostClient = useExtensionHostClient();
138+
const openFileRef = useLatestRef(deps.openFileInEditor);
139+
useEffect(() => {
140+
extensionHostClient.setOpenFileRequestListener((path: string) => {
141+
const name = basename(path);
142+
const langId = languageRegistryRef.current.getLanguageForFilename(name);
143+
openFileRef.current(path, name, 0, langId);
144+
});
145+
return () => extensionHostClient.setOpenFileRequestListener(null);
146+
}, [extensionHostClient, languageRegistryRef, openFileRef]);
147+
129148
// loadedExtensions changes as extensions load; kept for potential call-time reads.
130149
useLoadedExtensions();
131150

packages/ui/lib/features/extensions/ExtensionContainer.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,10 @@ interface EditorContainerWrapperProps {
13281328
languages?: EditorProps["languages"];
13291329
grammars?: EditorProps["grammars"];
13301330
onInteract?: () => void;
1331+
tabFiles?: Array<{ path: string; name: string; langId: string; dirty?: boolean }>;
1332+
activeTabIndex?: number;
1333+
onTabClose?: (index: number) => void;
1334+
onTabSelect?: (index: number) => void;
13311335
}
13321336

13331337
export function EditorContainer({
@@ -1343,6 +1347,10 @@ export function EditorContainer({
13431347
onClose,
13441348
onDirtyChange,
13451349
onInteract,
1350+
tabFiles,
1351+
activeTabIndex,
1352+
onTabClose,
1353+
onTabSelect,
13461354
}: EditorContainerWrapperProps) {
13471355
const languageRegistry = useLanguageRegistry();
13481356
const languages = languageRegistry.languages;
@@ -1491,6 +1499,60 @@ export function EditorContainer({
14911499
×
14921500
</button>
14931501
</div>
1502+
{tabFiles && tabFiles.length > 1 && (
1503+
<div
1504+
style={{
1505+
display: "flex",
1506+
overflow: "auto",
1507+
borderBottom: "1px solid var(--border, #333)",
1508+
flexShrink: 0,
1509+
minHeight: 30,
1510+
}}
1511+
>
1512+
{tabFiles.map((t, i) => (
1513+
<div
1514+
key={t.path}
1515+
onClick={() => onTabSelect?.(i)}
1516+
title={t.path}
1517+
style={{
1518+
display: "flex",
1519+
alignItems: "center",
1520+
gap: 4,
1521+
padding: "4px 10px",
1522+
cursor: "pointer",
1523+
fontSize: 12,
1524+
borderRight: "1px solid var(--border, #333)",
1525+
background: i === (activeTabIndex ?? 0) ? "var(--bg, #1e1e2e)" : "transparent",
1526+
color: i === (activeTabIndex ?? 0) ? "var(--fg, #cdd6f4)" : "var(--fg-muted, #6c7086)",
1527+
whiteSpace: "nowrap",
1528+
flexShrink: 0,
1529+
}}
1530+
>
1531+
<span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
1532+
{t.dirty ? "● " : ""}{t.name}
1533+
</span>
1534+
<button
1535+
onClick={(e) => {
1536+
e.stopPropagation();
1537+
onTabClose?.(i);
1538+
}}
1539+
style={{
1540+
background: "transparent",
1541+
border: "none",
1542+
cursor: "pointer",
1543+
fontSize: 14,
1544+
padding: "0 2px",
1545+
color: "inherit",
1546+
lineHeight: 1,
1547+
}}
1548+
aria-label={`Close ${t.name}`}
1549+
>
1550+
×
1551+
</button>
1552+
</div>
1553+
))}
1554+
</div>
1555+
)}
14941556
<div style={{ flex: 1, minHeight: 0 }}>
14951557
<ExtensionContainer
14961558
kind="editor"

packages/ui/lib/features/extensions/extensionHostClient.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class ExtensionHostClient {
219219
private commandRequestListener: CommandRequestListener | null = null;
220220
private configReadListener: ConfigReadListener | null = null;
221221
private configWriteListener: ConfigWriteListener | null = null;
222+
openFileRequestListener: ((path: string) => void) | null = null;
222223

223224
private queuedOutbound: MainToHostMessage[] = [];
224225
private workerReady = false;
@@ -318,6 +319,9 @@ export class ExtensionHostClient {
318319
setConfigWriteListener(listener: ConfigWriteListener | null): void {
319320
this.configWriteListener = listener;
320321
}
322+
setOpenFileRequestListener(listener: ((path: string) => void) | null): void {
323+
this.openFileRequestListener = listener;
324+
}
321325

322326
// ── Outbound: document / editor / workspace / configuration ──────
323327

@@ -683,6 +687,12 @@ export function ExtensionHostClientProvider({ children }: { children: ReactNode
683687
return undefined;
684688
});
685689
client.setCommandRequestListener(async (command, args) => {
690+
if (command === "__dotdir/openFile") {
691+
const path = String(args[0] ?? "");
692+
void bridge.utils.debugLog?.(`[ExtensionHost] openFile requested: ${path}`);
693+
client.openFileRequestListener?.(path);
694+
return null;
695+
}
686696
return handleWorkerCommand(bridge, command, args);
687697
});
688698
return () => {

packages/ui/lib/features/extensions/monacoBridge/providerBridge.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,10 @@ export class MonacoProviderBridge {
417417
break;
418418
}
419419
case "callHierarchy":
420-
// Call hierarchy provider is registered by the extension but can't be
421-
// bridged to Monaco — the standalone monaco-editor doesn't include the
422-
// call hierarchy tree view or navigation UI (VS Code-only feature).
420+
case "workspaceSymbol":
421+
// These providers are registered by the extension but can't be bridged
422+
// to Monaco — the standalone monaco-editor doesn't include the UI for
423+
// call hierarchy, workspace symbols, or other VS Code-only features.
423424
return;
424425
default:
425426
this.warn(`Provider kind ${reg.kind} not installed on Monaco`);

0 commit comments

Comments
 (0)