Skip to content

Commit 79af2b7

Browse files
authored
[ENG-1079] Cmd + Alt + Enter to open file to new leaf (#572)
* open file to new leaf * remove unused
1 parent d1e4c0a commit 79af2b7

File tree

2 files changed

+152
-59
lines changed

2 files changed

+152
-59
lines changed

apps/obsidian/src/components/canvas/TldrawViewComponent.tsx

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ import {
2828
TLDATA_DELIMITER_START,
2929
} from "~/constants";
3030
import { TFile } from "obsidian";
31-
import {
32-
ObsidianTLAssetStore,
33-
resolveLinkedTFileByBlockRef,
34-
extractBlockRefId,
35-
} from "~/components/canvas/stores/assetStore";
31+
import { ObsidianTLAssetStore } from "~/components/canvas/stores/assetStore";
3632
import {
3733
createDiscourseNodeUtil,
3834
DiscourseNodeShape,
@@ -51,7 +47,12 @@ import { RelationsOverlay } from "./overlays/RelationOverlay";
5147
import { showToast } from "./utils/toastUtils";
5248
import { WHITE_LOGO_SVG } from "~/icons";
5349
import { CustomContextMenu } from "./CustomContextMenu";
54-
import { openFileInSidebar, openFileInNewTab } from "./utils/openFileUtils";
50+
import {
51+
openFileInSidebar,
52+
openFileInNewTab,
53+
openFileInNewLeaf,
54+
resolveDiscourseNodeFile,
55+
} from "./utils/openFileUtils";
5556

5657
type TldrawPreviewProps = {
5758
store: TLStore;
@@ -69,6 +70,7 @@ export const TldrawPreviewComponent = ({
6970
const containerRef = useRef<HTMLDivElement>(null);
7071
const [currentStore, setCurrentStore] = useState<TLStore>(store);
7172
const [isReady, setIsReady] = useState(false);
73+
const [isEditorMounted, setIsEditorMounted] = useState(false);
7274
const isCreatingRelationRef = useRef(false);
7375
const saveTimeoutRef = useRef<NodeJS.Timeout>(null);
7476
const isSavingRef = useRef<boolean>(false);
@@ -103,6 +105,50 @@ export const TldrawPreviewComponent = ({
103105
return () => clearTimeout(timer);
104106
}, []);
105107

108+
// Add keyboard event listener for Meta+Alt+Enter when editor is mounted
109+
useEffect(() => {
110+
if (!isEditorMounted || !editorRef.current) return;
111+
112+
const editor = editorRef.current;
113+
114+
const handleKeyDown = (e: KeyboardEvent) => {
115+
// Check for Meta+Alt+Enter (Command+Alt+Enter on Mac)
116+
if (
117+
e.key === "Enter" &&
118+
e.metaKey &&
119+
e.altKey &&
120+
!e.shiftKey &&
121+
!e.ctrlKey
122+
) {
123+
const hoveredShapeId = editor.getHoveredShapeId();
124+
if (!hoveredShapeId) return;
125+
126+
const hoveredShape = editor.getShape(hoveredShapeId);
127+
if (!hoveredShape || hoveredShape.type !== "discourse-node") return;
128+
129+
const shape = hoveredShape as DiscourseNodeShape;
130+
void (async () => {
131+
const linkedFile = await resolveDiscourseNodeFile(
132+
shape,
133+
file,
134+
plugin.app,
135+
);
136+
137+
if (!linkedFile) return;
138+
139+
await openFileInNewLeaf(plugin.app, linkedFile);
140+
editor.selectNone();
141+
})();
142+
}
143+
};
144+
145+
window.addEventListener("keydown", handleKeyDown, true);
146+
147+
return () => {
148+
window.removeEventListener("keydown", handleKeyDown, true);
149+
};
150+
}, [isEditorMounted, file, plugin]);
151+
106152
const saveChanges = useCallback(async () => {
107153
// Prevent concurrent saves
108154
if (isSavingRef.current) {
@@ -218,8 +264,11 @@ export const TldrawPreviewComponent = ({
218264

219265
const handleMount = (editor: Editor) => {
220266
editorRef.current = editor;
267+
setIsEditorMounted(true);
221268

222269
editor.on("event", (event) => {
270+
// Handle pointer events
271+
if (event.type !== "pointer") return;
223272
const e = event as TLPointerEventInfo;
224273
if (e.type === "pointer" && e.name === "pointer_down") {
225274
const currentTool = editor.getCurrentTool();
@@ -262,58 +311,23 @@ export const TldrawPreviewComponent = ({
262311
return;
263312
}
264313

265-
const blockRefId = extractBlockRefId(shape.props.src ?? undefined);
266-
if (!blockRefId) {
267-
showToast({
268-
severity: "warning",
269-
title: "Cannot open node",
270-
description: "No valid block reference found",
271-
});
272-
return;
273-
}
314+
void (async () => {
315+
const linkedFile = await resolveDiscourseNodeFile(
316+
shape,
317+
file,
318+
plugin.app,
319+
);
274320

275-
const canvasFileCache = plugin.app.metadataCache.getFileCache(file);
276-
if (!canvasFileCache) {
277-
showToast({
278-
severity: "error",
279-
title: "Error",
280-
description: "Could not read canvas file",
281-
});
282-
return;
283-
}
321+
if (!linkedFile) return;
284322

285-
void resolveLinkedTFileByBlockRef({
286-
app: plugin.app,
287-
canvasFile: file,
288-
blockRefId,
289-
canvasFileCache,
290-
})
291-
.then(async (linkedFile) => {
292-
if (!linkedFile) {
293-
showToast({
294-
severity: "warning",
295-
title: "Cannot open node",
296-
description: "Linked file not found",
297-
});
298-
return;
299-
}
300-
301-
// Open in sidebar (Shift+Click) or new tab (Cmd+Click)
302-
if (openInNewTab) {
303-
await openFileInNewTab(plugin.app, linkedFile);
304-
} else {
305-
await openFileInSidebar(plugin.app, linkedFile);
306-
}
307-
editor.selectNone();
308-
})
309-
.catch((error) => {
310-
console.error("Error opening linked file:", error);
311-
showToast({
312-
severity: "error",
313-
title: "Error",
314-
description: "Failed to open linked file",
315-
});
316-
});
323+
// Open in sidebar (Shift+Click) or new tab (Cmd+Click)
324+
if (openInNewTab) {
325+
await openFileInNewTab(plugin.app, linkedFile);
326+
} else {
327+
await openFileInSidebar(plugin.app, linkedFile);
328+
}
329+
editor.selectNone();
330+
})();
317331
}
318332
}
319333
});

apps/obsidian/src/components/canvas/utils/openFileUtils.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,74 @@
11
import { App, TFile } from "obsidian";
2+
import { DiscourseNodeShape } from "~/components/canvas/shapes/DiscourseNodeShape";
3+
import {
4+
extractBlockRefId,
5+
resolveLinkedTFileByBlockRef,
6+
} from "~/components/canvas/stores/assetStore";
7+
import { showToast } from "./toastUtils";
28

3-
export const openFileInSidebar = async (app: App, file: TFile): Promise<void> => {
9+
/**
10+
* Resolves and validates a discourse node shape to get its linked file.
11+
* Handles all validation and error toasts internally.
12+
* @returns The linked TFile if valid, null otherwise
13+
*/
14+
export const resolveDiscourseNodeFile = async (
15+
shape: DiscourseNodeShape,
16+
canvasFile: TFile,
17+
app: App,
18+
): Promise<TFile | null> => {
19+
const blockRefId = extractBlockRefId(shape.props.src ?? undefined);
20+
if (!blockRefId) {
21+
showToast({
22+
severity: "warning",
23+
title: "Cannot open node",
24+
description: "No valid block reference found",
25+
});
26+
return null;
27+
}
28+
29+
const canvasFileCache = app.metadataCache.getFileCache(canvasFile);
30+
if (!canvasFileCache) {
31+
showToast({
32+
severity: "error",
33+
title: "Error",
34+
description: "Could not read canvas file",
35+
});
36+
return null;
37+
}
38+
39+
try {
40+
const linkedFile = await resolveLinkedTFileByBlockRef({
41+
app,
42+
canvasFile,
43+
blockRefId,
44+
canvasFileCache,
45+
});
46+
47+
if (!linkedFile) {
48+
showToast({
49+
severity: "warning",
50+
title: "Cannot open node",
51+
description: "Linked file not found",
52+
});
53+
return null;
54+
}
55+
56+
return linkedFile;
57+
} catch (error) {
58+
console.error("Error resolving linked file:", error);
59+
showToast({
60+
severity: "error",
61+
title: "Error",
62+
description: "Failed to open linked file",
63+
});
64+
return null;
65+
}
66+
};
67+
68+
export const openFileInSidebar = async (
69+
app: App,
70+
file: TFile,
71+
): Promise<void> => {
472
const rightSplit = app.workspace.rightSplit;
573
const rightLeaf = app.workspace.getRightLeaf(false);
674

@@ -17,9 +85,20 @@ export const openFileInSidebar = async (app: App, file: TFile): Promise<void> =>
1785
}
1886
};
1987

20-
export const openFileInNewTab = async (app: App, file: TFile): Promise<void> => {
88+
export const openFileInNewTab = async (
89+
app: App,
90+
file: TFile,
91+
): Promise<void> => {
2192
const leaf = app.workspace.getLeaf("tab");
2293
await leaf.openFile(file);
2394
app.workspace.setActiveLeaf(leaf);
2495
};
25-
96+
97+
export const openFileInNewLeaf = async (
98+
app: App,
99+
file: TFile,
100+
): Promise<void> => {
101+
const leaf = app.workspace.getLeaf("split");
102+
await leaf.openFile(file);
103+
app.workspace.setActiveLeaf(leaf);
104+
};

0 commit comments

Comments
 (0)