From d26321056d842fdcc9b6a8f7a692f15485374f77 Mon Sep 17 00:00:00 2001 From: Melody Ruch <396921921@qq.com> Date: Thu, 13 Aug 2026 19:28:31 +0800 Subject: [PATCH] fix(editor): seed search with multiline selection --- .../components/editor/EditorSearchPanel.vue | 2 +- .../EditorSearchPanel.newlineSeed.spec.ts | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/components/editor/__tests__/EditorSearchPanel.newlineSeed.spec.ts diff --git a/apps/desktop/src/components/editor/EditorSearchPanel.vue b/apps/desktop/src/components/editor/EditorSearchPanel.vue index 6a16248ff1..974d698e03 100644 --- a/apps/desktop/src/components/editor/EditorSearchPanel.vue +++ b/apps/desktop/src/components/editor/EditorSearchPanel.vue @@ -276,7 +276,7 @@ function openSearch(): boolean { cmOpenSearchPanel(v); const sel = v.state.selection.main; const selText = v.state.sliceDoc(sel.from, sel.to); - if (selText && !selText.includes("\n")) { + if (selText) { searchText.value = selText; } // Set scope when there's a multi-line selection diff --git a/apps/desktop/src/components/editor/__tests__/EditorSearchPanel.newlineSeed.spec.ts b/apps/desktop/src/components/editor/__tests__/EditorSearchPanel.newlineSeed.spec.ts new file mode 100644 index 0000000000..d67e4c4730 --- /dev/null +++ b/apps/desktop/src/components/editor/__tests__/EditorSearchPanel.newlineSeed.spec.ts @@ -0,0 +1,75 @@ +// @vitest-environment happy-dom + +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { createApp, defineComponent, h, nextTick, type App } from "vue"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("vue-i18n", () => ({ + useI18n: () => ({ t: (key: string) => key }), +})); + +vi.mock("@/stores/settingsStore", () => ({ + useSettingsStore: () => ({ + editorSettings: { regexMaxMatchCount: 1000 }, + }), +})); + +import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue"; + +const mountedApps: Array<{ app: App; host: HTMLElement }> = []; + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + for (const { app, host } of mountedApps.splice(0)) { + app.unmount(); + host.remove(); + } + vi.useRealTimers(); +}); + +describe("EditorSearchPanel openSearch selection seeding (issue #5435)", () => { + it("seeds the search query with the full selection, newline included, so cross-line matches are found without manually typing \\n", async () => { + // Document is exactly the selected text; a correctly seeded (newline-preserving) + // search query must find exactly one match. An HTML always strips \n + // from its rendered value (browser sanitization), so we assert on the actual + // search outcome (rendered match count) rather than the raw input DOM value. + const view = new EditorView({ + parent: document.createElement("div"), + state: EditorState.create({ doc: "line one\nline two" }), + }); + view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); + + let instance: { openSearch: () => boolean } | null = null; + const host = document.createElement("div"); + document.body.append(host); + const app = createApp( + defineComponent({ + setup() { + return () => + h(EditorSearchPanel, { + view, + ref: (el) => { + instance = el as unknown as { openSearch: () => boolean }; + }, + }); + }, + }), + ); + app.mount(host); + mountedApps.push({ app, host }); + + instance!.openSearch(); + await nextTick(); + await vi.advanceTimersByTimeAsync(1000); + await nextTick(); + + expect(host.textContent).toContain("1/1"); + expect(host.textContent).not.toContain("noResults"); + + view.destroy(); + }); +});