@marimo-team/frontend 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.
Files changed (73) hide show
  1. package/dist/assets/{CellStatus-oBL2iale.js → CellStatus-DpVrMkoX.js} +1 -1
  2. package/dist/assets/{JsonOutput-BKP4rBIw.js → JsonOutput-DqRUfM9x.js} +2 -2
  3. package/dist/assets/{MarimoErrorOutput-cw9gEb4T.js → MarimoErrorOutput-CI9ZOqK5.js} +1 -1
  4. package/dist/assets/{RenderHTML-Bgz4e362.js → RenderHTML-DCy5wLx9.js} +1 -1
  5. package/dist/assets/{add-cell-with-ai-DNbX7Ctg.js → add-cell-with-ai-1AcZwwMS.js} +1 -1
  6. package/dist/assets/{add-database-form-CP39qGit.js → add-database-form-B45yZ_nV.js} +1 -1
  7. package/dist/assets/{agent-panel-Bak-DtIc.js → agent-panel-BuwVy_u2.js} +1 -1
  8. package/dist/assets/{ai-model-dropdown-Dpr0DUJN.js → ai-model-dropdown-Ce9P2gAN.js} +1 -1
  9. package/dist/assets/{app-config-button-CAaYaq0L.js → app-config-button-V0ZGbZwL.js} +1 -1
  10. package/dist/assets/{cell-editor-B6jD1bv8.js → cell-editor-Bs8pOtdM.js} +1 -1
  11. package/dist/assets/{cell-link-CyIWDVXR.js → cell-link-Draa5Cqg.js} +1 -1
  12. package/dist/assets/{cells-DRiwSVs0.js → cells-D3A832Pq.js} +55 -55
  13. package/dist/assets/{chat-components-DDIZD9FU.js → chat-components-BIikypOv.js} +1 -1
  14. package/dist/assets/{chat-display-D-Wa-1Hv.js → chat-display-C9Ma_Z24.js} +1 -1
  15. package/dist/assets/{chat-panel-RtFQiyUV.js → chat-panel-hwTI5twF.js} +1 -1
  16. package/dist/assets/{column-preview-C72fVZoP.js → column-preview-BI7AO_9o.js} +1 -1
  17. package/dist/assets/{command-Bq3e85NA.js → command-BmRMpmeq.js} +1 -1
  18. package/dist/assets/{command-palette-D4NcN7PV.js → command-palette-Bf5XRuku.js} +1 -1
  19. package/dist/assets/{common-CeF2TOUZ.js → common-DhAkBsKv.js} +1 -1
  20. package/dist/assets/{datasource-AZ3l2P48.js → datasource-DH4rSucG.js} +1 -1
  21. package/dist/assets/{dependency-graph-panel-i3yTswTN.js → dependency-graph-panel-BgRP2qVg.js} +1 -1
  22. package/dist/assets/{documentation-panel-J2fwLjEP.js → documentation-panel-BKQG1zId.js} +1 -1
  23. package/dist/assets/download-BJ50folP.js +6 -0
  24. package/dist/assets/{edit-page-Bpl6BN5G.js → edit-page-Dzfy3wo-.js} +3 -3
  25. package/dist/assets/{error-panel-Cpfr8TZw.js → error-panel-B23uWV5g.js} +1 -1
  26. package/dist/assets/{file-explorer-panel-DsMwvQX7.js → file-explorer-panel-DQucxlTZ.js} +1 -1
  27. package/dist/assets/{floating-outline-yvPiOGQ2.js → floating-outline-C0NCQURJ.js} +1 -1
  28. package/dist/assets/{focus-B7xu7kpl.js → focus-DdU24YN9.js} +1 -1
  29. package/dist/assets/{form-BpwI8bBX.js → form-C4ZlGvVV.js} +1 -1
  30. package/dist/assets/{globals-BW23ny3Q.js → globals-DJ12V9Kv.js} +1 -1
  31. package/dist/assets/{home-page-C-JIKt-2.js → home-page-DUgD5VOD.js} +1 -1
  32. package/dist/assets/{hooks-2c2seyHG.js → hooks-ntzLWruV.js} +1 -1
  33. package/dist/assets/{html-to-image-Cy_LYuWW.js → html-to-image-B02su1Lp.js} +2 -2
  34. package/dist/assets/{index-CPfohPCM.js → index-BaIb0_Ut.js} +3 -3
  35. package/dist/assets/{kiosk-mode-D9jdUD5P.js → kiosk-mode-CiC9R5CJ.js} +1 -1
  36. package/dist/assets/{layout-BjIcxFO0.js → layout-CQQNB4AA.js} +1 -1
  37. package/dist/assets/{logs-panel-XVSOzGFv.js → logs-panel-BKwAB4Sl.js} +1 -1
  38. package/dist/assets/{markdown-renderer-BM4a9QeZ.js → markdown-renderer-BxPFtrG5.js} +1 -1
  39. package/dist/assets/{mode-BD6zDBBd.js → mode-BGSml4Q4.js} +1 -1
  40. package/dist/assets/{name-cell-input-CGevX71g.js → name-cell-input-wYLF2zUR.js} +1 -1
  41. package/dist/assets/{outline-panel-BgZXYbEO.js → outline-panel-Cp48IMlZ.js} +1 -1
  42. package/dist/assets/{packages-panel-8NDUaEZw.js → packages-panel-Drho5urs.js} +1 -1
  43. package/dist/assets/{panels-05G4QYfq.js → panels-DCSfU3wX.js} +1 -1
  44. package/dist/assets/{process-output-nNOt7QtH.js → process-output-Bw6cqAbr.js} +1 -1
  45. package/dist/assets/{readonly-python-code-CfLdThzF.js → readonly-python-code-CxRVkq45.js} +1 -1
  46. package/dist/assets/{run-page-DvWw2rQu.js → run-page-DI11aUJZ.js} +1 -1
  47. package/dist/assets/{scratchpad-panel-Bzreyj8q.js → scratchpad-panel-vzWFhKxS.js} +1 -1
  48. package/dist/assets/{session-panel-CxCUFR_x.js → session-panel-HNDQ-XhU.js} +1 -1
  49. package/dist/assets/{snippets-panel-BeqbqiKB.js → snippets-panel-Dx1FdjOx.js} +1 -1
  50. package/dist/assets/{state-DnUQ1uxR.js → state-D1OFbTRC.js} +1 -1
  51. package/dist/assets/{switch-Cx8dJhf6.js → switch-Cqx3FjIU.js} +1 -1
  52. package/dist/assets/{textarea-DellDgP4.js → textarea-BqB4s8zj.js} +1 -1
  53. package/dist/assets/{tracing-7U4WTsN0.js → tracing-Bu-VQRRo.js} +1 -1
  54. package/dist/assets/{tracing-panel-BPW1q7K3.js → tracing-panel-Fwxu1-K1.js} +2 -2
  55. package/dist/assets/{types-D5X7ikSD.js → types-D8lESWyo.js} +1 -1
  56. package/dist/assets/{useAddCell-bMoxoWAg.js → useAddCell-B_b-bEhC.js} +1 -1
  57. package/dist/assets/{useCellActionButton-0MDUWMcl.js → useCellActionButton-DjwaDuay.js} +1 -1
  58. package/dist/assets/{useDeleteCell-BUAQb9OH.js → useDeleteCell-7CQhKujJ.js} +1 -1
  59. package/dist/assets/{useDependencyPanelTab-odtlfdG2.js → useDependencyPanelTab-CB_Ogk32.js} +1 -1
  60. package/dist/assets/{useNotebookActions-CNWP5yqL.js → useNotebookActions-C47M79_m.js} +1 -1
  61. package/dist/assets/{useRunCells-DyBzMbHe.js → useRunCells-DCMO0B1l.js} +1 -1
  62. package/dist/assets/{useSplitCell-CyFavQ2l.js → useSplitCell-BAsTzBSc.js} +1 -1
  63. package/dist/assets/{utilities.esm-j_F9mYkM.js → utilities.esm-ZpMudNeK.js} +1 -1
  64. package/dist/index.html +31 -31
  65. package/package.json +1 -1
  66. package/src/components/data-table/TableActions.tsx +1 -1
  67. package/src/components/editor/header/__tests__/filename-form.test.tsx +124 -0
  68. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +120 -0
  69. package/src/core/codemirror/lsp/lens.ts +17 -1
  70. package/src/core/codemirror/lsp/notebook-lsp.ts +107 -37
  71. package/src/utils/download.ts +13 -1
  72. package/src/utils/html-to-image.ts +8 -2
  73. package/dist/assets/download-Y3BpaOoI.js +0 -6
