Skip to content

Commit 4842317

Browse files
committed
fix(editor): restore visual code block editing
1 parent 3415cbd commit 4842317

4 files changed

Lines changed: 322 additions & 63 deletions

File tree

packages/app/src/styles.css

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,12 @@
10881088

10891089
.markdown-paper .cm-content:has(.cm-markra-code-line:hover)
10901090
.cm-markra-code-header-actions,
1091-
.markdown-paper .cm-markra-code-header-actions:focus-within {
1091+
.markdown-paper .cm-markra-code-header-actions:focus-within,
1092+
.markdown-paper .cm-content:has(.cm-markra-code-line:hover)
1093+
.cm-markra-code-closing-line
1094+
.markra-code-language-control,
1095+
.markdown-paper .cm-markra-code-closing-line[data-code-block-active="true"] .markra-code-language-control,
1096+
.markdown-paper .cm-markra-code-closing-line .markra-code-language-control:focus-within {
10921097
opacity: 1 !important;
10931098
pointer-events: auto !important;
10941099
transform: translateY(0);
@@ -1127,52 +1132,116 @@
11271132
}
11281133

11291134
.markdown-paper .cm-line.cm-markra-code-content-line {
1135+
position: relative;
11301136
box-sizing: border-box;
11311137
padding-inline: 0 16px !important;
1132-
border-right: 1px solid var(--editor-border) !important;
1133-
border-left: 1px solid var(--editor-border) !important;
1138+
border: 0 !important;
1139+
background: transparent !important;
1140+
color: var(--editor-code-text) !important;
1141+
font-family: Menlo, Consolas, "DejaVu Sans Mono", monospace !important;
1142+
font-size: 14.4px !important;
1143+
line-height: 23.76px !important;
1144+
}
1145+
1146+
/* CodeMirror draws selections at z-index -1. Keeping the code surface at
1147+
-2 leaves selections visible without placing them over tokens or gutters. */
1148+
.markdown-paper .cm-line.cm-markra-code-content-line::after {
1149+
position: absolute;
1150+
inset: 0;
1151+
z-index: -2;
1152+
box-sizing: border-box;
1153+
border-right: 1px solid var(--editor-border);
1154+
border-left: 1px solid var(--editor-border);
11341155
background: linear-gradient(
11351156
90deg,
11361157
var(--editor-code-line-bg) 0 43px,
11371158
var(--editor-border) 43px 44px,
11381159
var(--editor-code-bg) 44px 100%
1139-
) !important;
1140-
color: var(--editor-code-text) !important;
1141-
font-family: Menlo, Consolas, "DejaVu Sans Mono", monospace !important;
1142-
font-size: 14.4px !important;
1143-
line-height: 23.76px !important;
1160+
);
1161+
content: "";
1162+
pointer-events: none;
11441163
}
11451164

11461165
.markdown-paper .cm-line.cm-markra-code-content-line::before {
1166+
position: relative;
1167+
z-index: 1;
11471168
box-sizing: border-box;
11481169
width: 43px !important;
11491170
min-width: 43px !important;
11501171
margin-right: 16px !important;
11511172
padding-right: 12px;
1173+
border-right: 1px solid var(--editor-border);
1174+
background: var(--editor-code-line-bg);
11521175
color: var(--editor-text-secondary) !important;
11531176
font: inherit;
1177+
box-shadow: 16px 0 0 var(--editor-code-bg);
11541178
}
11551179

11561180
.markdown-paper .cm-line.cm-markra-code-header-line +
11571181
.cm-line.cm-markra-code-content-line {
11581182
padding-block-start: 16px !important;
1159-
border-top: 1px solid var(--editor-border) !important;
1160-
border-radius: 4px 4px 0 0 !important;
1183+
}
1184+
1185+
.markdown-paper .cm-line.cm-markra-code-header-line +
1186+
.cm-line.cm-markra-code-content-line::after {
1187+
border-top: 1px solid var(--editor-border);
1188+
border-radius: 4px 4px 0 0;
11611189
}
11621190

