@tooee/shell 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/command-palette-provider.d.ts +5 -0
  2. package/dist/command-palette-provider.d.ts.map +1 -0
  3. package/dist/command-palette-provider.js +28 -0
  4. package/dist/command-palette-provider.js.map +1 -0
  5. package/dist/commands.d.ts +9 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +52 -1
  8. package/dist/commands.js.map +1 -1
  9. package/dist/copy-hook.d.ts +12 -0
  10. package/dist/copy-hook.d.ts.map +1 -0
  11. package/dist/copy-hook.js +35 -0
  12. package/dist/copy-hook.js.map +1 -0
  13. package/dist/copy-on-select.d.ts.map +1 -1
  14. package/dist/copy-on-select.js +1 -3
  15. package/dist/copy-on-select.js.map +1 -1
  16. package/dist/index.d.ts +8 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/navigation.d.ts +24 -0
  21. package/dist/navigation.d.ts.map +1 -0
  22. package/dist/navigation.js +239 -0
  23. package/dist/navigation.js.map +1 -0
  24. package/dist/provider.d.ts.map +1 -1
  25. package/dist/provider.js +4 -1
  26. package/dist/provider.js.map +1 -1
  27. package/dist/search-hook.d.ts +14 -0
  28. package/dist/search-hook.d.ts.map +1 -0
  29. package/dist/search-hook.js +120 -0
  30. package/dist/search-hook.js.map +1 -0
  31. package/package.json +20 -20
  32. package/src/command-palette-provider.tsx +39 -0
  33. package/src/commands.ts +55 -1
  34. package/src/copy-hook.ts +45 -0
  35. package/src/copy-on-select.ts +1 -4
  36. package/src/index.ts +15 -5
  37. package/src/navigation.ts +297 -0
  38. package/src/provider.tsx +6 -1
  39. package/src/search-hook.ts +147 -0
  40. package/dist/command-palette.d.ts +0 -9
  41. package/dist/command-palette.d.ts.map +0 -1
  42. package/dist/command-palette.js +0 -51
  43. package/dist/command-palette.js.map +0 -1
  44. package/dist/modal.d.ts +0 -33
  45. package/dist/modal.d.ts.map +0 -1
  46. package/dist/modal.js +0 -395
  47. package/dist/modal.js.map +0 -1
  48. package/src/command-palette.ts +0 -73
  49. package/src/modal.ts +0 -454
package/src/provider.tsx CHANGED
@@ -4,7 +4,9 @@ import { ThemeSwitcherProvider } from "@tooee/themes"
4
4
  import { CommandProvider, useProvideCommandContext, type Mode } from "@tooee/commands"
5
5
  import { ToastProvider, useToast, type ToastController } from "@tooee/toasts"
6
6
  import { OverlayProvider } from "./overlay.js"
7
+ import { CommandPaletteProvider } from "./command-palette-provider.js"
7
8
  import { useCopyOnSelect } from "./copy-on-select.js"
9
+ import { useDebugConsoleCommand } from "./commands.js"
8
10
 
