mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
revert(app): undo file Find merge pending maintainer review
Reverts 69510ac72f. The feature must return to an open pull request for maintainer review.
This commit is contained in:
@@ -1,293 +0,0 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { openFileExplorer, openFileFromExplorer } from "../support/helpers/file-explorer";
|
||||
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
|
||||
function source(page: Page) {
|
||||
return page.getByTestId("file-source-editor").filter({ visible: true }).locator(".cm-content");
|
||||
}
|
||||
function query(page: Page) {
|
||||
return page.getByRole("textbox", { name: "Find in pane", exact: true });
|
||||
}
|
||||
function status(page: Page) {
|
||||
return page.getByRole("status", { name: "Find matches" });
|
||||
}
|
||||
async function openSource(page: Page, filename: string) {
|
||||
await openFileExplorer(page);
|
||||
await openFileFromExplorer(page, filename);
|
||||
await expect(source(page)).toBeVisible();
|
||||
}
|
||||
async function findInSource(page: Page, text: string) {
|
||||
await source(page).focus();
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await query(page).fill(text);
|
||||
}
|
||||
async function expectQuerySelected(page: Page, text: string) {
|
||||
await expect(query(page)).toBeFocused();
|
||||
await expect(query(page)).toHaveValue(text);
|
||||
await expect
|
||||
.poll(() =>
|
||||
query(page).evaluate((input: HTMLInputElement) => [input.selectionStart, input.selectionEnd]),
|
||||
)
|
||||
.toEqual([0, text.length]);
|
||||
}
|
||||
async function closeFind(page: Page) {
|
||||
await query(page).press("Escape");
|
||||
await expect(source(page)).toBeFocused();
|
||||
}
|
||||
async function expectFindAtTopRight(page: Page) {
|
||||
const pane = await page
|
||||
.getByTestId("workspace-file-pane")
|
||||
.filter({ visible: true })
|
||||
.boundingBox();
|
||||
const widget = await page
|
||||
.getByLabel("Find", { exact: true })
|
||||
.filter({ visible: true })
|
||||
.boundingBox();
|
||||
expect(widget!.x + widget!.width).toBeLessThanOrEqual(pane!.x + pane!.width);
|
||||
expect(widget!.x).toBeGreaterThanOrEqual(pane!.x);
|
||||
expect(widget!.y).toBeLessThan(pane!.y + 70);
|
||||
}
|
||||
|
||||
test("finds literal text at the top of editable source and returns focus at the inspected match", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-" });
|
||||
const file = path.join(workspace.repoPath, "source.txt");
|
||||
await writeFile(file, "first a.b\nsecond axb\nthird a.b\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "source.txt");
|
||||
await findInSource(page, "a.b");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await query(page).fill("A.B");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await query(page).fill("a.b");
|
||||
await expectFindAtTopRight(page);
|
||||
await query(page).press("Enter");
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await query(page).press("Shift+Enter");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await page.screenshot({ path: testInfo.outputPath("editable-find.png") });
|
||||
await closeFind(page);
|
||||
await expect(query(page)).toBeHidden();
|
||||
await expect(page.getByLabel("Line 1, column 10")).toBeVisible();
|
||||
|
||||
await test.step("reopen from touch action, preserve replace and Undo/save", async () => {
|
||||
await page.getByRole("button", { name: "Find", exact: true }).click();
|
||||
await expect(query(page)).toBeFocused();
|
||||
await page.getByRole("button", { name: "Toggle replace" }).click();
|
||||
await page.getByRole("textbox", { name: "Replace with" }).fill("literal");
|
||||
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
||||
await expect(source(page)).toContainText("first literal");
|
||||
await expect(status(page)).toHaveText("1 of 1");
|
||||
await page.screenshot({ path: testInfo.outputPath("replace.png") });
|
||||
await closeFind(page);
|
||||
await source(page).press("ControlOrMeta+z");
|
||||
await expect(source(page)).toContainText("first a.b");
|
||||
await source(page).press("ControlOrMeta+s");
|
||||
await expect.poll(() => readFile(file, "utf8")).toBe("first a.b\nsecond axb\nthird a.b\n");
|
||||
await findInSource(page, "a.b");
|
||||
await page.getByRole("button", { name: "Toggle replace" }).click();
|
||||
await page.getByRole("textbox", { name: "Replace with" }).fill("all");
|
||||
await page.getByRole("button", { name: "Replace all", exact: true }).click();
|
||||
await expect(status(page)).toHaveText("No matches");
|
||||
await closeFind(page);
|
||||
await source(page).press("ControlOrMeta+s");
|
||||
await expect.poll(() => readFile(file, "utf8")).toBe("first all\nsecond axb\nthird all\n");
|
||||
});
|
||||
});
|
||||
|
||||
test("searches the unsaved buffer while disconnected", async ({ page, withWorkspace }) => {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-draft-" });
|
||||
const file = path.join(workspace.repoPath, "draft.txt");
|
||||
await writeFile(file, "saved on disk\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "draft.txt");
|
||||
await gate.drop();
|
||||
await source(page).fill("unsaved needle\nsecond needle\n");
|
||||
await findInSource(page, "needle");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
expect(await readFile(file, "utf8")).toBe("saved on disk\n");
|
||||
gate.restore();
|
||||
});
|
||||
|
||||
test("searches read-only source beyond the viewport without replace controls", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-readonly-" });
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "large.txt"),
|
||||
"needle first\n" + "plain source line\n".repeat(70_000) + "needle last\n",
|
||||
);
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "large.txt");
|
||||
await findInSource(page, "needle");
|
||||
await expect(source(page)).toHaveAttribute("contenteditable", "false");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await expect(page.getByRole("button", { name: "Toggle replace" })).toHaveCount(0);
|
||||
await query(page).press("Enter");
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await expect(source(page)).toContainText("needle last");
|
||||
await page.screenshot({ path: testInfo.outputPath("readonly-find.png") });
|
||||
await closeFind(page);
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await query(page).fill("plain");
|
||||
await expect(status(page)).toHaveText("1 of 10000+");
|
||||
});
|
||||
|
||||
test("targets the focused source in split panes and refocuses an open query", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-split-" });
|
||||
await writeFile(path.join(workspace.repoPath, "left.txt"), "left needle\n");
|
||||
await writeFile(path.join(workspace.repoPath, "right.txt"), "right needle\nright needle\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "left.txt");
|
||||
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
|
||||
await openFileFromExplorer(page, "right.txt");
|
||||
const left = source(page).filter({ hasText: "left needle" });
|
||||
const right = source(page).filter({ hasText: "right needle" });
|
||||
await expect(left).toBeVisible();
|
||||
await expect(right).toBeVisible();
|
||||
await left.click();
|
||||
await left.press("ControlOrMeta+f");
|
||||
await query(page).fill("needle");
|
||||
await expect(status(page)).toHaveText("1 of 1");
|
||||
await query(page).press("Escape");
|
||||
await right.click();
|
||||
await right.press("ControlOrMeta+f");
|
||||
await query(page).fill("needle");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await right.click();
|
||||
await right.press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await expect(query(page)).toHaveValue("needle");
|
||||
await page.screenshot({ path: testInfo.outputPath("split-find.png") });
|
||||
});
|
||||
|
||||
test.describe("narrow touch browser", () => {
|
||||
test.use({ hasTouch: true });
|
||||
test("opens Find by touch and keeps controls inside the source pane", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-touch-" });
|
||||
await writeFile(path.join(workspace.repoPath, "touch.txt"), "touch needle\nnext needle\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "touch.txt");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.getByRole("button", { name: "Find", exact: true }).tap();
|
||||
await expect(query(page)).toBeFocused();
|
||||
await query(page).fill("needle");
|
||||
await page.getByRole("button", { name: "Next match" }).tap();
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await expectFindAtTopRight(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("touch-find.png") });
|
||||
await page.getByRole("button", { name: "Close Find" }).tap();
|
||||
await expect(source(page)).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFirstTwoLines(page: Page) {
|
||||
await source(page).focus();
|
||||
await source(page).press("ControlOrMeta+Home");
|
||||
await source(page).press("Shift+ArrowDown");
|
||||
await source(page).press("Shift+End");
|
||||
}
|
||||
|
||||
async function selectLastWord(page: Page) {
|
||||
await source(page).focus();
|
||||
await source(page).press("ControlOrMeta+End");
|
||||
await source(page).press("ArrowUp");
|
||||
await source(page).press("Home");
|
||||
await source(page).press("Shift+End");
|
||||
}
|
||||
|
||||
test("declines multiline selection seeds and keeps replacement aligned with the visible query", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-single-line-" });
|
||||
const file = path.join(workspace.repoPath, "selection.txt");
|
||||
await writeFile(file, "alpha\nbeta\nalphabeta\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "selection.txt");
|
||||
|
||||
await selectFirstTwoLines(page);
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await expect(query(page)).toHaveValue("");
|
||||
await expect(page.getByRole("button", { name: "Next match" })).toBeDisabled();
|
||||
await query(page).press("Enter");
|
||||
await expect(query(page)).toHaveValue("");
|
||||
await closeFind(page);
|
||||
|
||||
await selectFirstTwoLines(page);
|
||||
await source(page).press("ControlOrMeta+g");
|
||||
await expect(query(page)).toHaveValue("");
|
||||
await expect(page.getByRole("button", { name: "Next match" })).toBeDisabled();
|
||||
await closeFind(page);
|
||||
|
||||
await selectLastWord(page);
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toHaveValue("alphabeta");
|
||||
await query(page).press("Enter");
|
||||
await expect(status(page)).toHaveText("1 of 1");
|
||||
await closeFind(page);
|
||||
await expect(page.getByLabel("Line 3, column 10")).toBeVisible();
|
||||
|
||||
await selectFirstTwoLines(page);
|
||||
await page.getByRole("button", { name: "Find", exact: true }).click();
|
||||
await expect(query(page)).toHaveValue("alphabeta");
|
||||
await query(page).press("Enter");
|
||||
await expect(status(page)).toHaveText("1 of 1");
|
||||
await page.getByRole("button", { name: "Toggle replace" }).click();
|
||||
await page.getByRole("textbox", { name: "Replace with" }).fill("replaced");
|
||||
await page.getByRole("button", { name: "Replace", exact: true }).click();
|
||||
await expect(source(page)).toContainText("alpha");
|
||||
await expect(source(page)).toContainText("beta");
|
||||
await expect(status(page)).toHaveText("No matches");
|
||||
await page.screenshot({ path: testInfo.outputPath("single-line-query-replacement.png") });
|
||||
await closeFind(page);
|
||||
await source(page).press("ControlOrMeta+s");
|
||||
await expect.poll(() => readFile(file, "utf8")).toBe("alpha\nbeta\nreplaced\n");
|
||||
});
|
||||
|
||||
for (const startingField of ["Find in pane", "Replace with"] as const) {
|
||||
test(`repeated Find from ${startingField} selects the query for new typing`, async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-repeat-input-" });
|
||||
await writeFile(path.join(workspace.repoPath, "source.txt"), "needle first\nneedle second\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "source.txt");
|
||||
await findInSource(page, "needle");
|
||||
await page.getByRole("button", { name: "Toggle replace" }).click();
|
||||
const replacement = page.getByRole("textbox", { name: "Replace with" });
|
||||
await replacement.fill("replacement");
|
||||
|
||||
await test.step("repeat Find from the input and type a fresh query", async () => {
|
||||
const startingInput = page.getByRole("textbox", { name: startingField, exact: true });
|
||||
await startingInput.press("End");
|
||||
await startingInput.press("ControlOrMeta+f");
|
||||
await expectQuerySelected(page, "needle");
|
||||
await page.keyboard.type("first");
|
||||
await expect(query(page)).toHaveValue("first");
|
||||
await expect(replacement).toHaveValue("replacement");
|
||||
await expect(status(page)).toHaveText("1 of 1");
|
||||
});
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath("repeat-find-input.png") });
|
||||
await closeFind(page);
|
||||
await expect(page.getByLabel("Line 1, column 13")).toBeVisible();
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { searchKeymap } from "@codemirror/search";
|
||||
import {
|
||||
EditorView,
|
||||
drawSelection,
|
||||
@@ -41,6 +42,7 @@ export function editorBaseExtensions(onSave: () => void) {
|
||||
indentWithTab,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...searchKeymap,
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -61,13 +63,6 @@ export function editorTheme(theme: EditorVisualTheme) {
|
||||
fontFamily: theme.monoFont,
|
||||
lineHeight: "1.45",
|
||||
},
|
||||
".cm-panels": { backgroundColor: theme.background, color: theme.foreground },
|
||||
".cm-panels-top": { borderBottom: "none" },
|
||||
".cm-searchMatch": { backgroundColor: theme.selection },
|
||||
".cm-searchMatch-selected": {
|
||||
backgroundColor: theme.selection,
|
||||
outline: `1px solid ${theme.cursor}`,
|
||||
},
|
||||
".cm-content": { caretColor: theme.foreground, padding: "16px 0" },
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: theme.cursor },
|
||||
".cm-gutters": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
||||
import { FileFind, FileFindModel } from "../find/index.web";
|
||||
import { useEffect, useRef, useSyncExternalStore } from "react";
|
||||
import { Annotation, Compartment, EditorState, Transaction } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { getLanguageForFile } from "@getpaseo/highlight";
|
||||
@@ -39,7 +38,6 @@ export function FileEditorView({
|
||||
onCursorChange,
|
||||
onVimModeChange,
|
||||
}: FileEditorViewProps) {
|
||||
const [find] = useState(() => new FileFindModel());
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const snapshot = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot);
|
||||
@@ -56,7 +54,6 @@ export function FileEditorView({
|
||||
doc: values.content,
|
||||
extensions: [
|
||||
vimCompartment.of(values.vimEnabled ? vim() : []),
|
||||
find.extension,
|
||||
...editorBaseExtensions(() => void values.model.save()),
|
||||
languageCompartment.of(getLanguageForFile(values.filename)?.extension ?? []),
|
||||
wrappingCompartment.of(wrappingForFile(values.filename)),
|
||||
@@ -84,7 +81,7 @@ export function FileEditorView({
|
||||
view.destroy();
|
||||
viewRef.current = null;
|
||||
};
|
||||
}, [find]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const view = viewRef.current;
|
||||
@@ -144,25 +141,15 @@ export function FileEditorView({
|
||||
}, [onVimModeChange, vimEnabled]);
|
||||
|
||||
return (
|
||||
<div style={FRAME_STYLE}>
|
||||
<div
|
||||
ref={hostRef}
|
||||
data-pmono=""
|
||||
data-testid="file-source-editor"
|
||||
aria-label={`Source editor for ${filename}`}
|
||||
style={HOST_STYLE}
|
||||
/>
|
||||
<FileFind model={find} editor={viewRef} />
|
||||
</div>
|
||||
<div
|
||||
ref={hostRef}
|
||||
data-pmono=""
|
||||
data-testid="file-source-editor"
|
||||
aria-label={`Source editor for ${filename}`}
|
||||
style={HOST_STYLE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const remoteUpdate = Annotation.define<boolean>();
|
||||
const FRAME_STYLE = {
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
} as const;
|
||||
const HOST_STYLE = { flex: 1, minHeight: 0, overflow: "hidden" } as const;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useRef,
|
||||
useSyncExternalStore,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { Search } from "lucide-react-native";
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PaneFind, type PaneFindHandle } from "@/pane-find";
|
||||
import { usePaneFocus } from "@/panels/pane-context";
|
||||
import { hasActiveWebOverlay } from "@/lib/overlay-root";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
||||
import { FileFindModel } from "./model.web";
|
||||
|
||||
export { FileFindModel } from "./model.web";
|
||||
|
||||
export function FileFind({
|
||||
model,
|
||||
editor,
|
||||
}: {
|
||||
model: FileFindModel;
|
||||
editor: RefObject<EditorView | null>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const state = useSyncExternalStore(model.subscribe, model.getSnapshot, model.getSnapshot);
|
||||
const widget = useRef<PaneFindHandle>(null);
|
||||
const { isInteractive, focusPane } = usePaneFocus();
|
||||
const active = useRetainedPanelActive();
|
||||
const open = useCallback(() => {
|
||||
focusPane();
|
||||
model.open(editor.current);
|
||||
widget.current?.focus();
|
||||
}, [editor, focusPane, model]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInteractive || !active) return;
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented || hasActiveWebOverlay() || isImeComposingKeyboardEvent(event))
|
||||
return;
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
event.key.toLowerCase() === "f"
|
||||
) {
|
||||
event.preventDefault();
|
||||
model.open(editor.current);
|
||||
widget.current?.focus();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [active, editor, isInteractive, model]);
|
||||
|
||||
const replace = useMemo(
|
||||
() =>
|
||||
state.readOnly
|
||||
? undefined
|
||||
: {
|
||||
value: state.replacement,
|
||||
onChange: model.setReplacement,
|
||||
onReplace: model.replace,
|
||||
onReplaceAll: model.replaceAll,
|
||||
},
|
||||
[model, state.readOnly, state.replacement],
|
||||
);
|
||||
if (!state.panel)
|
||||
return (
|
||||
<View style={styles.trigger}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={Search}
|
||||
accessibilityLabel={t("paneFind.title")}
|
||||
onPress={open}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
const total = `${state.total}${state.limited ? "+" : ""}`;
|
||||
let status = "";
|
||||
if (state.query) {
|
||||
if (state.total === 0) status = t("paneFind.noMatches");
|
||||
else if (state.current) status = t("paneFind.position", { current: state.current, total });
|
||||
else status = t("paneFind.total", { total });
|
||||
}
|
||||
return createPortal(
|
||||
<View style={styles.panel}>
|
||||
<PaneFind
|
||||
ref={widget}
|
||||
query={state.query}
|
||||
status={status}
|
||||
canNavigate={state.total > 0}
|
||||
onQueryChange={model.setSearch}
|
||||
onNext={model.next}
|
||||
onPrevious={model.previous}
|
||||
onClose={model.close}
|
||||
replace={replace}
|
||||
/>
|
||||
</View>,
|
||||
state.panel,
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
panel: { alignItems: "flex-end", padding: theme.spacing[2] },
|
||||
trigger: {
|
||||
position: "absolute",
|
||||
top: theme.spacing[2],
|
||||
right: theme.spacing[2],
|
||||
zIndex: 1,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
}));
|
||||
@@ -1,158 +0,0 @@
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import { EditorView, keymap, type Panel, type ViewUpdate } from "@codemirror/view";
|
||||
import {
|
||||
closeSearchPanel,
|
||||
findNext,
|
||||
findPrevious,
|
||||
getSearchQuery,
|
||||
openSearchPanel,
|
||||
replaceAll,
|
||||
replaceNext,
|
||||
search,
|
||||
searchKeymap,
|
||||
SearchQuery,
|
||||
setSearchQuery,
|
||||
} from "@codemirror/search";
|
||||
|
||||
const MATCH_STATUS_LIMIT = 10_000;
|
||||
|
||||
/** CodeMirror owns query, matching, selection, replacement, and panel lifetime. */
|
||||
export class FileFindModel {
|
||||
private view: EditorView | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
private matches: Array<{ from: number; to: number }> = [];
|
||||
private snapshot = {
|
||||
panel: null as HTMLElement | null,
|
||||
query: "",
|
||||
replacement: "",
|
||||
current: 0,
|
||||
total: 0,
|
||||
limited: false,
|
||||
readOnly: true,
|
||||
};
|
||||
readonly subscribe = (listener: () => void) => {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
readonly getSnapshot = () => this.snapshot;
|
||||
|
||||
readonly extension: Extension = [
|
||||
// The custom panel lives inside the editor's monospace subtree.
|
||||
EditorView.theme({
|
||||
".paseo-file-find, .paseo-file-find *": { fontFamily: "var(--paseo-ui-font)" },
|
||||
}),
|
||||
search({ literal: true, top: true, createPanel: (view) => this.createPanel(view) }),
|
||||
EditorState.transactionExtender.of((transaction) => {
|
||||
// CodeMirror seeds every search-opening command from the selection. A
|
||||
// single-line field cannot represent CR/LF, so keep the previous query.
|
||||
const multilineSeed = transaction.effects.some(
|
||||
(effect) => effect.is(setSearchQuery) && /[\r\n]/.test(effect.value.search),
|
||||
);
|
||||
if (!multilineSeed) return null;
|
||||
return { effects: setSearchQuery.of(getSearchQuery(transaction.startState)) };
|
||||
}),
|
||||
keymap.of(searchKeymap.filter((binding) => binding.key !== "Mod-f")),
|
||||
];
|
||||
|
||||
readonly open = (view = this.view) => {
|
||||
if (view) openSearchPanel(view);
|
||||
};
|
||||
readonly close = () => {
|
||||
if (this.view) {
|
||||
closeSearchPanel(this.view);
|
||||
this.view.focus();
|
||||
}
|
||||
};
|
||||
readonly next = () => {
|
||||
if (this.view) findNext(this.view);
|
||||
};
|
||||
readonly previous = () => {
|
||||
if (this.view) findPrevious(this.view);
|
||||
};
|
||||
readonly replace = () => {
|
||||
if (this.view) replaceNext(this.view);
|
||||
};
|
||||
readonly replaceAll = () => {
|
||||
if (this.view) replaceAll(this.view);
|
||||
};
|
||||
readonly setReplacement = (replacement: string) =>
|
||||
this.setQuery(this.snapshot.query, replacement);
|
||||
readonly setSearch = (query: string) => {
|
||||
this.setQuery(query, this.snapshot.replacement);
|
||||
if (this.view && this.matches.length > 0) {
|
||||
this.view.dispatch({ selection: { anchor: this.view.state.selection.main.from } });
|
||||
findNext(this.view);
|
||||
}
|
||||
};
|
||||
|
||||
private setQuery(query: string, replacement: string) {
|
||||
this.view?.dispatch({
|
||||
effects: setSearchQuery.of(
|
||||
new SearchQuery({ search: query, replace: replacement, literal: true }),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private createPanel(view: EditorView): Panel {
|
||||
this.view = view;
|
||||
const dom = document.createElement("div");
|
||||
dom.className = "paseo-file-find";
|
||||
return {
|
||||
dom,
|
||||
top: true,
|
||||
mount: () => {
|
||||
this.snapshot = { ...this.snapshot, panel: dom };
|
||||
this.update(view, true);
|
||||
},
|
||||
update: (update: ViewUpdate) => {
|
||||
const queryChanged = !getSearchQuery(update.startState).eq(getSearchQuery(update.state));
|
||||
if (update.docChanged || update.selectionSet || queryChanged)
|
||||
this.update(view, update.docChanged || queryChanged);
|
||||
},
|
||||
destroy: () => {
|
||||
this.snapshot = { ...this.snapshot, panel: null };
|
||||
this.publish();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private update(view: EditorView, recount: boolean) {
|
||||
const query = getSearchQuery(view.state);
|
||||
let limited = this.snapshot.limited;
|
||||
if (recount) {
|
||||
this.matches = [];
|
||||
limited = false;
|
||||
if (query.valid) {
|
||||
const cursor = query.getCursor(view.state);
|
||||
for (let match = cursor.next(); !match.done; match = cursor.next()) {
|
||||
if (this.matches.length === MATCH_STATUS_LIMIT) {
|
||||
limited = true;
|
||||
break;
|
||||
}
|
||||
this.matches.push(match.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
const selection = view.state.selection.main;
|
||||
const current =
|
||||
this.matches.findIndex(
|
||||
(match) => match.from === selection.from && match.to === selection.to,
|
||||
) + 1;
|
||||
this.snapshot = {
|
||||
...this.snapshot,
|
||||
query: query.search,
|
||||
replacement: query.replace,
|
||||
current,
|
||||
total: this.matches.length,
|
||||
limited,
|
||||
readOnly: view.state.readOnly,
|
||||
};
|
||||
this.publish();
|
||||
}
|
||||
|
||||
private publish() {
|
||||
for (const listener of this.listeners) listener();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FileFind, FileFindModel } from "../find/index.web";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Compartment, EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { getLanguageForFile } from "@getpaseo/highlight";
|
||||
@@ -60,7 +59,6 @@ function ReadonlyCodeMirror({
|
||||
}: Omit<FileSourceViewProps, "size" | "tooLargeMessage"> & {
|
||||
presentation: Exclude<SourcePresentation, "unsupported">;
|
||||
}) {
|
||||
const [find] = useState(() => new FileFindModel());
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const initial = useRef({ content, filename, presentation, theme });
|
||||
@@ -73,12 +71,7 @@ function ReadonlyCodeMirror({
|
||||
state: EditorState.create({
|
||||
doc: values.content,
|
||||
extensions: [
|
||||
find.extension,
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({
|
||||
tabindex: "0",
|
||||
"aria-label": `Source for ${values.filename}`,
|
||||
}),
|
||||
EditorView.editable.of(false),
|
||||
languageCompartment.of(
|
||||
languageFor({ filename: values.filename, presentation: values.presentation }),
|
||||
@@ -92,7 +85,7 @@ function ReadonlyCodeMirror({
|
||||
view.destroy();
|
||||
viewRef.current = null;
|
||||
};
|
||||
}, [find]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const view = viewRef.current;
|
||||
@@ -117,12 +110,7 @@ function ReadonlyCodeMirror({
|
||||
view.dispatch({ effects: EditorView.scrollIntoView(from, { y: "center" }) });
|
||||
}, [location.lineStart, navigationRevision]);
|
||||
|
||||
return (
|
||||
<div style={FRAME_STYLE}>
|
||||
<div ref={hostRef} data-testid="file-source-editor" style={HOST_STYLE} />
|
||||
<FileFind model={find} editor={viewRef} />
|
||||
</div>
|
||||
);
|
||||
return <div ref={hostRef} data-testid="file-source-editor" style={HOST_STYLE} />;
|
||||
}
|
||||
|
||||
function languageFor(input: {
|
||||
@@ -134,13 +122,6 @@ function languageFor(input: {
|
||||
: [];
|
||||
}
|
||||
|
||||
const FRAME_STYLE = {
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
} as const;
|
||||
const HOST_STYLE = { flex: 1, minHeight: 0, overflow: "hidden" } as const;
|
||||
const UNSUPPORTED_STYLE = {
|
||||
alignItems: "center",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const ar: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "بحث",
|
||||
placeholder: "بحث في اللوحة",
|
||||
close: "إغلاق البحث",
|
||||
matches: "نتائج البحث",
|
||||
previous: "التطابق السابق",
|
||||
next: "التطابق التالي",
|
||||
toggleReplace: "إظهار الاستبدال",
|
||||
replaceWith: "استبدال بـ",
|
||||
replace: "استبدال",
|
||||
replaceAll: "استبدال الكل",
|
||||
noMatches: "لا توجد تطابقات",
|
||||
position: "{{current}} من {{total}}",
|
||||
total: "{{total}} تطابقات",
|
||||
},
|
||||
common: {
|
||||
back: "خلف",
|
||||
loading: "تحميل...",
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
export const en = {
|
||||
paneFind: {
|
||||
title: "Find",
|
||||
placeholder: "Find in pane",
|
||||
close: "Close Find",
|
||||
matches: "Find matches",
|
||||
previous: "Previous match",
|
||||
next: "Next match",
|
||||
toggleReplace: "Toggle replace",
|
||||
replaceWith: "Replace with",
|
||||
replace: "Replace",
|
||||
replaceAll: "Replace all",
|
||||
noMatches: "No matches",
|
||||
position: "{{current}} of {{total}}",
|
||||
total: "{{total}} matches",
|
||||
},
|
||||
common: {
|
||||
back: "Back",
|
||||
loading: "Loading...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const es: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "Buscar",
|
||||
placeholder: "Buscar en el panel",
|
||||
close: "Cerrar búsqueda",
|
||||
matches: "Coincidencias",
|
||||
previous: "Coincidencia anterior",
|
||||
next: "Siguiente coincidencia",
|
||||
toggleReplace: "Mostrar reemplazo",
|
||||
replaceWith: "Reemplazar con",
|
||||
replace: "Reemplazar",
|
||||
replaceAll: "Reemplazar todo",
|
||||
noMatches: "Sin coincidencias",
|
||||
position: "{{current}} de {{total}}",
|
||||
total: "{{total}} coincidencias",
|
||||
},
|
||||
common: {
|
||||
back: "Atrás",
|
||||
loading: "Cargando...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const fr: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "Rechercher",
|
||||
placeholder: "Rechercher dans le panneau",
|
||||
close: "Fermer la recherche",
|
||||
matches: "Résultats de recherche",
|
||||
previous: "Résultat précédent",
|
||||
next: "Résultat suivant",
|
||||
toggleReplace: "Afficher le remplacement",
|
||||
replaceWith: "Remplacer par",
|
||||
replace: "Remplacer",
|
||||
replaceAll: "Tout remplacer",
|
||||
noMatches: "Aucun résultat",
|
||||
position: "{{current}} sur {{total}}",
|
||||
total: "{{total}} résultats",
|
||||
},
|
||||
common: {
|
||||
back: "Dos",
|
||||
loading: "Chargement...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const ja: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "検索",
|
||||
placeholder: "ペイン内を検索",
|
||||
close: "検索を閉じる",
|
||||
matches: "検索結果",
|
||||
previous: "前の一致",
|
||||
next: "次の一致",
|
||||
toggleReplace: "置換を切り替え",
|
||||
replaceWith: "置換後の文字列",
|
||||
replace: "置換",
|
||||
replaceAll: "すべて置換",
|
||||
noMatches: "一致なし",
|
||||
position: "{{current}} / {{total}}",
|
||||
total: "{{total}} 件の一致",
|
||||
},
|
||||
common: {
|
||||
back: "戻る",
|
||||
loading: "読み込み中...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const ko: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "찾기",
|
||||
placeholder: "패널에서 찾기",
|
||||
close: "찾기 닫기",
|
||||
matches: "검색 결과",
|
||||
previous: "이전 일치 항목",
|
||||
next: "다음 일치 항목",
|
||||
toggleReplace: "바꾸기 표시 전환",
|
||||
replaceWith: "바꿀 내용",
|
||||
replace: "바꾸기",
|
||||
replaceAll: "모두 바꾸기",
|
||||
noMatches: "일치 항목 없음",
|
||||
position: "{{current}} / {{total}}",
|
||||
total: "일치 항목 {{total}}개",
|
||||
},
|
||||
common: {
|
||||
back: "뒤로",
|
||||
loading: "불러오는 중...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const ptBR: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "Buscar",
|
||||
placeholder: "Buscar no painel",
|
||||
close: "Fechar busca",
|
||||
matches: "Correspondências",
|
||||
previous: "Correspondência anterior",
|
||||
next: "Próxima correspondência",
|
||||
toggleReplace: "Mostrar substituição",
|
||||
replaceWith: "Substituir por",
|
||||
replace: "Substituir",
|
||||
replaceAll: "Substituir tudo",
|
||||
noMatches: "Nenhuma correspondência",
|
||||
position: "{{current}} de {{total}}",
|
||||
total: "{{total}} correspondências",
|
||||
},
|
||||
common: {
|
||||
back: "Voltar",
|
||||
loading: "Carregando...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const ru: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "Найти",
|
||||
placeholder: "Найти в панели",
|
||||
close: "Закрыть поиск",
|
||||
matches: "Совпадения",
|
||||
previous: "Предыдущее совпадение",
|
||||
next: "Следующее совпадение",
|
||||
toggleReplace: "Показать замену",
|
||||
replaceWith: "Заменить на",
|
||||
replace: "Заменить",
|
||||
replaceAll: "Заменить всё",
|
||||
noMatches: "Нет совпадений",
|
||||
position: "{{current}} из {{total}}",
|
||||
total: "Совпадений: {{total}}",
|
||||
},
|
||||
common: {
|
||||
back: "Назад",
|
||||
loading: "Загрузка...",
|
||||
|
||||
@@ -2,21 +2,6 @@ import { en, type TranslationResources } from "./en";
|
||||
import { pluginSettings } from "./plugin-settings";
|
||||
|
||||
export const zhCN: TranslationResources = {
|
||||
paneFind: {
|
||||
title: "查找",
|
||||
placeholder: "在窗格中查找",
|
||||
close: "关闭查找",
|
||||
matches: "查找结果",
|
||||
previous: "上一个匹配项",
|
||||
next: "下一个匹配项",
|
||||
toggleReplace: "切换替换",
|
||||
replaceWith: "替换为",
|
||||
replace: "替换",
|
||||
replaceAll: "全部替换",
|
||||
noMatches: "无匹配项",
|
||||
position: "{{current}} / {{total}}",
|
||||
total: "{{total}} 个匹配项",
|
||||
},
|
||||
common: {
|
||||
back: "返回",
|
||||
loading: "加载中...",
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type KeyboardEvent,
|
||||
} from "react";
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
type NativeSyntheticEvent,
|
||||
type TextInputKeyPressEventData,
|
||||
} from "react-native";
|
||||
import { ArrowDown, ArrowUp, ChevronDown, ChevronRight, X } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EditingTextInput, type EditingTextInputHandle } from "@/components/ui/text-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CONTROL_HEIGHTS } from "@/components/ui/control-geometry";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
||||
|
||||
const TextInput = withUnistyles(EditingTextInput, (theme) => ({
|
||||
placeholderTextColor: theme.colors.foregroundMuted,
|
||||
}));
|
||||
|
||||
export interface PaneFindHandle {
|
||||
focus(): void;
|
||||
}
|
||||
|
||||
export interface PaneFindProps {
|
||||
query: string;
|
||||
status: string;
|
||||
canNavigate: boolean;
|
||||
onQueryChange(query: string): void;
|
||||
onNext(): void;
|
||||
onPrevious(): void;
|
||||
onClose(): void;
|
||||
replace?: {
|
||||
value: string;
|
||||
onChange(value: string): void;
|
||||
onReplace(): void;
|
||||
onReplaceAll(): void;
|
||||
};
|
||||
}
|
||||
|
||||
/** Pane-local chrome. The content owner supplies search state and commands. */
|
||||
export const PaneFind = forwardRef<PaneFindHandle, PaneFindProps>(function PaneFind(
|
||||
{ query, status, canNavigate, onQueryChange, onNext, onPrevious, onClose, replace },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const input = useRef<EditingTextInputHandle>(null);
|
||||
const replacementInput = useRef<EditingTextInputHandle>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (input.current?.getText() !== query) input.current?.replaceText(query);
|
||||
}, [query]);
|
||||
useLayoutEffect(() => {
|
||||
if (replace && replacementInput.current?.getText() !== replace.value)
|
||||
replacementInput.current?.replaceText(replace.value);
|
||||
}, [replace]);
|
||||
const [replaceExpanded, setReplaceExpanded] = useState(false);
|
||||
const focus = useCallback(() => {
|
||||
input.current?.focus();
|
||||
const text = input.current?.getText() ?? "";
|
||||
input.current?.replaceText(text, { start: 0, end: text.length });
|
||||
}, []);
|
||||
useImperativeHandle(ref, () => ({ focus }), [focus]);
|
||||
|
||||
const onKeyPress = useCallback(
|
||||
(event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
||||
const key = event.nativeEvent as TextInputKeyPressEventData & {
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
altKey?: boolean;
|
||||
isComposing?: boolean;
|
||||
keyCode?: number;
|
||||
};
|
||||
if (isImeComposingKeyboardEvent(key)) return;
|
||||
if (
|
||||
(key.metaKey || key.ctrlKey) &&
|
||||
!key.altKey &&
|
||||
!key.shiftKey &&
|
||||
key.key.toLowerCase() === "f"
|
||||
) {
|
||||
// RN Web inputs stop keydown before the pane's document listener.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
focus();
|
||||
} else if (key.key === "Escape" || key.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (key.key === "Escape") onClose();
|
||||
else if (key.shiftKey) onPrevious();
|
||||
else onNext();
|
||||
}
|
||||
},
|
||||
[focus, onClose, onNext, onPrevious],
|
||||
);
|
||||
const toggleReplace = useCallback(() => setReplaceExpanded((expanded) => !expanded), []);
|
||||
const replaceAccessibilityState = useMemo(
|
||||
() => ({ expanded: replaceExpanded }),
|
||||
[replaceExpanded],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.widget}
|
||||
accessibilityLabel={t("paneFind.title")}
|
||||
{...(isWeb
|
||||
? {
|
||||
onKeyDown: (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && !isImeComposingKeyboardEvent(event.nativeEvent)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<View style={styles.findRow}>
|
||||
<View style={styles.queryRow}>
|
||||
{replace ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={replaceExpanded ? ChevronDown : ChevronRight}
|
||||
accessibilityLabel={t("paneFind.toggleReplace")}
|
||||
accessibilityState={replaceAccessibilityState}
|
||||
onPress={toggleReplace}
|
||||
/>
|
||||
) : null}
|
||||
<TextInput
|
||||
ref={input}
|
||||
autoFocus
|
||||
selectTextOnFocus
|
||||
initialValue={query}
|
||||
onChangeText={onQueryChange}
|
||||
onKeyPress={onKeyPress}
|
||||
accessibilityLabel={t("paneFind.placeholder")}
|
||||
placeholder={t("paneFind.placeholder")}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
returnKeyType="search"
|
||||
blurOnSubmit={false}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text
|
||||
style={styles.status}
|
||||
role="status"
|
||||
accessibilityLabel={t("paneFind.matches")}
|
||||
accessibilityLiveRegion="polite"
|
||||
>
|
||||
{status}
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={ArrowUp}
|
||||
accessibilityLabel={t("paneFind.previous")}
|
||||
disabled={!canNavigate}
|
||||
onPress={onPrevious}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={ArrowDown}
|
||||
accessibilityLabel={t("paneFind.next")}
|
||||
disabled={!canNavigate}
|
||||
onPress={onNext}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={X}
|
||||
accessibilityLabel={t("paneFind.close")}
|
||||
onPress={onClose}
|
||||
/>
|
||||
</View>
|
||||
{replace && replaceExpanded ? (
|
||||
<View style={styles.replacement}>
|
||||
<TextInput
|
||||
ref={replacementInput}
|
||||
initialValue={replace.value}
|
||||
onChangeText={replace.onChange}
|
||||
onKeyPress={onKeyPress}
|
||||
accessibilityLabel={t("paneFind.replaceWith")}
|
||||
placeholder={t("paneFind.replaceWith")}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.input}
|
||||
/>
|
||||
<View style={styles.row}>
|
||||
<Button variant="ghost" size="sm" disabled={!canNavigate} onPress={replace.onReplace}>
|
||||
{t("paneFind.replace")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!canNavigate}
|
||||
onPress={replace.onReplaceAll}
|
||||
>
|
||||
{t("paneFind.replaceAll")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
widget: {
|
||||
width: 480,
|
||||
maxWidth: "100%",
|
||||
padding: theme.spacing[2],
|
||||
gap: theme.spacing[1],
|
||||
backgroundColor: theme.colors.surface1,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
findRow: { flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: theme.spacing[1] },
|
||||
queryRow: {
|
||||
flex: 1,
|
||||
minWidth: 140,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
row: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1] },
|
||||
replacement: { gap: theme.spacing[1] },
|
||||
input: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: CONTROL_HEIGHTS.compact,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foreground,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
},
|
||||
status: {
|
||||
flex: 1,
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user