@@ -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
- return startLine >= 0 && endLine < cellLines;
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
- async sync(): Promise<LSP.DidChangeTextDocumentParams> {
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
- if (!didChange) {
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
- return this.sync();
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
- // This LSP method has no version, so lets sync and then get the latest snapshot
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
- // This LSP method has no version, so lets sync and then get the latest snapshot
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
- * HACK
382
- * This whole function is a hack to work around the fact that we don't have
383
- * a great way to map the edits back to the appropriate LSP view plugin.
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
- * Instead, we parse out the new text from the response and then update the
386
- * code in the plugins manually, instead of using the LSP.
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
- // This LSP method has no version, so lets sync and then get the latest snapshot
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("No view for plugin", currentCellId);
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
- // This LSP method has no version, so lets sync and then get the latest snapshot
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
- // This LSP method has no version, so lets sync and then get the latest snapshot
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
  );
@@ -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
- return await toPng(element);
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
- return !node.classList.contains("mpl-toolbar");
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> {
@@ -1,6 +0,0 @@
1
- var ue=Object.defineProperty;var ce=(r,e,t)=>e in r?ue(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var O=(r,e,t)=>ce(r,typeof e!="symbol"?e+"":e,t);import{s as V,t as G}from"./chunk-LvLJmgfZ.js";import{t as q}from"./react-BGmjiNul.js";import{ii as de,zt as fe}from"./cells-DRiwSVs0.js";import{t as B}from"./compiler-runtime-DeeZ7FnK.js";import{d as me}from"./hotkeys-BHHWjLlp.js";import{t as pe}from"./jsx-runtime-ZmTK25f3.js";import{t as W}from"./cn-BKtXLv3a.js";import{t as ve}from"./requests-BsVD4CdD.js";import{t as he}from"./createLucideIcon-CnW3RofX.js";import{t as _}from"./use-toast-rmUWldD_.js";import{d as ge}from"./popover-D16ZremR.js";import{r as ye}from"./errors-2SszdW9t.js";import{t as z}from"./dist-CdxIjAOP.js";import{t as M}from"./html-to-image-Cy_LYuWW.js";var we=he("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),H=r=>{let e,t=new Set,a=(l,s)=>{let c=typeof l=="function"?l(e):l;if(!Object.is(c,e)){let d=e;e=s??(typeof c!="object"||!c)?c:Object.assign({},e,c),t.forEach(u=>u(e,d))}},n=()=>e,o={setState:a,getState:n,getInitialState:()=>i,subscribe:l=>(t.add(l),()=>t.delete(l)),destroy:()=>{t.clear()}},i=e=r(a,n,o);return o},xe=r=>r?H(r):H,be=G((r=>{var e=q(),t=ge();function a(d,u){return d===u&&(d!==0||1/d==1/u)||d!==d&&u!==u}var n=typeof Object.is=="function"?Object.is:a,o=t.useSyncExternalStore,i=e.useRef,l=e.useEffect,s=e.useMemo,c=e.useDebugValue;r.useSyncExternalStoreWithSelector=function(d,u,f,g,m){var p=i(null);if(p.current===null){var x={hasValue:!1,value:null};p.current=x}else x=p.current;p=s(function(){function U(y){if(!F){if(F=!0,L=y,y=g(y),m!==void 0&&x.hasValue){var N=x.value;if(m(N,y))return S=N}return S=y}if(N=S,n(L,y))return N;var C=g(y);return m!==void 0&&m(N,C)?(L=y,N):(L=y,S=C)}var F=!1,L,S,I=f===void 0?null:f;return[function(){return U(u())},I===null?void 0:function(){return U(I())}]},[u,f,g,m]);var b=o(d,p[0],p[1]);return l(function(){x.hasValue=!0,x.value=b},[b]),c(b),b}})),Ne=G(((r,e)=>{e.exports=be()}));const h={toMarkdown:r=>h.replace(r,"md"),toHTML:r=>h.replace(r,"html"),toPNG:r=>h.replace(r,"png"),toPDF:r=>h.replace(r,"pdf"),toPY:r=>h.replace(r,"py"),withoutExtension:r=>{let e=r.split(".");return e.length===1?r:e.slice(0,-1).join(".")},replace:(r,e)=>r.endsWith(`.${e}`)?r:`${h.withoutExtension(r)}.${e}`};var P=320,$=180;function X(r){if(!r||r==="about:blank")return null;try{let e=new URL(r,window.location.href);return e.origin===window.location.origin?null:e.href}catch{return r}}function Pe(r,e,t){let a=[],n="";for(let o of e){let i=n+o;r.measureText(i).width<=t?n=i:(n&&a.push(n),n=o)}return n&&a.push(n),a}function j(r){let e=window.devicePixelRatio||1,t=document.createElement("canvas");t.width=P*e,t.height=$*e;let a=t.getContext("2d");if(!a)return t.toDataURL("image/png");a.scale(e,e),a.fillStyle="#f3f4f6",a.fillRect(0,0,P,$),a.strokeStyle="#d1d5db",a.strokeRect(.5,.5,P-1,$-1),a.fillStyle="#6b7280",a.font="8px sans-serif",a.textAlign="center",a.textBaseline="middle";let n=P-32,o=r?Pe(a,r,n):[],i=($-(1+o.length)*14)/2+14/2;a.fillText("External iframe",P/2,i),i+=14;for(let l of o)a.fillText(l,P/2,i),i+=14;return t.toDataURL("image/png")}async function Y(r){var n;let e=r.querySelector("iframe");if(!e)return null;let t=X(e.getAttribute("src"));if(t)return j(t);let a;try{let o=e.contentDocument||((n=e.contentWindow)==null?void 0:n.document);if(!(o!=null&&o.body))return null;a=o}catch{return j(null)}for(let o of a.querySelectorAll("iframe")){let i=X(o.getAttribute("src"));if(i)return j(i)}try{return await M(a.body)}catch{return j(null)}}var J=class se{constructor(e){O(this,"progress",0);O(this,"listeners",new Set);this.total=e}static indeterminate(){return new se("indeterminate")}addTotal(e){this.total==="indeterminate"?this.total=e:this.total+=e,this.notifyListeners()}increment(e){this.progress+=e,this.notifyListeners()}getProgress(){return this.total==="indeterminate"?"indeterminate":this.progress/this.total*100}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){let e=this.getProgress();for(let t of this.listeners)t(e)}},v=V(q(),1),w=V(pe(),1);function Le(r,e=[]){let t=[];function a(o,i){let l=v.createContext(i);l.displayName=o+"Context";let s=t.length;t=[...t,i];let c=u=>{var b;let{scope:f,children:g,...m}=u,p=((b=f==null?void 0:f[r])==null?void 0:b[s])||l,x=v.useMemo(()=>m,Object.values(m));return(0,w.jsx)(p.Provider,{value:x,children:g})};c.displayName=o+"Provider";function d(u,f){var p;let g=((p=f==null?void 0:f[r])==null?void 0:p[s])||l,m=v.useContext(g);if(m)return m;if(i!==void 0)return i;throw Error(`\`${u}\` must be used within \`${o}\``)}return[c,d]}let n=()=>{let o=t.map(i=>v.createContext(i));return function(i){let l=(i==null?void 0:i[r])||o;return v.useMemo(()=>({[`__scope${r}`]:{...i,[r]:l}}),[i,l])}};return n.scopeName=r,[a,Se(n,...e)]}function Se(...r){let e=r[0];if(r.length===1)return e;let t=()=>{let a=r.map(n=>({useScope:n(),scopeName:n.scopeName}));return function(n){let o=a.reduce((i,{useScope:l,scopeName:s})=>{let c=l(n)[`__scope${s}`];return{...i,...c}},{});return v.useMemo(()=>({[`__scope${e.scopeName}`]:o}),[o])}};return t.scopeName=e.scopeName,t}var T="Progress",k=100,[$e,nt]=Le(T),[je,Ee]=$e(T),K=v.forwardRef((r,e)=>{let{__scopeProgress:t,value:a=null,max:n,getValueLabel:o=Re,...i}=r;(n||n===0)&&!te(n)&&console.error(De(`${n}`,"Progress"));let l=te(n)?n:k;a!==null&&!re(a,l)&&console.error(Oe(`${a}`,"Progress"));let s=re(a,l)?a:null,c=E(s)?o(s,l):void 0;return(0,w.jsx)(je,{scope:t,value:s,max:l,children:(0,w.jsx)(z.div,{"aria-valuemax":l,"aria-valuemin":0,"aria-valuenow":E(s)?s:void 0,"aria-valuetext":c,role:"progressbar","data-state":ee(s,l),"data-value":s??void 0,"data-max":l,...i,ref:e})})});K.displayName=T;var Q="ProgressIndicator",Z=v.forwardRef((r,e)=>{let{__scopeProgress:t,...a}=r,n=Ee(Q,t);return(0,w.jsx)(z.div,{"data-state":ee(n.value,n.max),"data-value":n.value??void 0,"data-max":n.max,...a,ref:e})});Z.displayName=Q;function Re(r,e){return`${Math.round(r/e*100)}%`}function ee(r,e){return r==null?"indeterminate":r===e?"complete":"loading"}function E(r){return typeof r=="number"}function te(r){return E(r)&&!isNaN(r)&&r>0}function re(r,e){return E(r)&&!isNaN(r)&&r<=e&&r>=0}function De(r,e){return`Invalid prop \`max\` of value \`${r}\` supplied to \`${e}\`. Only numbers greater than 0 are valid max values. Defaulting to \`${k}\`.`}function Oe(r,e){return`Invalid prop \`value\` of value \`${r}\` supplied to \`${e}\`. The \`value\` prop must be:
2
- - a positive number
3
- - less than the value passed to \`max\` (or ${k} if no \`max\` prop is set)
4
- - \`null\` or \`undefined\` if the progress is indeterminate.
5
-
6
- Defaulting to \`null\`.`}var ne=K,_e=Z,Me=B(),A=v.forwardRef((r,e)=>{let t=(0,Me.c)(20),a,n,o,i;t[0]===r?(a=t[1],n=t[2],o=t[3],i=t[4]):({className:a,value:i,indeterminate:n,...o}=r,t[0]=r,t[1]=a,t[2]=n,t[3]=o,t[4]=i);let l;t[5]===a?l=t[6]:(l=W("relative h-2 w-full overflow-hidden rounded-full bg-primary/20",a),t[5]=a,t[6]=l);let s=n?"w-1/3 animate-progress-indeterminate":"w-full transition-transform duration-300 ease-out",c;t[7]===s?c=t[8]:(c=W("h-full flex-1 bg-primary",s),t[7]=s,t[8]=c);let d;t[9]!==n||t[10]!==i?(d=n?void 0:{transform:`translateX(-${100-(i||0)}%)`},t[9]=n,t[10]=i,t[11]=d):d=t[11];let u;t[12]!==c||t[13]!==d?(u=(0,w.jsx)(_e,{className:c,style:d}),t[12]=c,t[13]=d,t[14]=u):u=t[14];let f;return t[15]!==o||t[16]!==e||t[17]!==l||t[18]!==u?(f=(0,w.jsx)(ne,{ref:e,className:l,...o,children:u}),t[15]=o,t[16]=e,t[17]=l,t[18]=u,t[19]=f):f=t[19],f});A.displayName=ne.displayName;var Te=B();const ke=r=>{let e=(0,Te.c)(13),{progress:t,showPercentage:a}=r,n=a===void 0?!1:a,o,i;e[0]===t?(o=e[1],i=e[2]):(o=g=>t.subscribe(g),i=()=>t.getProgress(),e[0]=t,e[1]=o,e[2]=i);let l=(0,v.useSyncExternalStore)(o,i),s=l==="indeterminate"||l===100,c=s?void 0:l,d;e[3]!==s||e[4]!==c?(d=(0,w.jsx)(A,{value:c,indeterminate:s}),e[3]=s,e[4]=c,e[5]=d):d=e[5];let u;e[6]!==s||e[7]!==n||e[8]!==l?(u=!s&&n&&(0,w.jsxs)("div",{className:"mt-1 text-xs text-muted-foreground text-right",children:[Math.round(l),"%"]}),e[6]=s,e[7]=n,e[8]=l,e[9]=u):u=e[9];let f;return e[10]!==d||e[11]!==u?(f=(0,w.jsxs)("div",{className:"mt-2 w-full min-w-[200px]",children:[d,u]}),e[10]=d,e[11]=u,e[12]=f):f=e[12],f};async function Ae(r,e){let t=J.indeterminate(),a=_({title:r,description:v.createElement(ke,{progress:t}),duration:1/0});try{let n=await e(t);return a.dismiss(),n}catch(n){throw a.dismiss(),n}}function ae(r){let e=document.getElementById(de.create(r));if(!e){me.error(`Output element not found for cell ${r}`);return}return e}var R=0;function Ue(){R++,R===1&&document.body.classList.add("printing")}function Fe(){R--,R===0&&document.body.classList.remove("printing")}function ie(r,e){r.classList.add("printing-output"),e&&Ue();let t=r.style.overflow;return r.style.overflow="auto",()=>{r.classList.remove("printing-output"),e&&Fe(),r.style.overflow=t}}async function Ie(r,e=!0){let t=ae(r);if(!t)return;let a=await Y(t);if(a)return a;let n=ie(t,e);try{return await M(t)}finally{n()}}async function Ce(r,e){let t=ae(r);if(!t)return;let a=await Y(t);if(a){D(a,h.toPNG(e));return}await oe({element:t,filename:e,prepare:()=>ie(t,!0)})}async function oe(r){let{element:e,filename:t,prepare:a}=r,n=document.getElementById("App"),o=(n==null?void 0:n.scrollTop)??0,i;a?i=a(e):document.body.classList.add("printing");try{D(await M(e),h.toPNG(t))}catch{_({title:"Error",description:"Failed to download as PNG.",variant:"danger"})}finally{i==null||i(),document.body.classList.contains("printing")&&document.body.classList.remove("printing"),requestAnimationFrame(()=>{n==null||n.scrollTo(0,o)})}}function D(r,e){let t=document.createElement("a");t.href=r,t.download=e,t.click(),t.remove()}function le(r,e){let t=URL.createObjectURL(r);D(t,e),URL.revokeObjectURL(t)}async function Ve(r){let e=ve(),{filename:t,webpdf:a}=r;try{let n=await e.exportAsPDF({webpdf:a}),o=fe.basename(t);le(n,h.toPDF(o))}catch(n){throw _({title:"Failed to download",description:ye(n),variant:"danger"}),n}}export{oe as a,A as c,Ne as d,xe as f,Ce as i,J as l,le as n,Ie as o,we as p,D as r,Ae as s,Ve as t,h as u};