Skip to content

Commit 8d6da5b

Browse files
committed
fix: project split inline content
1 parent 837c18a commit 8d6da5b

4 files changed

Lines changed: 20 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ sections and version names that match the application package version.
3030
- Preserve empty link destinations and GFM link titles while editing projected
3131
link source.
3232
- Let `Enter` and `Shift+Enter` continue through normal editor behavior after
33-
committing projected source, without using `Escape` to close projection.
33+
committing projected source, immediately project formatted content that moves
34+
with the caret, and avoid using `Escape` to close projection.
3435

3536
## [0.1.0-alpha.1] - 2026-07-10
3637

docs/specification.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ The editor is a unified hybrid Markdown surface. Behavior is governed by renderi
187187
the projection finalizes and preserves the user's selection range.
188188
- `Enter` and `Shift+Enter` internally finalize active projected source before
189189
continuing through the editor's normal line-break behavior in the same
190-
keypress. `Escape` leaves projection active while the caret remains on its
191-
target.
190+
keypress. When formatted content moves with the caret, its new inline target
191+
immediately enters projection. `Escape` leaves projection active while the
192+
caret remains on its target.
192193
- Normal click places the caret in a link; `Mod+click` opens it.
193194
- Footnote references render inline and expose editable raw Markdown syntax near
194195
the caret.

src/features/editor/plugins/sourceProjection.test.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ describe("source projection", () => {
741741
});
742742