11631191
.markdown-paper .cm-line.cm-markra-code-content-line:has(+ .cm-markra-code-closing-line) {
11641192
padding-block-end: 16px !important;
1165-
border-bottom: 1px solid var(--editor-border) !important;
1166-
border-radius: 0 0 4px 4px !important;
1193+
}
1194+
1195+
.markdown-paper .cm-line.cm-markra-code-content-line:has(+ .cm-markra-code-closing-line)::after {
1196+
border-bottom: 1px solid var(--editor-border);
1197+
border-radius: 0 0 4px 4px;
11671198
}
11681199

11691200
.markdown-paper .cm-line.cm-markra-code-closing-line {
1170-
height: 0 !important;
1171-
min-height: 0 !important;
1201+
position: relative !important;
1202+
height: 56px !important;
1203+
min-height: 56px !important;
11721204
padding: 0 !important;
11731205
border: 0 !important;
11741206
background: transparent !important;
1175-
line-height: 0 !important;
1207+
line-height: 12px !important;
1208+
overflow: visible !important;
1209+
}
1210+
1211+
.markdown-paper .cm-markra-code-exit-wrap {
1212+
display: inline-block !important;
1213+
width: 100% !important;
1214+
height: 56px !important;
1215+
}
1216+
1217+
.markdown-paper .cm-markra-code-exit {
1218+
display: inline-block !important;
1219+
width: 100% !important;
1220+
height: 12px !important;
1221+
cursor: var(--editor-text-cursor);
1222+
}
1223+
1224+
.markdown-paper .cm-line.cm-markra-code-closing-line .markra-code-language-control {
1225+
position: absolute !important;
1226+
top: 12px;
1227+
right: 0;
1228+
z-index: 4;
1229+
display: flex !important;
1230+
padding: 0 !important;
1231+
opacity: 0 !important;
1232+
pointer-events: none !important;
1233+
transform: translateY(-2px);
1234+
transition: opacity 150ms ease-out, transform 150ms ease-out;
1235+
}
1236+
1237+
.markdown-paper .cm-line.cm-markra-code-closing-line .markra-code-language-select {
1238+
height: 32px !important;
1239+
min-height: 32px !important;
1240+
min-width: 160px !important;
1241+
padding: 0 12px !important;
1242+
font-family: Menlo, Consolas, "DejaVu Sans Mono", monospace !important;
1243+
font-size: 13px !important;
1244+
line-height: 1 !important;
11761245
}
11771246

11781247
.markdown-paper .cm-markra-table-wrap {

packages/app/src/styles.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,65 @@ describe("editor stylesheet", () => {
5858
);
5959
});
6060

