@marimo-team/islands 0.19.7-dev27 → 0.19.7-dev30
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/dist/main.js +45 -39
- package/package.json +1 -1
- package/src/components/data-table/TableActions.tsx +1 -1
- package/src/components/editor/header/__tests__/filename-form.test.tsx +124 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +120 -0
- package/src/core/codemirror/lsp/lens.ts +17 -1
- package/src/core/codemirror/lsp/notebook-lsp.ts +107 -37
- package/src/utils/download.ts +13 -1
- package/src/utils/html-to-image.ts +8 -2
package/dist/main.js
CHANGED
|
@@ -32291,8 +32291,13 @@ ${c.sqlString}
|
|
|
32291
32291
|
transformRange: (e2, r2) => shiftRange(e2, f(r2)),
|
|
32292
32292
|
reverseRange: (e2, r2) => shiftRange(e2, -f(r2)),
|
|
32293
32293
|
isInRange: (e2, d2) => {
|
|
32294
|
-
let f2 = r[d2].split("\n").length, _2 = c.get(d2) || 0, v = e2.start.line - _2, y = e2.end.line - _2;
|
|
32295
|
-
return
|
|
32294
|
+
let f2 = r[d2].split("\n").length, _2 = c.get(d2) || 0, v = e2.start.line - _2, y = e2.end.line - _2, S = v >= 0 && v < f2, w = y >= 0 && y < f2;
|
|
32295
|
+
return S && !w && Logger.warn("[lsp] Cross-cell diagnostic detected: range starts in cell but ends outside", {
|
|
32296
|
+
cellId: d2,
|
|
32297
|
+
range: e2,
|
|
32298
|
+
cellLines: f2,
|
|
32299
|
+
offset: _2
|
|
32300
|
+
}), S && w;
|
|
32296
32301
|
},
|
|
32297
32302
|
getEditsForNewText: (e2) => {
|
|
32298
32303
|
let d2 = e2.split("\n"), f2 = _.split("\n");
|
|
@@ -32393,6 +32398,12 @@ ${c.sqlString}
|
|
|
32393
32398
|
});
|
|
32394
32399
|
}), this.snapshotter = new Snapshotter(this.getNotebookCode.bind(this));
|
|
32395
32400
|
}
|
|
32401
|
+
static pruneSeenCellUris(r) {
|
|
32402
|
+
for (let c of _a.SEEN_CELL_DOCUMENT_URIS) {
|
|
32403
|
+
let d = CellDocumentUri.parse(c);
|
|
32404
|
+
r.has(d) || _a.SEEN_CELL_DOCUMENT_URIS.delete(c);
|
|
32405
|
+
}
|
|
32406
|
+
}
|
|
32396
32407
|
onNotification(e) {
|
|
32397
32408
|
return this.client.onNotification(e);
|
|
32398
32409
|
}
|
|
@@ -32466,18 +32477,7 @@ ${c.sqlString}
|
|
|
32466
32477
|
} : r;
|
|
32467
32478
|
}
|
|
32468
32479
|
async sync() {
|
|
32469
|
-
let { lens: e, version: r, didChange: c } = this.snapshotter.snapshot()
|
|
32470
|
-
return c ? this.client.textDocumentDidChange({
|
|
32471
|
-
textDocument: {
|
|
32472
|
-
uri: this.documentUri,
|
|
32473
|
-
version: r
|
|
32474
|
-
},
|
|
32475
|
-
contentChanges: [
|
|
32476
|
-
{
|
|
32477
|
-
text: e.mergedText
|
|
32478
|
-
}
|
|
32479
|
-
]
|
|
32480
|
-
}) : {
|
|
32480
|
+
let { lens: e, version: r, didChange: c } = this.snapshotter.snapshot(), d = {
|
|
32481
32481
|
textDocument: {
|
|
32482
32482
|
uri: this.documentUri,
|
|
32483
32483
|
version: r
|
|
@@ -32488,15 +32488,22 @@ ${c.sqlString}
|
|
|
32488
32488
|
}
|
|
32489
32489
|
]
|
|
32490
32490
|
};
|
|
32491
|
+
return c && await this.client.textDocumentDidChange(d), {
|
|
32492
|
+
params: d,
|
|
32493
|
+
lens: e
|
|
32494
|
+
};
|
|
32491
32495
|
}
|
|
32492
32496
|
async textDocumentDidChange(e) {
|
|
32493
|
-
|
|
32497
|
+
if (Logger.debug("[lsp] textDocumentDidChange", e), e.contentChanges.length === 1) {
|
|
32498
|
+
let { params: e2 } = await this.sync();
|
|
32499
|
+
return e2;
|
|
32500
|
+
}
|
|
32501
|
+
return Logger.warn("[lsp] Unhandled textDocumentDidChange", e), this.client.textDocumentDidChange(e);
|
|
32494
32502
|
}
|
|
32495
32503
|
async textDocumentDefinition(e) {
|
|
32496
32504
|
let r = e.textDocument.uri;
|
|
32497
32505
|
if (!CellDocumentUri.is(r)) return Logger.warn("Invalid cell document URI", r), null;
|
|
32498
|
-
await this.sync();
|
|
32499
|
-
let { lens: c } = this.snapshotter.getLatestSnapshot(), d = CellDocumentUri.parse(r);
|
|
32506
|
+
let { lens: c } = await this.sync(), d = CellDocumentUri.parse(r);
|
|
32500
32507
|
return this.client.textDocumentDefinition({
|
|
32501
32508
|
...e,
|
|
32502
32509
|
textDocument: {
|
|
@@ -32508,8 +32515,7 @@ ${c.sqlString}
|
|
|
32508
32515
|
async textDocumentSignatureHelp(e) {
|
|
32509
32516
|
let r = e.textDocument.uri;
|
|
32510
32517
|
if (!CellDocumentUri.is(r)) return Logger.warn("Invalid cell document URI", r), null;
|
|
32511
|
-
await this.sync();
|
|
32512
|
-
let { lens: c } = this.snapshotter.getLatestSnapshot(), d = CellDocumentUri.parse(r);
|
|
32518
|
+
let { lens: c } = await this.sync(), d = CellDocumentUri.parse(r);
|
|
32513
32519
|
return await this.client.textDocumentSignatureHelp({
|
|
32514
32520
|
...e,
|
|
32515
32521
|
textDocument: {
|
|
@@ -32525,9 +32531,7 @@ ${c.sqlString}
|
|
|
32525
32531
|
var _a2;
|
|
32526
32532
|
let r = e.textDocument.uri;
|
|
32527
32533
|
if (!CellDocumentUri.is(r)) return Logger.warn("Invalid cell document URI", r), null;
|
|
32528
|
-
let c = CellDocumentUri.parse(r)
|
|
32529
|
-
await this.sync();
|
|
32530
|
-
let { lens: d } = this.snapshotter.getLatestSnapshot(), f = d.transformPosition(e.position, c), _ = await this.client.textDocumentRename({
|
|
32534
|
+
let c = CellDocumentUri.parse(r), { lens: d } = await this.sync(), f = d.transformPosition(e.position, c), _ = await this.client.textDocumentRename({
|
|
32531
32535
|
...e,
|
|
32532
32536
|
textDocument: {
|
|
32533
32537
|
uri: this.documentUri
|
|
@@ -32547,20 +32551,21 @@ ${c.sqlString}
|
|
|
32547
32551
|
let r2 = O.state.doc.line(e.position.line + 1).from + e.position.character, { startToken: c2, endToken: d2 } = getPositionAtWordBounds(O.state.doc, r2), f2 = O.state.doc.sliceString(c2, d2);
|
|
32548
32552
|
M = isPrivateVariable(f2), M && Logger.debug("[lsp] Private variable rename detected, limiting to current cell", f2);
|
|
32549
32553
|
}
|
|
32554
|
+
let I = [], z = 0;
|
|
32550
32555
|
for (let [e2, r2] of Objects.entries(E)) {
|
|
32551
32556
|
if (M && e2 !== c) continue;
|
|
32552
32557
|
let d2 = w.get(e2);
|
|
32553
32558
|
if (d2 == null) {
|
|
32554
|
-
Logger.warn("No new code for cell", e2);
|
|
32559
|
+
Logger.warn("[lsp] No new code for cell during rename", e2), I.push(e2);
|
|
32555
32560
|
continue;
|
|
32556
32561
|
}
|
|
32557
32562
|
if (!r2) {
|
|
32558
|
-
Logger.warn("No view for
|
|
32563
|
+
Logger.warn("[lsp] No editor view for cell during rename", e2), I.push(e2);
|
|
32559
32564
|
continue;
|
|
32560
32565
|
}
|
|
32561
|
-
getEditorCodeAsPython(r2) !== d2 && updateEditorCodeFromPython(r2, d2);
|
|
32566
|
+
getEditorCodeAsPython(r2) !== d2 && (updateEditorCodeFromPython(r2, d2), z++);
|
|
32562
32567
|
}
|
|
32563
|
-
return {
|
|
32568
|
+
return I.length > 0 && Logger.error(`[lsp] Rename partially failed: could not update ${I.length} cell(s)`, I), Logger.debug(`[lsp] Rename completed: updated ${z} cell(s)`), {
|
|
32564
32569
|
..._,
|
|
32565
32570
|
documentChanges: [
|
|
32566
32571
|
{
|
|
@@ -32582,8 +32587,7 @@ ${c.sqlString}
|
|
|
32582
32587
|
async textDocumentPrepareRename(e) {
|
|
32583
32588
|
let r = e.textDocument.uri;
|
|
32584
32589
|
if (!CellDocumentUri.is(r)) return Logger.warn("Invalid cell document URI", r), null;
|
|
32585
|
-
await this.sync();
|
|
32586
|
-
let { lens: c } = this.snapshotter.getLatestSnapshot(), d = CellDocumentUri.parse(r);
|
|
32590
|
+
let { lens: c } = await this.sync(), d = CellDocumentUri.parse(r);
|
|
32587
32591
|
return this.client.textDocumentPrepareRename({
|
|
32588
32592
|
...e,
|
|
32589
32593
|
textDocument: {
|
|
@@ -32593,8 +32597,8 @@ ${c.sqlString}
|
|
|
32593
32597
|
});
|
|
32594
32598
|
}
|
|
32595
32599
|
async textDocumentHover(e) {
|
|
32596
|
-
Logger.debug("[lsp] textDocumentHover", e)
|
|
32597
|
-
let { lens: r } = this.
|
|
32600
|
+
Logger.debug("[lsp] textDocumentHover", e);
|
|
32601
|
+
let { lens: r } = await this.sync(), c = CellDocumentUri.parse(e.textDocument.uri), d = await this.client.textDocumentHover({
|
|
32598
32602
|
...e,
|
|
32599
32603
|
textDocument: {
|
|
32600
32604
|
uri: this.documentUri
|
|
@@ -32631,11 +32635,13 @@ ${c.sqlString}
|
|
|
32631
32635
|
y.get(r2).push(c3);
|
|
32632
32636
|
break;
|
|
32633
32637
|
}
|
|
32634
|
-
let S = new Set(
|
|
32638
|
+
let S = new Set(_.cellIds);
|
|
32639
|
+
_a.pruneSeenCellUris(S);
|
|
32640
|
+
let w = new Set(_a.SEEN_CELL_DOCUMENT_URIS);
|
|
32635
32641
|
for (let [e, d2] of y.entries()) {
|
|
32636
32642
|
Logger.debug("[lsp] diagnostics for cell", e, d2);
|
|
32637
32643
|
let f2 = CellDocumentUri.of(e);
|
|
32638
|
-
|
|
32644
|
+
w.delete(f2), r({
|
|
32639
32645
|
...c2,
|
|
32640
32646
|
params: {
|
|
32641
32647
|
...c2.params,
|
|
@@ -32645,9 +32651,9 @@ ${c.sqlString}
|
|
|
32645
32651
|
}
|
|
32646
32652
|
});
|
|
32647
32653
|
}
|
|
32648
|
-
if (
|
|
32649
|
-
Logger.debug("[lsp] clearing diagnostics",
|
|
32650
|
-
for (let e of
|
|
32654
|
+
if (w.size > 0) {
|
|
32655
|
+
Logger.debug("[lsp] clearing diagnostics", w);
|
|
32656
|
+
for (let e of w) r({
|
|
32651
32657
|
method: "textDocument/publishDiagnostics",
|
|
32652
32658
|
params: {
|
|
32653
32659
|
uri: e,
|
|
@@ -61387,7 +61393,7 @@ ${r}
|
|
|
61387
61393
|
const defaultHtmlToImageOptions = {
|
|
61388
61394
|
filter: (e) => {
|
|
61389
61395
|
try {
|
|
61390
|
-
return "classList" in e
|
|
61396
|
+
return !("classList" in e && (e.classList.contains("mpl-toolbar") || e.classList.contains("no-print")));
|
|
61391
61397
|
} catch (e2) {
|
|
61392
61398
|
return Logger.error("Error filtering node:", e2), true;
|
|
61393
61399
|
}
|
|
@@ -61396,7 +61402,7 @@ ${r}
|
|
|
61396
61402
|
Logger.error("Error loading image:", e);
|
|
61397
61403
|
}
|
|
61398
61404
|
};
|
|
61399
|
-
function toPng(e, r) {
|
|
61405
|
+
async function toPng(e, r) {
|
|
61400
61406
|
return toPng$1(e, {
|
|
61401
61407
|
...defaultHtmlToImageOptions,
|
|
61402
61408
|
...r
|
|
@@ -62019,7 +62025,7 @@ Defaulting to \`null\`.`;
|
|
|
62019
62025
|
e === "too_many" ? "Unknown" : prettifyRowCount(e, c),
|
|
62020
62026
|
`${prettyNumber(r, c)} ${new PluralWord("column").pluralize(r)}`
|
|
62021
62027
|
].join(", "), TableActions = ({ enableSearch: e, onSearchQueryChange: r, isSearchEnabled: c, setIsSearchEnabled: d, pagination: f, totalColumns: _, selection: v, onRowSelectionChange: y, table: S, downloadAs: E, getRowIds: O, toggleDisplayHeader: M, showChartBuilder: I, showColumnExplorer: z, showRowExplorer: G, showPageSizeSelector: q, togglePanel: IY, isPanelOpen: LY, tableLoading: RY }) => (0, import_jsx_runtime.jsxs)("div", {
|
|
62022
|
-
className: "flex items-center shrink-0 pt-1",
|
|
62028
|
+
className: "flex items-center shrink-0 pt-1 no-print",
|
|
62023
62029
|
children: [
|
|
62024
62030
|
r && e && (0, import_jsx_runtime.jsx)(Tooltip, {
|
|
62025
62031
|
content: "Search",
|
|
@@ -73076,7 +73082,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
|
|
|
73076
73082
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
73077
73083
|
}
|
|
73078
73084
|
}
|
|
73079
|
-
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.7-
|
|
73085
|
+
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.7-dev30"), showCodeInRunModeAtom = atom(true);
|
|
73080
73086
|
atom(null);
|
|
73081
73087
|
var import_compiler_runtime$88 = require_compiler_runtime();
|
|
73082
73088
|
function useKeydownOnElement(e, r) {
|
package/package.json
CHANGED
|
@@ -107,7 +107,7 @@ export const TableActions = <TData,>({
|
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
return (
|
|
110
|
-
<div className="flex items-center shrink-0 pt-1">
|
|
110
|
+
<div className="flex items-center shrink-0 pt-1 no-print">
|
|
111
111
|
{onSearchQueryChange && enableSearch && (
|
|
112
112
|
<Tooltip content="Search">
|
|
113
113
|
<Button
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { fireEvent, render, waitFor } from "@testing-library/react";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { FilenameForm } from "../filename-form";
|
|
7
|
+
|
|
8
|
+
// Mock the hooks
|
|
9
|
+
vi.mock("@/core/saving/filename", () => ({
|
|
10
|
+
useUpdateFilename: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/core/saving/save-component", () => ({
|
|
14
|
+
useSaveNotebook: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock FilenameInput to make testing easier
|
|
18
|
+
vi.mock("@/components/editor/header/filename-input", () => ({
|
|
19
|
+
FilenameInput: ({
|
|
20
|
+
onNameChange,
|
|
21
|
+
initialValue,
|
|
22
|
+
}: {
|
|
23
|
+
onNameChange: (value: string) => void;
|
|
24
|
+
initialValue?: string | null;
|
|
25
|
+
}) => (
|
|
26
|
+
<input
|
|
27
|
+
data-testid="filename-input"
|
|
28
|
+
data-initial-value={initialValue || ""}
|
|
29
|
+
onChange={(e) => onNameChange(e.target.value)}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const mockUseUpdateFilename = vi.mocked(
|
|
35
|
+
await import("@/core/saving/filename"),
|
|
36
|
+
).useUpdateFilename;
|
|
37
|
+
|
|
38
|
+
const mockUseSaveNotebook = vi.mocked(
|
|
39
|
+
await import("@/core/saving/save-component"),
|
|
40
|
+
).useSaveNotebook;
|
|
41
|
+
|
|
42
|
+
describe("FilenameForm", () => {
|
|
43
|
+
const mockUpdateFilename = vi.fn();
|
|
44
|
+
const mockSaveNotebook = vi.fn();
|
|
45
|
+
const mockSaveOrNameNotebook = vi.fn();
|
|
46
|
+
const mockSaveIfNotebookIsPersistent = vi.fn();
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
|
|
51
|
+
mockUseUpdateFilename.mockReturnValue(mockUpdateFilename);
|
|
52
|
+
mockUseSaveNotebook.mockReturnValue({
|
|
53
|
+
saveNotebook: mockSaveNotebook,
|
|
54
|
+
saveOrNameNotebook: mockSaveOrNameNotebook,
|
|
55
|
+
saveIfNotebookIsPersistent: mockSaveIfNotebookIsPersistent,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should call saveNotebook when creating a new file from an unnamed notebook", async () => {
|
|
60
|
+
// Setup: updateFilename resolves with the new filename
|
|
61
|
+
mockUpdateFilename.mockResolvedValue("/path/to/new-file.py");
|
|
62
|
+
|
|
63
|
+
const { getByTestId } = render(<FilenameForm filename={null} />);
|
|
64
|
+
|
|
65
|
+
const input = getByTestId("filename-input");
|
|
66
|
+
|
|
67
|
+
// Simulate name change (user entering a filename)
|
|
68
|
+
fireEvent.change(input, { target: { value: "new-file.py" } });
|
|
69
|
+
|
|
70
|
+
// Wait for the promise to resolve
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(mockUpdateFilename).toHaveBeenCalledWith("new-file.py");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(mockSaveNotebook).toHaveBeenCalledWith(
|
|
77
|
+
"/path/to/new-file.py",
|
|
78
|
+
true,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should not call saveNotebook when renaming an existing file", async () => {
|
|
84
|
+
// Setup: updateFilename resolves with the new filename
|
|
85
|
+
mockUpdateFilename.mockResolvedValue("/path/to/renamed-file.py");
|
|
86
|
+
|
|
87
|
+
const { getByTestId } = render(
|
|
88
|
+
<FilenameForm filename="/path/to/existing-file.py" />,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const input = getByTestId("filename-input");
|
|
92
|
+
|
|
93
|
+
// Simulate name change
|
|
94
|
+
fireEvent.change(input, { target: { value: "renamed-file.py" } });
|
|
95
|
+
|
|
96
|
+
// Wait for the promise to resolve
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(mockUpdateFilename).toHaveBeenCalledWith("renamed-file.py");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// saveNotebook should NOT be called because the file was already named
|
|
102
|
+
expect(mockSaveNotebook).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should not call saveNotebook when updateFilename returns null", async () => {
|
|
106
|
+
// Setup: updateFilename resolves with null (e.g., user cancelled or error)
|
|
107
|
+
mockUpdateFilename.mockResolvedValue(null);
|
|
108
|
+
|
|
109
|
+
const { getByTestId } = render(<FilenameForm filename={null} />);
|
|
110
|
+
|
|
111
|
+
const input = getByTestId("filename-input");
|
|
112
|
+
|
|
113
|
+
// Simulate name change
|
|
114
|
+
fireEvent.change(input, { target: { value: "new-file.py" } });
|
|
115
|
+
|
|
116
|
+
// Wait for the promise to resolve
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(mockUpdateFilename).toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// saveNotebook should NOT be called because updateFilename returned null
|
|
122
|
+
expect(mockSaveNotebook).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -25,6 +25,30 @@ const Cells = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
describe("createNotebookLens", () => {
|
|
28
|
+
it("should produce correct lens for same inputs", () => {
|
|
29
|
+
// Use unique content for this test
|
|
30
|
+
const cellIds: CellId[] = [Cells.cell1, Cells.cell2];
|
|
31
|
+
const codes: Record<CellId, string> = {
|
|
32
|
+
[Cells.cell1]: "unique_memo_test_line1",
|
|
33
|
+
[Cells.cell2]: "unique_memo_test_line2",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const lens1 = createNotebookLens(cellIds, codes);
|
|
37
|
+
const lens2 = createNotebookLens(cellIds, codes);
|
|
38
|
+
|
|
39
|
+
// Same inputs should produce equivalent lens (same merged text)
|
|
40
|
+
expect(lens1.mergedText).toBe(lens2.mergedText);
|
|
41
|
+
expect(lens1.cellIds).toEqual(lens2.cellIds);
|
|
42
|
+
|
|
43
|
+
// Different content should produce a different lens
|
|
44
|
+
const differentCodes: Record<CellId, string> = {
|
|
45
|
+
[Cells.cell1]: "unique_memo_different",
|
|
46
|
+
[Cells.cell2]: "unique_memo_content",
|
|
47
|
+
};
|
|
48
|
+
const lens3 = createNotebookLens(cellIds, differentCodes);
|
|
49
|
+
expect(lens3.mergedText).not.toBe(lens1.mergedText);
|
|
50
|
+
});
|
|
51
|
+
|
|
28
52
|
it("should calculate correct line offsets", () => {
|
|
29
53
|
const cellIds: CellId[] = [Cells.cell1, Cells.cell2, Cells.cell3];
|
|
30
54
|
const codes: Record<CellId, string> = {
|
|
@@ -111,6 +135,7 @@ describe("createNotebookLens", () => {
|
|
|
111
135
|
),
|
|
112
136
|
).toBe(true);
|
|
113
137
|
|
|
138
|
+
// This triggers the cross-cell diagnostic warning (start in range, end outside)
|
|
114
139
|
expect(
|
|
115
140
|
lens.isInRange(
|
|
116
141
|
{
|
|
@@ -122,6 +147,24 @@ describe("createNotebookLens", () => {
|
|
|
122
147
|
).toBe(false);
|
|
123
148
|
});
|
|
124
149
|
|
|
150
|
+
it("should detect cross-cell diagnostics where start is in range but end is outside", () => {
|
|
151
|
+
const cellIds: CellId[] = [Cells.cell1, Cells.cell2];
|
|
152
|
+
const codes: Record<CellId, string> = {
|
|
153
|
+
[Cells.cell1]: "line1\nline2",
|
|
154
|
+
[Cells.cell2]: "line3",
|
|
155
|
+
};
|
|
156
|
+
const lens = createNotebookLens(cellIds, codes);
|
|
157
|
+
|
|
158
|
+
// Range that starts in cell1 (line 1) but ends in cell2 (line 2 = first line of cell2)
|
|
159
|
+
// This should return false and log a warning
|
|
160
|
+
const crossCellRange = {
|
|
161
|
+
start: { line: 1, character: 0 }, // line 1 is in cell1
|
|
162
|
+
end: { line: 2, character: 0 }, // line 2 is in cell2
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
expect(lens.isInRange(crossCellRange, Cells.cell1)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
125
168
|
it("should join all code into merged text", () => {
|
|
126
169
|
const cellIds: CellId[] = [Cells.cell1, Cells.cell2, Cells.cell3];
|
|
127
170
|
const codes: Record<CellId, string> = {
|
|
@@ -1120,6 +1163,83 @@ describe("NotebookLanguageServerClient", () => {
|
|
|
1120
1163
|
});
|
|
1121
1164
|
});
|
|
1122
1165
|
|
|
1166
|
+
describe("sync returns lens to prevent race conditions", () => {
|
|
1167
|
+
it("should return the lens used for synchronization", async () => {
|
|
1168
|
+
mockClient.textDocumentDidChange = vi
|
|
1169
|
+
.fn()
|
|
1170
|
+
.mockImplementation((params) => params);
|
|
1171
|
+
|
|
1172
|
+
const result = await notebookClient.sync();
|
|
1173
|
+
|
|
1174
|
+
// sync() should return both params and the lens
|
|
1175
|
+
expect(result).toHaveProperty("params");
|
|
1176
|
+
expect(result).toHaveProperty("lens");
|
|
1177
|
+
expect(result.lens.cellIds).toEqual([
|
|
1178
|
+
Cells.cell1,
|
|
1179
|
+
Cells.cell2,
|
|
1180
|
+
Cells.cell3,
|
|
1181
|
+
]);
|
|
1182
|
+
expect(result.lens.mergedText).toContain("# this is a comment");
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it("should return consistent lens even when called multiple times rapidly", async () => {
|
|
1186
|
+
mockClient.textDocumentDidChange = vi
|
|
1187
|
+
.fn()
|
|
1188
|
+
.mockImplementation((params) => params);
|
|
1189
|
+
|
|
1190
|
+
// Call sync multiple times rapidly
|
|
1191
|
+
const [result1, result2, result3] = await Promise.all([
|
|
1192
|
+
notebookClient.sync(),
|
|
1193
|
+
notebookClient.sync(),
|
|
1194
|
+
notebookClient.sync(),
|
|
1195
|
+
]);
|
|
1196
|
+
|
|
1197
|
+
// All should return the same lens content (memoized)
|
|
1198
|
+
expect(result1.lens.mergedText).toBe(result2.lens.mergedText);
|
|
1199
|
+
expect(result2.lens.mergedText).toBe(result3.lens.mergedText);
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
describe("SEEN_CELL_DOCUMENT_URIS memory management", () => {
|
|
1204
|
+
it("should track opened cells in SEEN_CELL_DOCUMENT_URIS", async () => {
|
|
1205
|
+
// Clear any existing state
|
|
1206
|
+
const seenUris = (NotebookLanguageServerClient as any)
|
|
1207
|
+
.SEEN_CELL_DOCUMENT_URIS;
|
|
1208
|
+
seenUris.clear();
|
|
1209
|
+
|
|
1210
|
+
// Open some cells to add them to SEEN_CELL_DOCUMENT_URIS
|
|
1211
|
+
await notebookClient.textDocumentDidOpen({
|
|
1212
|
+
textDocument: {
|
|
1213
|
+
uri: CellDocumentUri.of(Cells.cell1),
|
|
1214
|
+
languageId: "python",
|
|
1215
|
+
version: 1,
|
|
1216
|
+
text: "code1",
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
await notebookClient.textDocumentDidOpen({
|
|
1221
|
+
textDocument: {
|
|
1222
|
+
uri: CellDocumentUri.of(Cells.cell2),
|
|
1223
|
+
languageId: "python",
|
|
1224
|
+
version: 1,
|
|
1225
|
+
text: "code2",
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Verify cells were added
|
|
1230
|
+
expect(seenUris.has(CellDocumentUri.of(Cells.cell1))).toBe(true);
|
|
1231
|
+
expect(seenUris.has(CellDocumentUri.of(Cells.cell2))).toBe(true);
|
|
1232
|
+
expect(seenUris.size).toBe(2);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
it("should have pruneSeenCellUris static method", () => {
|
|
1236
|
+
// Verify the method exists and is callable
|
|
1237
|
+
expect(
|
|
1238
|
+
typeof (NotebookLanguageServerClient as any).pruneSeenCellUris,
|
|
1239
|
+
).toBe("function");
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1123
1243
|
describe("initialization and configuration", () => {
|
|
1124
1244
|
it("should send configuration after initialization", async () => {
|
|
1125
1245
|
const configNotifications: any[] = [];
|
|
@@ -68,12 +68,28 @@ export function createNotebookLens(
|
|
|
68
68
|
reverseRange: (range: LSP.Range, cellId: CellId) =>
|
|
69
69
|
shiftRange(range, -getCurrentLineOffset(cellId)),
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Check if a range falls entirely within the given cell.
|
|
73
|
+
* Returns false for ranges that span multiple cells (cross-cell diagnostics).
|
|
74
|
+
*/
|
|
71
75
|
isInRange: (range: LSP.Range, cellId: CellId) => {
|
|
72
76
|
const cellLines = codes[cellId].split("\n").length;
|
|
73
77
|
const offset = cellLineOffsets.get(cellId) || 0;
|
|
74
78
|
const startLine = range.start.line - offset;
|
|
75
79
|
const endLine = range.end.line - offset;
|
|
76
|
-
|
|
80
|
+
|
|
81
|
+
const startInRange = startLine >= 0 && startLine < cellLines;
|
|
82
|
+
const endInRange = endLine >= 0 && endLine < cellLines;
|
|
83
|
+
|
|
84
|
+
// Warn about cross-cell diagnostics that start in this cell but end outside
|
|
85
|
+
if (startInRange && !endInRange) {
|
|
86
|
+
Logger.warn(
|
|
87
|
+
"[lsp] Cross-cell diagnostic detected: range starts in cell but ends outside",
|
|
88
|
+
{ cellId, range, cellLines, offset },
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return startInRange && endInRange;
|
|
77
93
|
},
|
|
78
94
|
|
|
79
95
|
getEditsForNewText: (newText: string) => {
|
|
@@ -113,8 +113,28 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
113
113
|
EditorView | null | undefined
|
|
114
114
|
>;
|
|
115
115
|
private readonly initialSettings: Record<string, unknown>;
|
|
116
|
+
/**
|
|
117
|
+
* Tracks which cell document URIs have been opened with the LSP server.
|
|
118
|
+
* Used to clear diagnostics for cells that no longer have any.
|
|
119
|
+
*
|
|
120
|
+
* This set is pruned when diagnostics are processed to only include
|
|
121
|
+
* cells that exist in the current notebook snapshot.
|
|
122
|
+
*/
|
|
116
123
|
private static readonly SEEN_CELL_DOCUMENT_URIS = new Set<CellDocumentUri>();
|
|
117
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Remove cell URIs that are no longer in the notebook.
|
|
127
|
+
* Called during diagnostic processing to prevent memory leaks.
|
|
128
|
+
*/
|
|
129
|
+
private static pruneSeenCellUris(currentCellIds: Set<CellId>): void {
|
|
130
|
+
for (const uri of NotebookLanguageServerClient.SEEN_CELL_DOCUMENT_URIS) {
|
|
131
|
+
const cellId = CellDocumentUri.parse(uri);
|
|
132
|
+
if (!currentCellIds.has(cellId)) {
|
|
133
|
+
NotebookLanguageServerClient.SEEN_CELL_DOCUMENT_URIS.delete(uri);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
/**
|
|
119
139
|
* Cache of completion items to avoid jitter while typing in the same completion item
|
|
120
140
|
*/
|
|
@@ -274,26 +294,30 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
274
294
|
};
|
|
275
295
|
}
|
|
276
296
|
|
|
277
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Synchronize the document with the LSP server and return the lens used.
|
|
299
|
+
* This ensures the caller uses the same lens that was sent to the server,
|
|
300
|
+
* avoiding race conditions if cells change between sync and subsequent operations.
|
|
301
|
+
*/
|
|
302
|
+
async sync(): Promise<{
|
|
303
|
+
params: LSP.DidChangeTextDocumentParams;
|
|
304
|
+
lens: NotebookLens;
|
|
305
|
+
}> {
|
|
278
306
|
const { lens, version, didChange } = this.snapshotter.snapshot();
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
textDocument: {
|
|
282
|
-
uri: this.documentUri,
|
|
283
|
-
version: version,
|
|
284
|
-
},
|
|
285
|
-
contentChanges: [{ text: lens.mergedText }],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Update changes for merged doc, etc.
|
|
290
|
-
return this.client.textDocumentDidChange({
|
|
307
|
+
const params: LSP.DidChangeTextDocumentParams = {
|
|
291
308
|
textDocument: {
|
|
292
309
|
uri: this.documentUri,
|
|
293
310
|
version: version,
|
|
294
311
|
},
|
|
295
312
|
contentChanges: [{ text: lens.mergedText }],
|
|
296
|
-
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (didChange) {
|
|
316
|
+
// Update changes for merged doc
|
|
317
|
+
await this.client.textDocumentDidChange(params);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { params, lens };
|
|
297
321
|
}
|
|
298
322
|
|
|
299
323
|
public async textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
|
|
@@ -301,7 +325,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
301
325
|
// We know how to only handle single content changes
|
|
302
326
|
// But that is all we expect to receive
|
|
303
327
|
if (params.contentChanges.length === 1) {
|
|
304
|
-
|
|
328
|
+
const { params: syncParams } = await this.sync();
|
|
329
|
+
return syncParams;
|
|
305
330
|
}
|
|
306
331
|
|
|
307
332
|
Logger.warn("[lsp] Unhandled textDocumentDidChange", params);
|
|
@@ -319,9 +344,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
319
344
|
return null;
|
|
320
345
|
}
|
|
321
346
|
|
|
322
|
-
//
|
|
323
|
-
await this.sync();
|
|
324
|
-
const { lens } = this.snapshotter.getLatestSnapshot();
|
|
347
|
+
// Sync and use the same lens that was sent to the server
|
|
348
|
+
const { lens } = await this.sync();
|
|
325
349
|
|
|
326
350
|
// Find the lens for this cell
|
|
327
351
|
const cellId = CellDocumentUri.parse(cellDocumentUri);
|
|
@@ -345,9 +369,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
345
369
|
return null;
|
|
346
370
|
}
|
|
347
371
|
|
|
348
|
-
//
|
|
349
|
-
await this.sync();
|
|
350
|
-
const { lens } = this.snapshotter.getLatestSnapshot();
|
|
372
|
+
// Sync and use the same lens that was sent to the server
|
|
373
|
+
const { lens } = await this.sync();
|
|
351
374
|
|
|
352
375
|
const cellId = CellDocumentUri.parse(cellDocumentUri);
|
|
353
376
|
|
|
@@ -367,6 +390,23 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
367
390
|
return response;
|
|
368
391
|
}
|
|
369
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Code actions are currently disabled because mapping code action edits
|
|
395
|
+
* back to individual cells is complex and error-prone.
|
|
396
|
+
*
|
|
397
|
+
* The LSP server returns edits in merged document coordinates, but we need
|
|
398
|
+
* to apply them to individual cell editors. Unlike simple position transforms
|
|
399
|
+
* (hover, completion), code actions can include arbitrary text edits that
|
|
400
|
+
* may span multiple cells or change line counts, making the reverse mapping
|
|
401
|
+
* unreliable.
|
|
402
|
+
*
|
|
403
|
+
* To enable this, we would need to:
|
|
404
|
+
* 1. Transform edit ranges from merged doc coordinates to cell coordinates
|
|
405
|
+
* 2. Handle edits that span cell boundaries (split or reject them)
|
|
406
|
+
* 3. Apply edits atomically across multiple cell editors
|
|
407
|
+
*
|
|
408
|
+
* See textDocumentRename for a similar workaround that manually applies edits.
|
|
409
|
+
*/
|
|
370
410
|
textDocumentCodeAction(
|
|
371
411
|
params: LSP.CodeActionParams,
|
|
372
412
|
): Promise<(LSP.Command | LSP.CodeAction)[] | null> {
|
|
@@ -378,12 +418,23 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
378
418
|
}
|
|
379
419
|
|
|
380
420
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
421
|
+
* Rename implementation with manual edit application.
|
|
422
|
+
*
|
|
423
|
+
* This is a workaround because the standard LSP rename flow doesn't work well
|
|
424
|
+
* with our notebook architecture. The LSP server returns a WorkspaceEdit with
|
|
425
|
+
* edits in merged document coordinates, but CodeMirror's LSP plugin expects
|
|
426
|
+
* edits for individual cell documents.
|
|
427
|
+
*
|
|
428
|
+
* Instead of trying to transform the WorkspaceEdit (which is complex because
|
|
429
|
+
* edits can span cells, change line counts, etc.), we:
|
|
430
|
+
* 1. Request the rename from the LSP server
|
|
431
|
+
* 2. Extract the new merged text from the response
|
|
432
|
+
* 3. Split it back into per-cell text using the lens
|
|
433
|
+
* 4. Manually update each cell's editor
|
|
384
434
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
435
|
+
* This approach is simpler and more reliable, though it bypasses the normal
|
|
436
|
+
* LSP edit application flow. The trade-off is that we lose undo grouping
|
|
437
|
+
* across cells.
|
|
387
438
|
*/
|
|
388
439
|
async textDocumentRename(
|
|
389
440
|
params: LSP.RenameParams,
|
|
@@ -397,9 +448,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
397
448
|
|
|
398
449
|
const cellId = CellDocumentUri.parse(cellDocumentUri);
|
|
399
450
|
|
|
400
|
-
//
|
|
401
|
-
await this.sync();
|
|
402
|
-
const { lens } = this.snapshotter.getLatestSnapshot();
|
|
451
|
+
// Sync and use the same lens that was sent to the server
|
|
452
|
+
const { lens } = await this.sync();
|
|
403
453
|
|
|
404
454
|
const transformedPosition = lens.transformPosition(params.position, cellId);
|
|
405
455
|
|
|
@@ -468,6 +518,9 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
468
518
|
}
|
|
469
519
|
}
|
|
470
520
|
|
|
521
|
+
const failedCells: CellId[] = [];
|
|
522
|
+
let updatedCount = 0;
|
|
523
|
+
|
|
471
524
|
for (const [currentCellId, ev] of Objects.entries(editors)) {
|
|
472
525
|
// For private variable renames, only update the originating cell
|
|
473
526
|
if (isPrivateRename && currentCellId !== cellId) {
|
|
@@ -476,12 +529,17 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
476
529
|
|
|
477
530
|
const newCode = editsToNewCode.get(currentCellId);
|
|
478
531
|
if (newCode == null) {
|
|
479
|
-
Logger.warn("No new code for cell", currentCellId);
|
|
532
|
+
Logger.warn("[lsp] No new code for cell during rename", currentCellId);
|
|
533
|
+
failedCells.push(currentCellId);
|
|
480
534
|
continue;
|
|
481
535
|
}
|
|
482
536
|
|
|
483
537
|
if (!ev) {
|
|
484
|
-
Logger.warn(
|
|
538
|
+
Logger.warn(
|
|
539
|
+
"[lsp] No editor view for cell during rename",
|
|
540
|
+
currentCellId,
|
|
541
|
+
);
|
|
542
|
+
failedCells.push(currentCellId);
|
|
485
543
|
continue;
|
|
486
544
|
}
|
|
487
545
|
|
|
@@ -489,9 +547,19 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
489
547
|
const code = getEditorCodeAsPython(ev);
|
|
490
548
|
if (code !== newCode) {
|
|
491
549
|
updateEditorCodeFromPython(ev, newCode);
|
|
550
|
+
updatedCount++;
|
|
492
551
|
}
|
|
493
552
|
}
|
|
494
553
|
|
|
554
|
+
if (failedCells.length > 0) {
|
|
555
|
+
Logger.error(
|
|
556
|
+
`[lsp] Rename partially failed: could not update ${failedCells.length} cell(s)`,
|
|
557
|
+
failedCells,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
Logger.debug(`[lsp] Rename completed: updated ${updatedCount} cell(s)`);
|
|
562
|
+
|
|
495
563
|
return {
|
|
496
564
|
...response,
|
|
497
565
|
documentChanges: [
|
|
@@ -531,9 +599,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
531
599
|
return null;
|
|
532
600
|
}
|
|
533
601
|
|
|
534
|
-
//
|
|
535
|
-
await this.sync();
|
|
536
|
-
const { lens } = this.snapshotter.getLatestSnapshot();
|
|
602
|
+
// Sync and use the same lens that was sent to the server
|
|
603
|
+
const { lens } = await this.sync();
|
|
537
604
|
|
|
538
605
|
const cellId = CellDocumentUri.parse(cellDocumentUri);
|
|
539
606
|
|
|
@@ -549,9 +616,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
549
616
|
public async textDocumentHover(params: LSP.HoverParams) {
|
|
550
617
|
Logger.debug("[lsp] textDocumentHover", params);
|
|
551
618
|
|
|
552
|
-
//
|
|
553
|
-
await this.sync();
|
|
554
|
-
const { lens } = this.snapshotter.getLatestSnapshot();
|
|
619
|
+
// Sync and use the same lens that was sent to the server
|
|
620
|
+
const { lens } = await this.sync();
|
|
555
621
|
|
|
556
622
|
const cellId = CellDocumentUri.parse(params.textDocument.uri);
|
|
557
623
|
|
|
@@ -655,6 +721,10 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
|
|
|
655
721
|
}
|
|
656
722
|
}
|
|
657
723
|
|
|
724
|
+
// Prune any cell URIs for cells that no longer exist
|
|
725
|
+
const currentCellIds = new Set(lens.cellIds);
|
|
726
|
+
NotebookLanguageServerClient.pruneSeenCellUris(currentCellIds);
|
|
727
|
+
|
|
658
728
|
const cellsToClear = new Set(
|
|
659
729
|
NotebookLanguageServerClient.SEEN_CELL_DOCUMENT_URIS,
|
|
660
730
|
);
|
package/src/utils/download.ts
CHANGED
|
@@ -94,6 +94,8 @@ function prepareCellElementForScreenshot(
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const THRESHOLD_TIME_MS = 500;
|
|
98
|
+
|
|
97
99
|
/**
|
|
98
100
|
* Capture a cell output as a PNG data URL.
|
|
99
101
|
*
|
|
@@ -119,7 +121,17 @@ export async function getImageDataUrlForCell(
|
|
|
119
121
|
const cleanup = prepareCellElementForScreenshot(element, enablePrintMode);
|
|
120
122
|
|
|
121
123
|
try {
|
|
122
|
-
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
const dataUrl = await toPng(element);
|
|
126
|
+
const timeTaken = Date.now() - startTime;
|
|
127
|
+
if (timeTaken > THRESHOLD_TIME_MS) {
|
|
128
|
+
Logger.debug(
|
|
129
|
+
"toPng operation for element",
|
|
130
|
+
element,
|
|
131
|
+
`took ${timeTaken} ms (exceeds threshold)`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return dataUrl;
|
|
123
135
|
} finally {
|
|
124
136
|
cleanup();
|
|
125
137
|
}
|
|
@@ -13,7 +13,13 @@ export const defaultHtmlToImageOptions: HtmlToImageOptions = {
|
|
|
13
13
|
try {
|
|
14
14
|
if ("classList" in node) {
|
|
15
15
|
// Filter out matplotlib toolbars
|
|
16
|
-
|
|
16
|
+
if (node.classList.contains("mpl-toolbar")) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (node.classList.contains("no-print")) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
return true;
|
|
19
25
|
} catch (error) {
|
|
@@ -30,7 +36,7 @@ export const defaultHtmlToImageOptions: HtmlToImageOptions = {
|
|
|
30
36
|
* Convert an HTML element to a PNG data URL.
|
|
31
37
|
* This is a wrapper around html-to-image's toPng with default options applied.
|
|
32
38
|
*/
|
|
33
|
-
export function toPng(
|
|
39
|
+
export async function toPng(
|
|
34
40
|
element: HTMLElement,
|
|
35
41
|
options?: HtmlToImageOptions,
|
|
36
42
|
): Promise<string> {
|