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
1 change: 1 addition & 0 deletions apps/desktop/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const desktopRuntime = {
listenSettingsWindowTarget: windowRuntime.listenNativeSettingsWindowTarget,
listenWindowCloseRequested: windowRuntime.listenNativeWindowCloseRequested,
minimizeWindow: windowRuntime.minimizeNativeWindow,
openBlankEditorWindow: windowRuntime.openNativeBlankEditorWindow,
openExternalUrl: windowRuntime.openNativeExternalUrl,
openSettingsWindow: windowRuntime.openSettingsWindow,
prewarmSettingsWindow: windowRuntime.prewarmSettingsWindow,
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/runtime/tauri/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
hideSettingsWindow,
markSettingsWindowReady,
minimizeNativeWindow,
openNativeBlankEditorWindow,
openSettingsWindow,
prewarmSettingsWindow,
setNativeEditorWindowRestoreState,
Expand Down Expand Up @@ -144,6 +145,14 @@ describe("native window actions", () => {
expect(mockedGetCurrentWindow).not.toHaveBeenCalled();
});

it("opens a blank editor window through the native command", async () => {
mockedInvoke.mockResolvedValue(undefined);

await openNativeBlankEditorWindow();

expect(mockedInvoke).toHaveBeenCalledWith("open_blank_editor_window");
});

it("shows the current Tauri window", async () => {
const isVisible = vi.fn().mockResolvedValue(false);
const show = vi.fn().mockResolvedValue(undefined);
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/runtime/tauri/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export function hideSettingsWindow() {
return invokeNative("hide_settings_window");
}

export function openNativeBlankEditorWindow() {
return invokeNative("open_blank_editor_window");
}

export async function listenNativeSettingsWindowTarget(onTarget: (target: NativeSettingsWindowTarget) => unknown) {
if (!("__TAURI_INTERNALS__" in window)) {
return () => {};
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ import type {
import {
closeNativeWindow,
hideSettingsWindow,
openBlankEditorWindow,
openNativeExternalUrl,
openSettingsWindow,
prewarmSettingsWindow,
Expand Down Expand Up @@ -2857,6 +2858,9 @@ function WorkspaceApp() {
const handleOpenSettings = useCallback(() => {
openSettingsWindow().catch(() => {});
}, []);
const handleOpenBlankEditorWindow = useCallback(() => {
openBlankEditorWindow().catch(() => {});
}, []);
const handleShowAbout = useCallback(() => {
showNativeAppAbout().catch(() => {});
}, []);
Expand Down Expand Up @@ -3860,6 +3864,7 @@ function WorkspaceApp() {
exportHtml: exportFeatureEnabled ? exportHtmlDocument : undefined,
exportPdf: exportFeatureEnabled ? exportPdfDocument : undefined,
markdownShortcuts: editorPreferences.preferences.markdownShortcuts,
openBlankEditorWindow: windowsSelfDrawnChromeEnabled ? handleOpenBlankEditorWindow : undefined,
openDocument: handleOpenMarkdownFile,
openDocumentReplace: handleDocumentReplaceOpen,
openDocumentSearch: handleDocumentSearchOpen,
Expand Down Expand Up @@ -4439,6 +4444,7 @@ function WorkspaceApp() {
onSelectViewMode={handleViewModeSelect}
onCreateMarkdownFile={handleQuickCreateMarkdownTreeFile}
onExitApp={handleExitApp}
onOpenBlankEditorWindow={windowsSelfDrawnChromeEnabled ? handleOpenBlankEditorWindow : undefined}
onOpenMarkdown={handleOpenMarkdownFile}
onOpenMarkdownFolder={handleOpenMarkdownFolder}
onOpenSettings={handleOpenSettings}
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/components/NativeTitleBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ describe("NativeTitleBar", () => {
});

it("opens self-drawn Windows app menu dropdowns from the top chrome", () => {
const openBlankEditorWindow = vi.fn();
const openMarkdown = vi.fn();
const saveMarkdown = vi.fn();
const saveMarkdownAs = vi.fn();
Expand Down Expand Up @@ -583,6 +584,7 @@ describe("NativeTitleBar", () => {
</div>
)}
onToggleAiAgent={() => {}}
onOpenBlankEditorWindow={openBlankEditorWindow}
onOpenMarkdown={openMarkdown}
onSaveMarkdown={saveMarkdown}
onShowAbout={showAbout}
Expand Down Expand Up @@ -625,6 +627,10 @@ describe("NativeTitleBar", () => {
fireEvent.click(screen.getByRole("button", { name: "File" }));

expect(screen.getByRole("menu", { name: "File" })).toBeInTheDocument();
fireEvent.click(screen.getByRole("menuitem", { name: "New Ctrl+N" }));
expect(openBlankEditorWindow).toHaveBeenCalledTimes(1);

fireEvent.click(screen.getByRole("button", { name: "File" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Open... Ctrl+O" }));
expect(openMarkdown).toHaveBeenCalledTimes(1);

Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/components/NativeTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type NativeTitleBarProps = {
onSelectViewMode?: (mode: ViewMode) => unknown;
onCreateMarkdownFile?: () => unknown;
onExitApp?: () => unknown;
onOpenBlankEditorWindow?: () => unknown;
onOpenMarkdown: () => unknown;
onOpenMarkdownFolder?: () => unknown;
onOpenSettings?: () => unknown;
Expand Down Expand Up @@ -148,6 +149,7 @@ export function NativeTitleBar({
onSelectViewMode,
onCreateMarkdownFile,
onExitApp,
onOpenBlankEditorWindow,
onOpenMarkdown,
onOpenMarkdownFolder,
onOpenSettings,
Expand Down Expand Up @@ -718,6 +720,7 @@ export function NativeTitleBar({
renderTitleContent={renderTitleContent}
onCreateMarkdownFile={onCreateMarkdownFile}
onExitApp={onExitApp}
onOpenBlankEditorWindow={onOpenBlankEditorWindow}
onOpenMarkdown={onOpenMarkdown}
onOpenMarkdownFolder={onOpenMarkdownFolder}
onOpenSettings={onOpenSettings}
Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/components/WindowsNativeTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type WindowsNativeTitleBarProps = {
renderTitleContent: (className: string, style?: CSSProperties) => ReactNode;
onCreateMarkdownFile?: () => unknown;
onExitApp?: () => unknown;
onOpenBlankEditorWindow?: () => unknown;
onOpenMarkdown: () => unknown;
onOpenMarkdownFolder?: () => unknown;
onOpenSettings?: () => unknown;
Expand Down Expand Up @@ -124,6 +125,7 @@ export function WindowsNativeTitleBar({
renderTitleContent,
onCreateMarkdownFile,
onExitApp,
onOpenBlankEditorWindow,
onOpenMarkdown,
onOpenMarkdownFolder,
onOpenSettings,
Expand Down Expand Up @@ -193,7 +195,12 @@ export function WindowsNativeTitleBar({

if (menuId === "file") {
return [
contextMenuItem("newDocument", label("menu.newDocument"), "Ctrl+N", onCreateMarkdownFile),
contextMenuItem(
"newDocument",
label("menu.newDocument"),
"Ctrl+N",
onOpenBlankEditorWindow ?? onCreateMarkdownFile
),
contextMenuItem("openDocument", label("menu.openDocument"), "Ctrl+O", onOpenMarkdown),
onOpenMarkdownFolder
? contextMenuItem("openFolder", label("app.openFolderDialog"), "Ctrl+Shift+O", onOpenMarkdownFolder)
Expand Down
67 changes: 63 additions & 4 deletions packages/app/src/hooks/useMarkdownDocument.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4086,7 +4086,7 @@ describe("useMarkdownDocument", () => {

it("opens dropped Markdown files in current workspace tabs when enabled", async () => {
const currentPath = "/mock-files/vault/current.md";
const droppedPath = "/mock-files/external/dropped.md";
const droppedPath = "/mock-files/vault/dropped.md";
const onTreeRootFromFilePath = vi.fn();
mockedReadNativeMarkdownFile.mockImplementation(async (path) => ({
content: path === droppedPath ? "# Dropped" : "# Current",
Expand Down Expand Up @@ -4124,8 +4124,8 @@ describe("useMarkdownDocument", () => {
expect(result.current.tabs.map((tab) => tab.path)).toEqual([currentPath, droppedPath]);
});

it("preserves the current workspace when a file replaces an empty tab", async () => {
const droppedPath = "/mock-files/external/dropped.md";
it("reuses an empty tab for a dropped file in the current workspace", async () => {
const droppedPath = "/mock-files/vault/dropped.md";
const onTreeRootFromFilePath = vi.fn();
mockedReadNativeMarkdownFile.mockResolvedValue({
content: "# Dropped",
Expand Down Expand Up @@ -4161,7 +4161,7 @@ describe("useMarkdownDocument", () => {

it("reuses an empty active tab while retaining other open documents", async () => {
const currentPath = "/mock-files/vault/current.md";
const droppedPath = "/mock-files/external/dropped.md";
const droppedPath = "/mock-files/vault/dropped.md";
const onTreeRootFromFilePath = vi.fn();
mockedReadNativeMarkdownFile.mockImplementation(async (path) => ({
content: path === droppedPath ? "# Dropped" : "# Current",
Expand Down Expand Up @@ -4199,6 +4199,38 @@ describe("useMarkdownDocument", () => {
expect(result.current.tabs.map((tab) => tab.path)).toEqual([currentPath, droppedPath]);
});

it("opens a dropped file outside the current Windows workspace in a new window", async () => {
const droppedPath = "C:\\mock-incoming\\outside.md";
mockedReadNativeMarkdownFile.mockResolvedValue({
content: "# Outside",
name: "outside.md",
path: droppedPath
});
const { result } = renderHook(() =>
useMarkdownDocument({
documentTabsEnabled: true,
getCurrentMarkdown: (fallbackContent) => fallbackContent,
onTreeRootFromFilePath: vi.fn(),
onTreeRootFromFolderPath: vi.fn(),
openDroppedFilesInTabs: true,
preferencesReady: false,
restoreWorkspaceOnStartup: false,
workspaceSourcePath: "C:\\mock-vault"
})
);

await act(async () => {
await result.current.handleDroppedMarkdownPath({
kind: "file",
name: "outside.md",
path: droppedPath
});
});

expect(mockedOpenNativeMarkdownFileInNewWindow).toHaveBeenCalledWith(droppedPath);
expect(mockedReadNativeMarkdownFile).not.toHaveBeenCalledWith(droppedPath);
});

it("establishes a workspace from a file dropped into a fresh window", async () => {
const droppedPath = "/mock-files/vault/dropped.md";
const onTreeRootFromFilePath = vi.fn();
Expand Down Expand Up @@ -4307,6 +4339,33 @@ describe("useMarkdownDocument", () => {
);
});

it("opens a dropped folder outside the current Windows workspace in a new window", async () => {
const folderPath = "C:\\mock-incoming";
const onTreeRootFromFolderPath = vi.fn();
const { result } = renderHook(() =>
useMarkdownDocument({
documentTabsEnabled: true,
getCurrentMarkdown: (fallbackContent) => fallbackContent,
onTreeRootFromFilePath: vi.fn(),
onTreeRootFromFolderPath,
preferencesReady: false,
restoreWorkspaceOnStartup: false,
workspaceSourcePath: "C:\\mock-vault"
})
);

await act(async () => {
await result.current.handleDroppedMarkdownPath({
kind: "folder",
name: "mock-incoming",
path: folderPath
});
});

expect(mockedOpenNativeMarkdownFolderInNewWindow).toHaveBeenCalledWith(folderPath);
expect(onTreeRootFromFolderPath).not.toHaveBeenCalled();
});

it("keeps opening dropped Markdown files in new windows by default", async () => {
const currentPath = "/mock-files/vault/current.md";
const droppedPath = "/mock-files/external/dropped.md";
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/hooks/useMarkdownDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1768,6 +1768,21 @@ export function useMarkdownDocument({

const handleDroppedMarkdownPath = useCallback(
async (target: NativeMarkdownDroppedTarget) => {
const workspaceRootPath = workspaceRootForSource(workspaceSourcePath, documentRef.current.path);
if (
target.kind !== "image" &&
workspaceRootPath &&
!isPathWithinRoot(target.path, workspaceRootPath)
) {
// Keep each window bound to one workspace; tab reuse only applies to paths inside it.
if (target.kind === "folder") {
await openNativeMarkdownFolderInNewWindow(target.path);
} else {
await openNativeMarkdownFileInNewWindow(target.path);
}
return;
}

if (target.kind === "folder") {
if (!isCurrentWindowEmptyUntitled()) {
await openNativeMarkdownFolderInNewWindow(target.path);
Expand Down
19 changes: 19 additions & 0 deletions packages/app/src/hooks/useNativeBindings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,25 @@ describe("useApplicationShortcuts", () => {
expect(openSettings).toHaveBeenCalledTimes(1);
});

it("opens a blank editor window from Ctrl+N on Windows", () => {
const openBlankEditorWindow = vi.fn();
renderHook(() =>
useApplicationShortcuts({
...baseOptions,
openBlankEditorWindow,
platform: "windows"
})
);

const handled = fireEvent.keyDown(window, {
ctrlKey: true,
key: "n"
});

expect(handled).toBe(false);
expect(openBlankEditorWindow).toHaveBeenCalledTimes(1);
});

it("does not treat Control+Meta as the platform modifier for static shortcuts", () => {
const openSettings = vi.fn();
renderHook(() =>
Expand Down
14 changes: 14 additions & 0 deletions packages/app/src/hooks/useNativeBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type ApplicationShortcutOptions = {
openDocument: () => unknown | Promise<unknown>;
openDocumentReplace?: () => unknown | Promise<unknown>;
openDocumentSearch?: () => unknown | Promise<unknown>;
openBlankEditorWindow?: () => unknown | Promise<unknown>;
openSettings?: () => unknown | Promise<unknown>;
openWorkspaceSearch?: () => unknown | Promise<unknown>;
openFolder: () => unknown | Promise<unknown>;
Expand Down Expand Up @@ -435,6 +436,7 @@ export function useApplicationShortcuts({
exportHtml,
exportPdf,
markdownShortcuts,
openBlankEditorWindow,
openDocument,
openDocumentReplace,
openDocumentSearch,
Expand Down Expand Up @@ -519,6 +521,17 @@ export function useApplicationShortcuts({
exportPdf();
}
return;
} else if (
platform === "windows" &&
key === "n" &&
event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
openBlankEditorWindow
) {
event.preventDefault();
event.stopPropagation();
if (!event.repeat) openBlankEditorWindow();
} else if (key === "s" && event.shiftKey) {
event.preventDefault();
saveDocumentAs();
Expand Down Expand Up @@ -552,6 +565,7 @@ export function useApplicationShortcuts({
exportPdf,
closeDocument,
normalizedMarkdownShortcuts,
openBlankEditorWindow,
openDocument,
openDocumentReplace,
openDocumentSearch,
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/lib/tauri/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function hideSettingsWindow() {
return getAppRuntime().window.hideSettingsWindow();
}

export function openBlankEditorWindow() {
return getAppRuntime().window.openBlankEditorWindow();
}

export function listenNativeSettingsWindowTarget(onTarget: (target: NativeSettingsWindowTarget) => unknown) {
return getAppRuntime().window.listenSettingsWindowTarget(onTarget);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export type AppWindowRuntime = {
onCloseRequested: (event: NativeWindowCloseRequestEvent) => unknown | Promise<unknown>
) => Promise<RuntimeCleanup>;
minimizeWindow: () => Promise<unknown>;
openBlankEditorWindow: () => Promise<unknown>;
openExternalUrl: (url: string) => Promise<unknown>;
openSettingsWindow: (target?: NativeSettingsWindowTarget) => Promise<unknown>;
prewarmSettingsWindow: () => Promise<unknown>;
Expand Down Expand Up @@ -593,6 +594,7 @@ export function createDefaultAppRuntime(): AppRuntime {
listenSettingsWindowTarget: async () => () => undefined,
listenWindowCloseRequested: async () => () => undefined,
minimizeWindow: async () => undefined,
openBlankEditorWindow: async () => undefined,
openExternalUrl: async (url) => {
if (typeof window !== "undefined") {
window.open(url, "_blank", "noopener,noreferrer");
Expand Down
Loading