From daf21c64e476b0cda237de67b5bd36bbc4f8ad18 Mon Sep 17 00:00:00 2001 From: Quinten Kock Date: Sun, 3 May 2026 04:32:58 +0200 Subject: [PATCH] Add quick-open panel --- src/app/editorgrid.ts | 6 +++ src/app/foldernav.ts | 18 ++++++- src/app/quickopen.ts | 119 ++++++++++++++++++++++++++++++++++++++++++ src/app/renderer.ts | 2 + 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/app/quickopen.ts diff --git a/src/app/editorgrid.ts b/src/app/editorgrid.ts index 09342fa..bd1775f 100644 --- a/src/app/editorgrid.ts +++ b/src/app/editorgrid.ts @@ -7,6 +7,7 @@ import * as u from "./utils"; import { Editor } from "./editor"; import { Terminal } from "./terminal"; import { Displayable } from "./displayable"; +import { QuickOpen } from "./quickopen"; const EditorWrapper = ( editor: State, @@ -135,6 +136,11 @@ function shortcutHandler(e: KeyboardEvent) { addTerminal(); } e.preventDefault(); + } else if (e.key === "p" && e.ctrlKey) { + if (e.type === "keydown") { + QuickOpen.open(); + } + e.preventDefault(); } } diff --git a/src/app/foldernav.ts b/src/app/foldernav.ts index 5c81ac9..ff7d908 100644 --- a/src/app/foldernav.ts +++ b/src/app/foldernav.ts @@ -57,7 +57,7 @@ window.electronAPI.onFsEvent(async (ev: { event: string; path: string }) => { // Read latest contents from disk const data = await window.electronAPI .readFile(ev.path) - .catch(() => null); + .catch((): any => null); if (!data) return; if (ev.event === "unlink") { openFile.knownDiskContent.val = null; @@ -125,3 +125,19 @@ const FsItemView = (tree: FolderTree): HTMLElement => { return folder; }; + +function allFilesFromTree(t: FolderTree): string[] { + if (t.type == "directory") { + return t.children.flatMap(allFilesFromTree); + } else if (t.type == "file") { + return [t.path]; + } +} + +export const allFiles = van.derive(() => { + if (folderTreeState.val) { + return allFilesFromTree(folderTreeState.val); + } else { + return []; + } +}) \ No newline at end of file diff --git a/src/app/quickopen.ts b/src/app/quickopen.ts new file mode 100644 index 0000000..e29493e --- /dev/null +++ b/src/app/quickopen.ts @@ -0,0 +1,119 @@ +import van, { State } from "vanjs-core"; +import { OpenFile } from "./filestate"; +import { addEditor } from "./editorgrid"; +import { allFiles } from "./foldernav"; + +const v = van.tags; + +export const QuickOpen = (() => { + const isOpen = van.state(false); + const query = van.state(""); + const files = van.state([]); + const filteredFiles = van.derive(() => { + if (!query.val) return files.val.slice(0, 100); + return files.val.filter(f => f.toLowerCase().includes(query.val.toLowerCase())).slice(0, 100); + }); + const selectedIndex = van.state(0); + + // Update selectedIndex when filteredFiles changes + van.derive(() => { + filteredFiles.val; // track dependency + selectedIndex.val = 0; + }); + + const close = () => { + isOpen.val = false; + query.val = ""; + selectedIndex.val = 0; + }; + + const openSelectedFile = async () => { + const path = filteredFiles.val[selectedIndex.val]; + if (path) { + const file = await OpenFile.openFile(path); + if (file) { + addEditor(file); + } + } + close(); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (!isOpen.val) return; + + if (e.key === "Escape") { + close(); + e.preventDefault(); + } else if (e.key === "Enter") { + openSelectedFile(); + e.preventDefault(); + } else if (e.key === "ArrowDown") { + selectedIndex.val = (selectedIndex.val + 1) % filteredFiles.val.length; + e.preventDefault(); + } else if (e.key === "ArrowUp") { + selectedIndex.val = (selectedIndex.val - 1 + filteredFiles.val.length) % filteredFiles.val.length; + e.preventDefault(); + } + }; + + const open = async () => { + query.val = ""; + files.val = allFiles.val; + console.log("setting allfiles to ", allFiles.val); + + isOpen.val = true; + // Focus the input after the DOM is updated + setTimeout(() => { + const input = document.getElementById("quick-open-input") as HTMLInputElement; + input?.focus(); + }, 0); + }; + + document.addEventListener("keydown", handleKeyDown, { capture: true }); + + const modal = v.div( + { + class: () => isOpen.val ? "fixed inset-0 z-50 flex items-start justify-center pt-20 bg-black bg-opacity-50" : "hidden", + onclick: (e: MouseEvent) => { + if ((e.target as HTMLElement).id === "quick-open-overlay") { + close(); + } + } + }, + v.div( + { + id: "quick-open-overlay", + class: "bg-white dark:bg-gray-800 w-1/2 max-w-2xl rounded shadow-lg overflow-hidden flex flex-col", + }, + v.input( + { + id: "quick-open-input", + class: "w-full p-4 text-lg border-b dark:border-gray-700 dark:bg-gray-800 dark:text-white outline-none", + placeholder: "Quick Open...", + value: query, + oninput: (e: any) => (query.val = e.target.value), + }, + ), + v.div( + { class: "max-h-96 overflow-y-auto" }, + () => v.ul( + filteredFiles.val.map((path, i) => v.div( + { + class: () => `p-2 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900 ${selectedIndex.val === i ? "bg-blue-200 dark:bg-blue-800" : ""}`, + onclick: () => { + selectedIndex.val = filteredFiles.val.indexOf(path); + openSelectedFile(); + }, + }, + path + ) + )) + ) + ) + ); + + return { + dom: modal, + open, + }; +})(); diff --git a/src/app/renderer.ts b/src/app/renderer.ts index 6d18aa8..8953b54 100644 --- a/src/app/renderer.ts +++ b/src/app/renderer.ts @@ -6,6 +6,7 @@ const v = van.tags; import { FolderTreeView } from "./foldernav"; import { EditorTabs, addTab, addEditor } from "./editorgrid"; +import { QuickOpen } from "./quickopen"; import * as u from "./utils"; import { OpenFile } from "./filestate"; @@ -25,6 +26,7 @@ const app = v.div( FolderTreeView, ), EditorTabs, + QuickOpen.dom, ); van.add(document.body, app);