Compare commits
1 Commits
main
...
searchpane
| Author | SHA1 | Date |
|---|---|---|
|
|
637801b0d1 |
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "miller",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "miller",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@lydell/node-pty": "^1.1.0",
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vscode/ripgrep": "^1.18.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
|
|
@ -3860,6 +3861,195 @@
|
|||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/ripgrep": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.18.0.tgz",
|
||||
"integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@vscode/ripgrep-darwin-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-darwin-x64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-arm": "1.18.0",
|
||||
"@vscode/ripgrep-linux-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-ia32": "1.18.0",
|
||||
"@vscode/ripgrep-linux-ppc64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-riscv64": "1.18.0",
|
||||
"@vscode/ripgrep-linux-s390x": "1.18.0",
|
||||
"@vscode/ripgrep-linux-x64": "1.18.0",
|
||||
"@vscode/ripgrep-win32-arm64": "1.18.0",
|
||||
"@vscode/ripgrep-win32-ia32": "1.18.0",
|
||||
"@vscode/ripgrep-win32-x64": "1.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-darwin-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-arm64/-/ripgrep-darwin-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-darwin-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-x64/-/ripgrep-darwin-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-arm": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm/-/ripgrep-linux-arm-1.18.0.tgz",
|
||||
"integrity": "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm64/-/ripgrep-linux-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-ia32": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ia32/-/ripgrep-linux-ia32-1.18.0.tgz",
|
||||
"integrity": "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-ppc64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ppc64/-/ripgrep-linux-ppc64-1.18.0.tgz",
|
||||
"integrity": "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-riscv64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-riscv64/-/ripgrep-linux-riscv64-1.18.0.tgz",
|
||||
"integrity": "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-s390x": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-s390x/-/ripgrep-linux-s390x-1.18.0.tgz",
|
||||
"integrity": "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-linux-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-x64/-/ripgrep-linux-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-arm64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-arm64/-/ripgrep-win32-arm64-1.18.0.tgz",
|
||||
"integrity": "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-ia32": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-ia32/-/ripgrep-win32-ia32-1.18.0.tgz",
|
||||
"integrity": "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/ripgrep-win32-x64": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-x64/-/ripgrep-win32-x64-1.18.0.tgz",
|
||||
"integrity": "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vscode/sudo-prompt": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vscode/ripgrep": "^1.18.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
|
|
|
|||
|
|
@ -209,6 +209,14 @@ export class Editor extends Displayable {
|
|||
return this.file.filePath.val + (this.file.isDirty() ? "*" : "");
|
||||
}
|
||||
|
||||
jumpTo(line: number) {
|
||||
const lineDoc = this.view.state.doc.line(line);
|
||||
this.view.dispatch({
|
||||
selection: { anchor: lineDoc.from, head: lineDoc.from },
|
||||
});
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.deleteFn) {
|
||||
this.file.removeEditor(this, this.deleteFn);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Terminal } from "./terminal";
|
|||
import { Displayable } from "./displayable";
|
||||
import { QuickOpen } from "./quickopen";
|
||||
import { toggleSidebar } from "./renderer";
|
||||
import { SearchPane } from "./searchPane";
|
||||
|
||||
const EditorWrapper = (
|
||||
editor: State<Displayable>,
|
||||
|
|
@ -99,6 +100,12 @@ export function addTerminal() {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
export function addSearchPane() {
|
||||
const pane = new SearchPane();
|
||||
editors[currentTab.val].push(vanX.noreactive(pane));
|
||||
pane.focus();
|
||||
}
|
||||
|
||||
const TabHeader = (tab: State<Editor[]>, del: () => void, k: number) =>
|
||||
v.div(
|
||||
{
|
||||
|
|
@ -146,6 +153,11 @@ function shortcutHandler(e: KeyboardEvent) {
|
|||
if (e.type === "keydown") {
|
||||
toggleSidebar();
|
||||
}
|
||||
} else if (e.key === "f" && e.altKey) {
|
||||
if (e.type === "keydown") {
|
||||
addSearchPane();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import van, { State } from "vanjs-core";
|
||||
const v = van.tags;
|
||||
|
||||
import { Displayable } from "./displayable";
|
||||
import { addEditor } from "./editorgrid";
|
||||
import { OpenFile } from "./filestate";
|
||||
import * as u from "./utils";
|
||||
|
||||
interface SearchMatch {
|
||||
path: string;
|
||||
line: number;
|
||||
column: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class SearchPane extends Displayable {
|
||||
query = van.state("");
|
||||
results = van.state<SearchMatch[]>([]);
|
||||
isSearching = van.state(false);
|
||||
private unsubscribeResult?: () => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async startSearch() {
|
||||
const q = this.query.val;
|
||||
if (!q) return;
|
||||
|
||||
if (this.unsubscribeResult) {
|
||||
this.unsubscribeResult();
|
||||
this.unsubscribeResult = undefined;
|
||||
}
|
||||
|
||||
this.isSearching.val = true;
|
||||
this.results.val = [];
|
||||
|
||||
const args = ["--json", q, "."];
|
||||
|
||||
try {
|
||||
await window.electronAPI.startSearch(args);
|
||||
|
||||
this.unsubscribeResult = window.electronAPI.onSearchResult((result: any) => {
|
||||
console.log("Message from ripgrep: ", result);
|
||||
if (result.type === "match") {
|
||||
const match: SearchMatch = {
|
||||
path: result.data.path.text,
|
||||
line: result.data.line_number,
|
||||
column: result.data.column_number,
|
||||
text: result.data.lines.text,
|
||||
};
|
||||
this.results.val = [...this.results.val, match];
|
||||
}
|
||||
|
||||
if (result.type === "summary") {
|
||||
this.isSearching.val = false;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Search failed", err);
|
||||
this.isSearching.val = false;
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
const input = document.getElementById("search-input") as HTMLInputElement;
|
||||
input?.focus();
|
||||
}
|
||||
|
||||
close(): boolean {
|
||||
// TODO: stop ripgrep
|
||||
if (this.unsubscribeResult) {
|
||||
this.unsubscribeResult();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
title(): string {
|
||||
return "Search";
|
||||
}
|
||||
|
||||
get dom() {
|
||||
return v.div(
|
||||
{ class: "flex flex-col h-full" },
|
||||
v.div(
|
||||
{ class: "p-2 border-b dark:border-gray-700 flex gap-2" },
|
||||
v.input(
|
||||
{
|
||||
id: "search-input",
|
||||
class: "flex-1 p-1 border rounded dark:bg-gray-800 dark:text-white outline-none",
|
||||
placeholder: "Search...",
|
||||
value: this.query,
|
||||
onkeydown: (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
this.startSearch();
|
||||
}
|
||||
},
|
||||
oninput: (e: any) => (this.query.val = e.target.value),
|
||||
},
|
||||
),
|
||||
u.Button(() => this.startSearch(), "🔍"),
|
||||
),
|
||||
v.div(
|
||||
{ class: "flex-1 overflow-y-auto p-2" },
|
||||
() => this.isSearching.val
|
||||
? v.p({ class: "text-center text-gray-500" }, "Searching...")
|
||||
: v.div(
|
||||
this.results.val.map((match) =>
|
||||
v.div(
|
||||
{
|
||||
class: "p-2 mb-2 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900 rounded border border-transparent hover:border-blue-300",
|
||||
onclick: async () => {
|
||||
// TODO: use absolute path!
|
||||
const file = await OpenFile.openFile(match.path);
|
||||
if (file) {
|
||||
const editor = addEditor(file);
|
||||
editor.jumpTo(match.line);
|
||||
}
|
||||
},
|
||||
},
|
||||
v.div({ class: "text-xs text-gray-500 dark:text-gray-400" }, match.path),
|
||||
v.div({ class: "text-sm font-mono" }, `${match.line}:${match.column} ${match.text}`),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import path from "node:path";
|
|||
import started from "electron-squirrel-startup";
|
||||
import { setupLangServer } from "./langserver";
|
||||
import { setMenu } from "./menu";
|
||||
import { searchService } from "./searchService";
|
||||
/// <reference types="./forge-vite-env.d.ts" />
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
|
|
@ -139,6 +140,19 @@ app.whenReady().then(() => {
|
|||
return terminalManager.closeTerminal(id);
|
||||
});
|
||||
|
||||
ipcMain.handle("search:start", async (event, args: string[]) => {
|
||||
const senderWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!senderWindow) return;
|
||||
|
||||
await searchService.startSearch(
|
||||
senderWindow,
|
||||
args,
|
||||
(result) => {
|
||||
senderWindow.webContents.send("search:result", result);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
createWindow();
|
||||
if (process.platform === "darwin") {
|
||||
app.on("activate", function () {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { ChildProcess, spawn } from "child_process";
|
||||
import { rgPath } from "@vscode/ripgrep";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { getCurrentWorkspaceRoot } from "./fileOperations";
|
||||
|
||||
export interface SearchResult {
|
||||
type: "match" | "error" | "line" | "path" | "context";
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class SearchService {
|
||||
private currentProcess: ChildProcess = null;
|
||||
|
||||
async startSearch(
|
||||
mainWindow: BrowserWindow,
|
||||
args: string[],
|
||||
onResult: (result: any) => void,
|
||||
) {
|
||||
this.stopSearch();
|
||||
|
||||
const cwd = getCurrentWorkspaceRoot();
|
||||
console.log("Spawning ripgrep: ", rgPath, cwd, args);
|
||||
|
||||
this.currentProcess = spawn(rgPath, args, {cwd});
|
||||
|
||||
this.currentProcess.stdin.end();
|
||||
|
||||
this.currentProcess.stdout.on("data", (data: Buffer) => {
|
||||
console.log("ripgrep data: ", data.toString());
|
||||
const lines = data.toString().split("\n");
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
onResult(json);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ripgrep JSON line:", line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.currentProcess.stderr.on("data", (data: Buffer) => {
|
||||
const errorLine = data.toString().trim();
|
||||
if (errorLine) {
|
||||
console.error("ripgrep stderr:", errorLine);
|
||||
}
|
||||
});
|
||||
|
||||
this.currentProcess.on("close", (code: number) => {
|
||||
console.log(`ripgrep process exited with code ${code}`);
|
||||
this.currentProcess = null;
|
||||
});
|
||||
|
||||
this.currentProcess.on("error", (err: any) => {
|
||||
console.log("Ripgrep failed to start: ", err);
|
||||
});
|
||||
|
||||
this.currentProcess.on("exit", (code: number) => {
|
||||
console.log("Ripgrep exited: ", code);
|
||||
})
|
||||
}
|
||||
|
||||
stopSearch() {
|
||||
if (this.currentProcess) {
|
||||
this.currentProcess.kill();
|
||||
this.currentProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const searchService = new SearchService();
|
||||
|
|
@ -85,6 +85,18 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||
closeTerminal: (id: string) =>
|
||||
ipcRenderer.invoke("terminal:close", id) as Promise<boolean>,
|
||||
|
||||
// Search operations
|
||||
startSearch: (args: string[]) =>
|
||||
ipcRenderer.invoke("search:start", args) as Promise<void>,
|
||||
|
||||
onSearchResult: (callback: (result: any) => void) => {
|
||||
const listener = (_ev: any, result: any) => {
|
||||
callback(result);
|
||||
};
|
||||
ipcRenderer.on("search:result", listener);
|
||||
return () => ipcRenderer.removeListener("search:result", listener);
|
||||
},
|
||||
|
||||
// Terminal events (subscribe/unsubscribe)
|
||||
onTerminalData: (id: string, callback: (data: string) => void) => {
|
||||
terminalDataCallbacks.set(id, callback);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ declare global {
|
|||
language?: string;
|
||||
root?: string;
|
||||
}) => Promise<any>;
|
||||
|
||||
startSearch: (args: string[]) => Promise<any>;
|
||||
onSearchResult: (callback: any) => any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue