Skip to content

Commit 5b1ab71

Browse files
committed
fix: update markdown cells in real time
1 parent 73478f2 commit 5b1ab71

2 files changed

Lines changed: 87 additions & 15 deletions

File tree

frontend/src/components/notebooks/NotebookEditor.vue

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import {
2020
faColumns,
2121
faTableList,
2222
faBroom,
23+
faPlus,
24+
faFileLines,
2325
} from "@fortawesome/free-solid-svg-icons";
2426
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
2527
import KernelStatusPill from "./KernelStatusPill.vue";
2628
import MwctipyReferenceModal from "./MwctipyReferenceModal.vue";
2729
import OutputPanel from "./OutputPanel.vue";
28-
import { parseCells, findCellAtLine } from "./cellDelimiterParser";
30+
import { parseCells, findCellAtLine, uuid } from "./cellDelimiterParser";
2931
3032
const props = defineProps({
3133
notebookId: { type: Number, default: null },
@@ -76,14 +78,6 @@ const liveNotebook = computed(() => {
7678
);
7779
});
7880
79-
const liveSource = computed(() => {
80-
// Prefer the editor model (so the panel reflects unsaved edits) over the
81-
// store copy.
82-
const model = models.get(currentNotebook.value?.id);
83-
if (model) return model.getValue();
84-
return liveNotebook.value?.source || "";
85-
});
86-
8781
const liveCellOutputs = computed(() => liveNotebook.value?.cell_outputs || {});
8882
const liveCellMeta = computed(() => {
8983
if (!currentNotebook.value) return {};
@@ -257,12 +251,16 @@ watch(
257251
{ immediate: true },
258252
);
259253
260-
// Expose a tracked source string so the OutputPanel re-renders on edits.
261-
// `void _editTick.value` forces a reactive subscription on the tick ref so
262-
// computed re-runs whenever the model emits onDidChangeContent.
254+
// Source string the OutputPanel reads. The Monaco model is not reactive, so
255+
// we subscribe to ``_editTick`` (bumped on every onDidChangeContent) and
256+
// call ``model.getValue()`` inline — wrapping it in another computed would
257+
// hide the read behind a cached layer and stale-render markdown until the
258+
// next notebook switch.
263259
const trackedSource = computed(() => {
264260
void _editTick.value;
265-
return liveSource.value;
261+
const model = models.get(currentNotebook.value?.id);
262+
if (model) return model.getValue();
263+
return liveNotebook.value?.source || "";
266264
});
267265
268266
// ── Run cell / Run all / Interrupt / Restart ────────────────────────────
@@ -360,6 +358,46 @@ async function runCellById(cellId) {
360358
}
361359
}
362360
361+
// Insert a new code or markdown cell after the cell at the cursor (or at the
362+
// end of the document if the cursor isn't inside a cell). Uses Monaco's edit
363+
// API so undo/redo and onDidChangeContent (auto-save) all behave correctly.
364+
function insertCell(type) {
365+
if (!currentNotebook.value || readOnly.value || isLibrary.value) return;
366+
if (!editorRef.value || !monaco.value) return;
367+
const model = models.get(currentNotebook.value.id);
368+
if (!model) return;
369+
370+
const cells = parseCells(model.getValue());
371+
const pos = editorRef.value.getPosition();
372+
let endLine = model.getLineCount();
373+
if (pos && cells.length > 0) {
374+
const cell = findCellAtLine(cells, pos.lineNumber);
375+
if (cell) endLine = cell.sourceEndLine;
376+
}
377+
378+
const lineLen = model.getLineContent(endLine).length;
379+
// Don't prepend a newline when the doc is a single empty line — otherwise
380+
// we'd seed a permanent leading blank line.
381+
const isEmptyDoc = endLine === 1 && lineLen === 0;
382+
const delimiter = `# %% [id=${uuid()}] ${type}\n`;
383+
const text = (isEmptyDoc ? "" : "\n") + delimiter;
384+
385+
const range = new monaco.value.Range(
386+
endLine,
387+
lineLen + 1,
388+
endLine,
389+
lineLen + 1,
390+
);
391+
editorRef.value.executeEdits("insert-cell", [
392+
{ range, text, forceMoveMarkers: true },
393+
]);
394+
395+
const cursorLine = isEmptyDoc ? 2 : endLine + 2;
396+
editorRef.value.setPosition({ lineNumber: cursorLine, column: 1 });
397+
editorRef.value.revealLineInCenterIfOutsideViewport(cursorLine);
398+
editorRef.value.focus();
399+
}
400+
363401
async function clearOutputs() {
364402
if (!currentNotebook.value) return;
365403
if (!confirm("Clear all outputs from this notebook?")) return;
@@ -617,9 +655,43 @@ const saveLabel = computed(() => {
617655
:class="{ 'editor-pane-split': showOutputs }"
618656
>
619657
<div
620-
class="editor-pane-toolbar border-bottom px-2 py-1 d-flex align-items-center"
658+
class="editor-pane-toolbar border-bottom px-2 py-1 d-flex align-items-center gap-2"
621659
>
622660
<small class="text-muted font-monospace me-auto">python</small>
661+
<div v-if="!readOnly" class="dropdown">
662+
<button
663+
type="button"
664+
class="btn btn-outline-secondary btn-sm dropdown-toggle"
665+
data-bs-toggle="dropdown"
666+
aria-expanded="false"
667+
title="Add a new cell after the current cell"
668+
>
669+
<FontAwesomeIcon :icon="faPlus" class="me-1" />
670+
Add
671+
</button>
672+
<ul class="dropdown-menu dropdown-menu-end">
673+
<li>
674+
<button
675+
type="button"
676+
class="dropdown-item"
677+
@click="insertCell('code')"
678+
>
679+
<FontAwesomeIcon :icon="faCode" class="me-2" />
680+
Code
681+
</button>
682+
</li>
683+
<li>
684+
<button
685+
type="button"
686+
class="dropdown-item"
687+
@click="insertCell('markdown')"
688+
>
689+
<FontAwesomeIcon :icon="faFileLines" class="me-2" />
690+
Markdown
691+
</button>
692+
</li>
693+
</ul>
694+
</div>
623695
<button
624696
class="btn btn-outline-secondary btn-sm"
625697
data-bs-toggle="modal"

frontend/src/components/notebooks/cellDelimiterParser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
const DELIMITER_RE =
1111
/^#\s*%%(?:\s*\[id=([0-9a-fA-F-]+)\])?(?:\s+(code|markdown))?\s*(.*)$/;
1212

13-
function uuid() {
13+
export function uuid() {
1414
if (typeof crypto !== "undefined" && crypto.randomUUID) {
1515
return crypto.randomUUID();
1616
}

0 commit comments

Comments
 (0)