diff --git a/README.md b/README.md
index 099040b..5939485 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ cloud, or a bare editor with no live preview at all.
MarkPad is the small, local-first alternative — a native desktop app that opens
quickly, keeps every file on your machine, and shows your Markdown rendered side
-by side as you type. A recent-files sidebar, folder-wide content search, a
+by side as you type. A recent-files sidebar, open-file and folder-wide search, a
one-click formatting toolbar, light/dark theming, optional auto-save, and real OS
file-association handling make it usable day to day, without the bloat.
@@ -60,11 +60,11 @@ side-by-side work:
- **Live split-pane preview.** Edit Markdown in a line-numbered editor on the left, see it rendered on the right.
- **Synced scrolling.** In split view the panes follow each other — scroll either one and the other tracks the same part of the document, staying aligned even across tall images and long code blocks.
- **Find in the active document.** Use the toolbar magnifier or `Ctrl/⌘+F` to open MarkPad's own search bar instead of the webview's full-interface find. Every match is highlighted with a live position/count; `Enter` and `Shift+Enter` move forward and backward with wraparound, and `Escape` closes search. Searching from preview-only mode reveals the editor so the active match stays visible.
-- **Search text across a folder.** Open the dedicated search sidebar from the toolbar or with `Ctrl/⌘+Shift+F`, choose a workspace folder, and search every Markdown, JSON, and YAML file beneath it. Results are grouped by file with relative paths, matching line numbers, and text previews; selecting one opens the file and places the cursor on that line.
+- **Search across open files or a folder.** Open the dedicated search sidebar from the toolbar or with `Ctrl/⌘+Shift+F`. It searches every file currently open in MarkPad by default — including unsaved drafts and edits — or you can switch to a selected folder and scan every Markdown, JSON, and YAML file beneath it. Results are grouped by file with paths, matching line numbers, and text previews; selecting one activates the file and places the cursor on that line.
-
+
- **In-document link navigation.** Headings get anchor ids, so clicking an in-page link in the preview — like a table of contents `[Section](#section)` — smooth-scrolls to that heading within the preview pane.
diff --git a/docs/images/search-dark.png b/docs/images/search-dark.png
index 8f8d2b5..484cc11 100644
Binary files a/docs/images/search-dark.png and b/docs/images/search-dark.png differ
diff --git a/docs/images/search-light.png b/docs/images/search-light.png
index 4531ae0..d63b1a4 100644
Binary files a/docs/images/search-light.png and b/docs/images/search-light.png differ
diff --git a/src/App.tsx b/src/App.tsx
index 4820d35..8876485 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -50,9 +50,13 @@ import {
type SearchStatus,
} from "./lib/documentSearch";
import {
+ DEFAULT_WORKSPACE_SEARCH_SCOPE,
isWorkspaceSearchShortcut,
+ searchOpenFiles,
searchWorkspace,
+ type OpenFileSearchSource,
type WorkspaceSearchFile,
+ type WorkspaceSearchScope,
type WorkspaceSearchStatus,
} from "./lib/workspaceSearch";
import {
@@ -110,7 +114,7 @@ type ItemSnapshot = {
type SidebarView = "recents" | "workspaceSearch";
type PendingLocation = {
- path: string;
+ itemId: ItemId;
lineNumber: number;
};
@@ -190,7 +194,7 @@ function EmptyState({ modKey }: { modKey: string }) {
{modKey}+SSave current file{modKey}+Shift+F
- Search files in a folder
+ Search open files or a folder
@@ -221,6 +225,8 @@ function App() {
EMPTY_SEARCH_STATUS,
);
const [workspaceRoot, setWorkspaceRoot] = useState(null);
+ const [workspaceSearchScope, setWorkspaceSearchScope] =
+ useState(DEFAULT_WORKSPACE_SEARCH_SCOPE);
const [workspaceSearchQuery, setWorkspaceSearchQuery] = useState("");
const [workspaceSearchFiles, setWorkspaceSearchFiles] = useState<
WorkspaceSearchFile[]
@@ -504,7 +510,7 @@ function App() {
const location = pendingLocation;
if (
location === null ||
- activeItem?.path !== location.path ||
+ activeItem?.id !== location.itemId ||
!activeItem.loaded
) {
return;
@@ -516,7 +522,7 @@ function App() {
);
});
return () => cancelAnimationFrame(frame);
- }, [activeItem?.loaded, activeItem?.path, activeText, pendingLocation]);
+ }, [activeItem?.id, activeItem?.loaded, activeText, pendingLocation]);
// Prune per-item bookkeeping for items no longer in the list.
useEffect(() => {
@@ -1110,6 +1116,16 @@ function App() {
setWorkspaceSearchFocusRequest((request) => request + 1);
}
+ function handleWorkspaceSearchScopeChange(next: WorkspaceSearchScope) {
+ if (next === workspaceSearchScope) return;
+ workspaceSearchSeqRef.current++;
+ setWorkspaceSearchScope(next);
+ setWorkspaceSearchFiles([]);
+ setWorkspaceSearchStatus("idle");
+ setWorkspaceSearchError(null);
+ setWorkspaceSearchFocusRequest((request) => request + 1);
+ }
+
function handleWorkspaceSearchQueryChange(next: string) {
workspaceSearchSeqRef.current++;
setWorkspaceSearchQuery(next);
@@ -1119,9 +1135,51 @@ function App() {
}
async function handleRunWorkspaceSearch() {
- const rootPath = workspaceRoot;
const query = workspaceSearchQuery.trim();
- if (rootPath === null || query.length === 0) return;
+ if (query.length === 0) return;
+
+ if (workspaceSearchScope === "openFiles") {
+ const openItems = itemsRef.current;
+ if (openItems.length === 0) return;
+
+ const sequence = ++workspaceSearchSeqRef.current;
+ setWorkspaceSearchStatus("searching");
+ setWorkspaceSearchError(null);
+ const sources = (
+ await Promise.all(
+ openItems.map(
+ async (item): Promise => {
+ if (item.loaded) {
+ return {
+ itemId: item.id,
+ name: item.name,
+ path: item.path,
+ content: item.text,
+ };
+ }
+ if (item.path === null) return null;
+ const result = await openTextFileByPath(item.path);
+ return result.kind === "ok"
+ ? {
+ itemId: item.id,
+ name: item.name,
+ path: item.path,
+ content: result.content,
+ }
+ : null;
+ },
+ ),
+ )
+ ).filter((source): source is OpenFileSearchSource => source !== null);
+ if (sequence !== workspaceSearchSeqRef.current) return;
+
+ setWorkspaceSearchFiles(searchOpenFiles(sources, query));
+ setWorkspaceSearchStatus("complete");
+ return;
+ }
+
+ const rootPath = workspaceRoot;
+ if (rootPath === null) return;
const sequence = ++workspaceSearchSeqRef.current;
setWorkspaceSearchStatus("searching");
@@ -1140,15 +1198,20 @@ function App() {
}
async function handleWorkspaceSearchResult(
- path: string,
+ file: WorkspaceSearchFile,
lineNumber: number,
) {
if (viewMode !== "editor") {
handleSetViewMode("editor");
}
- const openedId = await openPaths([path]);
+ let openedId = file.itemId ?? null;
+ if (openedId !== null) {
+ await activateItem(openedId);
+ } else if (file.path !== null) {
+ openedId = await openPaths([file.path]);
+ }
if (openedId !== null) {
- setPendingLocation({ path, lineNumber });
+ setPendingLocation({ itemId: openedId, lineNumber });
}
}
@@ -1312,17 +1375,20 @@ function App() {
/>
) : (
void handleChooseWorkspaceRoot()}
onQueryChange={handleWorkspaceSearchQueryChange}
onSearch={() => void handleRunWorkspaceSearch()}
- onSelectResult={(path, lineNumber) =>
- void handleWorkspaceSearchResult(path, lineNumber)
+ onSelectResult={(file, lineNumber) =>
+ void handleWorkspaceSearchResult(file, lineNumber)
}
onClose={handleCloseWorkspaceSearch}
/>
diff --git a/src/components/WorkspaceSearchPanel.tsx b/src/components/WorkspaceSearchPanel.tsx
index 9e32416..e872350 100644
--- a/src/components/WorkspaceSearchPanel.tsx
+++ b/src/components/WorkspaceSearchPanel.tsx
@@ -2,20 +2,24 @@ import { useEffect, useMemo, useRef } from "react";
import {
countWorkspaceSearchMatches,
type WorkspaceSearchFile,
+ type WorkspaceSearchScope,
type WorkspaceSearchStatus,
} from "../lib/workspaceSearch";
type WorkspaceSearchPanelProps = {
+ scope: WorkspaceSearchScope;
rootPath: string | null;
+ openFileCount: number;
query: string;
files: WorkspaceSearchFile[];
status: WorkspaceSearchStatus;
error: string | null;
focusRequest: number;
+ onScopeChange: (scope: WorkspaceSearchScope) => void;
onChooseFolder: () => void;
onQueryChange: (query: string) => void;
onSearch: () => void;
- onSelectResult: (path: string, lineNumber: number) => void;
+ onSelectResult: (file: WorkspaceSearchFile, lineNumber: number) => void;
onClose: () => void;
};
@@ -60,6 +64,25 @@ function FolderIcon() {
);
}
+function FilesIcon() {
+ return (
+
+ );
+}
+
function CloseIcon() {
return (