Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/search-dark.png">
<img src="docs/images/search-light.png" alt="MarkPad file search sidebar: the workspace picker and search field are ready to search Markdown, JSON, and YAML files, with the Search files toolbar button active">
<img src="docs/images/search-light.png" alt="MarkPad file search sidebar with Open files selected: a search for open file finds three matching lines across two unsaved drafts beside the editor and preview">
</picture>

- **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.
Expand Down
Binary file modified docs/images/search-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/search-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 77 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -110,7 +114,7 @@ type ItemSnapshot = {
type SidebarView = "recents" | "workspaceSearch";

type PendingLocation = {
path: string;
itemId: ItemId;
lineNumber: number;
};

Expand Down Expand Up @@ -190,7 +194,7 @@ function EmptyState({ modKey }: { modKey: string }) {
<kbd className={kbdClass}>{modKey}+S</kbd>
<span>Save current file</span>
<kbd className={kbdClass}>{modKey}+Shift+F</kbd>
<span>Search files in a folder</span>
<span>Search open files or a folder</span>
</div>
</div>
</div>
Expand Down Expand Up @@ -221,6 +225,8 @@ function App() {
EMPTY_SEARCH_STATUS,
);
const [workspaceRoot, setWorkspaceRoot] = useState<string | null>(null);
const [workspaceSearchScope, setWorkspaceSearchScope] =
useState<WorkspaceSearchScope>(DEFAULT_WORKSPACE_SEARCH_SCOPE);
const [workspaceSearchQuery, setWorkspaceSearchQuery] = useState("");
const [workspaceSearchFiles, setWorkspaceSearchFiles] = useState<
WorkspaceSearchFile[]
Expand Down Expand Up @@ -504,7 +510,7 @@ function App() {
const location = pendingLocation;
if (
location === null ||
activeItem?.path !== location.path ||
activeItem?.id !== location.itemId ||
!activeItem.loaded
) {
return;
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand All @@ -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<OpenFileSearchSource | null> => {
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");
Expand All @@ -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 });
}
}

Expand Down Expand Up @@ -1312,17 +1375,20 @@ function App() {
/>
) : (
<WorkspaceSearchPanel
scope={workspaceSearchScope}
rootPath={workspaceRoot}
openFileCount={items.length}
query={workspaceSearchQuery}
files={workspaceSearchFiles}
status={workspaceSearchStatus}
error={workspaceSearchError}
focusRequest={workspaceSearchFocusRequest}
onScopeChange={handleWorkspaceSearchScopeChange}
onChooseFolder={() => void handleChooseWorkspaceRoot()}
onQueryChange={handleWorkspaceSearchQueryChange}
onSearch={() => void handleRunWorkspaceSearch()}
onSelectResult={(path, lineNumber) =>
void handleWorkspaceSearchResult(path, lineNumber)
onSelectResult={(file, lineNumber) =>
void handleWorkspaceSearchResult(file, lineNumber)
}
onClose={handleCloseWorkspaceSearch}
/>
Expand Down
Loading