v0.10.0 0f4e9eb

stores.svelte.ts RawHistoryFolder

1/**
2 * Omnibox State Management
3 *
4 * Svelte 5 rune-based state management for omnibox functionality
5 */
6
7import type {
8  OmniboxMode,
9  Suggestion,
10  FindMatch,
11  HighlightNode,
12  OmniboxConfig,
13} from "./types";
14
15// Default configuration
16const DEFAULT_CONFIG: Required<OmniboxConfig> = {
17  maxMatches: 2000,
18  debounceDelay: 120,
19  defaultLimit: 10,
20};
21
22// Global state using Svelte 5 runes
23let visible = $state(false);
24let mode = $state<OmniboxMode>("omnibox");
25let suggestions = $state<Suggestion[]>([]);
26let matches = $state<FindMatch[]>([]);
27let selectedIndex = $state(-1);
28let activeIndex = $state(-1);
29let faded = $state(false);
30let inputValue = $state("");
31let highlightNodes = $state<HighlightNode[]>([]);
32let prevOverflow = $state("");
33let debounceTimer = $state(0);
34
35// Configuration
36let config = $state<Required<OmniboxConfig>>(DEFAULT_CONFIG);
37
38export const omniboxStore = {
39  // State getters
40  get visible() {
41    return visible;
42  },
43  get mode() {
44    return mode;
45  },
46  get suggestions() {
47    return suggestions;
48  },
49  get matches() {
50    return matches;
51  },
52  get selectedIndex() {
53    return selectedIndex;
54  },
55  get activeIndex() {
56    return activeIndex;
57  },
58  get faded() {
59    return faded;
60  },
61  get inputValue() {
62    return inputValue;
63  },
64  get highlightNodes() {
65    return highlightNodes;
66  },
67  get prevOverflow() {
68    return prevOverflow;
69  },
70  get config() {
71    return config;
72  },
73
74  // Computed getters
75  get hasContent() {
76    return (
77      (mode === "omnibox" && suggestions.length > 0) ||
78      (mode === "find" && matches.length > 0)
79    );
80  },
81  get totalItems() {
82    return mode === "omnibox" ? suggestions.length : matches.length;
83  },
84  get selectedItem() {
85    if (selectedIndex < 0) return null;
86    return mode === "omnibox"
87      ? suggestions[selectedIndex]
88      : matches[selectedIndex];
89  },
90
91  // Actions
92  setVisible(value: boolean) {
93    if (value && !visible) {
94      // Store scroll state when opening
95      prevOverflow = document.documentElement.style.overflow;
96      document.documentElement.style.overflow = "hidden";
97    } else if (!value && visible) {
98      // Restore scroll state when closing
99      document.documentElement.style.overflow = prevOverflow || "";
100      prevOverflow = "";
101
102      // Clear find highlights when closing
103      if (mode === "find") {
104        this.clearHighlights();
105      }
106    }
107    visible = value;
108  },
109
110  setMode(newMode: OmniboxMode) {
111    // Clear previous mode's state
112    if (newMode === "omnibox") {
113      // Switching to omnibox mode - clear find state
114      this.clearHighlights();
115    } else if (newMode === "find") {
116      // Switching to find mode - clear suggestions
117      this.updateSuggestions([]);
118    }
119
120    mode = newMode;
121    selectedIndex = -1;
122    faded = false;
123    inputValue = ""; // Clear input for fresh start
124  },
125
126  setInputValue(value: string) {
127    inputValue = value;
128  },
129
130  setFaded(value: boolean) {
131    faded = value;
132  },
133
134  setSelectedIndex(index: number) {
135    const maxIndex = this.totalItems - 1;
136    if (maxIndex < 0) {
137      selectedIndex = -1;
138    } else {
139      selectedIndex = Math.max(0, Math.min(index, maxIndex));
140    }
141  },
142
143  selectNext() {
144    if (this.totalItems === 0) return;
145    const newIndex =
146      selectedIndex < 0 ? 0 : (selectedIndex + 1) % this.totalItems;
147    this.setSelectedIndex(newIndex);
148    faded = true;
149  },
150
151  selectPrevious() {
152    if (this.totalItems === 0) return;
153    const newIndex =
154      selectedIndex <= 0 ? this.totalItems - 1 : selectedIndex - 1;
155    this.setSelectedIndex(newIndex);
156    faded = true;
157  },
158
159  open(newMode: OmniboxMode = "omnibox", initialQuery?: string) {
160    this.setMode(newMode);
161    this.setVisible(true);
162    if (typeof initialQuery === "string") {
163      inputValue = initialQuery;
164    }
165  },
166
167  close() {
168    this.setVisible(false);
169    faded = false;
170  },
171
172  toggle() {
173    if (!visible) {
174      // When opening via toggle (Ctrl+L), always open in omnibox mode
175      this.setMode("omnibox");
176      this.setVisible(true);
177    } else {
178      this.setVisible(false);
179    }
180  },
181
182  updateSuggestions(newSuggestions: Suggestion[]) {
183    suggestions = Array.isArray(newSuggestions) ? newSuggestions : [];
184    selectedIndex = -1;
185  },
186
187  updateMatches(newMatches: FindMatch[]) {
188    matches = Array.isArray(newMatches) ? newMatches : [];
189    selectedIndex = newMatches.length > 0 ? 0 : -1;
190  },
191
192  clearHighlights() {
193    try {
194      highlightNodes.forEach(({ span, text }) => {
195        const parent = span.parentNode;
196        if (parent) {
197          parent.replaceChild(text, span);
198          parent.normalize();
199        }
200      });
201    } catch (error) {
202      console.warn("Error clearing highlights:", error);
203    }
204    highlightNodes = [];
205    matches = [];
206    selectedIndex = -1;
207    activeIndex = -1;
208  },
209
210  addHighlightNode(node: HighlightNode) {
211    highlightNodes = [...highlightNodes, node];
212  },
213
214  setActiveIndex(index: number) {
215    activeIndex = index;
216  },
217
218  setDebounceTimer(timerId: number) {
219    if (debounceTimer) {
220      clearTimeout(debounceTimer);
221    }
222    debounceTimer = timerId;
223  },
224
225  clearDebounceTimer() {
226    if (debounceTimer) {
227      clearTimeout(debounceTimer);
228      debounceTimer = 0;
229    }
230  },
231
232  updateConfig(newConfig: Partial<OmniboxConfig>) {
233    config = { ...config, ...newConfig };
234  },
235
236  reset() {
237    visible = false;
238    mode = "omnibox";
239    suggestions = [];
240    matches = [];
241    selectedIndex = -1;
242    activeIndex = -1;
243    faded = false;
244    inputValue = "";
245    prevOverflow = "";
246    this.clearHighlights();
247    this.clearDebounceTimer();
248  },
249};

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close