Skip to content
2 changes: 2 additions & 0 deletions src/features/command-palette/constants/markdown-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface MarkdownActionsParams {
isVirtual?: boolean,
diffData?: any,
isMarkdownPreview?: boolean,
isHtmlPreview?: boolean,
sourceFilePath?: string,
) => string;
onClose: () => void;
Expand Down Expand Up @@ -50,6 +51,7 @@ export const createMarkdownActions = (params: MarkdownActionsParams): Action[] =
true, // isVirtual
undefined, // diffData
true, // isMarkdownPreview
false, // isHtmlPreview
activeBuffer.path, // sourceFilePath
);
onClose();
Expand Down
4 changes: 4 additions & 0 deletions src/features/editor/components/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { HoverTooltip } from "../lsp/hover-tooltip";
import { MarkdownPreview } from "../markdown/markdown-preview";
import { ScrollDebugOverlay } from "./debug/scroll-debug-overlay";
import { Editor } from "./editor";
import { HtmlPreview } from "./html/html-preview";
import { EditorStylesheet } from "./stylesheet";
import Breadcrumb from "./toolbar/breadcrumb";
import FindBar from "./toolbar/find-bar";
Expand Down Expand Up @@ -58,6 +59,7 @@ const CodeEditor = ({ className }: CodeEditorProps) => {
const onChange = activeBuffer ? handleContentChange : () => {};

const showMarkdownPreview = activeBuffer?.isMarkdownPreview || false;
const showHtmlPreview = activeBuffer?.isHtmlPreview || false;

// Initialize refs in store
useEffect(() => {
Expand Down Expand Up @@ -279,6 +281,8 @@ const CodeEditor = ({ className }: CodeEditorProps) => {
<div className="absolute inset-0 bg-primary-bg">
{showMarkdownPreview ? (
<MarkdownPreview />
) : showHtmlPreview ? (
<HtmlPreview />
) : (
<Editor
onMouseMove={hoverHandlers.handleHover}
Expand Down
78 changes: 78 additions & 0 deletions src/features/editor/components/html/html-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { convertFileSrc } from "@tauri-apps/api/core";
import { useEffect, useMemo, useRef, useState } from "react";
import { useBufferStore } from "@/features/editor/stores/buffer-store";

export function HtmlPreview() {
const buffers = useBufferStore.use.buffers();
const activeBufferId = useBufferStore.use.activeBufferId();
const activeBuffer = buffers.find((b) => b.id === activeBufferId);

// If this is a preview buffer, find the source buffer
const sourceBuffer = activeBuffer?.sourceFilePath
? buffers.find((b) => b.path === activeBuffer.sourceFilePath)
: activeBuffer;

const [iframeContent, setIframeContent] = useState("");
const containerRef = useRef<HTMLDivElement>(null);

// Memoize the asset URL for the directory
const assetBaseUrl = useMemo(() => {
if (!sourceBuffer?.path) return "";

// Get directory path
const lastSlashIndex = sourceBuffer.path.lastIndexOf("/");
const dirPath =
lastSlashIndex !== -1 ? sourceBuffer.path.substring(0, lastSlashIndex) : sourceBuffer.path;

// Convert to Tauri asset URL
// convertFileSrc handles the protocol logic (e.g. asset:// or http://asset.localhost)
const url = convertFileSrc(dirPath);

// Ensure it ends with slash for correct base resolution
return url.endsWith("/") ? url : `${url}/`;
}, [sourceBuffer?.path]);

useEffect(() => {
if (!sourceBuffer) return;

let content = sourceBuffer.content;

// Inject <base> tag to allow relative links (CSS/JS/Images) to work
if (assetBaseUrl) {
const baseTag = `<base href="${assetBaseUrl}">`;

// Try to inject in head
if (content.includes("<head>")) {
content = content.replace("<head>", `<head>\n${baseTag}`);
} else if (content.includes("<html>")) {
content = content.replace("<html>", `<html>\n<head>${baseTag}</head>`);
} else {
// No head/html tags, just prepend
content = `${baseTag}\n${content}`;
}
}

// Add script to handle errors and console logs if needed in future
// For now keeping it simple with just content rendering
setIframeContent(content);
}, [sourceBuffer?.content, assetBaseUrl]);

if (!sourceBuffer) {
return (
<div className="flex h-full items-center justify-center text-text-lighter">
No active buffer
</div>
);
}

return (
<div ref={containerRef} className="html-preview h-full w-full bg-white">
<iframe
title="HTML Preview"
srcDoc={iframeContent}
className="h-full w-full border-none"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
);
}
17 changes: 14 additions & 3 deletions src/features/editor/components/toolbar/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,22 @@ export default function Breadcrumb() {
return extension === "md" || extension === "markdown";
};

const isHtmlFile = () => {
if (!activeBuffer) return false;
const extension = activeBuffer.path.split(".").pop()?.toLowerCase();
return extension === "html" || extension === "htm";
};

const handlePreviewClick = () => {
if (!activeBuffer || activeBuffer.isMarkdownPreview) return;
if (!activeBuffer || activeBuffer.isMarkdownPreview || activeBuffer.isHtmlPreview) return;

const { openBuffer } = useBufferStore.getState().actions;
const previewPath = `${activeBuffer.path}:preview`;
const previewName = `${activeBuffer.name} (Preview)`;

const isMarkdown = isMarkdownFile();
const isHtml = isHtmlFile();

openBuffer(
previewPath,
previewName,
Expand All @@ -70,7 +79,8 @@ export default function Breadcrumb() {
false, // isDiff
true, // isVirtual
undefined, // diffData
true, // isMarkdownPreview
isMarkdown, // isMarkdownPreview
isHtml, // isHtmlPreview
activeBuffer.path, // sourceFilePath
);
};
Expand Down Expand Up @@ -246,7 +256,8 @@ export default function Breadcrumb() {
))}
</div>
<div className="flex items-center gap-1">
{isMarkdownFile() && !activeBuffer?.isMarkdownPreview && (
{((isMarkdownFile() && !activeBuffer?.isMarkdownPreview) ||
(isHtmlFile() && !activeBuffer?.isHtmlPreview)) && (
<button
onClick={handlePreviewClick}
className="flex h-5 w-5 items-center justify-center rounded text-text-lighter transition-colors hover:bg-hover hover:text-text"
Expand Down
27 changes: 25 additions & 2 deletions src/features/editor/stores/buffer-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Buffer {
isSQLite: boolean;
isDiff: boolean;
isMarkdownPreview: boolean;
isHtmlPreview: boolean;
isExternalEditor: boolean;
isWebViewer: boolean;
isPullRequest: boolean;
Expand Down Expand Up @@ -82,6 +83,7 @@ interface BufferActions {
isVirtual?: boolean,
diffData?: GitDiff | MultiFileDiff,
isMarkdownPreview?: boolean,
isHtmlPreview?: boolean,
sourceFilePath?: string,
isPreview?: boolean,
) => string;
Expand Down Expand Up @@ -149,6 +151,7 @@ const saveSessionToStore = (buffers: Buffer[], activeBufferId: string | null) =>
!b.isImage &&
!b.isSQLite &&
!b.isMarkdownPreview &&
!b.isHtmlPreview &&
!b.isExternalEditor &&
!b.isWebViewer &&
!b.isPullRequest,
Expand All @@ -168,6 +171,7 @@ const saveSessionToStore = (buffers: Buffer[], activeBufferId: string | null) =>
!activeBuffer.isImage &&
!activeBuffer.isSQLite &&
!activeBuffer.isMarkdownPreview &&
!activeBuffer.isHtmlPreview &&
!activeBuffer.isExternalEditor &&
!activeBuffer.isWebViewer &&
!activeBuffer.isPullRequest
Expand Down Expand Up @@ -197,14 +201,21 @@ export const useBufferStore = createSelectors(
isVirtual = false,
diffData?: GitDiff | MultiFileDiff,
isMarkdownPreview = false,
isHtmlPreview = false,
sourceFilePath?: string,
isPreview = true,
) => {
const { buffers, maxOpenTabs } = get();

// Special buffers should never be in preview mode
const shouldBePreview =
isPreview && !isImage && !isSQLite && !isDiff && !isVirtual && !isMarkdownPreview;
isPreview &&
!isImage &&
!isSQLite &&
!isDiff &&
!isVirtual &&
!isMarkdownPreview &&
!isHtmlPreview;

// Check if already open
const existing = buffers.find((b) => b.path === path);
Expand Down Expand Up @@ -252,6 +263,7 @@ export const useBufferStore = createSelectors(
isSQLite,
isDiff,
isMarkdownPreview,
isHtmlPreview,
isExternalEditor: false,
isWebViewer: false,
isPullRequest: false,
Expand All @@ -268,7 +280,14 @@ export const useBufferStore = createSelectors(
});

// Track in recent files (only for real files, not virtual/diff/markdown preview buffers)
if (!isVirtual && !isDiff && !isImage && !isSQLite && !isMarkdownPreview) {
if (
!isVirtual &&
!isDiff &&
!isImage &&
!isSQLite &&
!isMarkdownPreview &&
!isHtmlPreview
) {
useRecentFilesStore.getState().addOrUpdateRecentFile(path, name);

// Check if extension is available and start LSP or prompt installation
Expand Down Expand Up @@ -408,6 +427,7 @@ export const useBufferStore = createSelectors(
isSQLite: false,
isDiff: false,
isMarkdownPreview: false,
isHtmlPreview: false,
isExternalEditor: true,
isWebViewer: false,
isPullRequest: false,
Expand Down Expand Up @@ -480,6 +500,7 @@ export const useBufferStore = createSelectors(
isSQLite: false,
isDiff: false,
isMarkdownPreview: false,
isHtmlPreview: false,
isExternalEditor: false,
isWebViewer: true,
isPullRequest: false,
Expand Down Expand Up @@ -536,6 +557,7 @@ export const useBufferStore = createSelectors(
isSQLite: false,
isDiff: false,
isMarkdownPreview: false,
isHtmlPreview: false,
isExternalEditor: false,
isWebViewer: false,
isPullRequest: true,
Expand Down Expand Up @@ -594,6 +616,7 @@ export const useBufferStore = createSelectors(
!closedBuffer.isImage &&
!closedBuffer.isSQLite &&
!closedBuffer.isMarkdownPreview &&
!closedBuffer.isHtmlPreview &&
!closedBuffer.isExternalEditor &&
!closedBuffer.isWebViewer
) {
Expand Down
1 change: 1 addition & 0 deletions src/features/file-system/controllers/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export const useFileSystemStore = createSelectors(
false,
undefined,
undefined,
false,
undefined,
isPreview,
);
Expand Down