9
11
  declare module "@tooee/commands" {
10
12
  interface CommandContext {
@@ -49,7 +51,9 @@ function TooeeProviderInner({
49
51
  <CommandProvider leader={leader} keymap={config.keys} initialMode={initialMode}>
50
52
  <ToastProvider>
51
53
  <ToastContextBridge>
52
- <OverlayProvider>{children}</OverlayProvider>
54
+ <OverlayProvider>
55
+ <CommandPaletteProvider>{children}</CommandPaletteProvider>
56
+ </OverlayProvider>
53
57
  </ToastContextBridge>
54
58
  </ToastProvider>
55
59
  </CommandProvider>
@@ -65,6 +69,7 @@ function ToastContextBridge({ children }: { children: ReactNode }) {
65
69
  }))
66
70
 
67
71
  useCopyOnSelect()
72
+ useDebugConsoleCommand()
68
73
 
69
74
  return <>{children}</>
70
75
  }
@@ -0,0 +1,147 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react"
2
+ import { useCommand, useMode, useSetMode, type Mode } from "@tooee/commands"
3
+
4
+ export interface UseSearchOptions {
5
+ match: (query: string) => number[]
6
+ onJump: (index: number) => void
7
+ }
8
+
9
+ export interface SearchState {
10
+ searchQuery: string
11
+ searchActive: boolean
12
+ setSearchQuery: (query: string) => void
13
+ matchingLines: number[]
14
+ currentMatchIndex: number
15
+ submitSearch: () => void
16
+ }
17
+
18
+ const EMPTY: number[] = []
19
+ const CURSOR_MODES: Mode[] = ["cursor"]
20
+ const ALL_MODES: Mode[] = ["cursor", "select", "insert"]
21
+
22
+ export function useSearch({ match, onJump }: UseSearchOptions): SearchState {
23
+ const mode = useMode()
24
+ const setMode = useSetMode()
25
+
26
+ const [searchQuery, setSearchQuery] = useState("")
27
+ const [searchActive, setSearchActive] = useState(false)
28
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
29
+ const [committedQuery, setCommittedQuery] = useState("")
30
+ const preSearchModeRef = useRef<Mode>("cursor")
31
+
32
+ const matchRef = useRef(match)
33
+ matchRef.current = match
34
+
35
+ const onJumpRef = useRef(onJump)
36
+ onJumpRef.current = onJump
37
+
38
+ const activeQuery = searchActive ? searchQuery : committedQuery
39
+
40
+ const matchingLines = useMemo(() => {
41
+ if (!activeQuery) return EMPTY
42
+ return matchRef.current(activeQuery)
43
+ }, [activeQuery])
44
+
45
+ const matchingLinesRef = useRef(matchingLines)
46
+ matchingLinesRef.current = matchingLines
47
+
48
+ // Imperatively set search query, reset match index, and jump to first match.
49
+ const updateSearchQuery = useCallback((query: string) => {
50
+ setSearchQuery(query)
51
+ setCurrentMatchIndex(0)
52
+ const matches = query ? matchRef.current(query) : []
53
+ if (matches[0] != null) {
54
+ onJumpRef.current(matches[0])
55
+ }
56
+ }, [])
57
+
58
+ useCommand({
59
+ id: "cursor-search-start",
60
+ title: "Search",
61
+ hotkey: "/",
62
+ modes: CURSOR_MODES,
63
+ handler: () => {
64
+ preSearchModeRef.current = mode
65
+ setSearchActive(true)
66
+ setSearchQuery("")
67
+ setMode("insert")
68
+ },
69
+ })
70
+
71
+ useCommand({
72
+ id: "cursor-search-next",
73
+ title: "Next match",
74
+ hotkey: "n",
75
+ modes: CURSOR_MODES,
76
+ when: () => !searchActive,
77
+ handler: () => {
78
+ const matches = matchingLinesRef.current
79
+ if (matches.length === 0) return
80
+
81
+ setCurrentMatchIndex((index) => {
82
+ const nextIndex = (index + 1) % matches.length
83
+ const nextMatch = matches[nextIndex]
84
+ if (nextMatch != null) {
85
+ onJumpRef.current(nextMatch)
86
+ }
87
+ return nextIndex
88
+ })
89
+ },
90
+ })
91
+
92
+ useCommand({
93
+ id: "cursor-search-prev",
94
+ title: "Previous match",
95
+ hotkey: "shift+n",
96
+ modes: CURSOR_MODES,
97
+ when: () => !searchActive,
98
+ handler: () => {
99
+ const matches = matchingLinesRef.current
100
+ if (matches.length === 0) return
101
+
102
+ setCurrentMatchIndex((index) => {
103
+ const nextIndex = (index - 1 + matches.length) % matches.length
104
+ const nextMatch = matches[nextIndex]
105
+ if (nextMatch != null) {
106
+ onJumpRef.current(nextMatch)
107
+ }
108
+ return nextIndex
109
+ })
110
+ },
111
+ })
112
+
113
+ useCommand({
114
+ id: "search-cancel",
115
+ title: "Cancel search",
116
+ hotkey: "escape",
117
+ modes: ALL_MODES,
118
+ when: () => searchActive,
119
+ handler: () => {
120
+ setSearchActive(false)
121
+ setSearchQuery("")
122
+ setCommittedQuery("")
123
+ setCurrentMatchIndex(0)
124
+ setMode(preSearchModeRef.current)
125
+ },
126
+ })
127
+
128
+ const submitSearch = useCallback(() => {
129
+ setCommittedQuery(searchQuery)
130
+ setSearchActive(false)
131
+ setCurrentMatchIndex(0)
132
+ const matches = searchQuery ? matchRef.current(searchQuery) : []
133
+ if (matches[0] != null) {
134
+ onJumpRef.current(matches[0])
135
+ }
136
+ setMode(preSearchModeRef.current)
137
+ }, [searchQuery, setMode])
138
+
139
+ return {
140
+ searchQuery,
141
+ searchActive,
142
+ setSearchQuery: updateSearchQuery,
143
+ matchingLines,
144
+ currentMatchIndex,
145
+ submitSearch,
146
+ }
147
+ }
@@ -1,9 +0,0 @@
1
- import type { CommandPaletteEntry } from "@tooee/renderers";
2
- export interface CommandPaletteState {
3
- isOpen: boolean;
4
- open: () => void;
5
- close: () => void;
6
- entries: CommandPaletteEntry[];
7
- }
8
- export declare function useCommandPalette(): CommandPaletteState;
9
- //# sourceMappingURL=command-palette.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"command-palette.d.ts","sourceRoot":"","sources":["../src/command-palette.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAM3D,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,mBAAmB,EAAE,CAAA;CAC/B;AAED,wBAAgB,iBAAiB,IAAI,mBAAmB,CAqDvD"}
@@ -1,51 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from "react";
2
- import { createElement } from "react";
3
- import { useCommandContext, useCommand, useMode } from "@tooee/commands";
4
- import { useOverlay } from "@tooee/overlays";
5
- import { CommandPaletteOverlay } from "./CommandPaletteOverlay.js";
6
- const DEFAULT_MODES = ["cursor"];
7
- const OVERLAY_ID = "command-palette";
8
- export function useCommandPalette() {
9
- const { commands } = useCommandContext();
10
- const mode = useMode();
11
- const overlay = useOverlay();
12
- const [isOpen, setIsOpen] = useState(false);
13
- const launchModeRef = useRef(mode);
14
- const launchMode = launchModeRef.current;
15
- const entries = useMemo(() => {
16
- return commands
17
- .filter((cmd) => !cmd.hidden)
18
- .filter((cmd) => {
19
- const cmdModes = cmd.modes ?? DEFAULT_MODES;
20
- return cmdModes.includes(launchMode);
21
- })
22
- .map((cmd) => ({
23
- id: cmd.id,
24
- title: cmd.title,
25
- hotkey: cmd.defaultHotkey,
26
- category: cmd.category,
27
- icon: cmd.icon,
28
- }));
29
- }, [commands, launchMode]);
30
- const close = useCallback(() => {
31
- setIsOpen(false);
32
- overlay.hide(OVERLAY_ID);
33
- }, [overlay]);
34
- const open = useCallback(() => {
35
- launchModeRef.current = mode;
36
- setIsOpen(true);
37
- overlay.open(OVERLAY_ID, ({ close }) => createElement(CommandPaletteOverlay, {
38
- launchMode: mode,
39
- close: () => close(),
40
- }), null, { mode: "insert", dismissOnEscape: true, onClose: () => setIsOpen(false) });
41
- }, [overlay, mode]);
42
- useCommand({
43
- id: "command-palette",
44
- title: "Command Palette",
45
- hotkey: ":",
46
- modes: ["cursor"],
47
- handler: open,
48
- });
49
- return { isOpen, open, close, entries };
50
- }
51
- //# sourceMappingURL=command-palette.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"command-palette.js","sourceRoot":"","sources":["../src/command-palette.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AACrC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAExE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAG5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAElE,MAAM,aAAa,GAAW,CAAC,QAAQ,CAAC,CAAA;AACxC,MAAM,UAAU,GAAG,iBAAiB,CAAA;AASpC,MAAM,UAAU,iBAAiB;IAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,iBAAiB,EAAE,CAAA;IACxC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,aAAa,GAAG,MAAM,CAAO,IAAI,CAAC,CAAA;IAExC,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,CAAA;IACxC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,OAAO,QAAQ;aACZ,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;aAC5B,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;YACd,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,IAAI,aAAa,CAAA;YAC3C,OAAO,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;QACtC,CAAC,CAAC;aACD,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACb,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,aAAa;YACzB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,IAAI,EAAE,GAAG,CAAC,IAAI;SACf,CAAC,CAAC,CAAA;IACP,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAA;IAE1B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,SAAS,CAAC,KAAK,CAAC,CAAA;QAChB,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;IAEb,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5B,aAAa,CAAC,OAAO,GAAG,IAAI,CAAA;QAC5B,SAAS,CAAC,IAAI,CAAC,CAAA;QACf,OAAO,CAAC,IAAI,CACV,UAAU,EACV,CAAC,EAAE,KAAK,EAAoD,EAAE,EAAE,CAC9D,aAAa,CAAC,qBAAqB,EAAE;YACnC,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE;SACrB,CAAC,EACJ,IAAI,EACJ,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC3E,CAAA;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAA;IAEnB,UAAU,CAAC;QACT,EAAE,EAAE,iBAAiB;QACrB,KAAK,EAAE,iBAAiB;QACxB,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,CAAC,QAAQ,CAAC;QACjB,OAAO,EAAE,IAAI;KACd,CAAC,CAAA;IAEF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;AACzC,CAAC"}
package/dist/modal.d.ts DELETED
@@ -1,33 +0,0 @@
1
- import { type Mode } from "@tooee/commands";
2
- export interface Position {
3
- line: number;
4
- col: number;
5
- }
6
- export interface ModalNavigationState {
7
- mode: Mode;
8
- setMode: (mode: Mode) => void;
9
- cursor: Position | null;
10
- selection: {
11
- start: Position;
12
- end: Position;
13
- } | null;
14
- toggledIndices: Set<number>;
15
- searchQuery: string;
16
- searchActive: boolean;
17
- setSearchQuery: (query: string) => void;
18
- matchingLines: number[];
19
- currentMatchIndex: number;
20
- submitSearch: () => void;
21
- }
22
- export interface ModalNavigationOptions {
23
- totalLines: number;
24
- viewportHeight?: number;
25
- getText?: () => string | undefined;
26
- blockCount?: number;
27
- blockLineMap?: number[];
28
- /** Offset to subtract from search line numbers to get block indices (for when getText has different line structure than visual) */
29
- searchLineOffset?: number;
30
- multiSelect?: boolean;
31
- }
32
- export declare function useModalNavigationCommands(opts: ModalNavigationOptions): ModalNavigationState;
33
- //# sourceMappingURL=modal.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"AACA,OAAO,EAAmC,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAK5E,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IAC7B,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAA;IACpD,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,mIAAmI;IACnI,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,sBAAsB,GAAG,oBAAoB,CAia7F"}
package/dist/modal.js DELETED
@@ -1,395 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect, useMemo } from "react";
2
- import { useCommand, useMode, useSetMode } from "@tooee/commands";
3
- import { copyToClipboard } from "@tooee/clipboard";
4
- import { useTerminalDimensions } from "@opentui/react";
5
- import { findMatchingLines } from "./search.js";
6
- export function useModalNavigationCommands(opts) {
7
- const { height: terminalHeight } = useTerminalDimensions();
8
- const { totalLines, viewportHeight = terminalHeight - 2, getText, blockCount, blockLineMap, searchLineOffset = 0, multiSelect = false, } = opts;
9
- const mode = useMode();
10
- const setMode = useSetMode();
11
- const [cursor, setCursor] = useState(null);
12
- const [selectionAnchor, setSelectionAnchor] = useState(null);
13
- const [toggledIndices, setToggledIndices] = useState(new Set());
14
- const [searchQuery, setSearchQuery] = useState("");
15
- const [searchActive, setSearchActive] = useState(false);
16
- const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
17
- const preSearchModeRef = useRef("cursor");
18
- // Track the "committed" search query (persists after search bar closes)
19
- const [committedQuery, setCommittedQuery] = useState("");
20
- // Derived state: search matches computed from committed query + content
21
- const matchingLines = useMemo(() => {
22
- if (!committedQuery)
23
- return [];
24
- const text = getText?.();
25
- return text ? findMatchingLines(text, committedQuery) : [];
26
- }, [committedQuery]); // eslint-disable-line react-hooks/exhaustive-deps
27
- // Live-update committed query while search bar is open
28
- useEffect(() => {
29
- if (searchActive && searchQuery) {
30
- setCommittedQuery(searchQuery);
31
- }
32
- }, [searchActive, searchQuery]);
33
- const searchQueryRef = useRef(searchQuery);
34
- searchQueryRef.current = searchQuery;
35
- const matchingLinesRef = useRef(matchingLines);
36
- matchingLinesRef.current = matchingLines;
37
- // Auto-jump to first match when search results change
38
- useEffect(() => {
39
- setCurrentMatchIndex(0);
40
- if (matchingLines.length > 0) {
41
- setCursor((c) => (c ? { line: matchingLines[0], col: 0 } : c));
42
- }
43
- }, [matchingLines]);
44
- const isBlockMode = blockCount != null;
45
- const cursorMax = isBlockMode ? Math.max(0, blockCount - 1) : Math.max(0, totalLines - 1);
46
- const clampCursor = useCallback((value) => Math.max(0, Math.min(value, cursorMax)), [cursorMax]);
47
- // Clean up toggled indices when cursorMax shrinks (render-time state adjustment)
48
- const [prevCursorMax, setPrevCursorMax] = useState(cursorMax);
49
- if (cursorMax !== prevCursorMax) {
50
- setPrevCursorMax(cursorMax);
51
- setToggledIndices((prev) => {
52
- const filtered = Array.from(prev).filter((index) => index <= cursorMax);
53
- return filtered.length === prev.size ? prev : new Set(filtered);
54
- });
55
- }
56
- // When entering cursor mode, initialize cursor if not already set
57
- const prevMode = useRef(null);
58
- useEffect(() => {
59
- if (mode === "cursor" && prevMode.current !== "cursor" && prevMode.current !== "select") {
60
- setCursor((existing) => {
61
- // If cursor already exists (e.g. set by a command via palette), preserve it
62
- if (existing)
63
- return existing;
64
- // If there's an active search match, start cursor there; otherwise start at scroll position
65
- const matches = matchingLinesRef.current;
66
- if (matches.length > 0) {
67
- const matchLine = matches[currentMatchIndex] ?? matches[0];
68
- // In block mode, convert search line to block index using offset
69
- if (isBlockMode) {
70
- const blockIndex = Math.max(0, matchLine - searchLineOffset);
71
- return { line: Math.min(blockIndex, cursorMax), col: 0 };
72
- }
73
- return { line: matchLine, col: 0 };
74
- }
75
- // No search match - start at top
76
- return { line: 0, col: 0 };
77
- });
78
- }
79
- if (mode === "select" && prevMode.current === "cursor" && cursor) {
80
- setSelectionAnchor({ ...cursor });
81
- }
82
- prevMode.current = mode;
83
- }, [mode]); // eslint-disable-line react-hooks/exhaustive-deps
84
- // === CURSOR MODE ===
85
- useCommand({
86
- id: "cursor-down",
87
- title: "Cursor down",
88
- hotkey: "j",
89
- modes: ["cursor"],
90
- handler: () => {
91
- setCursor((c) => {
92
- if (!c)
93
- return c;
94
- return { line: clampCursor(c.line + 1), col: 0 };
95
- });
96
- },
97
- });
98
- useCommand({
99
- id: "cursor-toggle",
100
- title: "Toggle selection",
101
- hotkey: "tab",
102
- modes: ["cursor"],
103
- when: () => multiSelect,
104
- handler: () => {
105
- setToggledIndices((prev) => {
106
- if (!cursor)
107
- return prev;
108
- const next = new Set(prev);
109
- const idx = cursor.line;
110
- if (next.has(idx)) {
111
- next.delete(idx);
112
- }
113
- else {
114
- next.add(idx);
115
- }
116
- return next;
117
- });
118
- },
119
- });
120
- useCommand({
121
- id: "cursor-toggle-up",
122
- title: "Toggle and move up",
123
- hotkey: "shift+tab",
124
- modes: ["cursor"],
125
- when: () => multiSelect,
126
- handler: () => {
127
- setToggledIndices((prev) => {
128
- if (!cursor)
129
- return prev;
130
- const next = new Set(prev);
131
- const idx = cursor.line;
132
- if (next.has(idx)) {
133
- next.delete(idx);
134
- }
135
- else {
136
- next.add(idx);
137
- }
138
- return next;
139
- });
140
- setCursor((c) => {
141
- if (!c)
142
- return c;
143
- return { line: clampCursor(c.line - 1), col: 0 };
144
- });
145
- },
146
- });
147
- useCommand({
148
- id: "cursor-up",
149
- title: "Cursor up",
150
- hotkey: "k",
151
- modes: ["cursor"],
152
- handler: () => {
153
- setCursor((c) => {
154
- if (!c)
155
- return c;
156
- return { line: clampCursor(c.line - 1), col: 0 };
157
- });
158
- },
159
- });
160
- useCommand({
161
- id: "cursor-half-down",
162
- title: "Cursor half page down",
163
- hotkey: "ctrl+d",
164
- modes: ["cursor"],
165
- handler: () => {
166
- setCursor((c) => {
167
- if (!c)
168
- return c;
169
- const step = isBlockMode ? Math.floor(cursorMax / 4) || 1 : Math.floor(viewportHeight / 2);
170
- return { line: clampCursor(c.line + step), col: 0 };
171
- });
172
- },
173
- });
174
- useCommand({
175
- id: "cursor-half-up",
176
- title: "Cursor half page up",
177
- hotkey: "ctrl+u",
178
- modes: ["cursor"],
179
- handler: () => {
180
- setCursor((c) => {
181
- if (!c)
182
- return c;
183
- const step = isBlockMode ? Math.floor(cursorMax / 4) || 1 : Math.floor(viewportHeight / 2);
184
- return { line: clampCursor(c.line - step), col: 0 };
185
- });
186
- },
187
- });
188
- useCommand({
189
- id: "cursor-top",
190
- title: "Cursor to top",
191
- hotkey: "g g",
192
- modes: ["cursor"],
193
- handler: () => {
194
- setCursor({ line: 0, col: 0 });
195
- },
196
- });
197
- useCommand({
198
- id: "cursor-bottom",
199
- title: "Cursor to bottom",
200
- hotkey: "shift+g",
201
- modes: ["cursor"],
202
- handler: () => {
203
- setCursor({ line: cursorMax, col: 0 });
204
- },
205
- });
206
- useCommand({
207
- id: "cursor-search-start",
208
- title: "Search",
209
- hotkey: "/",
210
- modes: ["cursor"],
211
- handler: () => {
212
- preSearchModeRef.current = mode;
213
- setSearchActive(true);
214
- setSearchQuery("");
215
- setMode("insert");
216
- },
217
- });
218
- useCommand({
219
- id: "cursor-search-next",
220
- title: "Next match",
221
- hotkey: "n",
222
- modes: ["cursor"],
223
- when: () => !searchActive,
224
- handler: () => {
225
- const matches = matchingLinesRef.current;
226
- if (matches.length === 0)
227
- return;
228
- setCurrentMatchIndex((idx) => {
229
- const next = (idx + 1) % matches.length;
230
- const line = matches[next];
231
- setCursor({ line, col: 0 });
232
- return next;
233
- });
234
- },
235
- });
236
- useCommand({
237
- id: "cursor-search-prev",
238
- title: "Previous match",
239
- hotkey: "shift+n",
240
- modes: ["cursor"],
241
- when: () => !searchActive,
242
- handler: () => {
243
- const matches = matchingLinesRef.current;
244
- if (matches.length === 0)
245
- return;
246
- setCurrentMatchIndex((idx) => {
247
- const next = (idx - 1 + matches.length) % matches.length;
248
- const line = matches[next];
249
- setCursor({ line, col: 0 });
250
- return next;
251
- });
252
- },
253
- });
254
- // === SELECT MODE ===
255
- useCommand({
256
- id: "select-down",
257
- title: "Extend selection down",
258
- hotkey: "j",
259
- modes: ["select"],
260
- handler: () => {
261
- setCursor((c) => {
262
- if (!c)
263
- return c;
264
- return { line: clampCursor(c.line + 1), col: 0 };
265
- });
266
- },
267
- });
268
- useCommand({
269
- id: "select-up",
270
- title: "Extend selection up",
271
- hotkey: "k",
272
- modes: ["select"],
273
- handler: () => {
274
- setCursor((c) => {
275
- if (!c)
276
- return c;
277
- return { line: clampCursor(c.line - 1), col: 0 };
278
- });
279
- },
280
- });
281
- useCommand({
282
- id: "select-toggle",
283
- title: "Toggle selection",
284
- hotkey: "tab",
285
- modes: ["select"],
286
- when: () => multiSelect,
287
- handler: () => {
288
- setToggledIndices((prev) => {
289
- if (!cursor)
290
- return prev;
291
- const next = new Set(prev);
292
- const idx = cursor.line;
293
- if (next.has(idx)) {
294
- next.delete(idx);
295
- }
296
- else {
297
- next.add(idx);
298
- }
299
- return next;
300
- });
301
- },
302
- });
303
- useCommand({
304
- id: "select-copy",
305
- title: "Copy selection",
306
- hotkey: "y",
307
- modes: ["select"],
308
- handler: () => {
309
- if (!getText || !selectionAnchor || !cursor)
310
- return;
311
- const text = getText();
312
- if (!text)
313
- return;
314
- if (isBlockMode && blockLineMap) {
315
- // Block-based copy: use blockLineMap to find line ranges
316
- const startBlock = Math.min(selectionAnchor.line, cursor.line);
317
- const endBlock = Math.max(selectionAnchor.line, cursor.line);
318
- const startLine = blockLineMap[startBlock] ?? 0;
319
- const endLine = endBlock + 1 < blockLineMap.length
320
- ? (blockLineMap[endBlock + 1] ?? text.split("\n").length)
321
- : text.split("\n").length;
322
- const lines = text.split("\n");
323
- const selected = lines.slice(startLine, endLine).join("\n");
324
- if (selected) {
325
- void copyToClipboard(selected);
326
- }
327
- }
328
- else {
329
- const lines = text.split("\n");
330
- const startLine = Math.min(selectionAnchor.line, cursor.line);
331
- const endLine = Math.max(selectionAnchor.line, cursor.line);
332
- const selected = lines.slice(startLine, endLine + 1).join("\n");
333
- if (selected) {
334
- void copyToClipboard(selected);
335
- }
336
- }
337
- setMode("cursor");
338
- },
339
- });
340
- useCommand({
341
- id: "select-cancel",
342
- title: "Cancel selection",
343
- hotkey: "escape",
344
- modes: ["select"],
345
- handler: () => setMode("cursor"),
346
- });
347
- // === MODE TRANSITIONS ===
348
- useCommand({
349
- id: "enter-select",
350
- title: "Enter select mode",
351
- hotkey: "v",
352
- modes: ["cursor"],
353
- handler: () => setMode("select"),
354
- });
355
- // === SEARCH CANCEL (any mode) ===
356
- useCommand({
357
- id: "search-cancel",
358
- title: "Cancel search",
359
- hotkey: "escape",
360
- modes: ["cursor", "select", "insert"],
361
- when: () => searchActive,
362
- handler: () => {
363
- setSearchActive(false);
364
- setSearchQuery("");
365
- setCommittedQuery("");
366
- setCurrentMatchIndex(0);
367
- setMode(preSearchModeRef.current);
368
- },
369
- });
370
- // Compute selection from anchor + cursor
371
- const selection = selectionAnchor && cursor && mode === "select"
372
- ? {
373
- start: selectionAnchor.line <= cursor.line ? selectionAnchor : cursor,
374
- end: selectionAnchor.line <= cursor.line ? cursor : selectionAnchor,
375
- }
376
- : null;
377
- const submitSearch = useCallback(() => {
378
- setSearchActive(false);
379
- setMode(preSearchModeRef.current);
380
- }, [setMode]);
381
- return {
382
- mode,
383
- setMode,
384
- cursor,
385
- selection,
386
- toggledIndices,
387
- searchQuery,
388
- searchActive,
389
- setSearchQuery,
390
- matchingLines,
391
- currentMatchIndex,
392
- submitSearch,
393
- };
394
- }
395
- //# sourceMappingURL=modal.js.map