Skip to content

Commit 0bac195

Browse files
Refactor shell command handling and account prep hooks
1 parent 9125f89 commit 0bac195

17 files changed

Lines changed: 597 additions & 449 deletions

app/src/features/editor-pane/hooks/use-find-bar.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
} from "react";
88

99
import { type NoteEditorHandle } from "@/features/editor/note-editor";
10+
import { useShellCommandStore } from "@/features/shell/store/use-shell-command-store";
1011
import {
1112
isEditorFindShortcut,
1213
isNotesSearchShortcut,
1314
} from "@/shared/lib/keyboard";
1415
import { resolveActiveEditorSearch } from "@/shared/lib/search";
15-
import { OPEN_EDITOR_FIND_EVENT } from "@/features/editor-pane/lib/editor-pane-utils";
1616

1717
export function useFindBar({
1818
noteId,
@@ -30,8 +30,12 @@ export function useFindBar({
3030
const [findQuery, setFindQuery] = useState("");
3131
const [activeFindMatchIndex, setActiveFindMatchIndex] = useState(0);
3232
const [findScrollRevision, setFindScrollRevision] = useState(0);
33+
const editorFindRequestId = useShellCommandStore(
34+
(state) => state.editorFindRequestId,
35+
);
3336
const findInputRef = useRef<HTMLInputElement | null>(null);
3437
const lastActiveNoteIdRef = useRef(noteId);
38+
const lastHandledEditorFindRequestIdRef = useRef(0);
3539
const hasEditorFindQuery = findOpen && findQuery.trim().length > 0;
3640

3741
const activeEditorSearch = resolveActiveEditorSearch({
@@ -134,19 +138,25 @@ export function useFindBar({
134138
}
135139
});
136140

137-
const handleOpenEditorFind = useEffectEvent((_event: Event) => {
138-
openFind();
139-
});
140-
141141
useEffect(() => {
142142
window.addEventListener("keydown", handleGlobalFindKeyDown);
143-
window.addEventListener(OPEN_EDITOR_FIND_EVENT, handleOpenEditorFind);
144143
return () => {
145144
window.removeEventListener("keydown", handleGlobalFindKeyDown);
146-
window.removeEventListener(OPEN_EDITOR_FIND_EVENT, handleOpenEditorFind);
147145
};
148146
}, []);
149147

148+
useEffect(() => {
149+
if (
150+
editorFindRequestId === 0 ||
151+
lastHandledEditorFindRequestIdRef.current === editorFindRequestId
152+
) {
153+
return;
154+
}
155+
156+
lastHandledEditorFindRequestIdRef.current = editorFindRequestId;
157+
openFind();
158+
}, [editorFindRequestId, openFind]);
159+
150160
return {
151161
findOpen,
152162
findMatchCount: activeEditorFindMatchCount,

app/src/features/editor-pane/lib/editor-pane-utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { format } from "date-fns";
22
import type { LogicalPosition } from "@tauri-apps/api/dpi";
33
import { CheckMenuItem, Menu, Submenu } from "@tauri-apps/api/menu";
4-
5-
export const OPEN_EDITOR_FIND_EVENT = "comet:open-editor-find";
64
export const TOOLBAR_ENTER_ANIMATION = {
75
damping: 28,
86
mass: 0.8,

app/src/features/editor-pane/ui/editor-pane.tsx

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
useShowEditorToolbar,
1515
useUIActions,
1616
} from "@/features/settings/store/use-ui-store";
17+
import { useShellCommandStore } from "@/features/shell/store/use-shell-command-store";
1718
import { useShellNavigationStore } from "@/features/shell/store/use-shell-navigation-store";
1819
import cometLogo from "@/assets/comet.svg";
1920
import { LogicalPosition } from "@tauri-apps/api/dpi";
@@ -30,10 +31,6 @@ import {
3031
NoteEditor,
3132
type NoteEditorHandle,
3233
} from "@/features/editor/note-editor";
33-
import {
34-
type FocusEditorDetail,
35-
FOCUS_EDITOR_EVENT,
36-
} from "@/shared/lib/pane-navigation";
3734
import { Button } from "@/shared/ui/button";
3835
import { PopoverPopup, PopoverRoot, PopoverTrigger } from "@/shared/ui/popover";
3936
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
@@ -126,6 +123,9 @@ export function EditorPane({
126123
const [toolbarContainer, setToolbarContainer] = useState<HTMLElement | null>(
127124
null,
128125
);
126+
const focusEditorRequest = useShellCommandStore(
127+
(state) => state.focusEditorRequest,
128+
);
129129
const editorFontSize = useEditorFontSize();
130130
const notesPanelVisible = useNotesPanelVisible();
131131
const showToolbar = useShowEditorToolbar();
@@ -195,6 +195,7 @@ export function EditorPane({
195195
setToolbarContainer(node);
196196
};
197197
const editorLoadKey = noteId ? (editorKey ?? noteId) : null;
198+
const lastHandledFocusEditorRequestIdRef = useRef(0);
198199
const editorContent = (() => {
199200
if (noteId === null) {
200201
return null;
@@ -327,32 +328,37 @@ export function EditorPane({
327328
window.removeEventListener("keydown", handleGlobalHistoryKeyDown);
328329
}, []);
329330

330-
const handleFocusEditor = useEffectEvent((event: Event) => {
331-
if (!noteId) {
332-
return;
333-
}
334-
335-
const customEvent = event as CustomEvent<FocusEditorDetail>;
336-
const scrollTo = customEvent.detail?.scrollTo ?? "preserve";
337-
338-
setFocusedPane("editor");
339-
requestAnimationFrame(() => {
340-
if (scrollTo === "top") {
341-
editorRef.current?.focusAtStart();
342-
scrollContainerRef.current?.scrollTo({ top: 0 });
331+
const handleFocusEditor = useEffectEvent(
332+
(scrollTo: "preserve" | "top" = "preserve") => {
333+
if (!noteId) {
343334
return;
344335
}
345336

346-
editorRef.current?.focus();
347-
});
348-
});
337+
setFocusedPane("editor");
338+
requestAnimationFrame(() => {
339+
if (scrollTo === "top") {
340+
editorRef.current?.focusAtStart();
341+
scrollContainerRef.current?.scrollTo({ top: 0 });
342+
return;
343+
}
344+
345+
editorRef.current?.focus();
346+
});
347+
},
348+
);
349349

350350
useEffect(() => {
351-
window.addEventListener(FOCUS_EDITOR_EVENT, handleFocusEditor);
352-
return () => {
353-
window.removeEventListener(FOCUS_EDITOR_EVENT, handleFocusEditor);
354-
};
355-
}, []);
351+
if (
352+
!focusEditorRequest ||
353+
lastHandledFocusEditorRequestIdRef.current ===
354+
focusEditorRequest.requestId
355+
) {
356+
return;
357+
}
358+
359+
lastHandledFocusEditorRequestIdRef.current = focusEditorRequest.requestId;
360+
handleFocusEditor(focusEditorRequest.scrollTo);
361+
}, [focusEditorRequest, handleFocusEditor]);
356362

357363
const backlinksButton =
358364
backlinks.length > 0 ? (

app/src/features/editor/extensions/link-interactions.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import {
77
} from "@codemirror/lang-markdown";
88
import { EditorView } from "@codemirror/view";
99
import { afterEach, describe, expect, it, vi } from "vitest";
10+
import {
11+
resetShellCommandState,
12+
useShellCommandStore,
13+
} from "@/features/shell/store/use-shell-command-store";
1014
import { shellStore } from "@/features/shell/store/use-shell-store";
1115
import { WikiLinkGrammar } from "@/features/editor/extensions/markdown-decorations/wikilink-syntax";
12-
import { CREATE_NOTE_FROM_WIKILINK_EVENT } from "@/shared/lib/note-navigation";
1316

1417
const { invokeMock, openUrlMock } = vi.hoisted(() => ({
1518
invokeMock: vi.fn(),
@@ -66,6 +69,7 @@ async function flush() {
6669
afterEach(() => {
6770
invokeMock.mockReset();
6871
openUrlMock.mockClear();
72+
resetShellCommandState();
6973
shellStore.setState({
7074
draftMarkdown: "",
7175
draftNoteId: null,
@@ -450,8 +454,6 @@ describe("Editor link interactions", () => {
450454

451455
it("dispatches create-note for unresolved wikilinks", async () => {
452456
invokeMock.mockResolvedValueOnce(null);
453-
const eventHandler = vi.fn();
454-
window.addEventListener(CREATE_NOTE_FROM_WIKILINK_EVENT, eventHandler);
455457

456458
const { view } = createView("[[Target]]", false, "note-1");
457459
await flush();
@@ -480,14 +482,15 @@ describe("Editor link interactions", () => {
480482
title: "Target",
481483
},
482484
});
483-
expect(eventHandler).toHaveBeenCalledTimes(1);
484-
expect((eventHandler.mock.calls[0][0] as CustomEvent).detail).toEqual({
485+
expect(
486+
useShellCommandStore.getState().createNoteFromWikilinkRequest,
487+
).toEqual({
485488
location: 0,
489+
requestId: 1,
486490
sourceNoteId: "note-1",
487491
title: "Target",
488492
});
489493

490-
window.removeEventListener(CREATE_NOTE_FROM_WIKILINK_EVENT, eventHandler);
491494
view.destroy();
492495
});
493496
});

app/src/features/notes-pane/ui/notes-pane.tsx

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ import {
2323
useSearchQuery,
2424
useTagViewActive,
2525
} from "@/features/shell/store/use-shell-store";
26+
import { useShellCommandStore } from "@/features/shell/store/use-shell-command-store";
2627
import { useShellNavigationStore } from "@/features/shell/store/use-shell-navigation-store";
27-
import {
28-
type FocusNotesPaneDetail,
29-
FOCUS_NOTES_PANE_EVENT,
30-
} from "@/shared/lib/pane-navigation";
3128
import {
3229
type NoteSortDirection,
3330
type NoteSortField,
@@ -99,6 +96,12 @@ export function NotesPane({
9996
const noteFilter = tagViewActive ? "all" : storeNoteFilter;
10097
const searchQuery = useSearchQuery();
10198
const creatingNoteId = useCreatingSelectedNoteId();
99+
const focusNotesPaneRequest = useShellCommandStore(
100+
(state) => state.focusNotesPaneRequest,
101+
);
102+
const focusNotesSearchRequestId = useShellCommandStore(
103+
(state) => state.focusNotesSearchRequestId,
104+
);
102105
const { setFocusedPane, setSearchQuery: onChangeSearch } =
103106
useShellNavigationStore((state) => state.actions);
104107
const { setNoteSortPrefs } = useUIActions();
@@ -149,9 +152,11 @@ export function NotesPane({
149152

150153
const shouldSkipAnimation = Date.now() < skipAnimationUntilRef.current;
151154
const searchInputRef = useRef<HTMLInputElement | null>(null);
152-
const pendingNotesPaneSelectionRef = useRef<
153-
FocusNotesPaneDetail["selection"] | null
154-
>(null);
155+
const pendingNotesPaneSelectionRef = useRef<"first" | "selected" | null>(
156+
null,
157+
);
158+
const lastHandledFocusNotesPaneRequestIdRef = useRef(0);
159+
const lastHandledFocusNotesSearchRequestIdRef = useRef(0);
155160
const noteRowRefs = useRef(new Map<string, HTMLButtonElement | null>());
156161
const shouldRestoreSelectedRowFocusRef = useRef(false);
157162
const [isSearchFocused, setIsSearchFocused] = useState(false);
@@ -239,56 +244,63 @@ export function NotesPane({
239244
}, [isSearchOpen]);
240245

241246
useEffect(() => {
242-
const handleFocusSearch = () => {
243-
setFocusedPane("notes");
244-
setIsSearchOpen(true);
245-
focusSearchInput();
246-
};
247-
window.addEventListener("comet:focus-search", handleFocusSearch);
248-
return () =>
249-
window.removeEventListener("comet:focus-search", handleFocusSearch);
250-
}, [focusSearchInput, setFocusedPane]);
247+
if (
248+
focusNotesSearchRequestId === 0 ||
249+
lastHandledFocusNotesSearchRequestIdRef.current ===
250+
focusNotesSearchRequestId
251+
) {
252+
return;
253+
}
254+
255+
lastHandledFocusNotesSearchRequestIdRef.current = focusNotesSearchRequestId;
256+
setFocusedPane("notes");
257+
setIsSearchOpen(true);
258+
focusSearchInput();
259+
}, [focusNotesSearchRequestId, focusSearchInput, setFocusedPane]);
251260

252261
useEffect(() => {
253-
const handleFocusNotesPane = (event: Event) => {
254-
const customEvent = event as CustomEvent<FocusNotesPaneDetail>;
255-
let selection = customEvent.detail?.selection ?? "selected";
256-
257-
// If the selected note isn't in the current filtered list, fall back
258-
// to selecting the first visible note instead of focusing an empty
259-
// scroll container.
260-
if (
261-
selection === "selected" &&
262-
selectedNoteId &&
263-
!filteredNotes.some((n) => n.id === selectedNoteId)
264-
) {
265-
selection = "first";
266-
}
262+
if (
263+
!focusNotesPaneRequest ||
264+
lastHandledFocusNotesPaneRequestIdRef.current ===
265+
focusNotesPaneRequest.requestId
266+
) {
267+
return;
268+
}
267269

268-
if (selection === "first") {
269-
if (isNotesPlaceholderData) {
270-
pendingNotesPaneSelectionRef.current = "first";
271-
return;
272-
}
270+
lastHandledFocusNotesPaneRequestIdRef.current =
271+
focusNotesPaneRequest.requestId;
272+
let selection = focusNotesPaneRequest.selection ?? "selected";
273+
274+
// If the selected note isn't in the current filtered list, fall back
275+
// to selecting the first visible note instead of focusing an empty
276+
// scroll container.
277+
if (
278+
selection === "selected" &&
279+
selectedNoteId &&
280+
!filteredNotes.some((n) => n.id === selectedNoteId)
281+
) {
282+
selection = "first";
283+
}
273284

274-
setIsSearchFocused(false);
275-
setFocusedPane("notes");
276-
selectFirstVisibleNote();
277-
focusNotesPaneTarget(scrollContainerRef.current);
285+
if (selection === "first") {
286+
if (isNotesPlaceholderData) {
287+
pendingNotesPaneSelectionRef.current = "first";
278288
return;
279289
}
280290

281-
pendingNotesPaneSelectionRef.current = "selected";
282-
setFocusedPane("notes");
283291
setIsSearchFocused(false);
292+
setFocusedPane("notes");
293+
selectFirstVisibleNote();
284294
focusNotesPaneTarget(scrollContainerRef.current);
285-
};
295+
return;
296+
}
286297

287-
window.addEventListener(FOCUS_NOTES_PANE_EVENT, handleFocusNotesPane);
288-
return () => {
289-
window.removeEventListener(FOCUS_NOTES_PANE_EVENT, handleFocusNotesPane);
290-
};
298+
pendingNotesPaneSelectionRef.current = "selected";
299+
setFocusedPane("notes");
300+
setIsSearchFocused(false);
301+
focusNotesPaneTarget(scrollContainerRef.current);
291302
}, [
303+
focusNotesPaneRequest,
292304
filteredNotes,
293305
isNotesPlaceholderData,
294306
selectFirstVisibleNote,

0 commit comments

Comments
 (0)