Skip to content
Open
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
40 changes: 32 additions & 8 deletions src/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ export function Editor({
const pinNote = notesCtx?.pinNote;
const unpinNote = notesCtx?.unpinNote;
const notes = notesCtx?.notes;
const { textDirection } = useTheme();
const { textDirection, pasteMode } = useTheme();
const [isSaving, setIsSaving] = useState(false);
// Force re-render when selection changes to update toolbar active states
const [, setSelectionKey] = useState(0);
Expand Down Expand Up @@ -1213,22 +1213,46 @@ export function Editor({
}
}

// Handle markdown text paste
const text = clipboardData.getData("text/plain");
if (!text) return false;

// Check if text looks like markdown (has common markdown patterns)
const currentEditor = editorRef.current;
if (!currentEditor) return false;

// Paste as plain text — strip all markdown formatting
if (pasteMode === "plain") {
currentEditor.commands.insertContent(
text.replace(/\r\n/g, "\n"),
{ parseOptions: { preserveWhitespace: "full" } },
);
return true;
}

// Paste as code block wrapped in ``` ```
if (pasteMode === "code-block") {
const manager = currentEditor.storage.markdown?.manager;
if (manager && typeof manager.parse === "function") {
try {
const fenced = "```\n" + text + "\n```";
const parsed = manager.parse(fenced);
if (parsed) {
currentEditor.commands.insertContent(parsed);
return true;
}
} catch {
// fall through to default
}
}
return false;
}
Comment on lines +1232 to +1247

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guarantee code-block mode even when markdown parse fails.

If parse fails (or manager is unavailable), this returns false, so default paste behavior runs instead of code-block mode. Add a deterministic fallback that inserts a codeBlock node and still returns true.

Suggested fix
         if (pasteMode === "code-block") {
+          const normalized = text.replace(/\r\n/g, "\n");
           const manager = currentEditor.storage.markdown?.manager;
           if (manager && typeof manager.parse === "function") {
             try {
-              const fenced = "```\n" + text + "\n```";
+              const fenced = "```\n" + normalized + "\n```";
               const parsed = manager.parse(fenced);
               if (parsed) {
                 currentEditor.commands.insertContent(parsed);
                 return true;
               }
             } catch {
               // fall through to default
             }
           }
-          return false;
+          currentEditor
+            .chain()
+            .focus()
+            .insertContent({
+              type: "codeBlock",
+              attrs: { language: null },
+              content: normalized ? [{ type: "text", text: normalized }] : [],
+            })
+            .run();
+          return true;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (pasteMode === "code-block") {
const manager = currentEditor.storage.markdown?.manager;
if (manager && typeof manager.parse === "function") {
try {
const fenced = "```\n" + text + "\n```";
const parsed = manager.parse(fenced);
if (parsed) {
currentEditor.commands.insertContent(parsed);
return true;
}
} catch {
// fall through to default
}
}
return false;
}
if (pasteMode === "code-block") {
const normalized = text.replace(/\r\n/g, "\n");
const manager = currentEditor.storage.markdown?.manager;
if (manager && typeof manager.parse === "function") {
try {
const fenced = "
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/editor/Editor.tsx` around lines 1232 - 1247, When pasteMode
=== "code-block", ensure a deterministic fallback inserts a codeBlock and
returns true even if manager is missing or manager.parse throws; if parsing
succeeds keep existing behavior (currentEditor.commands.insertContent(parsed));
otherwise normalize the pasted text (use a variable e.g. normalized =
text.replace(/\r\n?/g, '\n') or similar), then call
currentEditor.chain().focus().insertContent({ type: "codeBlock", attrs: {
language: null }, content: normalized ? [{ type: "text", text: normalized }] :
[] }).run(); finally return true so paste handling stays in code-block mode
instead of falling back to default.


// Default: "markdown" — detect and parse markdown patterns
const markdownPatterns =
/^#{1,6}\s|^\s*[-*+]\s|^\s*\d+\.\s|^\s*>\s|```|^\s*\[.*\]\(.*\)|^\s*!\[|\*\*.*\*\*|__.*__|~~.*~~|^\s*[-*_]{3,}\s*$|^\|.+\||\$\$[\s\S]+?\$\$/m;
if (!markdownPatterns.test(text)) {
// Not markdown, let TipTap handle it normally
return false;
}

// Parse markdown and insert using editor ref
const currentEditor = editorRef.current;
if (!currentEditor) return false;

const manager = currentEditor.storage.markdown?.manager;
if (manager && typeof manager.parse === "function") {
try {
Expand Down
39 changes: 39 additions & 0 deletions src/components/settings/EditorSettingsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
TextDirection,
EditorWidth,
ThemeColorKey,
PasteMode,
} from "../../types/note";
import { ChevronRightIcon, EyeIcon, MinusIcon, PlusIcon } from "../icons";
import { cn } from "../../lib/utils";
Expand Down Expand Up @@ -48,6 +49,13 @@ const fontFamilyOptions: { value: FontFamily; label: string }[] = [
{ value: "monospace", label: "Mono" },
];

// Paste mode options
const pasteModeOptions: { value: PasteMode; label: string; description: string }[] = [
{ value: "markdown", label: "Markdown", description: "Parse and render markdown syntax" },
{ value: "plain", label: "Plain text", description: "Insert as plain text, no formatting" },
{ value: "code-block", label: "Code block", description: "Wrap in a ``` code block" },
];

// Bold weight options (medium excluded for monospace)
const boldWeightOptions = [
{ value: 500, label: "Medium", excludeForMonospace: true },
Expand Down Expand Up @@ -77,6 +85,8 @@ export function AppearanceSettingsSection() {
setCustomColor,
resetCustomColor,
resetAllCustomColors,
pasteMode,
setPasteMode,
} = useTheme();

// Validated numeric change handler
Expand Down Expand Up @@ -458,6 +468,35 @@ export function AppearanceSettingsSection() {
<div className="absolute bottom-0 left-0 right-0 h-40 bg-linear-to-t from-bg to-transparent pointer-events-none" />
</div>
</section>

{/* Divider */}
<div className="border-t border-border border-dashed" />

{/* Editing Section */}
<section className="pb-2">
<h2 className="text-xl font-medium mb-3">Editing</h2>
<div className="rounded-[10px] border border-border pl-4 py-3 pr-3 space-y-2">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-0.5">
<label className="text-sm text-text font-medium">Paste mode</label>
<span className="text-xs text-text-muted">
{pasteModeOptions.find((o) => o.value === pasteMode)?.description}
</span>
</div>
<Select
value={pasteMode}
onChange={(e) => setPasteMode(e.target.value as PasteMode)}
className="w-40"
>
{pasteModeOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
</div>
</div>
</section>
</div>
);
}
Expand Down
23 changes: 23 additions & 0 deletions src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
EditorWidth,
CustomColors,
ThemeColorKey,
PasteMode,
} from "../types/note";

type ThemeMode = "light" | "dark" | "system";
Expand Down Expand Up @@ -117,6 +118,8 @@ interface ThemeContextType {
setCustomColor: (mode: "light" | "dark", key: ThemeColorKey, value: string) => void;
resetCustomColor: (mode: "light" | "dark", key: ThemeColorKey) => void;
resetAllCustomColors: (mode: "light" | "dark") => void;
pasteMode: PasteMode;
setPasteMode: (mode: PasteMode) => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);
Expand Down Expand Up @@ -189,6 +192,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
);
const [customColorsLight, setCustomColorsLightState] = useState<CustomColors>({});
const [customColorsDark, setCustomColorsDarkState] = useState<CustomColors>({});
const [pasteMode, setPasteModeState] = useState<PasteMode>("markdown");
const [isInitialized, setIsInitialized] = useState(false);

const [systemTheme, setSystemTheme] = useState<"light" | "dark">(() => {
Expand Down Expand Up @@ -248,6 +252,13 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
if (settings.customColorsDark) {
setCustomColorsDarkState(settings.customColorsDark);
}
if (
settings.pasteMode === "markdown" ||
settings.pasteMode === "plain" ||
settings.pasteMode === "code-block"
) {
setPasteModeState(settings.pasteMode);
}
} catch {
// If settings can't be loaded, use defaults
}
Expand Down Expand Up @@ -544,6 +555,16 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
[],
);

const setPasteMode = useCallback(async (mode: PasteMode) => {
setPasteModeState(mode);
try {
const settings = await getSettings();
await updateSettings({ ...settings, pasteMode: mode });
} catch (error) {
Comment on lines +558 to +563

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid read-modify-write overwrites when persisting pasteMode.

This setter reads the full settings object and writes it back with one field changed. Concurrent setting updates can clobber each other and drop unrelated fields. Prefer an atomic backend patch command (e.g., update only pasteMode) or a versioned/serialized write strategy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 558 - 563, The setPasteMode setter
currently does a read-modify-write by calling getSettings() then
updateSettings({...settings, pasteMode: mode}), which can clobber concurrent
changes; change setPasteMode (and related callers) to persist only the changed
field atomically by calling an API that accepts a partial update (e.g.,
updateSettings({ pasteMode: mode }) or a new updateSettingField('pasteMode',
mode)) instead of spreading the full settings object, or implement a
versioned/conditional write in updateSettings to prevent lost updates; keep
setPasteModeState(mode) locally, await the atomic patch/field-update call (or
handle version conflicts) and preserve the existing error handling in the catch
block.

console.error("Failed to save paste mode:", error);
}
}, []);

// Live CSS variable update during drag (no persistence)
const setEditorMaxWidthLive = useCallback((value: string) => {
document.documentElement.style.setProperty("--editor-max-width", value);
Expand Down Expand Up @@ -579,6 +600,8 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
setCustomColor,
resetCustomColor,
resetAllCustomColors,
pasteMode,
setPasteMode,
}}
>
{children}
Expand Down
2 changes: 2 additions & 0 deletions src/types/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ThemeSettings {
export type FontFamily = "system-sans" | "serif" | "monospace";
export type TextDirection = "auto" | "ltr" | "rtl";
export type EditorWidth = "narrow" | "normal" | "wide" | "full" | "custom";
export type PasteMode = "markdown" | "plain" | "code-block";

export interface EditorFontSettings {
baseFontFamily?: FontFamily;
Expand Down Expand Up @@ -59,6 +60,7 @@ export interface Settings {
ignoredPatterns?: string[];
customColorsLight?: CustomColors;
customColorsDark?: CustomColors;
pasteMode?: PasteMode;
}

export interface FolderNode {
Expand Down