Skip to content
Closed
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
4 changes: 4 additions & 0 deletions apps/desktop/src/main2/home/note-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@
background-repeat: no-repeat;
background-size: contain;
}

.main2-daily-note-editor .ProseMirror .is-empty::before {
color: #b5b5b5;
}
27 changes: 27 additions & 0 deletions apps/desktop/src/main2/home/note-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type JSONContent,
NoteEditor,
type NoteEditorRef,
type PlaceholderFunction,
schema,
} from "@hypr/editor/note";
import { useTaskStorageOptional } from "@hypr/editor/task-storage";
Expand Down Expand Up @@ -41,6 +42,31 @@ type Store = NonNullable<ReturnType<typeof main.UI.useStore>>;
const emptyDoc: JSONContent = { type: "doc", content: [{ type: "paragraph" }] };
const extraNodeViews = { appLink: AppLinkView, session: SessionNodeView };

const dailyPlaceholder: PlaceholderFunction = ({ node, pos, parent }) => {
const type = node.type.name;
const parentType = parent?.type.name;

if (type === "heading") {
const level = typeof node.attrs.level === "number" ? node.attrs.level : 1;
return `Heading ${level}`;
}

if (type === "codeBlock") {
return "Code";
}

if (type === "paragraph") {
if (parentType === "listItem") return "List";
if (parentType === "taskItem") return "Task";
if (parentType === "blockquote") return "Quote";
if (parentType === "doc" && pos === 0) {
return "Type anything or press / for commands and start your Daily Note...";
}
}

return "";
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated placeholder logic across two files

Low Severity

dailyPlaceholder and Placeholder in raw.tsx share identical logic for mapping node types to placeholder strings — the heading level check, codeBlock"Code", and the listItem/taskItem/blockquote parent-type checks are copy-pasted. If a new block type is added, both functions need updating independently, risking inconsistency. A shared helper accepting only the doc-level paragraph text as a parameter could eliminate this.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f5b552. Configure here.


function getSessionTitle(store: Store, sessionId: string): string {
const title = store.getCell("sessions", sessionId, "title");
return typeof title === "string" ? title : "";
Expand Down Expand Up @@ -381,6 +407,7 @@ export function DailyNoteEditor({
initialContent={initialContentRef.current}
handleChange={handleChange}
linkedItemOpenBehavior="new"
placeholderComponent={dailyPlaceholder}
taskSource={taskSource}
extraNodeViews={extraNodeViews}
/>
Expand Down
24 changes: 22 additions & 2 deletions apps/desktop/src/session/components/note-input/raw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,28 @@ export const RawEditor = forwardRef<
);
});

const Placeholder: PlaceholderFunction = ({ node, pos }) => {
if (node.type.name !== "paragraph") {
const Placeholder: PlaceholderFunction = ({ node, pos, parent }) => {
const type = node.type.name;
const parentType = parent?.type.name;

if (type === "heading") {
const level = typeof node.attrs.level === "number" ? node.attrs.level : 1;
return `Heading ${level}`;
}

if (type === "codeBlock") {
return "Code";
}

if (type !== "paragraph") {
return "";
}

if (parentType === "listItem") return "List";
if (parentType === "taskItem") return "Task";
if (parentType === "blockquote") return "Quote";

if (parentType !== "doc") {
return "";
}

Expand Down
36 changes: 33 additions & 3 deletions packages/editor/src/plugins/placeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Decoration, DecorationSet } from "prosemirror-view";
export type PlaceholderFunction = (props: {
node: PMNode;
pos: number;
parent: PMNode | null;
hasAnchor: boolean;
}) => string;

Expand All @@ -16,15 +17,15 @@ export function placeholderPlugin(placeholder?: PlaceholderFunction) {
props: {
decorations(state) {
const { doc, selection } = state;
const { anchor } = selection;
const { anchor, $anchor } = selection;
const decorations: Decoration[] = [];

const isEmptyDoc =
doc.childCount === 1 &&
doc.firstChild!.isTextblock &&
doc.firstChild!.content.size === 0;

doc.descendants((node, pos) => {
doc.descendants((node, pos, parent) => {
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
const isEmpty = !node.isLeaf && node.content.size === 0;

Expand All @@ -33,7 +34,7 @@ export function placeholderPlugin(placeholder?: PlaceholderFunction) {
if (isEmptyDoc) classes.push("is-editor-empty");

const text = placeholder
? placeholder({ node, pos, hasAnchor })
? placeholder({ node, pos, parent, hasAnchor })
: "";

if (text) {
Expand All @@ -49,6 +50,35 @@ export function placeholderPlugin(placeholder?: PlaceholderFunction) {
return false;
});

if (placeholder && $anchor.depth > 1) {
const node = $anchor.parent;
if (!node.isLeaf && node.content.size === 0) {
const parent = $anchor.node($anchor.depth - 1);
const pos = $anchor.before($anchor.depth);
const text = placeholder({
node,
pos,
parent,
hasAnchor: true,
});

if (text) {
decorations.push(
Decoration.widget(
pos + 1,
() => {
const span = document.createElement("span");
span.className = "is-empty";
span.setAttribute("data-placeholder", text);
return span;
},
{ side: 0, key: `placeholder-${pos}-${text}` },
),
);
}
}
}

return DecorationSet.create(doc, decorations);
},
},
Expand Down
Loading