Skip to content

Commit 3ac5779

Browse files
Add markdown preview in new tab via command palette (#430)
* Add markdown preview in new tab via command palette * Fix type error * Fix type error --------- Signed-off-by: DevKoko <[email protected]> Co-authored-by: DevKoko <[email protected]>
1 parent 446ce82 commit 3ac5779

File tree

8 files changed

+120
-45
lines changed

8 files changed

+120
-45
lines changed

src/features/command-palette/components/command-palette.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import KeybindingBadge from "@/ui/keybinding-badge";
3131
import { createAdvancedActions } from "../constants/advanced-actions";
3232
import { createFileActions } from "../constants/file-actions";
3333
import { createGitActions } from "../constants/git-actions";
34+
import { createMarkdownActions } from "../constants/markdown-actions";
3435
import { createNavigationActions } from "../constants/navigation-actions";
3536
import { createSettingsActions } from "../constants/settings-actions";
3637
import { createViewActions } from "../constants/view-actions";
@@ -78,6 +79,7 @@ const CommandPalette = () => {
7879
const { showToast } = useToast();
7980
const buffers = useBufferStore.use.buffers();
8081
const activeBufferId = useBufferStore.use.activeBufferId();
82+
const activeBuffer = buffers.find((b) => b.id === activeBufferId) || null;
8183
const {
8284
closeBuffer,
8385
setActiveBuffer,
@@ -86,9 +88,23 @@ const CommandPalette = () => {
8688
reopenClosedTab,
8789
} = useBufferStore.use.actions();
8890
const { zoomIn, zoomOut, resetZoom } = useZoomStore.use.actions();
91+
const { openBuffer } = useBufferStore.use.actions();
92+
93+
// Helper function to check if the active buffer is a markdown file
94+
const isMarkdownFile = () => {
95+
if (!activeBuffer) return false;
96+
const extension = activeBuffer.path.split(".").pop()?.toLowerCase();
97+
return extension === "md" || extension === "markdown";
98+
};
8999

90100
// Create all actions using factory functions
91101
const allActions: Action[] = [
102+
...createMarkdownActions({
103+
isMarkdownFile: isMarkdownFile(),
104+
activeBuffer,
105+
openBuffer,
106+
onClose,
107+
}),
92108
...createViewActions({
93109
isSidebarVisible,
94110
setIsSidebarVisible,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Eye } from "lucide-react";
2+
import type { Buffer } from "@/features/editor/stores/buffer-store";
3+
import type { Action } from "../models/action.types";
4+
5+
interface MarkdownActionsParams {
6+
isMarkdownFile: boolean;
7+
activeBuffer: Buffer | null;
8+
openBuffer: (
9+
path: string,
10+
name: string,
11+
content: string,
12+
isImage?: boolean,
13+
isSQLite?: boolean,
14+
isDiff?: boolean,
15+
isVirtual?: boolean,
16+
diffData?: any,
17+
isMarkdownPreview?: boolean,
18+
sourceFilePath?: string,
19+
) => string;
20+
onClose: () => void;
21+
}
22+
23+
export const createMarkdownActions = (params: MarkdownActionsParams): Action[] => {
24+
const { isMarkdownFile, activeBuffer, openBuffer, onClose } = params;
25+
26+
if (!isMarkdownFile || !activeBuffer) {
27+
return [];
28+
}
29+
30+
return [
31+
{
32+
id: "markdown-preview",
33+
label: "Markdown: Preview Markdown",
34+
description: "Open markdown preview in a new tab",
35+
icon: <Eye size={14} />,
36+
category: "Markdown",
37+
action: () => {
38+
// Create a virtual path for the preview
39+
const previewPath = `${activeBuffer.path}:preview`;
40+
const previewName = `${activeBuffer.name} (Preview)`;
41+
42+
// Open a new buffer for the preview
43+
openBuffer(
44+
previewPath,
45+
previewName,
46+
activeBuffer.content,
47+
false, // isImage
48+
false, // isSQLite
49+
false, // isDiff
50+
true, // isVirtual
51+
undefined, // diffData
52+
true, // isMarkdownPreview
53+
activeBuffer.path, // sourceFilePath
54+
);
55+
onClose();
56+
},
57+
},
58+
];
59+
};

src/features/command-palette/models/action.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ export interface Action {
1010
action: () => void;
1111
}
1212

13-
export type ActionCategory = "View" | "Settings" | "File" | "Window" | "Navigation";
13+
export type ActionCategory = "View" | "Settings" | "File" | "Window" | "Navigation" | "Markdown";

src/features/editor/components/code-editor.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { useAppStore } from "@/stores/app-store";
1313
import { useZoomStore } from "@/stores/zoom-store";
1414
import { HoverTooltip } from "../lsp/hover-tooltip";
1515
import { MarkdownPreview } from "../markdown/markdown-preview";
16-
import { isMarkdownFile } from "../utils/lines";
1716
import { Editor } from "./editor";
1817
import { EditorStylesheet } from "./stylesheet";
1918
import Breadcrumb from "./toolbar/breadcrumb";
@@ -41,7 +40,6 @@ const CodeEditor = ({ className }: CodeEditorProps) => {
4140
const { setRefs, setContent, setFileInfo } = useEditorStateStore.use.actions();
4241
// No longer need to sync content - editor-view-store computes from buffer
4342
const { setDisabled } = useEditorSettingsStore.use.actions();
44-
const isMarkdownPreview = useEditorSettingsStore.use.isMarkdownPreview();
4543

4644
const buffers = useBufferStore.use.buffers();
4745
const activeBufferId = useBufferStore.use.activeBufferId();
@@ -61,8 +59,7 @@ const CodeEditor = ({ className }: CodeEditorProps) => {
6159
const filePath = activeBuffer?.path || "";
6260
const onChange = activeBuffer ? handleContentChange : () => {};
6361

64-
const showMarkdownPreview =
65-
activeBuffer && isMarkdownFile(activeBuffer.path) && isMarkdownPreview;
62+
const showMarkdownPreview = activeBuffer?.isMarkdownPreview || false;
6663

6764
// Initialize refs in store
6865
useEffect(() => {

src/features/editor/components/toolbar/breadcrumb.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { ArrowLeft, ChevronRight, Eye, Search, Sparkles } from "lucide-react";
1+
import { ArrowLeft, ChevronRight, Search, Sparkles } from "lucide-react";
22
import { type RefObject, useRef, useState } from "react";
33
import { createPortal } from "react-dom";
44
import { useEventListener, useOnClickOutside } from "usehooks-ts";
55
import { EDITOR_CONSTANTS } from "@/features/editor/config/constants";
66
import { useBufferStore } from "@/features/editor/stores/buffer-store";
7-
import { useEditorSettingsStore } from "@/features/editor/stores/settings-store";
87
import { useEditorStateStore } from "@/features/editor/stores/state-store";
98
import { logger } from "@/features/editor/utils/logger";
109
import FileIcon from "@/features/file-explorer/views/file.icon";
@@ -28,8 +27,6 @@ export default function Breadcrumb() {
2827
const activeBuffer = buffers.find((b) => b.id === activeBufferId) || null;
2928
const { rootFolderPath, handleFileSelect } = useFileSystemStore();
3029
const { isFindVisible, setIsFindVisible } = useUIState();
31-
const isMarkdownPreview = useEditorSettingsStore.use.isMarkdownPreview();
32-
const { setIsMarkdownPreview } = useEditorSettingsStore.use.actions();
3330
const { toggle: toggleInlineEditToolbar } = useInlineEditToolbarStore.use.actions();
3431
const selection = useEditorStateStore.use.selection?.();
3532

@@ -45,22 +42,12 @@ export default function Breadcrumb() {
4542
setIsFindVisible(!isFindVisible);
4643
};
4744

48-
const handlePreviewClick = () => {
49-
setIsMarkdownPreview(!isMarkdownPreview);
50-
};
51-
5245
const handleInlineEditClick = () => {
5346
toggleInlineEditToolbar();
5447
};
5548

5649
const hasSelection = selection && selection.start.offset !== selection.end.offset;
5750

58-
const isMarkdownFile = () => {
59-
if (!activeBuffer) return false;
60-
const extension = activeBuffer.path.split(".").pop()?.toLowerCase();
61-
return extension === "md" || extension === "markdown";
62-
};
63-
6451
const filePath = activeBuffer?.path || "";
6552
const rootPath = rootFolderPath;
6653
const onNavigate = handleNavigate;
@@ -232,17 +219,6 @@ export default function Breadcrumb() {
232219
))}
233220
</div>
234221
<div className="flex items-center gap-1">
235-
{isMarkdownFile() && (
236-
<button
237-
onClick={handlePreviewClick}
238-
className={`flex h-5 w-5 items-center justify-center rounded transition-colors hover:bg-hover hover:text-text ${
239-
isMarkdownPreview ? "bg-hover text-accent" : "text-text-lighter"
240-
}`}
241-
title="Toggle markdown preview"
242-
>
243-
<Eye size={12} />
244-
</button>
245-
)}
246222
<button
247223
onClick={handleInlineEditClick}
248224
disabled={!hasSelection}

src/features/editor/markdown/markdown-preview.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ export function MarkdownPreview() {
2020
const [html, setHtml] = useState("");
2121
const containerRef = useRef<HTMLDivElement>(null);
2222

23+
// Get the source buffer if this is a preview buffer
24+
const sourceBuffer = activeBuffer?.sourceFilePath
25+
? buffers.find((b) => b.path === activeBuffer.sourceFilePath)
26+
: activeBuffer;
27+
2328
useEffect(() => {
24-
if (!activeBuffer) return;
25-
const parsedHtml = parseMarkdown(activeBuffer.content);
29+
if (!sourceBuffer) return;
30+
const parsedHtml = parseMarkdown(sourceBuffer.content);
2631
setHtml(parsedHtml);
27-
}, [activeBuffer?.content, activeBuffer]);
32+
}, [sourceBuffer?.content, sourceBuffer]);
2833

2934
const resolvePath = useCallback(
3035
(href: string, currentFilePath: string): string => {
@@ -98,9 +103,9 @@ export function MarkdownPreview() {
98103
return;
99104
}
100105

101-
if (!activeBuffer) return;
106+
if (!sourceBuffer) return;
102107

103-
const targetPath = resolvePath(href, activeBuffer.path);
108+
const targetPath = resolvePath(href, sourceBuffer.path);
104109

105110
try {
106111
const fileExists = await exists(targetPath);
@@ -121,7 +126,7 @@ export function MarkdownPreview() {
121126
logger.error("MarkdownPreview", "Failed to handle link:", error);
122127
}
123128
},
124-
[activeBuffer, handleFileSelect, resolvePath],
129+
[sourceBuffer, handleFileSelect, resolvePath],
125130
);
126131

127132
return (

src/features/editor/stores/buffer-store.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { GitDiff } from "@/features/version-control/git/types/git";
1313
import { useSessionStore } from "@/stores/session-store";
1414
import { createSelectors } from "@/utils/zustand-selectors";
1515

16-
interface Buffer {
16+
export interface Buffer {
1717
id: string;
1818
path: string;
1919
name: string;
@@ -24,11 +24,14 @@ interface Buffer {
2424
isImage: boolean;
2525
isSQLite: boolean;
2626
isDiff: boolean;
27+
isMarkdownPreview: boolean;
2728
isExternalEditor: boolean;
2829
isActive: boolean;
2930
language?: string; // File language for syntax highlighting and formatting
3031
// For diff buffers, store the parsed diff data (single or multi-file)
3132
diffData?: GitDiff | MultiFileDiff;
33+
// For markdown preview buffers, store the source file path
34+
sourceFilePath?: string;
3235
// For external editor buffers, store the terminal connection ID
3336
terminalConnectionId?: string;
3437
// Cached syntax highlighting tokens
@@ -71,6 +74,8 @@ interface BufferActions {
7174
isDiff?: boolean,
7275
isVirtual?: boolean,
7376
diffData?: GitDiff | MultiFileDiff,
77+
isMarkdownPreview?: boolean,
78+
sourceFilePath?: string,
7479
) => string;
7580
openExternalEditorBuffer: (path: string, name: string, terminalConnectionId: string) => string;
7681
closeBuffer: (bufferId: string) => void;
@@ -126,7 +131,15 @@ const saveSessionToStore = (buffers: Buffer[], activeBufferId: string | null) =>
126131

127132
// Only save real files, not virtual/diff/image/sqlite/external editor buffers
128133
const persistableBuffers = buffers
129-
.filter((b) => !b.isVirtual && !b.isDiff && !b.isImage && !b.isSQLite && !b.isExternalEditor)
134+
.filter(
135+
(b) =>
136+
!b.isVirtual &&
137+
!b.isDiff &&
138+
!b.isImage &&
139+
!b.isSQLite &&
140+
!b.isMarkdownPreview &&
141+
!b.isExternalEditor,
142+
)
130143
.map((b) => ({
131144
path: b.path,
132145
name: b.name,
@@ -141,6 +154,7 @@ const saveSessionToStore = (buffers: Buffer[], activeBufferId: string | null) =>
141154
!activeBuffer.isDiff &&
142155
!activeBuffer.isImage &&
143156
!activeBuffer.isSQLite &&
157+
!activeBuffer.isMarkdownPreview &&
144158
!activeBuffer.isExternalEditor
145159
? activeBuffer.path
146160
: null;
@@ -167,6 +181,8 @@ export const useBufferStore = createSelectors(
167181
isDiff = false,
168182
isVirtual = false,
169183
diffData?: GitDiff | MultiFileDiff,
184+
isMarkdownPreview = false,
185+
sourceFilePath?: string,
170186
) => {
171187
const { buffers, maxOpenTabs } = get();
172188

@@ -202,10 +218,12 @@ export const useBufferStore = createSelectors(
202218
isImage,
203219
isSQLite,
204220
isDiff,
221+
isMarkdownPreview,
205222
isExternalEditor: false,
206223
isActive: true,
207224
language: detectLanguageFromFileName(name),
208225
diffData,
226+
sourceFilePath,
209227
tokens: [],
210228
};
211229

@@ -214,8 +232,8 @@ export const useBufferStore = createSelectors(
214232
state.activeBufferId = newBuffer.id;
215233
});
216234

217-
// Track in recent files (only for real files, not virtual/diff buffers)
218-
if (!isVirtual && !isDiff && !isImage && !isSQLite) {
235+
// Track in recent files (only for real files, not virtual/diff/markdown preview buffers)
236+
if (!isVirtual && !isDiff && !isImage && !isSQLite && !isMarkdownPreview) {
219237
useRecentFilesStore.getState().addOrUpdateRecentFile(path, name);
220238

221239
// Check if extension is available and start LSP or prompt installation
@@ -333,6 +351,7 @@ export const useBufferStore = createSelectors(
333351
isImage: false,
334352
isSQLite: false,
335353
isDiff: false,
354+
isMarkdownPreview: false,
336355
isExternalEditor: true,
337356
isActive: true,
338357
language: detectLanguageFromFileName(name),
@@ -392,6 +411,7 @@ export const useBufferStore = createSelectors(
392411
!closedBuffer.isDiff &&
393412
!closedBuffer.isImage &&
394413
!closedBuffer.isSQLite &&
414+
!closedBuffer.isMarkdownPreview &&
395415
!closedBuffer.isExternalEditor
396416
) {
397417
// Stop LSP for this file in background (don't block buffer closing)
@@ -690,7 +710,13 @@ export const useBufferStore = createSelectors(
690710

691711
reloadBufferFromDisk: async (bufferId: string): Promise<void> => {
692712
const buffer = get().buffers.find((b) => b.id === bufferId);
693-
if (!buffer || buffer.isVirtual || buffer.isImage || buffer.isSQLite) {
713+
if (
714+
!buffer ||
715+
buffer.isVirtual ||
716+
buffer.isImage ||
717+
buffer.isSQLite ||
718+
buffer.isMarkdownPreview
719+
) {
694720
return;
695721
}
696722

src/features/editor/stores/settings-store.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ interface EditorSettingsState {
1111
lineNumbers: boolean;
1212
disabled: boolean;
1313
theme: string;
14-
isMarkdownPreview: boolean;
1514
actions: EditorSettingsActions;
1615
}
1716

@@ -23,7 +22,6 @@ interface EditorSettingsActions {
2322
setLineNumbers: (show: boolean) => void;
2423
setDisabled: (disabled: boolean) => void;
2524
setTheme: (theme: string) => void;
26-
setIsMarkdownPreview: (preview: boolean) => void;
2725
}
2826

2927
export const useEditorSettingsStore = createSelectors(
@@ -36,7 +34,6 @@ export const useEditorSettingsStore = createSelectors(
3634
lineNumbers: true,
3735
disabled: false,
3836
theme: "auto",
39-
isMarkdownPreview: false,
4037
actions: {
4138
setFontSize: (size) => set({ fontSize: size }),
4239
setFontFamily: (family) => set({ fontFamily: family }),
@@ -45,7 +42,6 @@ export const useEditorSettingsStore = createSelectors(
4542
setLineNumbers: (show) => set({ lineNumbers: show }),
4643
setDisabled: (disabled) => set({ disabled }),
4744
setTheme: (theme) => set({ theme }),
48-
setIsMarkdownPreview: (preview) => set({ isMarkdownPreview: preview }),
4945
},
5046
})),
5147
),

0 commit comments

Comments
 (0)