@typbase/codemirror 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -661
- package/dist/index.d.mts +135 -65
- package/dist/index.mjs +648 -387
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { acceptCompletion, autocompletion, closeBracketsKeymap, completionKeymap, snippet, startCompletion } from "@codemirror/autocomplete";
|
|
1
|
+
import { acceptCompletion, autocompletion, closeBracketsKeymap, closeCompletion, completionKeymap, snippet, startCompletion } from "@codemirror/autocomplete";
|
|
2
2
|
import { indentService } from "@codemirror/language";
|
|
3
3
|
import { Decoration, EditorView, ViewPlugin, WidgetType, hoverTooltip, keymap, showTooltip } from "@codemirror/view";
|
|
4
|
-
import { ChangeSet, EditorSelection, EditorState, Facet,
|
|
4
|
+
import { ChangeSet, EditorSelection, EditorState, Facet, RangeSetBuilder, StateEffect, StateField, countColumn, findColumn } from "@codemirror/state";
|
|
5
5
|
import { copyLineDown, copyLineUp, indentLess, indentMore, insertBlankLine, moveLineDown, moveLineUp, redo, redoSelection, simplifySelection, standardKeymap, toggleBlockComment, toggleLineComment, undo, undoSelection } from "@codemirror/commands";
|
|
6
6
|
import { linter, nextDiagnostic, setDiagnostics } from "@codemirror/lint";
|
|
7
7
|
import { closeSearchPanel, findNext, findPrevious, gotoLine, openSearchPanel, selectNextOccurrence, selectSelectionMatches } from "@codemirror/search";
|
|
@@ -20,15 +20,29 @@ const typstSyntaxHighlighting = (fileId, typstState) => StateField.define({
|
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
function buildDecorations$2(state, fileId, typstState) {
|
|
23
|
-
const builder = new RangeSetBuilder();
|
|
24
23
|
let tokens;
|
|
24
|
+
let equations;
|
|
25
25
|
try {
|
|
26
26
|
tokens = typstState.highlight(fileId, state.doc.toString());
|
|
27
|
+
equations = typstState.extractEquations(fileId, state.doc.toString());
|
|
27
28
|
} catch (error) {
|
|
28
29
|
console.error("[typst] highlight panicked:", error);
|
|
29
30
|
return Decoration.none;
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
const builder = new RangeSetBuilder();
|
|
33
|
+
const equationMark = Decoration.mark({ class: "typ-equation" });
|
|
34
|
+
let nextEquation = 0;
|
|
35
|
+
for (const token of tokens) {
|
|
36
|
+
while (nextEquation < equations.length && equations[nextEquation].from <= token.range.start) {
|
|
37
|
+
const equation = equations[nextEquation++];
|
|
38
|
+
builder.add(equation.from, equation.to, equationMark);
|
|
39
|
+
}
|
|
40
|
+
builder.add(token.range.start, token.range.end, Decoration.mark({ class: token.tag }));
|
|
41
|
+
}
|
|
42
|
+
for (; nextEquation < equations.length; nextEquation++) {
|
|
43
|
+
const equation = equations[nextEquation];
|
|
44
|
+
builder.add(equation.from, equation.to, equationMark);
|
|
45
|
+
}
|
|
32
46
|
return builder.finish();
|
|
33
47
|
}
|
|
34
48
|
const parseBackticks = (str, into) => {
|
|
@@ -372,8 +386,19 @@ function cursorAddVertical(view, offset) {
|
|
|
372
386
|
}
|
|
373
387
|
//#endregion
|
|
374
388
|
//#region src/keymap.ts
|
|
389
|
+
/**
|
|
390
|
+
* Whether the editor is running vim bindings. Vim handles keys from a DOM
|
|
391
|
+
* listener that CodeMirror's keymap runs before, so Escape has to be left
|
|
392
|
+
* unhandled or it never reaches the vim plugin. Set through the same
|
|
393
|
+
* compartment as the vim extension.
|
|
394
|
+
*/
|
|
395
|
+
const vimKeymapFacet = Facet.define({ combine: (values) => values.some(Boolean) });
|
|
396
|
+
/**
|
|
397
|
+
* The app's shortcuts dialog mirrors the common entries here. When a chord
|
|
398
|
+
* changes, update `EDITOR_SHORTCUTS` in `apps/web/src/lib/keybindings.ts` too.
|
|
399
|
+
*/
|
|
375
400
|
const typstKeymap = keymap.of([
|
|
376
|
-
...completionKeymap,
|
|
401
|
+
...completionKeymap.filter((binding) => binding.key !== "Escape"),
|
|
377
402
|
...closeBracketsKeymap,
|
|
378
403
|
{
|
|
379
404
|
key: "Tab",
|
|
@@ -488,7 +513,13 @@ const typstKeymap = keymap.of([
|
|
|
488
513
|
},
|
|
489
514
|
{
|
|
490
515
|
key: "Escape",
|
|
491
|
-
run:
|
|
516
|
+
run: (view) => {
|
|
517
|
+
if (view.state.facet(vimKeymapFacet)) {
|
|
518
|
+
closeCompletion(view);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
return closeCompletion(view) || simplifySelection(view);
|
|
522
|
+
}
|
|
492
523
|
},
|
|
493
524
|
{
|
|
494
525
|
key: "Mod-Alt-ArrowUp",
|
|
@@ -524,7 +555,7 @@ const typstKeymap = keymap.of([
|
|
|
524
555
|
run: selectNextOccurrence,
|
|
525
556
|
preventDefault: true
|
|
526
557
|
},
|
|
527
|
-
...standardKeymap
|
|
558
|
+
...standardKeymap.filter((binding) => binding.key !== "Escape")
|
|
528
559
|
]);
|
|
529
560
|
//#endregion
|
|
530
561
|
//#region src/language.ts
|
|
@@ -583,6 +614,25 @@ const typstLanguageData = EditorState.languageData.of(() => [{
|
|
|
583
614
|
}]);
|
|
584
615
|
//#endregion
|
|
585
616
|
//#region src/notebook.ts
|
|
617
|
+
/** Every type, in the order the menu lists them. */
|
|
618
|
+
const NOTEBOOK_CELL_TYPES = [
|
|
619
|
+
"prose",
|
|
620
|
+
"code",
|
|
621
|
+
"log",
|
|
622
|
+
"hidden"
|
|
623
|
+
];
|
|
624
|
+
/** True when the cell renders in place rather than showing source plus output. */
|
|
625
|
+
function rendersInPlace(kind) {
|
|
626
|
+
return kind === "prose";
|
|
627
|
+
}
|
|
628
|
+
/** True when the cell's source is not part of what the reader sees. */
|
|
629
|
+
function hidesSource(kind) {
|
|
630
|
+
return kind === "log" || kind === "hidden";
|
|
631
|
+
}
|
|
632
|
+
/** True when the cell has an output area below its source. */
|
|
633
|
+
function hasOutput(kind) {
|
|
634
|
+
return kind === "code" || kind === "log";
|
|
635
|
+
}
|
|
586
636
|
/** The options of the notebook extension active in this state, if any. */
|
|
587
637
|
const notebookOptionsFacet = Facet.define({ combine: (values) => values.at(-1) });
|
|
588
638
|
function notebookOptions(state) {
|
|
@@ -590,13 +640,12 @@ function notebookOptions(state) {
|
|
|
590
640
|
}
|
|
591
641
|
/** Re-renders notebooks from cached frames without recompiling. */
|
|
592
642
|
const notebookRefreshEffect = StateEffect.define();
|
|
593
|
-
/**
|
|
594
|
-
const notebookRunEffect = StateEffect.define();
|
|
595
|
-
/** First position that belongs to the cell (marker line, or content). */
|
|
643
|
+
/** First position that belongs to the cell (attribute line, or content). */
|
|
596
644
|
function cellStart(cell) {
|
|
597
645
|
return cell.marker_end > cell.marker_start ? cell.marker_start : cell.content_start;
|
|
598
646
|
}
|
|
599
|
-
|
|
647
|
+
/** True when the cell opened with an attribute line rather than a heading. */
|
|
648
|
+
function hasAttribute(cell) {
|
|
600
649
|
return cell.marker_end > cell.marker_start;
|
|
601
650
|
}
|
|
602
651
|
/** Index of the cell containing `pos`. Gaps belong to the cell above. */
|
|
@@ -615,128 +664,137 @@ function blockEnd(cells, index, text) {
|
|
|
615
664
|
const next = cells[index + 1];
|
|
616
665
|
return next ? cellStart(next) : text.length;
|
|
617
666
|
}
|
|
618
|
-
/**
|
|
619
|
-
|
|
620
|
-
|
|
667
|
+
/**
|
|
668
|
+
* The attribute line for a cell type. Prose needs no `kind=`, so a plain cell is
|
|
669
|
+
* three characters.
|
|
670
|
+
*/
|
|
671
|
+
function attributeText(type, name) {
|
|
672
|
+
const fields = [name ? `name=${name}` : "", type === "prose" ? "" : `kind=${type}`].filter(Boolean);
|
|
673
|
+
return fields.length ? `//% ${fields.join(" ")}` : "//%";
|
|
621
674
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Matches a whole attribute line, indentation and newline included. The rest of
|
|
677
|
+
* the line is taken whole rather than field by field, because this is a text
|
|
678
|
+
* cleaner for exports and the engine already decided what is an attribute.
|
|
679
|
+
*/
|
|
680
|
+
const ATTRIBUTE_LINE = /^[ \t]*\/\/%[^\r\n]*\r?\n?/gm;
|
|
681
|
+
/**
|
|
682
|
+
* Removes cell attribute lines. Notebooks are comments-plus-content, so a
|
|
683
|
+
* cleaned file is the same document for anyone who does not know the
|
|
684
|
+
* convention. Only `//%` goes: any other comment is left as written.
|
|
685
|
+
*/
|
|
686
|
+
function stripCellAttributes(text) {
|
|
687
|
+
return text.replace(ATTRIBUTE_LINE, "");
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Splits a cell at `pos` inside its content. The new cell takes the text from
|
|
691
|
+
* `pos` and inherits the old cell's kind, because a split is a continuation.
|
|
692
|
+
*/
|
|
693
|
+
function splitCellText(text, cells, index, pos) {
|
|
635
694
|
const cell = cells[index];
|
|
636
|
-
|
|
695
|
+
if (!cell || pos < cell.content_start || pos > cell.content_end) return null;
|
|
696
|
+
const trimmed = text.slice(pos, cell.content_end).replace(/^\n+/, "");
|
|
697
|
+
const insert = `\n\n${attributeText(cell.kind, cell.name)}\n\n${trimmed}`;
|
|
637
698
|
return {
|
|
638
699
|
changes: {
|
|
639
|
-
from,
|
|
640
|
-
|
|
700
|
+
from: pos,
|
|
701
|
+
to: cell.content_end,
|
|
702
|
+
insert
|
|
641
703
|
},
|
|
642
|
-
anchor:
|
|
704
|
+
anchor: pos + 2 + attributeText(cell.kind, cell.name).length + 2
|
|
643
705
|
};
|
|
644
706
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
function
|
|
707
|
+
/**
|
|
708
|
+
* Merges a cell into the one above. The result takes the lower cell's kind,
|
|
709
|
+
* since that is the one the reader was last looking at.
|
|
710
|
+
*/
|
|
711
|
+
function mergeCellText(cells, index) {
|
|
650
712
|
const cell = cells[index];
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
from: 0,
|
|
654
|
-
to: text.length,
|
|
655
|
-
insert: ""
|
|
656
|
-
},
|
|
657
|
-
anchor: 0
|
|
658
|
-
};
|
|
713
|
+
const above = cells[index - 1];
|
|
714
|
+
if (!cell || !above) return null;
|
|
659
715
|
const from = cellStart(cell);
|
|
716
|
+
const head = above.content_end > above.content_start ? above.content_end : from;
|
|
717
|
+
const gap = text_between_gap(head, cell.content_start);
|
|
660
718
|
return {
|
|
661
719
|
changes: {
|
|
662
|
-
from,
|
|
663
|
-
to:
|
|
664
|
-
insert:
|
|
720
|
+
from: head,
|
|
721
|
+
to: cell.content_start,
|
|
722
|
+
insert: gap
|
|
665
723
|
},
|
|
666
|
-
anchor:
|
|
724
|
+
anchor: head
|
|
667
725
|
};
|
|
668
726
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
return
|
|
727
|
+
/** The whitespace between two offsets, which a merge keeps as a separator. */
|
|
728
|
+
function text_between_gap(from, to) {
|
|
729
|
+
return from < to ? "\n\n" : "";
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Rewrites a cell's attribute line. A cell that began at a heading has no line
|
|
733
|
+
* to rewrite, so one is inserted.
|
|
734
|
+
*/
|
|
735
|
+
function setCellTypeText(text, cells, index, type) {
|
|
736
|
+
const cell = cells[index];
|
|
737
|
+
if (!cell) return null;
|
|
738
|
+
if (cell.kind === type) return null;
|
|
739
|
+
const attribute = attributeText(type, cell.name);
|
|
740
|
+
if (hasAttribute(cell)) return {
|
|
681
741
|
changes: {
|
|
682
|
-
from,
|
|
683
|
-
to,
|
|
684
|
-
insert:
|
|
742
|
+
from: cell.marker_start,
|
|
743
|
+
to: cell.marker_end,
|
|
744
|
+
insert: attribute
|
|
685
745
|
},
|
|
686
|
-
anchor:
|
|
746
|
+
anchor: cell.content_start + (attribute.length - (cell.marker_end - cell.marker_start))
|
|
687
747
|
};
|
|
688
|
-
}
|
|
689
|
-
function splitCellText(text, cells, index, pos) {
|
|
690
|
-
const cell = cells[index];
|
|
691
|
-
if (pos < cell.content_start || pos > cell.content_end) return null;
|
|
692
748
|
return {
|
|
693
749
|
changes: {
|
|
694
|
-
from:
|
|
695
|
-
insert:
|
|
750
|
+
from: cellStart(cell),
|
|
751
|
+
insert: `${attribute}\n`
|
|
696
752
|
},
|
|
697
|
-
anchor:
|
|
753
|
+
anchor: cell.content_start + attribute.length + 1
|
|
698
754
|
};
|
|
699
755
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
756
|
+
/** Appends a cell of `type` after `index` (or at the end of the document). */
|
|
757
|
+
function insertCellText(text, cells, index, type, content = "") {
|
|
758
|
+
const attribute = attributeText(type);
|
|
759
|
+
const at = blockEnd(cells, index, text);
|
|
760
|
+
const prefix = text.slice(Math.max(0, at - 2), at).includes("\n\n") || at === 0 ? "" : "\n\n";
|
|
703
761
|
return {
|
|
704
762
|
changes: {
|
|
705
|
-
from:
|
|
706
|
-
|
|
707
|
-
insert: ""
|
|
763
|
+
from: at,
|
|
764
|
+
insert: `${prefix}${attribute}\n${content}\n\n`
|
|
708
765
|
},
|
|
709
|
-
anchor:
|
|
766
|
+
anchor: at + prefix.length + attribute.length + 1 + content.length
|
|
710
767
|
};
|
|
711
768
|
}
|
|
712
|
-
function
|
|
769
|
+
function deleteCellText(cells, index, docLength) {
|
|
713
770
|
const cell = cells[index];
|
|
714
|
-
|
|
715
|
-
|
|
771
|
+
if (!cell) return null;
|
|
772
|
+
const next = cells[index + 1];
|
|
773
|
+
const nextStart = next ? cellStart(next) : docLength;
|
|
774
|
+
const from = hasAttribute(cell) ? cell.marker_start : cell.content_start;
|
|
775
|
+
if (!cells.length || index === 0 && !next && from === 0) return {
|
|
716
776
|
changes: {
|
|
717
|
-
from:
|
|
718
|
-
to:
|
|
719
|
-
insert:
|
|
777
|
+
from: 0,
|
|
778
|
+
to: docLength,
|
|
779
|
+
insert: ""
|
|
720
780
|
},
|
|
721
|
-
anchor:
|
|
781
|
+
anchor: 0
|
|
722
782
|
};
|
|
723
783
|
return {
|
|
724
784
|
changes: {
|
|
725
|
-
from
|
|
726
|
-
|
|
785
|
+
from,
|
|
786
|
+
to: nextStart,
|
|
787
|
+
insert: ""
|
|
727
788
|
},
|
|
728
|
-
anchor:
|
|
789
|
+
anchor: from
|
|
729
790
|
};
|
|
730
791
|
}
|
|
731
|
-
/**
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
*/
|
|
738
|
-
function stripCellMarkers(text) {
|
|
739
|
-
return text.replace(MARKER_LINE, "");
|
|
792
|
+
/** Copies a cell's kind, name, and body below it. */
|
|
793
|
+
function duplicateCellText(text, cells, index) {
|
|
794
|
+
const cell = cells[index];
|
|
795
|
+
if (!cell) return null;
|
|
796
|
+
const body = text.slice(cell.content_start, cell.content_end);
|
|
797
|
+
return insertCellText(text, cells, index, cell.kind, body);
|
|
740
798
|
}
|
|
741
799
|
function dispatchEdit(view, edit) {
|
|
742
800
|
view.dispatch({
|
|
@@ -746,22 +804,13 @@ function dispatchEdit(view, edit) {
|
|
|
746
804
|
});
|
|
747
805
|
return true;
|
|
748
806
|
}
|
|
749
|
-
/**
|
|
750
|
-
function runCell(view, index) {
|
|
751
|
-
if (!notebookOptions(view.state) || index < 0) return false;
|
|
752
|
-
view.dispatch({ effects: notebookRunEffect.of({ index }) });
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
function runAllCells(view) {
|
|
756
|
-
if (!notebookOptions(view.state)) return false;
|
|
757
|
-
view.dispatch({ effects: notebookRunEffect.of({ index: "all" }) });
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
807
|
+
/** Selects a cell's content so the keyboard commands act on it. */
|
|
760
808
|
function focusCell(view, index, edge = "start") {
|
|
761
809
|
const cell = cellsOf(view.state)[index];
|
|
762
810
|
if (!cell) return false;
|
|
811
|
+
const pos = edge === "start" ? cell.content_start : cell.content_end;
|
|
763
812
|
view.dispatch({
|
|
764
|
-
selection: { anchor:
|
|
813
|
+
selection: { anchor: pos },
|
|
765
814
|
scrollIntoView: true
|
|
766
815
|
});
|
|
767
816
|
view.focus();
|
|
@@ -770,148 +819,105 @@ function focusCell(view, index, edge = "start") {
|
|
|
770
819
|
function currentCell(view) {
|
|
771
820
|
return cellIndexAt(cellsOf(view.state), view.state.selection.main.head);
|
|
772
821
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
function insertCellAt(view, index, where) {
|
|
822
|
+
/** Inserts a cell below `index`, or at the end of the document. */
|
|
823
|
+
function insertCell(view, type, content = "", index = -1) {
|
|
777
824
|
const cells = cellsOf(view.state);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
return dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, where, type));
|
|
781
|
-
}
|
|
782
|
-
function duplicateCell(view) {
|
|
783
|
-
return duplicateCellAt(view, currentCell(view));
|
|
825
|
+
const edit = insertCellText(view.state.doc.toString(), cells, index >= 0 ? index : cells.length ? currentCell(view) : -1, type, content);
|
|
826
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
784
827
|
}
|
|
785
|
-
function
|
|
828
|
+
function duplicateCell(view, index = -1) {
|
|
786
829
|
const cells = cellsOf(view.state);
|
|
787
|
-
|
|
788
|
-
|
|
830
|
+
const at = index >= 0 ? index : currentCell(view);
|
|
831
|
+
const edit = duplicateCellText(view.state.doc.toString(), cells, at);
|
|
832
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
789
833
|
}
|
|
790
|
-
function deleteCell(view) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
function deleteCellAt(view, index) {
|
|
794
|
-
const cells = cellsOf(view.state);
|
|
795
|
-
if (index < 0 || !cells[index]) return false;
|
|
796
|
-
return dispatchEdit(view, deleteCellText(view.state.doc.toString(), cells, index));
|
|
834
|
+
function deleteCell(view, index = -1) {
|
|
835
|
+
const edit = deleteCellText(cellsOf(view.state), index >= 0 ? index : currentCell(view), view.state.doc.length);
|
|
836
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
797
837
|
}
|
|
838
|
+
/** Moves the active cell up or down. */
|
|
798
839
|
function moveCell(view, delta) {
|
|
799
|
-
return moveCellAt(view, currentCell(view), delta);
|
|
800
|
-
}
|
|
801
|
-
function moveCellAt(view, index, delta) {
|
|
802
|
-
const cells = cellsOf(view.state);
|
|
803
|
-
if (index < 0 || !cells[index]) return false;
|
|
804
|
-
const edit = moveCellText(view.state.doc.toString(), cells, index, delta);
|
|
805
|
-
if (!edit) return false;
|
|
806
|
-
return dispatchEdit(view, edit);
|
|
807
|
-
}
|
|
808
|
-
function splitCell(view) {
|
|
809
840
|
const cells = cellsOf(view.state);
|
|
810
841
|
const index = currentCell(view);
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
842
|
+
const next = index + delta;
|
|
843
|
+
if (next < 0 || next >= cells.length || index < 0) return false;
|
|
844
|
+
const text = view.state.doc.toString();
|
|
845
|
+
const first = cells[index];
|
|
846
|
+
const second = cells[next];
|
|
847
|
+
const from = cellStart(first);
|
|
848
|
+
const to = cellStart(second);
|
|
849
|
+
const firstEnd = blockEnd(cells, index, text);
|
|
850
|
+
const secondEnd = blockEnd(cells, next, text);
|
|
851
|
+
view.dispatch({
|
|
852
|
+
changes: [{
|
|
853
|
+
from,
|
|
854
|
+
to: firstEnd,
|
|
855
|
+
insert: text.slice(to, secondEnd)
|
|
856
|
+
}, {
|
|
857
|
+
from: to,
|
|
858
|
+
to: secondEnd,
|
|
859
|
+
insert: text.slice(from, firstEnd)
|
|
860
|
+
}],
|
|
861
|
+
selection: { anchor: from },
|
|
862
|
+
scrollIntoView: true
|
|
863
|
+
});
|
|
864
|
+
return true;
|
|
830
865
|
}
|
|
831
866
|
function setCellType(view, index, type) {
|
|
832
867
|
const cells = cellsOf(view.state);
|
|
833
|
-
|
|
834
|
-
return dispatchEdit(view,
|
|
868
|
+
const edit = setCellTypeText(view.state.doc.toString(), cells, index, type);
|
|
869
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
835
870
|
}
|
|
836
|
-
function
|
|
837
|
-
const cells = cellsOf(view.state);
|
|
838
|
-
const index = currentCell(view);
|
|
839
|
-
if (index < 0) return false;
|
|
840
|
-
runCell(view, index);
|
|
841
|
-
if (cells[index + 1]) focusCell(view, index + 1);
|
|
842
|
-
else dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, "below", cells[index]?.kind ?? "markup"));
|
|
843
|
-
return true;
|
|
844
|
-
}
|
|
845
|
-
function runAndInsert(view) {
|
|
871
|
+
function splitCell(view) {
|
|
846
872
|
const cells = cellsOf(view.state);
|
|
847
873
|
const index = currentCell(view);
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return dispatchEdit(view, insertCellText(view.state.doc.toString(), cells, index, "below", cells[index]?.kind ?? "markup"));
|
|
851
|
-
}
|
|
852
|
-
function runCurrent(view) {
|
|
853
|
-
const index = currentCell(view);
|
|
854
|
-
return index < 0 ? false : runCell(view, index);
|
|
874
|
+
const edit = splitCellText(view.state.doc.toString(), cells, index, view.state.selection.main.from);
|
|
875
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
855
876
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const range = state.selection.main;
|
|
860
|
-
if (!range.empty) return false;
|
|
861
|
-
const cells = cellsOf(state);
|
|
862
|
-
const index = cellIndexAt(cells, range.head);
|
|
863
|
-
const cell = cells[index];
|
|
864
|
-
if (!cell) return false;
|
|
865
|
-
if (direction === -1) {
|
|
866
|
-
const first = state.doc.lineAt(cell.content_start);
|
|
867
|
-
if (range.head < first.from || range.head > first.to || index === 0) return false;
|
|
868
|
-
return focusCell(view, index - 1, "end");
|
|
869
|
-
}
|
|
870
|
-
const last = state.doc.lineAt(cell.content_end);
|
|
871
|
-
if (range.head < last.from || range.head > last.to || index >= cells.length - 1) return false;
|
|
872
|
-
return focusCell(view, index + 1);
|
|
877
|
+
function mergeCell(view) {
|
|
878
|
+
const edit = mergeCellText(cellsOf(view.state), currentCell(view));
|
|
879
|
+
return edit ? dispatchEdit(view, edit) : false;
|
|
873
880
|
}
|
|
874
881
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
882
|
+
* The editor's cell commands. Everything here is also in the cell menu, and
|
|
883
|
+
* every one of them is a text edit, so the undo stack covers them all.
|
|
877
884
|
*/
|
|
878
|
-
const notebookKeymap =
|
|
879
|
-
{
|
|
880
|
-
key: "Shift-Enter",
|
|
881
|
-
run: runAndAdvance
|
|
882
|
-
},
|
|
885
|
+
const notebookKeymap = keymap.of([
|
|
883
886
|
{
|
|
884
887
|
key: "Mod-Enter",
|
|
885
|
-
|
|
888
|
+
preventDefault: true,
|
|
889
|
+
run: (view) => focusCell(view, currentCell(view), "end")
|
|
886
890
|
},
|
|
887
891
|
{
|
|
888
|
-
key: "
|
|
889
|
-
|
|
892
|
+
key: "Mod-Shift-Enter",
|
|
893
|
+
preventDefault: true,
|
|
894
|
+
run: (view) => splitCell(view)
|
|
890
895
|
},
|
|
891
896
|
{
|
|
892
|
-
key: "
|
|
897
|
+
key: "Mod-Shift-ArrowUp",
|
|
898
|
+
preventDefault: true,
|
|
893
899
|
run: (view) => moveCell(view, -1)
|
|
894
900
|
},
|
|
895
901
|
{
|
|
896
|
-
key: "
|
|
902
|
+
key: "Mod-Shift-ArrowDown",
|
|
903
|
+
preventDefault: true,
|
|
897
904
|
run: (view) => moveCell(view, 1)
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
key: "Escape",
|
|
901
|
-
run: (view) => {
|
|
902
|
-
notebookOptions(view.state)?.onCommandMode?.();
|
|
903
|
-
return true;
|
|
904
|
-
}
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
key: "ArrowUp",
|
|
908
|
-
run: (view) => boundaryMove(view, -1)
|
|
909
|
-
},
|
|
910
|
-
{
|
|
911
|
-
key: "ArrowDown",
|
|
912
|
-
run: (view) => boundaryMove(view, 1)
|
|
913
905
|
}
|
|
914
|
-
])
|
|
906
|
+
]);
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/sync.ts
|
|
909
|
+
/**
|
|
910
|
+
* Keeps the engine's sources in step with the editor, without rendering.
|
|
911
|
+
*/
|
|
912
|
+
const typstSourceSync = (fileId, prelude, typstState) => EditorState.transactionExtender.of((transaction) => {
|
|
913
|
+
if (!transaction.docChanged) return null;
|
|
914
|
+
try {
|
|
915
|
+
typstState.syncSource(fileId, transaction.newDoc.toString(), prelude.value);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
console.error("[typst] source sync panicked:", error);
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
});
|
|
915
921
|
//#endregion
|
|
916
922
|
//#region src/diagnostics.ts
|
|
917
923
|
/**
|
|
@@ -919,7 +925,7 @@ const notebookKeymap = Prec.high(keymap.of([
|
|
|
919
925
|
* diagnostics of every compile here, so the linter source can reuse them
|
|
920
926
|
* instead of compiling the same text a second time.
|
|
921
927
|
*/
|
|
922
|
-
const diagnosticsCache =
|
|
928
|
+
const diagnosticsCache = new LRUCache({ max: 2 });
|
|
923
929
|
function rememberDiagnostics(path, text, diagnostics) {
|
|
924
930
|
diagnosticsCache.set(path, {
|
|
925
931
|
text,
|
|
@@ -958,11 +964,6 @@ function toLintDiagnostics(typstDiagnostics) {
|
|
|
958
964
|
}
|
|
959
965
|
/**
|
|
960
966
|
* Typst diagnostics as a CodeMirror linter source.
|
|
961
|
-
*
|
|
962
|
-
* This runs in every editor mode and combines with other sources (the
|
|
963
|
-
* spellcheck linter), so neither one replaces the other. The WYSIWYG plugin's
|
|
964
|
-
* compile feeds the cache. Split and source mode fall back to a compile here
|
|
965
|
-
* so they get diagnostics too.
|
|
966
967
|
*/
|
|
967
968
|
const typstLinter = (fileId, spaceId, path, prelude, typstState, options = {}) => linter(async (view) => {
|
|
968
969
|
const text = view.state.doc.toString();
|
|
@@ -970,14 +971,14 @@ const typstLinter = (fileId, spaceId, path, prelude, typstState, options = {}) =
|
|
|
970
971
|
if (cached) return toLintDiagnostics(cached);
|
|
971
972
|
let result;
|
|
972
973
|
try {
|
|
973
|
-
result = typstState.
|
|
974
|
+
result = typstState.diagnosePaged(fileId, text, prelude.value);
|
|
974
975
|
} catch (error) {
|
|
975
976
|
console.error("[typst] diagnostics compile panicked:", error);
|
|
976
977
|
options.onPanic?.(fileId);
|
|
977
978
|
return [];
|
|
978
979
|
}
|
|
979
980
|
if (result.requests.length > 0 && options.onRequests) {
|
|
980
|
-
if (await options.onRequests(result.requests, spaceId)) result = typstState.
|
|
981
|
+
if (await options.onRequests(result.requests, spaceId)) result = typstState.diagnosePaged(fileId, text, prelude.value);
|
|
981
982
|
}
|
|
982
983
|
rememberDiagnostics(path, text, result.diagnostics);
|
|
983
984
|
options.onCompile?.();
|
|
@@ -986,6 +987,26 @@ const typstLinter = (fileId, spaceId, path, prelude, typstState, options = {}) =
|
|
|
986
987
|
//#endregion
|
|
987
988
|
//#region src/frames.ts
|
|
988
989
|
const containerCache = new LRUCache({ max: 128 });
|
|
990
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
991
|
+
let sharedDefs;
|
|
992
|
+
/**
|
|
993
|
+
* Places the glyph and gradient definitions a frame set references.
|
|
994
|
+
*/
|
|
995
|
+
function installSharedDefs(defs) {
|
|
996
|
+
if (!defs) return;
|
|
997
|
+
if (!sharedDefs || !sharedDefs.isConnected) {
|
|
998
|
+
const host = document.createElementNS(SVG_NS, "svg");
|
|
999
|
+
host.setAttribute("width", "0");
|
|
1000
|
+
host.setAttribute("height", "0");
|
|
1001
|
+
host.setAttribute("aria-hidden", "true");
|
|
1002
|
+
host.style.position = "absolute";
|
|
1003
|
+
sharedDefs = document.createElementNS(SVG_NS, "defs");
|
|
1004
|
+
host.append(sharedDefs);
|
|
1005
|
+
document.body.append(host);
|
|
1006
|
+
}
|
|
1007
|
+
sharedDefs.innerHTML = "";
|
|
1008
|
+
sharedDefs.innerHTML = defs;
|
|
1009
|
+
}
|
|
989
1010
|
const openInNewTab = (url) => {
|
|
990
1011
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
991
1012
|
};
|
|
@@ -1147,46 +1168,46 @@ var TypstWidget = class extends WidgetType {
|
|
|
1147
1168
|
};
|
|
1148
1169
|
//#endregion
|
|
1149
1170
|
//#region src/notebook-widgets.ts
|
|
1150
|
-
function
|
|
1171
|
+
function icon(name, size) {
|
|
1151
1172
|
const span = document.createElement("span");
|
|
1152
1173
|
span.className = "ms-icon material-symbols-rounded";
|
|
1153
|
-
span.textContent = name;
|
|
1154
1174
|
span.setAttribute("aria-hidden", "true");
|
|
1175
|
+
if (size !== void 0) span.style.fontSize = `calc(${size}px * var(--ui-size, 1))`;
|
|
1176
|
+
span.textContent = name;
|
|
1155
1177
|
return span;
|
|
1156
1178
|
}
|
|
1157
|
-
/**
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
toggleSource: "Collapse cell",
|
|
1168
|
-
noOutput: "No output",
|
|
1169
|
-
error: "Cell error"
|
|
1170
|
-
};
|
|
1171
|
-
/** Buttons must not hand the mousedown to CodeMirror's selection handling. */
|
|
1172
|
-
function actionButton(icon, label, onClick, className = "") {
|
|
1173
|
-
const button = document.createElement("button");
|
|
1174
|
-
button.type = "button";
|
|
1175
|
-
button.className = `tb-cell-btn ${className}`.trim();
|
|
1176
|
-
button.title = label;
|
|
1177
|
-
button.setAttribute("aria-label", label);
|
|
1178
|
-
button.append(iconSpan(icon));
|
|
1179
|
-
button.addEventListener("mousedown", (event) => event.preventDefault());
|
|
1180
|
-
button.addEventListener("click", (event) => {
|
|
1179
|
+
/** A button that must not hand the mousedown to CodeMirror's selection. */
|
|
1180
|
+
function button(name, label, onClick, className = "") {
|
|
1181
|
+
const element = document.createElement("button");
|
|
1182
|
+
element.type = "button";
|
|
1183
|
+
element.className = `tb-cell-btn${className ? ` ${className}` : ""}`;
|
|
1184
|
+
element.title = label;
|
|
1185
|
+
element.setAttribute("aria-label", label);
|
|
1186
|
+
element.append(icon(name, RAIL_ICON_SIZE));
|
|
1187
|
+
element.addEventListener("mousedown", (event) => event.preventDefault());
|
|
1188
|
+
element.addEventListener("click", (event) => {
|
|
1181
1189
|
event.preventDefault();
|
|
1182
1190
|
event.stopPropagation();
|
|
1183
|
-
onClick(
|
|
1191
|
+
onClick();
|
|
1184
1192
|
});
|
|
1185
|
-
return
|
|
1193
|
+
return element;
|
|
1186
1194
|
}
|
|
1187
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Icon size across the rail: the chip glyph, its overflow button, and the menu's
|
|
1197
|
+
* items, which is the same row and `UiMenuItem`'s default.
|
|
1198
|
+
*/
|
|
1199
|
+
const RAIL_ICON_SIZE = 20;
|
|
1200
|
+
/** The glyph and label for a cell kind. */
|
|
1201
|
+
const KIND_PRESENTATION = {
|
|
1202
|
+
prose: "notes",
|
|
1203
|
+
code: "code",
|
|
1204
|
+
log: "terminal",
|
|
1205
|
+
hidden: "visibility_off"
|
|
1206
|
+
};
|
|
1207
|
+
var NotebookRailWidget = class extends WidgetType {
|
|
1188
1208
|
view;
|
|
1189
1209
|
args;
|
|
1210
|
+
menu = null;
|
|
1190
1211
|
constructor(view, args) {
|
|
1191
1212
|
super();
|
|
1192
1213
|
this.view = view;
|
|
@@ -1195,48 +1216,139 @@ var NotebookHeaderWidget = class extends WidgetType {
|
|
|
1195
1216
|
eq(other) {
|
|
1196
1217
|
const a = this.args;
|
|
1197
1218
|
const b = other.args;
|
|
1198
|
-
return a.index === b.index && a.kind === b.kind && a.
|
|
1219
|
+
return a.index === b.index && a.kind === b.kind && a.name === b.name && a.collapsed === b.collapsed && a.held === b.held && a.labels === b.labels;
|
|
1220
|
+
}
|
|
1221
|
+
ignoreEvent(event) {
|
|
1222
|
+
return event.type === "mousedown" && event.target.classList.contains("tb-cell-rail");
|
|
1199
1223
|
}
|
|
1200
1224
|
toDOM() {
|
|
1201
|
-
const { index, kind, labels } = this.args;
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1225
|
+
const { index, kind, name, labels } = this.args;
|
|
1226
|
+
const rail = document.createElement("div");
|
|
1227
|
+
rail.className = "tb-cell-rail";
|
|
1228
|
+
rail.dataset.kind = kind;
|
|
1229
|
+
rail.dataset.cell = String(index);
|
|
1230
|
+
const chip = document.createElement("span");
|
|
1231
|
+
chip.className = "tb-cell-chip";
|
|
1232
|
+
chip.append(icon(KIND_PRESENTATION[kind], RAIL_ICON_SIZE));
|
|
1233
|
+
chip.append(document.createTextNode(name || labels[kind]));
|
|
1234
|
+
rail.append(chip);
|
|
1235
|
+
if (this.args.held) {
|
|
1236
|
+
const held = document.createElement("span");
|
|
1237
|
+
held.className = "tb-cell-held";
|
|
1238
|
+
held.textContent = labels.held;
|
|
1239
|
+
rail.append(held);
|
|
1214
1240
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const actions = document.createElement("span");
|
|
1220
|
-
actions.className = "tb-cell-actions";
|
|
1221
|
-
actions.append(actionButton("arrow_upward", labels.moveUp, () => moveCellAt(this.view, index, -1)), actionButton("arrow_downward", labels.moveDown, () => moveCellAt(this.view, index, 1)), actionButton("content_copy", labels.duplicate, () => duplicateCellAt(this.view, index)), actionButton(this.args.collapsed ? "unfold_more" : "unfold_less", labels.toggleSource, () => {
|
|
1222
|
-
notebookOptions(this.view.state)?.onToggleCollapse?.(index);
|
|
1223
|
-
this.view.dispatch({ effects: notebookRefreshEffect.of(null) });
|
|
1224
|
-
}), actionButton("delete_sweep", labels.clearOutput, () => {
|
|
1225
|
-
notebookOptions(this.view.state)?.onClearOutput?.(index);
|
|
1226
|
-
this.view.dispatch({ effects: notebookRefreshEffect.of(null) });
|
|
1227
|
-
}), actionButton("delete", labels.remove, () => deleteCellAt(this.view, index)));
|
|
1228
|
-
header.append(actions);
|
|
1229
|
-
header.addEventListener("mousedown", (event) => {
|
|
1230
|
-
if (event.target?.closest("button")) return;
|
|
1241
|
+
const menu = button("more_vert", labels.menu, () => this.toggleMenu(rail), "tb-cell-btn--menu");
|
|
1242
|
+
rail.append(menu);
|
|
1243
|
+
rail.addEventListener("mousedown", (event) => {
|
|
1244
|
+
if (event.target.closest("button")) return;
|
|
1231
1245
|
event.preventDefault();
|
|
1232
1246
|
focusCell(this.view, index);
|
|
1233
1247
|
});
|
|
1234
|
-
return
|
|
1248
|
+
return rail;
|
|
1235
1249
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1250
|
+
toggleMenu(anchor) {
|
|
1251
|
+
if (this.menu) {
|
|
1252
|
+
this.closeMenu();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const { labels, index, collapsed, held } = this.args;
|
|
1256
|
+
const items = [
|
|
1257
|
+
{
|
|
1258
|
+
command: "toggleSource",
|
|
1259
|
+
label: labels.toggleSource,
|
|
1260
|
+
name: collapsed ? "unfold_more" : "unfold_less"
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
command: "split",
|
|
1264
|
+
label: labels.split,
|
|
1265
|
+
name: "call_split"
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
command: "merge",
|
|
1269
|
+
label: labels.merge,
|
|
1270
|
+
name: "merge_type"
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
command: "moveUp",
|
|
1274
|
+
label: labels.moveUp,
|
|
1275
|
+
name: "keyboard_double_arrow_up"
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
command: "moveDown",
|
|
1279
|
+
label: labels.moveDown,
|
|
1280
|
+
name: "keyboard_double_arrow_down"
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
command: "duplicate",
|
|
1284
|
+
label: labels.duplicate,
|
|
1285
|
+
name: "content_copy"
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
command: "copy",
|
|
1289
|
+
label: labels.copy,
|
|
1290
|
+
name: "content_paste"
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
command: held ? "release" : "hold",
|
|
1294
|
+
label: held ? labels.release : labels.hold,
|
|
1295
|
+
name: held ? "play_arrow" : "pause"
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
command: "clearOutput",
|
|
1299
|
+
label: labels.clearOutput,
|
|
1300
|
+
name: "delete_sweep"
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
command: "remove",
|
|
1304
|
+
label: labels.remove,
|
|
1305
|
+
name: "delete"
|
|
1306
|
+
}
|
|
1307
|
+
];
|
|
1308
|
+
const menu = document.createElement("div");
|
|
1309
|
+
menu.className = "menu tb-cell-menu";
|
|
1310
|
+
menu.setAttribute("role", "menu");
|
|
1311
|
+
for (const item of items) {
|
|
1312
|
+
const entry = document.createElement("button");
|
|
1313
|
+
entry.type = "button";
|
|
1314
|
+
entry.className = "menu__item";
|
|
1315
|
+
entry.setAttribute("role", "menuitem");
|
|
1316
|
+
entry.append(icon(item.name, RAIL_ICON_SIZE));
|
|
1317
|
+
entry.append(document.createTextNode(item.label));
|
|
1318
|
+
entry.addEventListener("click", () => {
|
|
1319
|
+
this.closeMenu();
|
|
1320
|
+
this.args.onCommand(item.command, index);
|
|
1321
|
+
});
|
|
1322
|
+
menu.append(entry);
|
|
1323
|
+
}
|
|
1324
|
+
document.body.append(menu);
|
|
1325
|
+
const box = anchor.getBoundingClientRect();
|
|
1326
|
+
menu.style.position = "fixed";
|
|
1327
|
+
menu.style.left = `${Math.round(box.left)}px`;
|
|
1328
|
+
menu.style.top = `${Math.round(box.bottom + 4)}px`;
|
|
1329
|
+
this.menu = menu;
|
|
1330
|
+
const dismiss = (event) => {
|
|
1331
|
+
if (menu.contains(event.target)) return;
|
|
1332
|
+
this.closeMenu();
|
|
1333
|
+
};
|
|
1334
|
+
setTimeout(() => document.addEventListener("mousedown", dismiss), 0);
|
|
1335
|
+
document.addEventListener("keyup", this.onEscape);
|
|
1336
|
+
}
|
|
1337
|
+
onEscape = (event) => {
|
|
1338
|
+
if (event.key === "Escape") this.closeMenu();
|
|
1339
|
+
};
|
|
1340
|
+
closeMenu() {
|
|
1341
|
+
this.menu?.remove();
|
|
1342
|
+
this.menu = null;
|
|
1343
|
+
document.removeEventListener("keyup", this.onEscape);
|
|
1344
|
+
}
|
|
1345
|
+
destroy() {
|
|
1346
|
+
this.closeMenu();
|
|
1238
1347
|
}
|
|
1239
1348
|
};
|
|
1349
|
+
/**
|
|
1350
|
+
* A cell's output: its frames and the diagnostics whose span falls inside it.
|
|
1351
|
+
*/
|
|
1240
1352
|
var NotebookOutputWidget = class extends WidgetType {
|
|
1241
1353
|
view;
|
|
1242
1354
|
fileId;
|
|
@@ -1251,30 +1363,25 @@ var NotebookOutputWidget = class extends WidgetType {
|
|
|
1251
1363
|
this.args = args;
|
|
1252
1364
|
this.openExternal = openExternal;
|
|
1253
1365
|
}
|
|
1254
|
-
/** Hash of everything the DOM depends on, since DOM identity is expensive. */
|
|
1255
1366
|
get key() {
|
|
1256
1367
|
const frames = this.args.frames.map((frame) => {
|
|
1257
1368
|
const size = frameSize(frame);
|
|
1258
1369
|
return `${frame.render.hash}:${Math.round(size.width)}x${Math.round(size.height)}`;
|
|
1259
1370
|
}).join(",");
|
|
1260
1371
|
const diagnostics = this.args.diagnostics.map((diagnostic) => diagnostic.message).join("|");
|
|
1261
|
-
return `${this.args.index}:${this.args.cleared}:${this.args.
|
|
1372
|
+
return `${this.args.index}:${this.args.cleared}:${this.args.held}:${frames}:${diagnostics}`;
|
|
1262
1373
|
}
|
|
1263
1374
|
eq(other) {
|
|
1264
1375
|
return this.key === other.key;
|
|
1265
1376
|
}
|
|
1266
1377
|
toDOM() {
|
|
1267
1378
|
const root = document.createElement("div");
|
|
1268
|
-
const { cleared, labels } = this.args;
|
|
1379
|
+
const { cleared, held, labels } = this.args;
|
|
1269
1380
|
root.className = "tb-cell-output";
|
|
1270
1381
|
if (cleared) {
|
|
1271
1382
|
root.classList.add("tb-cell-output--cleared");
|
|
1272
1383
|
return root;
|
|
1273
1384
|
}
|
|
1274
|
-
if (!(this.args.frames.length > 0 || this.args.diagnostics.length > 0 || this.args.hasRun)) {
|
|
1275
|
-
root.classList.add("tb-cell-output--empty");
|
|
1276
|
-
return root;
|
|
1277
|
-
}
|
|
1278
1385
|
const body = document.createElement("div");
|
|
1279
1386
|
body.className = "tb-cell-output-body";
|
|
1280
1387
|
for (const diagnostic of this.args.diagnostics) {
|
|
@@ -1300,7 +1407,12 @@ var NotebookOutputWidget = class extends WidgetType {
|
|
|
1300
1407
|
holder.append(container);
|
|
1301
1408
|
body.append(holder);
|
|
1302
1409
|
}
|
|
1303
|
-
if (
|
|
1410
|
+
if (held) {
|
|
1411
|
+
const note = document.createElement("div");
|
|
1412
|
+
note.className = "tb-cell-stale";
|
|
1413
|
+
note.textContent = labels.held;
|
|
1414
|
+
body.append(note);
|
|
1415
|
+
} else if (!this.args.frames.length && !this.args.diagnostics.length) {
|
|
1304
1416
|
const empty = document.createElement("div");
|
|
1305
1417
|
empty.className = "tb-cell-empty";
|
|
1306
1418
|
empty.textContent = labels.noOutput;
|
|
@@ -1315,51 +1427,109 @@ var NotebookOutputWidget = class extends WidgetType {
|
|
|
1315
1427
|
return height + 16;
|
|
1316
1428
|
}
|
|
1317
1429
|
};
|
|
1430
|
+
/**
|
|
1431
|
+
* The height an off-screen cell's output reserves, without building any of it.
|
|
1432
|
+
*/
|
|
1433
|
+
var NotebookOutputSpacer = class extends WidgetType {
|
|
1434
|
+
height;
|
|
1435
|
+
constructor(height) {
|
|
1436
|
+
super();
|
|
1437
|
+
this.height = height;
|
|
1438
|
+
}
|
|
1439
|
+
eq(other) {
|
|
1440
|
+
return this.height === other.height;
|
|
1441
|
+
}
|
|
1442
|
+
toDOM() {
|
|
1443
|
+
const root = document.createElement("div");
|
|
1444
|
+
root.className = "tb-cell-output tb-cell-output--offscreen";
|
|
1445
|
+
return root;
|
|
1446
|
+
}
|
|
1447
|
+
get estimatedHeight() {
|
|
1448
|
+
return this.height;
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1318
1451
|
function decorateNotebook(args) {
|
|
1319
|
-
const { view, state, cells, frames, diagnostics, fileId, typstState, locked, onExternalLink, options } = args;
|
|
1452
|
+
const { view, state, cells, frames, diagnostics, fileId, typstState, locked, readOnly, renderFrames, activeIndex, onCommand, onExternalLink, options } = args;
|
|
1320
1453
|
const decorations = [];
|
|
1321
1454
|
const byCell = cells.map(() => []);
|
|
1322
1455
|
for (const frame of frames) {
|
|
1323
1456
|
const index = cellIndexAt(cells, frame.range.start);
|
|
1324
1457
|
if (index >= 0) byCell[index].push(frame);
|
|
1325
1458
|
}
|
|
1326
|
-
const
|
|
1459
|
+
const labels = options.labels ?? FALLBACK_LABELS;
|
|
1460
|
+
const cellState = (index) => options.state?.(index) ?? {};
|
|
1461
|
+
const visible = /* @__PURE__ */ new Set();
|
|
1462
|
+
for (const range of view.visibleRanges) {
|
|
1463
|
+
const from = cellIndexAt(cells, range.from);
|
|
1464
|
+
const to = cellIndexAt(cells, range.to);
|
|
1465
|
+
for (let index = Math.max(0, from); index <= Math.min(cells.length - 1, to); index++) visible.add(index);
|
|
1466
|
+
}
|
|
1327
1467
|
cells.forEach((cell, index) => {
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
decorations.push(Decoration.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1468
|
+
const own = cellState(index);
|
|
1469
|
+
const firstLine = state.doc.lineAt(cellStart(cell)).from;
|
|
1470
|
+
if (index === activeIndex) decorations.push(Decoration.line({ class: "tb-cell-active" }).range(firstLine));
|
|
1471
|
+
if (!readOnly) {
|
|
1472
|
+
decorations.push(Decoration.widget({
|
|
1473
|
+
widget: new NotebookRailWidget(view, {
|
|
1474
|
+
index,
|
|
1475
|
+
kind: cell.kind,
|
|
1476
|
+
name: cell.name,
|
|
1477
|
+
collapsed: own.collapsed ?? false,
|
|
1478
|
+
held: own.held ?? false,
|
|
1479
|
+
labels,
|
|
1480
|
+
onCommand
|
|
1481
|
+
}),
|
|
1482
|
+
block: !hasAttribute(cell),
|
|
1483
|
+
side: -1
|
|
1484
|
+
}).range(firstLine));
|
|
1485
|
+
if (hasAttribute(cell)) decorations.push(Decoration.line({ class: "tb-cell-attribute" }).range(state.doc.lineAt(cell.marker_start).from));
|
|
1486
|
+
}
|
|
1487
|
+
const contentStart = cell.content_start;
|
|
1488
|
+
const contentEnd = cell.content_end;
|
|
1489
|
+
const hideSource = (own.collapsed ?? false) || hidesSource(cell.kind);
|
|
1490
|
+
if (hideSource && contentEnd > contentStart) decorations.push(Decoration.replace({}).range(contentStart, contentEnd));
|
|
1491
|
+
if (hideSource) {
|
|
1492
|
+
if (renderFrames) pushDiagnostics(decorations, state, cell, diagnostics);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (rendersInPlace(cell.kind)) {
|
|
1496
|
+
if (!renderFrames) return;
|
|
1345
1497
|
for (const frame of byCell[index] ?? []) {
|
|
1346
1498
|
const { start, end } = frame.range;
|
|
1347
1499
|
if (!frame.render || end <= start) continue;
|
|
1348
1500
|
if (frameIsInactive(view, state, start, end)) decorations.push(Decoration.replace({ widget: new TypstWidget(view, frame, locked, fileId, typstState, onExternalLink) }).range(start, end));
|
|
1349
1501
|
else decorations.push(...frameActiveDecorations(view, state, frame));
|
|
1350
1502
|
}
|
|
1503
|
+
if (renderFrames) pushDiagnostics(decorations, state, cell, diagnostics);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (!hasOutput(cell.kind)) return;
|
|
1507
|
+
if (!renderFrames) return;
|
|
1508
|
+
const outputPos = contentEnd > contentStart ? state.doc.lineAt(contentEnd).to : contentStart;
|
|
1509
|
+
const held = own.held ?? false;
|
|
1510
|
+
const output = held ? own.frozen ?? {
|
|
1511
|
+
frames: [],
|
|
1512
|
+
diagnostics: []
|
|
1513
|
+
} : {
|
|
1514
|
+
frames: byCell[index] ?? [],
|
|
1515
|
+
diagnostics: cellDiagnosticsFor(cell, diagnostics)
|
|
1516
|
+
};
|
|
1517
|
+
if (!visible.has(index)) {
|
|
1518
|
+
decorations.push(Decoration.widget({
|
|
1519
|
+
widget: new NotebookOutputSpacer(outputHeight(output.frames)),
|
|
1520
|
+
block: true,
|
|
1521
|
+
side: 1
|
|
1522
|
+
}).range(outputPos));
|
|
1351
1523
|
return;
|
|
1352
1524
|
}
|
|
1353
|
-
const outputPos = cell.content_end > cell.content_start ? state.doc.lineAt(cell.content_end).to : cell.content_start;
|
|
1354
|
-
const cellDiagnostics = diagnostics.filter((diagnostic) => diagnostic.range.start >= cell.content_start && diagnostic.range.start <= cell.content_end);
|
|
1355
1525
|
decorations.push(Decoration.widget({
|
|
1356
1526
|
widget: new NotebookOutputWidget(view, fileId, typstState, {
|
|
1357
1527
|
index,
|
|
1358
|
-
frames:
|
|
1359
|
-
diagnostics:
|
|
1360
|
-
cleared:
|
|
1361
|
-
|
|
1362
|
-
labels
|
|
1528
|
+
frames: output.frames,
|
|
1529
|
+
diagnostics: output.diagnostics,
|
|
1530
|
+
cleared: own.cleared ?? false,
|
|
1531
|
+
held,
|
|
1532
|
+
labels
|
|
1363
1533
|
}, onExternalLink),
|
|
1364
1534
|
block: true,
|
|
1365
1535
|
side: 1
|
|
@@ -1367,9 +1537,79 @@ function decorateNotebook(args) {
|
|
|
1367
1537
|
});
|
|
1368
1538
|
return decorations;
|
|
1369
1539
|
}
|
|
1540
|
+
function outputHeight(frames) {
|
|
1541
|
+
let height = 0;
|
|
1542
|
+
for (const frame of frames) height += frameSize(frame).height;
|
|
1543
|
+
return height + 16;
|
|
1544
|
+
}
|
|
1545
|
+
/** The diagnostics whose span starts inside a cell's content. */
|
|
1546
|
+
function cellDiagnosticsFor(cell, diagnostics) {
|
|
1547
|
+
return diagnostics.filter((diagnostic) => diagnostic.range.start >= cell.content_start && diagnostic.range.start <= cell.content_end);
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* A cell's diagnostics as a block under its content, so an error is locatable
|
|
1551
|
+
* by cell rather than only in the lint panel.
|
|
1552
|
+
*/
|
|
1553
|
+
function pushDiagnostics(decorations, state, cell, diagnostics) {
|
|
1554
|
+
const own = cellDiagnosticsFor(cell, diagnostics);
|
|
1555
|
+
if (!own.length) return;
|
|
1556
|
+
const pos = cell.content_end > cell.content_start ? state.doc.lineAt(cell.content_end).to : cell.content_start;
|
|
1557
|
+
decorations.push(Decoration.widget({
|
|
1558
|
+
widget: new NotebookDiagnosticsWidget(own),
|
|
1559
|
+
block: true,
|
|
1560
|
+
side: 1
|
|
1561
|
+
}).range(pos));
|
|
1562
|
+
}
|
|
1563
|
+
var NotebookDiagnosticsWidget = class extends WidgetType {
|
|
1564
|
+
diagnostics;
|
|
1565
|
+
constructor(diagnostics) {
|
|
1566
|
+
super();
|
|
1567
|
+
this.diagnostics = diagnostics;
|
|
1568
|
+
}
|
|
1569
|
+
eq(other) {
|
|
1570
|
+
return this.diagnostics.length === other.diagnostics.length && this.diagnostics.every((diagnostic, index) => diagnostic.message === other.diagnostics[index]?.message);
|
|
1571
|
+
}
|
|
1572
|
+
toDOM() {
|
|
1573
|
+
const root = document.createElement("div");
|
|
1574
|
+
root.className = "tb-cell-diagnostics";
|
|
1575
|
+
for (const diagnostic of this.diagnostics) {
|
|
1576
|
+
const item = document.createElement("div");
|
|
1577
|
+
item.className = `tb-cell-diagnostic tb-cell-diagnostic--${diagnostic.severity}`;
|
|
1578
|
+
item.textContent = diagnostic.message;
|
|
1579
|
+
root.append(item);
|
|
1580
|
+
}
|
|
1581
|
+
return root;
|
|
1582
|
+
}
|
|
1583
|
+
get estimatedHeight() {
|
|
1584
|
+
return this.diagnostics.length * 22 + 8;
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
const FALLBACK_LABELS = {
|
|
1588
|
+
code: "Code",
|
|
1589
|
+
prose: "Text",
|
|
1590
|
+
log: "Log",
|
|
1591
|
+
hidden: "Hidden",
|
|
1592
|
+
menu: "Cell actions",
|
|
1593
|
+
split: "Split cell",
|
|
1594
|
+
merge: "Merge with the cell above",
|
|
1595
|
+
moveUp: "Move cell up",
|
|
1596
|
+
moveDown: "Move cell down",
|
|
1597
|
+
duplicate: "Duplicate cell",
|
|
1598
|
+
remove: "Delete cell",
|
|
1599
|
+
copy: "Copy cell",
|
|
1600
|
+
hold: "Hold output",
|
|
1601
|
+
release: "Release output",
|
|
1602
|
+
clearOutput: "Clear output",
|
|
1603
|
+
toggleSource: "Collapse cell",
|
|
1604
|
+
noOutput: "No output",
|
|
1605
|
+
held: "Held"
|
|
1606
|
+
};
|
|
1370
1607
|
//#endregion
|
|
1371
1608
|
//#region src/widgets.ts
|
|
1372
|
-
|
|
1609
|
+
/**
|
|
1610
|
+
* Compiled frames per cache key, where the key is `path:revision:appRevision`.
|
|
1611
|
+
*/
|
|
1612
|
+
const compileCache = new LRUCache({ max: 2 });
|
|
1373
1613
|
const updateFlagStore = /* @__PURE__ */ new Set();
|
|
1374
1614
|
/**
|
|
1375
1615
|
* Dispatch this to recompile even when the document did not change. Used when
|
|
@@ -1390,11 +1630,14 @@ function editorRenderWidth(scrollDOM, contentDOM) {
|
|
|
1390
1630
|
const tooltipsStateEffect = StateEffect.define();
|
|
1391
1631
|
const tooltipsStateField = StateField.define({
|
|
1392
1632
|
create() {
|
|
1393
|
-
return
|
|
1633
|
+
return {
|
|
1634
|
+
tooltips: [],
|
|
1635
|
+
ranges: []
|
|
1636
|
+
};
|
|
1394
1637
|
},
|
|
1395
|
-
update(
|
|
1638
|
+
update(overlays, transaction) {
|
|
1396
1639
|
const effect = transaction.effects.find((e) => e.is(tooltipsStateEffect));
|
|
1397
|
-
return effect ? effect.value :
|
|
1640
|
+
return effect ? effect.value : overlays;
|
|
1398
1641
|
}
|
|
1399
1642
|
});
|
|
1400
1643
|
/**
|
|
@@ -1440,25 +1683,27 @@ function measureLineHeights(view, frames) {
|
|
|
1440
1683
|
}
|
|
1441
1684
|
return heights;
|
|
1442
1685
|
}
|
|
1443
|
-
function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidget: _updateInWidget, widthChanged, forced, typstState, revision, onRequests, onPanic, onCompile, onExternalLink, notebook }) {
|
|
1686
|
+
function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidget: _updateInWidget, widthChanged, forced, typstState, revision, onRequests, onPanic, onCompile, onExternalLink, notebook, renderFrames = true }) {
|
|
1444
1687
|
const text = update.state.doc.toString();
|
|
1445
1688
|
const isFlaggedForUpdate = updateFlagStore.has(path);
|
|
1446
1689
|
const cacheKey = compileCacheKey(path, typstState, revision);
|
|
1447
1690
|
let frames;
|
|
1448
1691
|
let tooltips;
|
|
1692
|
+
let equationRanges;
|
|
1449
1693
|
let diagnostics;
|
|
1450
1694
|
if (forced || update.docChanged || widthChanged || !compileCache.has(cacheKey)) {
|
|
1451
1695
|
if (isFlaggedForUpdate) updateFlagStore.delete(path);
|
|
1452
1696
|
else updateFlagStore.add(path);
|
|
1453
1697
|
let compileResult;
|
|
1454
1698
|
try {
|
|
1455
|
-
compileResult = typstState.compilePaged(fileId, text, prelude);
|
|
1699
|
+
compileResult = typstState.compilePaged(fileId, text, prelude, update.state.selection.main.from);
|
|
1456
1700
|
} catch (error) {
|
|
1457
1701
|
console.error("[typst] compile panicked:", error);
|
|
1458
1702
|
onPanic?.(fileId);
|
|
1459
1703
|
return {
|
|
1460
1704
|
decorations: Decoration.none,
|
|
1461
1705
|
tooltips: [],
|
|
1706
|
+
equationRanges: [],
|
|
1462
1707
|
frames: [],
|
|
1463
1708
|
active: []
|
|
1464
1709
|
};
|
|
@@ -1470,21 +1715,18 @@ function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidg
|
|
|
1470
1715
|
if (!wasUpdated) return;
|
|
1471
1716
|
update.view.dispatch({ effects: typstRecompileEffect.of(null) });
|
|
1472
1717
|
});
|
|
1473
|
-
(
|
|
1718
|
+
installSharedDefs(compileResult.defs);
|
|
1719
|
+
({frames, tooltips, equation_ranges: equationRanges} = compileResult);
|
|
1474
1720
|
diagnostics = compileResult.diagnostics;
|
|
1475
1721
|
compileCache.set(cacheKey, {
|
|
1476
1722
|
frames,
|
|
1477
1723
|
tooltips,
|
|
1478
|
-
|
|
1479
|
-
});
|
|
1480
|
-
notebook?.onCompile?.({
|
|
1481
|
-
text,
|
|
1482
|
-
frames,
|
|
1724
|
+
equationRanges,
|
|
1483
1725
|
diagnostics
|
|
1484
1726
|
});
|
|
1485
1727
|
} else {
|
|
1486
1728
|
const cached = compileCache.get(cacheKey);
|
|
1487
|
-
({frames, tooltips, diagnostics} = cached);
|
|
1729
|
+
({frames, tooltips, equationRanges, diagnostics} = cached);
|
|
1488
1730
|
}
|
|
1489
1731
|
const { view, state } = update;
|
|
1490
1732
|
if (notebook) {
|
|
@@ -1497,12 +1739,19 @@ function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidg
|
|
|
1497
1739
|
return {
|
|
1498
1740
|
decorations: Decoration.none,
|
|
1499
1741
|
tooltips: [],
|
|
1742
|
+
equationRanges: [],
|
|
1500
1743
|
frames: [],
|
|
1501
1744
|
active: []
|
|
1502
1745
|
};
|
|
1503
1746
|
}
|
|
1504
1747
|
notebook.onCells?.(cells);
|
|
1505
|
-
notebook.
|
|
1748
|
+
notebook.onCompile?.({
|
|
1749
|
+
text,
|
|
1750
|
+
frames,
|
|
1751
|
+
diagnostics
|
|
1752
|
+
});
|
|
1753
|
+
const activeIndex = cells.length ? cellIndexAt(cells, state.selection.main.head) : null;
|
|
1754
|
+
notebook.onActiveCell?.(activeIndex);
|
|
1506
1755
|
return {
|
|
1507
1756
|
decorations: Decoration.set(decorateNotebook({
|
|
1508
1757
|
view,
|
|
@@ -1513,10 +1762,15 @@ function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidg
|
|
|
1513
1762
|
fileId,
|
|
1514
1763
|
typstState,
|
|
1515
1764
|
locked,
|
|
1765
|
+
readOnly: notebook.readOnly?.() ?? false,
|
|
1766
|
+
renderFrames,
|
|
1767
|
+
activeIndex,
|
|
1516
1768
|
onExternalLink,
|
|
1769
|
+
onCommand: (command, index) => notebook.onCommand?.(command, index),
|
|
1517
1770
|
options: notebook
|
|
1518
1771
|
}), true),
|
|
1519
1772
|
tooltips,
|
|
1773
|
+
equationRanges,
|
|
1520
1774
|
frames,
|
|
1521
1775
|
active: []
|
|
1522
1776
|
};
|
|
@@ -1533,6 +1787,7 @@ function decorate({ fileId, spaceId, path, prelude, locked, update, updateInWidg
|
|
|
1533
1787
|
return {
|
|
1534
1788
|
decorations: built.decorations,
|
|
1535
1789
|
tooltips,
|
|
1790
|
+
equationRanges,
|
|
1536
1791
|
frames,
|
|
1537
1792
|
active: built.active
|
|
1538
1793
|
};
|
|
@@ -1552,6 +1807,7 @@ const typstStateField = StateField.define({
|
|
|
1552
1807
|
const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstState, options = {}) => ViewPlugin.define((_view) => {
|
|
1553
1808
|
let firstUpdate = true;
|
|
1554
1809
|
let resizeTimer;
|
|
1810
|
+
const renderFrames = options.renderFrames ?? true;
|
|
1555
1811
|
const remeasureKey = {};
|
|
1556
1812
|
/**
|
|
1557
1813
|
* A pass that shows a block's source runs while the block's widget is still
|
|
@@ -1591,12 +1847,9 @@ const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstStat
|
|
|
1591
1847
|
return {
|
|
1592
1848
|
update(update) {
|
|
1593
1849
|
let widthChanged = false;
|
|
1594
|
-
const
|
|
1595
|
-
const run = effects.find((effect) => effect.is(notebookRunEffect));
|
|
1596
|
-
const refresh = effects.some((effect) => effect.is(notebookRefreshEffect));
|
|
1850
|
+
const refresh = update.transactions.flatMap((transaction) => transaction.effects).some((effect) => effect.is(notebookRefreshEffect));
|
|
1597
1851
|
const forced = firstUpdate || update.transactions.some((transaction) => transaction.effects.some((effect) => effect.is(typstRecompileEffect)));
|
|
1598
1852
|
firstUpdate = false;
|
|
1599
|
-
if (run) options.notebook?.onRun?.(run.value.index);
|
|
1600
1853
|
if (update.geometryChanged) {
|
|
1601
1854
|
const { scrollDOM, contentDOM } = update.view;
|
|
1602
1855
|
try {
|
|
@@ -1613,7 +1866,7 @@ const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstStat
|
|
|
1613
1866
|
update.view.dispatch({ effects: typstRecompileEffect.of(null) });
|
|
1614
1867
|
}, 150);
|
|
1615
1868
|
}
|
|
1616
|
-
if (update.docChanged || update.selectionSet || update.focusChanged || forced ||
|
|
1869
|
+
if (update.docChanged || update.selectionSet || update.focusChanged || forced || refresh) {
|
|
1617
1870
|
queueMicrotask(() => {
|
|
1618
1871
|
let result;
|
|
1619
1872
|
try {
|
|
@@ -1626,14 +1879,15 @@ const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstStat
|
|
|
1626
1879
|
update,
|
|
1627
1880
|
updateInWidget: false,
|
|
1628
1881
|
widthChanged,
|
|
1629
|
-
forced
|
|
1882
|
+
forced,
|
|
1630
1883
|
typstState,
|
|
1631
1884
|
revision: options.revision,
|
|
1632
1885
|
onRequests: options.onRequests,
|
|
1633
1886
|
onPanic: options.onPanic,
|
|
1634
1887
|
onCompile: options.onCompile,
|
|
1635
1888
|
onExternalLink: options.onExternalLink,
|
|
1636
|
-
notebook: options.notebook
|
|
1889
|
+
notebook: options.notebook,
|
|
1890
|
+
renderFrames
|
|
1637
1891
|
});
|
|
1638
1892
|
} catch (error) {
|
|
1639
1893
|
console.error("[typst] decorate panicked:", error);
|
|
@@ -1641,7 +1895,10 @@ const typstViewPlugin = (fileId, spaceId, path, text, prelude, locked, typstStat
|
|
|
1641
1895
|
return;
|
|
1642
1896
|
}
|
|
1643
1897
|
if (result) {
|
|
1644
|
-
const stateEffects = [typstStateEffect.of({ decorations: result.decorations }), tooltipsStateEffect.of(
|
|
1898
|
+
const stateEffects = [typstStateEffect.of({ decorations: result.decorations }), tooltipsStateEffect.of({
|
|
1899
|
+
tooltips: result.tooltips,
|
|
1900
|
+
ranges: result.equationRanges
|
|
1901
|
+
})];
|
|
1645
1902
|
update.view.dispatch({ effects: stateEffects });
|
|
1646
1903
|
remeasureActive(update, result);
|
|
1647
1904
|
}
|
|
@@ -1680,7 +1937,13 @@ const tooltipViewPlugin = () => ViewPlugin.define((view) => ({ update(update) {
|
|
|
1680
1937
|
}
|
|
1681
1938
|
const state = view.state;
|
|
1682
1939
|
const pos = state.selection.main.from;
|
|
1683
|
-
const tooltips = state.field(tooltipsStateField);
|
|
1940
|
+
const { tooltips, ranges } = state.field(tooltipsStateField);
|
|
1941
|
+
if (!tooltips.some(({ range }) => pos >= range.start && pos <= range.end)) {
|
|
1942
|
+
if (ranges.some(([start, end]) => pos >= start && pos <= end)) {
|
|
1943
|
+
view.dispatch({ effects: typstRecompileEffect.of(null) });
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1684
1947
|
let tooltip = null;
|
|
1685
1948
|
for (const { render, range } of tooltips) {
|
|
1686
1949
|
const { start, end } = range;
|
|
@@ -1694,8 +1957,8 @@ const tooltipViewPlugin = () => ViewPlugin.define((view) => ({ update(update) {
|
|
|
1694
1957
|
svg.setHTMLUnsafe(render.svg);
|
|
1695
1958
|
container.append(svg);
|
|
1696
1959
|
tooltip = {
|
|
1697
|
-
pos: start
|
|
1698
|
-
end
|
|
1960
|
+
pos: start,
|
|
1961
|
+
end,
|
|
1699
1962
|
create(tooltipView) {
|
|
1700
1963
|
return {
|
|
1701
1964
|
dom: container,
|
|
@@ -1704,8 +1967,8 @@ const tooltipViewPlugin = () => ViewPlugin.define((view) => ({ update(update) {
|
|
|
1704
1967
|
const endCoords = tooltipView.coordsAtPos(end);
|
|
1705
1968
|
return {
|
|
1706
1969
|
left: startCoords.left,
|
|
1707
|
-
right:
|
|
1708
|
-
top:
|
|
1970
|
+
right: endCoords.right,
|
|
1971
|
+
top: startCoords.top,
|
|
1709
1972
|
bottom: endCoords.bottom
|
|
1710
1973
|
};
|
|
1711
1974
|
}
|
|
@@ -1718,8 +1981,35 @@ const tooltipViewPlugin = () => ViewPlugin.define((view) => ({ update(update) {
|
|
|
1718
1981
|
view.dispatch({ effects: tooltipStateEffect.of(tooltip) });
|
|
1719
1982
|
});
|
|
1720
1983
|
} }));
|
|
1721
|
-
|
|
1722
|
-
|
|
1984
|
+
const typstEditingExtensions = [EditorView.inputHandler.of((view, from, to, text) => {
|
|
1985
|
+
if (text !== " " || from !== to) return false;
|
|
1986
|
+
const { state } = view;
|
|
1987
|
+
const pos = from;
|
|
1988
|
+
const bracketPairs = {
|
|
1989
|
+
"(": ")",
|
|
1990
|
+
"[": "]",
|
|
1991
|
+
"{": "}",
|
|
1992
|
+
$: "$"
|
|
1993
|
+
};
|
|
1994
|
+
const before = state.doc.sliceString(pos - 1, pos);
|
|
1995
|
+
const after = state.doc.sliceString(pos, pos + 1);
|
|
1996
|
+
if (!bracketPairs[before] || after !== bracketPairs[before]) return false;
|
|
1997
|
+
view.dispatch({
|
|
1998
|
+
changes: {
|
|
1999
|
+
from: pos,
|
|
2000
|
+
to: pos,
|
|
2001
|
+
insert: " "
|
|
2002
|
+
},
|
|
2003
|
+
selection: { anchor: pos + 1 }
|
|
2004
|
+
});
|
|
2005
|
+
return true;
|
|
2006
|
+
}), indentService.of((ctx, pos) => {
|
|
2007
|
+
const last = Math.max(0, pos - 1);
|
|
2008
|
+
const prev = ctx.lineAt(last).text;
|
|
2009
|
+
if (prev.endsWith("$") && prev !== "$") return 0;
|
|
2010
|
+
const indent = /[{[($]\s*$/.test(prev);
|
|
2011
|
+
return ctx.lineIndent(last) + (indent ? ctx.unit : 0);
|
|
2012
|
+
})];
|
|
1723
2013
|
const typstPlugin = (fileId, spaceId, path, text, prelude, locked, typstState, options = {}) => {
|
|
1724
2014
|
const extensions = [
|
|
1725
2015
|
typstStateField,
|
|
@@ -1730,43 +2020,14 @@ const typstPlugin = (fileId, spaceId, path, text, prelude, locked, typstState, o
|
|
|
1730
2020
|
autocompletion({ override: [(context) => autocomplete(context, fileId, typstState)] }),
|
|
1731
2021
|
typstKeymap,
|
|
1732
2022
|
typstLanguageData,
|
|
2023
|
+
typstSourceSync(fileId, prelude, typstState),
|
|
1733
2024
|
typstSyntaxHighlighting(fileId, typstState),
|
|
1734
2025
|
typstHoverTooltip(fileId, typstState),
|
|
1735
|
-
|
|
1736
|
-
indentService.of((ctx, pos) => {
|
|
1737
|
-
const last = Math.max(0, pos - 1);
|
|
1738
|
-
const prev = ctx.lineAt(last).text;
|
|
1739
|
-
if (prev.endsWith("$") && prev !== "$") return 0;
|
|
1740
|
-
const indent = /[{[($]\s*$/.test(prev);
|
|
1741
|
-
return ctx.lineIndent(last) + (indent ? ctx.unit : 0);
|
|
1742
|
-
})
|
|
2026
|
+
typstEditingExtensions
|
|
1743
2027
|
];
|
|
1744
2028
|
if (options.notebook) extensions.push(notebookOptionsFacet.of(options.notebook), notebookKeymap);
|
|
1745
2029
|
return extensions;
|
|
1746
2030
|
};
|
|
1747
|
-
const addSpaceBeforeClosingBracket = EditorView.inputHandler.of((view, from, to, text) => {
|
|
1748
|
-
if (text === " ") {
|
|
1749
|
-
const { state } = view;
|
|
1750
|
-
const pos = from;
|
|
1751
|
-
const bracketPairs = {
|
|
1752
|
-
"(": ")",
|
|
1753
|
-
"[": "]",
|
|
1754
|
-
"{": "}",
|
|
1755
|
-
$: "$"
|
|
1756
|
-
};
|
|
1757
|
-
const before = state.doc.sliceString(pos - 1, pos);
|
|
1758
|
-
const after = state.doc.sliceString(pos, pos + 1);
|
|
1759
|
-
if (bracketPairs[before] && after === bracketPairs[before]) view.dispatch({
|
|
1760
|
-
changes: {
|
|
1761
|
-
from: pos,
|
|
1762
|
-
to: pos,
|
|
1763
|
-
insert: " "
|
|
1764
|
-
},
|
|
1765
|
-
selection: { anchor: pos + 1 }
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
return false;
|
|
1769
|
-
});
|
|
1770
2031
|
//#endregion
|
|
1771
2032
|
//#region src/static-highlight.ts
|
|
1772
2033
|
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -2098,4 +2359,4 @@ function stringEnd(line, from) {
|
|
|
2098
2359
|
return line.length;
|
|
2099
2360
|
}
|
|
2100
2361
|
//#endregion
|
|
2101
|
-
export { TypstWidget, attachFrameInteractions, autocomplete, cellIndexAt, cellStart, cellsOf, compileCache, createFrameContainer, cursorAddVertical, cycleHeading, deleteCell,
|
|
2362
|
+
export { NOTEBOOK_CELL_TYPES, TypstWidget, attachFrameInteractions, attributeText, autocomplete, cellIndexAt, cellStart, cellsOf, compileCache, createFrameContainer, currentCell, cursorAddVertical, cycleHeading, deleteCell, deleteCellText, duplicateCell, duplicateCellText, focusCell, frameActiveDecorations, frameIsInactive, frameSize, hasAttribute, hasOutput, hidesSource, insertAtCursor, insertCell, insertCellText, insertFontFamily, insertTabLike, installSharedDefs, mergeCell, mergeCellText, moveCell, notebookKeymap, notebookOptions, notebookOptionsFacet, notebookRefreshEffect, parseBackticks, rememberDiagnostics, rendersInPlace, setCellType, setCellTypeText, splitCell, splitCellText, stripCellAttributes, syncFrameContainer, toLintDiagnostics, toggleAroundSelection, toggleCode, toggleEmph, toggleEnum, toggleList, toggleListLike, toggleMath, toggleStrike, toggleStrong, toggleUnderline, tooltipStateField, tooltipViewPlugin, tooltipsStateEffect, tooltipsStateField, typstEditingExtensions, typstHoverTooltip, typstKeymap, typstLanguageData, typstLinter, typstPlugin, typstRecompileEffect, typstSourceSync, typstStateField, typstStaticHighlighting, typstSyntaxHighlighting, typstViewPlugin, vimKeymapFacet, wrapOrInsert };
|