61+
it("keeps code block selections between the background and foreground", () => {
62+
const styles = readFileSync(`${process.cwd()}/src/styles.css`, "utf8");
63+
const codeLineStart = styles.indexOf(
64+
".markdown-paper .cm-line.cm-markra-code-content-line {",
65+
);
66+
const codeLineEnd = styles.indexOf("\n }", codeLineStart);
67+
const codeLineRule = styles.slice(codeLineStart, codeLineEnd);
68+
const backdropStart = styles.indexOf(
69+
".markdown-paper .cm-line.cm-markra-code-content-line::after {",
70+
);
71+
const backdropEnd = styles.indexOf("\n }", backdropStart);
72+
const backdropRule = styles.slice(backdropStart, backdropEnd);
73+
const lineNumberStart = styles.indexOf(
74+
".markdown-paper .cm-line.cm-markra-code-content-line::before {",
75+
);
76+
const lineNumberEnd = styles.indexOf("\n }", lineNumberStart);
77+
const lineNumberRule = styles.slice(lineNumberStart, lineNumberEnd);
78+
79+
expect(styles).not.toContain(".markdown-paper .cm-selectionLayer {");
80+
expect(styles).not.toContain(".markdown-paper .cm-cursorLayer {");
81+
expect(codeLineRule).toContain("position: relative");
82+
expect(codeLineRule).toContain("background: transparent !important");
83+
expect(backdropStart).toBeGreaterThanOrEqual(0);
84+
expect(backdropRule).toContain("z-index: -2");
85+
expect(backdropRule).toContain("background: linear-gradient(");
86+
expect(lineNumberRule).toContain("position: relative");
87+
expect(lineNumberRule).toContain("z-index: 1");
88+
expect(lineNumberRule).toContain("background: var(--editor-code-line-bg)");
89+
expect(lineNumberRule).toContain("box-shadow: 16px 0 0 var(--editor-code-bg)");
90+
});
91+
92+
it("matches the original code block control layout", () => {
93+
const styles = readFileSync(`${process.cwd()}/src/styles.css`, "utf8");
94+
const closingLineStart = styles.indexOf(
95+
".markdown-paper .cm-line.cm-markra-code-closing-line {",
96+
);
97+
const closingLineEnd = styles.indexOf("\n }", closingLineStart);
98+
const closingLineRule = styles.slice(closingLineStart, closingLineEnd);
99+
const languageControlStart = styles.indexOf(
100+
".markdown-paper .cm-line.cm-markra-code-closing-line .markra-code-language-control {",
101+
);
102+
const languageControlEnd = styles.indexOf("\n }", languageControlStart);
103+
const languageControlRule = styles.slice(
104+
languageControlStart,
105+
languageControlEnd,
106+
);
107+
108+
expect(closingLineRule).toContain("position: relative");
109+
expect(closingLineRule).toContain("height: 56px");
110+
expect(languageControlStart).toBeGreaterThanOrEqual(0);
111+
expect(languageControlRule).toContain("position: absolute");
112+
expect(languageControlRule).toContain("top: 12px");
113+
expect(languageControlRule).toContain("right: 0");
114+
expect(languageControlRule).toContain("padding: 0");
115+
expect(styles).toContain(
116+
'.cm-markra-code-closing-line[data-code-block-active="true"] .markra-code-language-control',
117+
);
118+
});
119+
61120
it("keeps hidden Markdown empty lines available for pointer hit testing", () => {
62121
const styles = readFileSync(`${process.cwd()}/src/styles.css`, "utf8");
63122
const emptyLineBreakRule = [

packages/editor/src/codemirror/code-block.test.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,41 @@ describe("codeBlockPreviewPlugin", () => {
6060
expect(view.state.doc.toString()).toBe(codeDocument);
6161
});
6262

63-
it("reveals fences and info when the selection enters the block", () => {
63+
it("keeps the visual code block chrome while editing its content", () => {
6464
const view = createView();
6565

6666
view.dispatch({
6767
selection: { anchor: codeDocument.indexOf("answer =") + 2 },
6868
});
6969

70-
expect(view.dom.querySelector(".cm-markra-code-header")).toBeNull();
71-
expect(renderedLines(view)).toContain("```ts");
72-
expect(renderedLines(view)).toContain("```");
70+
expect(view.dom.querySelector(".cm-markra-code-header")?.textContent).toBe(
71+
"ts",
72+
);
73+
expect(renderedLines(view)).not.toContain("```ts");
74+
expect(renderedLines(view)).not.toContain("```");
75+
expect(renderedLines(view)).toContain("const answer = 42;");
76+
expect(
77+
view.dom
78+
.querySelector(".cm-markra-code-closing-line")
79+
?.getAttribute("data-code-block-active"),
80+
).toBe("true");
81+
expect(view.dom.querySelector(".markra-code-language-select")).not.toBeNull();
82+
});
83+
84+
it("moves a click on the folded closing line to an editable line after the fence", () => {
85+
const view = createView("```ts\nconst answer = 42;\n```");
86+
const closingLine = view.dom.querySelector<HTMLElement>(
87+
".cm-markra-code-closing-line",
88+
);
89+
90+
expect(closingLine).not.toBeNull();
91+
closingLine?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
92+
view.dispatch(view.state.replaceSelection("Outside"));
93+
94+
expect(view.state.doc.toString()).toBe(
95+
"```ts\nconst answer = 42;\n```\nOutside",
96+
);
97+
expect(view.state.selection.main.head).toBe(view.state.doc.length);
7398
});
7499

75100
it("selects only the current code content on the first Mod+A", () => {
@@ -192,6 +217,8 @@ describe("codeBlockPreviewPlugin", () => {
192217
expect(copy?.querySelector(".markra-code-copy-icon")).not.toBeNull();
193218
expect(copy?.querySelector(".markra-code-copy-check-icon")).not.toBeNull();
194219
expect(language?.closest(".markra-code-language-control")).not.toBeNull();
220+
expect(copy?.closest(".cm-markra-code-header-line")).not.toBeNull();
221+
expect(language?.closest(".cm-markra-code-closing-line")).not.toBeNull();
195222
copy?.click();
196223

197224
expect(writeText).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)