743743
describe("keyboard handoff", () => {
744-
it("finalizes projected source and delegates Enter to Milkdown", async () => {
744+
it("delegates Enter and projects the formatted content after the split", async () => {
745745
const mounted = await mountProjectionEditor("**LeftRight**");
746746

747747
enterProjection(mounted, "strong");
@@ -754,16 +754,19 @@ describe("source projection", () => {
754754
const { handled } = runKeyDownHandlers(mounted.view, "Enter");
755755

756756
expect(handled).toBe(true);
757-
expect(hasActiveSourceProjection(mounted.view.state)).toBe(false);
757+
expect(hasActiveSourceProjection(mounted.view.state)).toBe(true);
758758
expect(mounted.view.dom.querySelectorAll("p")).toHaveLength(2);
759759
expect(
760760
Array.from(mounted.view.dom.querySelectorAll("strong"), (element) => element.textContent),
761-
).toEqual(["Left edited", "Right"]);
762-
expect(mounted.view.state.selection.$from.parent.textContent).toBe("Right");
763-
expect(mounted.view.state.selection.$from.parentOffset).toBe(0);
761+
).toEqual(["Left edited"]);
762+
expect(
763+
mounted.view.dom.querySelector(".leafdown-source-projection__content--strong"),
764+
).toHaveTextContent("Right");
765+
expect(mounted.view.state.selection.$from.parent.textContent).toBe("**Right**");
766+
expect(mounted.view.state.selection.$from.parentOffset).toBe(2);
764767
});
765768

766-
it("finalizes projected source and delegates Shift+Enter to Milkdown", async () => {
769+
it("delegates Shift+Enter and projects the formatted content after the break", async () => {
767770
const mounted = await mountProjectionEditor("**LeftRight**");
768771

769772
enterProjection(mounted, "strong");
@@ -775,11 +778,14 @@ describe("source projection", () => {
775778
const { handled } = runKeyDownHandlers(mounted.view, "Enter", { shift: true });
776779

777780
expect(handled).toBe(true);
778-
expect(hasActiveSourceProjection(mounted.view.state)).toBe(false);
781+
expect(hasActiveSourceProjection(mounted.view.state)).toBe(true);
779782
expect(mounted.view.dom.querySelector("br")).toBeInTheDocument();
780783
expect(
781784
Array.from(mounted.view.dom.querySelectorAll("strong"), (element) => element.textContent),
782-
).toEqual(["Left", "Right"]);
785+
).toEqual(["Left"]);
786+
expect(
787+
mounted.view.dom.querySelector(".leafdown-source-projection__content--strong"),
788+
).toHaveTextContent("Right");
783789
});
784790

785791
it("delegates Enter after committing invalid projected source literally", async () => {

src/features/editor/plugins/sourceProjection.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { getRangeText, getTextBetween, type TextRange } from "../utils/textRange
2424
const EMPTY_PROJECTION_STATE: SourceProjectionPluginState = {
2525
pendingCommit: null,
2626
session: null,
27-
suppressAfterEnter: false,
2827
suppressAt: null,
2928
};
3029

@@ -49,14 +48,12 @@ interface PendingProjectionCommit extends TextRange {
4948
interface SourceProjectionPluginState {
5049
pendingCommit: PendingProjectionCommit | null;
5150
session: ProjectionSession | null;
52-
suppressAfterEnter: boolean;
5351
suppressAt: number | null;
5452
}
5553

5654
type ProjectionHistoryDirection = "redo" | "undo";
5755

5856
type ProjectionMeta =
59-
| { type: "delegateEnter" }
6057
| { type: "enter"; session: ProjectionSession }
6158
| { type: "enterFromUserEdit"; session: ProjectionSession }
6259
| { type: "userEdit"; previousSource: string }
@@ -202,7 +199,6 @@ export const isSourceProjectionHousekeepingTransaction = (transaction: Transacti
202199
const meta = getProjectionMeta(transaction);
203200

204201
return (
205-
meta?.type === "delegateEnter" ||
206202
meta?.type === "enter" ||
207203
meta?.type === "restoreBeforeCommit" ||
208204
meta?.type === "commitAfterRestore"
@@ -253,18 +249,10 @@ const applyProjectionTransaction = (
253249
): SourceProjectionPluginState => {
254250
const meta = getProjectionMeta(transaction);
255251

256-
if (meta?.type === "delegateEnter") {
257-
return {
258-
...pluginState,
259-
suppressAfterEnter: true,
260-
};
261-
}
262-
263252
if (meta?.type === "enter" || meta?.type === "enterFromUserEdit") {
264253
return {
265254
pendingCommit: null,
266255
session: meta.session,
267-
suppressAfterEnter: false,
268256
suppressAt: null,
269257
};
270258
}
@@ -273,7 +261,6 @@ const applyProjectionTransaction = (
273261
return {
274262
pendingCommit: meta.pendingCommit,
275263
session: null,
276-
suppressAfterEnter: pluginState.suppressAfterEnter,
277264
suppressAt: meta.suppressAt,
278265
};
279266
}
@@ -282,22 +269,15 @@ const applyProjectionTransaction = (
282269
return {
283270
pendingCommit: null,
284271
session: null,
285-
suppressAfterEnter: pluginState.suppressAfterEnter,
286272
suppressAt: meta.suppressAt,
287273
};
288274
}
289275

290-
const didHandleDelegatedEnter = pluginState.suppressAfterEnter && transaction.docChanged;
291-
const suppressAfterEnter = didHandleDelegatedEnter ? false : pluginState.suppressAfterEnter;
292-
const suppressAt =
293-
didHandleDelegatedEnter && transaction.selection.empty
294-
? transaction.selection.from
295-
: getMappedSuppressPosition(pluginState.suppressAt, transaction);
276+
const suppressAt = getMappedSuppressPosition(pluginState.suppressAt, transaction);
296277

297278
if (!pluginState.session) {
298279
return {
299280
...pluginState,
300-
suppressAfterEnter,
301281
suppressAt,
302282
};
303283
}
@@ -317,7 +297,6 @@ const applyProjectionTransaction = (
317297
? session.undoStack
318298
: [...session.undoStack, meta.previousSource],
319299
},
320-
suppressAfterEnter,
321300
suppressAt,
322301
};
323302
}
@@ -330,7 +309,6 @@ const applyProjectionTransaction = (
330309
redoStack: [...session.redoStack, meta.currentSource],
331310
undoStack: session.undoStack.slice(0, -1),
332311
},
333-
suppressAfterEnter,
334312
suppressAt,
335313
};
336314
}
@@ -343,7 +321,6 @@ const applyProjectionTransaction = (
343321
redoStack: session.redoStack.slice(0, -1),
344322
undoStack: [...session.undoStack, meta.currentSource],
345323
},
346-
suppressAfterEnter,
347324
suppressAt,
348325
};
349326
}
@@ -352,15 +329,13 @@ const applyProjectionTransaction = (
352329
return {
353330
pendingCommit: null,
354331
session: null,
355-
suppressAfterEnter,
356332
suppressAt,
357333
};
358334
}
359335

360336
return {
361337
...pluginState,
362338
session,
363-
suppressAfterEnter,
364339
suppressAt,
365340
};
366341
};
@@ -534,11 +509,6 @@ const handleProjectionKeyDown = (view: EditorView, event: KeyboardEvent) => {
534509
}
535510

536511
if (event.key === "Enter") {
537-
view.dispatch(
538-
view.state.tr.setMeta("addToHistory", false).setMeta(leafdownSourceProjectionPluginKey, {
539-
type: "delegateEnter",
540-
} satisfies ProjectionMeta),
541-
);
542512
finalizeSourceProjection(view);
543513

544514
return false;

0 commit comments

Comments
 (0)