Skip to content

Commit 5a63767

Browse files
committed
wire up title input and session input movement
1 parent 13fefe0 commit 5a63767

File tree

9 files changed

+376
-295
lines changed

9 files changed

+376
-295
lines changed

apps/desktop/src/editor/index.tsx

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import {
33
ProseMirrorDoc,
44
reactKeys,
55
useEditorEffect,
6+
useEditorEventCallback,
67
} from "@handlewithcare/react-prosemirror";
78
import { dropCursor } from "prosemirror-dropcursor";
89
import { gapCursor } from "prosemirror-gapcursor";
910
import { history } from "prosemirror-history";
1011
import { Node as PMNode } from "prosemirror-model";
11-
import { EditorState, type Transaction } from "prosemirror-state";
12+
import {
13+
EditorState,
14+
Selection,
15+
TextSelection,
16+
type Transaction,
17+
} from "prosemirror-state";
1218
import type { EditorView } from "prosemirror-view";
1319
import {
1420
forwardRef,
@@ -58,9 +64,17 @@ export interface JSONContent {
5864
text?: string;
5965
}
6066

67+
export interface EditorCommands {
68+
focus: () => void;
69+
focusAtStart: () => void;
70+
focusAtPixelWidth: (pixelWidth: number) => void;
71+
insertAtStartAndFocus: (content: string) => void;
72+
}
73+
6174
export interface NoteEditorRef {
6275
view: EditorView | null;
6376
searchStorage: SearchAndReplaceStorage;
77+
commands: EditorCommands;
6478
}
6579

6680
interface EditorProps {
@@ -69,7 +83,7 @@ interface EditorProps {
6983
mentionConfig?: MentionConfig;
7084
placeholderComponent?: PlaceholderFunction;
7185
fileHandlerConfig?: FileHandlerConfig;
72-
onNavigateToTitle?: () => void;
86+
onNavigateToTitle?: (pixelWidth?: number) => void;
7387
}
7488

7589
const nodeViews = {
@@ -93,6 +107,91 @@ function ViewCapture({
93107
return null;
94108
}
95109

110+
const noopCommands: EditorCommands = {
111+
focus: () => {},
112+
focusAtStart: () => {},
113+
focusAtPixelWidth: () => {},
114+
insertAtStartAndFocus: () => {},
115+
};
116+
117+
function EditorCommandsBridge({
118+
commandsRef,
119+
}: {
120+
commandsRef: React.RefObject<EditorCommands>;
121+
}) {
122+
commandsRef.current.focus = useEditorEventCallback((view) => {
123+
if (!view) return;
124+
view.focus();
125+
});
126+
127+
commandsRef.current.focusAtStart = useEditorEventCallback((view) => {
128+
if (!view) return;
129+
view.dispatch(
130+
view.state.tr.setSelection(Selection.atStart(view.state.doc)),
131+
);
132+
view.focus();
133+
});
134+
135+
commandsRef.current.focusAtPixelWidth = useEditorEventCallback(
136+
(view, pixelWidth: number) => {
137+
if (!view) return;
138+
139+
const blockStart = Selection.atStart(view.state.doc).from;
140+
const firstTextNode = view.dom.querySelector(".ProseMirror > *");
141+
if (firstTextNode) {
142+
const editorStyle = window.getComputedStyle(firstTextNode);
143+
const canvas = document.createElement("canvas");
144+
const ctx = canvas.getContext("2d");
145+
if (ctx) {
146+
ctx.font = `${editorStyle.fontWeight} ${editorStyle.fontSize} ${editorStyle.fontFamily}`;
147+
const firstBlock = view.state.doc.firstChild;
148+
if (firstBlock && firstBlock.textContent) {
149+
const text = firstBlock.textContent;
150+
let charPos = 0;
151+
for (let i = 0; i <= text.length; i++) {
152+
const currentWidth = ctx.measureText(text.slice(0, i)).width;
153+
if (currentWidth >= pixelWidth) {
154+
charPos = i;
155+
break;
156+
}
157+
charPos = i;
158+
}
159+
const targetPos = Math.min(
160+
blockStart + charPos,
161+
view.state.doc.content.size - 1,
162+
);
163+
view.dispatch(
164+
view.state.tr.setSelection(
165+
TextSelection.create(view.state.doc, targetPos),
166+
),
167+
);
168+
view.focus();
169+
return;
170+
}
171+
}
172+
}
173+
174+
view.dispatch(
175+
view.state.tr.setSelection(Selection.atStart(view.state.doc)),
176+
);
177+
view.focus();
178+
},
179+
);
180+
181+
commandsRef.current.insertAtStartAndFocus = useEditorEventCallback(
182+
(view, content: string) => {
183+
if (!view || !content) return;
184+
const pos = Selection.atStart(view.state.doc).from;
185+
const tr = view.state.tr.insertText(content, pos);
186+
tr.setSelection(TextSelection.create(tr.doc, pos));
187+
view.dispatch(tr);
188+
view.focus();
189+
},
190+
);
191+
192+
return null;
193+
}
194+
96195
const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
97196
const {
98197
handleChange,
@@ -106,10 +205,21 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
106205
const previousContentRef = useRef<JSONContent | undefined>(initialContent);
107206
const searchStorage = useMemo(() => createSearchStorage(), []);
108207
const viewRef = useRef<EditorView | null>(null);
208+
const commandsRef = useRef<EditorCommands>(noopCommands);
109209

110-
useImperativeHandle(ref, () => ({ view: viewRef.current, searchStorage }), [
111-
searchStorage,
112-
]);
210+
useImperativeHandle(
211+
ref,
212+
() => ({
213+
get view() {
214+
return viewRef.current;
215+
},
216+
searchStorage,
217+
get commands() {
218+
return commandsRef.current;
219+
},
220+
}),
221+
[searchStorage],
222+
);
113223

114224
const onUpdate = useDebounceCallback((view: EditorView) => {
115225
if (!handleChange) return;
@@ -208,6 +318,7 @@ const NoteEditor = forwardRef<NoteEditorRef, EditorProps>((props, ref) => {
208318
>
209319
<ProseMirrorDoc />
210320
<ViewCapture viewRef={viewRef} onViewReady={onViewReady} />
321+
<EditorCommandsBridge commandsRef={commandsRef} />
211322
{mentionConfig && <MentionSuggestion config={mentionConfig} />}
212323
</ProseMirror>
213324
);

apps/desktop/src/editor/keymap.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
sinkListItem,
2727
splitListItem,
2828
} from "prosemirror-schema-list";
29-
import type { Command, EditorState } from "prosemirror-state";
29+
import { Selection, type Command, type EditorState } from "prosemirror-state";
3030

3131
import { schema } from "./schema";
3232

@@ -118,7 +118,7 @@ const mac =
118118
? /Mac|iP(hone|[oa]d)/.test(navigator.platform)
119119
: false;
120120

121-
export function buildKeymap(onNavigateToTitle?: () => void) {
121+
export function buildKeymap(onNavigateToTitle?: (pixelWidth?: number) => void) {
122122
const hardBreak = schema.nodes.hardBreak;
123123

124124
const keys: Record<string, Command> = {};
@@ -209,20 +209,45 @@ export function buildKeymap(onNavigateToTitle?: () => void) {
209209
};
210210

211211
if (onNavigateToTitle) {
212-
keys["ArrowUp"] = (state) => {
213-
const { $head } = state.selection;
212+
keys["ArrowLeft"] = (state) => {
213+
const { $head, empty } = state.selection;
214+
if (!empty) return false;
215+
if ($head.pos !== Selection.atStart(state.doc).from) return false;
214216

215-
let node = state.doc.firstChild;
216-
let firstTextBlockPos = 0;
217-
while (node && !node.isTextblock) {
218-
firstTextBlockPos += 1;
219-
node = node.firstChild;
220-
}
217+
onNavigateToTitle();
218+
return true;
219+
};
221220

222-
if (!node) return false;
223-
const isInFirstBlock = $head.start($head.depth) === firstTextBlockPos + 1;
221+
keys["ArrowUp"] = (state, _dispatch, view) => {
222+
const { $head } = state.selection;
223+
const firstBlockStart = Selection.atStart(state.doc).from;
224+
if (
225+
$head.start($head.depth) !==
226+
state.doc.resolve(firstBlockStart).start($head.depth)
227+
) {
228+
return false;
229+
}
224230

225-
if (!isInFirstBlock) return false;
231+
if (view) {
232+
const firstBlock = state.doc.firstChild;
233+
if (firstBlock && firstBlock.textContent) {
234+
const text = firstBlock.textContent;
235+
const posInBlock = $head.pos - $head.start();
236+
const textBeforeCursor = text.slice(0, posInBlock);
237+
const firstTextNode = view.dom.querySelector(".ProseMirror > *");
238+
if (firstTextNode) {
239+
const style = window.getComputedStyle(firstTextNode);
240+
const canvas = document.createElement("canvas");
241+
const ctx = canvas.getContext("2d");
242+
if (ctx) {
243+
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
244+
const pixelWidth = ctx.measureText(textBeforeCursor).width;
245+
onNavigateToTitle(pixelWidth);
246+
return true;
247+
}
248+
}
249+
}
250+
}
226251

227252
onNavigateToTitle();
228253
return true;

apps/desktop/src/session/components/note-input/enhanced/editor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import * as main from "~/store/tinybase/store/main";
99

1010
export const EnhancedEditor = forwardRef<
1111
NoteEditorRef,
12-
{ sessionId: string; enhancedNoteId: string; onNavigateToTitle?: () => void }
12+
{
13+
sessionId: string;
14+
enhancedNoteId: string;
15+
onNavigateToTitle?: (pixelWidth?: number) => void;
16+
}
1317
>(({ sessionId, enhancedNoteId, onNavigateToTitle }, ref) => {
1418
const onImageUpload = useImageUpload(sessionId);
1519
const content = main.UI.useCell(

apps/desktop/src/session/components/note-input/enhanced/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { createTaskId } from "~/store/zustand/ai-task/task-configs";
1313

1414
export const Enhanced = forwardRef<
1515
NoteEditorRef,
16-
{ sessionId: string; enhancedNoteId: string; onNavigateToTitle?: () => void }
16+
{
17+
sessionId: string;
18+
enhancedNoteId: string;
19+
onNavigateToTitle?: (pixelWidth?: number) => void;
20+
}
1721
>(({ sessionId, enhancedNoteId, onNavigateToTitle }, ref) => {
1822
const taskId = createTaskId(enhancedNoteId, "enhance");
1923
const llmStatus = useLLMConnectionStatus();

0 commit comments

Comments
 (0)