Skip to content

Commit 9825d2b

Browse files
committed
Fix and update tools for vscode handling
1 parent 066471e commit 9825d2b

34 files changed

+590
-546
lines changed

.licenserc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ header:
5757
- '**/*.patch'
5858
- '**/*.disabled'
5959
- '**/*.lexical'
60+
- 'examples/next-js/next-env.d.ts'
6061

6162
comment: on-failure

examples/next-js/next-env.d.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/*
2-
* Copyright (c) 2021-2023 Datalayer, Inc.
3-
*
4-
* MIT License
5-
*/
6-
71
/// <reference types="next" />
82
/// <reference types="next/image-types/global" />
93

packages/lexical/src/state/LexicalAdapter.ts

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,78 @@ export class LexicalAdapter {
341341
}
342342

343343
/**
344-
* Delete a block by ID
344+
* Delete multiple blocks by their IDs.
345+
* Handles complex logic including validation, sorting, and cascading deletions.
346+
*
347+
* @param blockIds - Array of block IDs to delete
348+
* @returns Promise with operation result including deleted blocks info
349+
*/
350+
async deleteBlock(
351+
blockIds: string[],
352+
): Promise<OperationResult & { deletedBlocks?: Array<{ id: string }> }> {
353+
try {
354+
// Read all blocks to validate IDs exist and get positions
355+
const blocks = await this.getBlocks('detailed');
356+
const blockIdSet = new Set(blocks.map(block => block.block_id));
357+
358+
// Validate ALL IDs exist
359+
const missingIds: string[] = [];
360+
for (const id of blockIds) {
361+
if (!blockIdSet.has(id)) {
362+
missingIds.push(id);
363+
}
364+
}
365+
366+
if (missingIds.length > 0) {
367+
return {
368+
success: false,
369+
error: `Block ID(s) not found: ${missingIds.join(', ')}. Document has ${blocks.length} blocks.`,
370+
};
371+
}
372+
373+
// Sort IDs in reverse order to delete children before parents (collapsibles last)
374+
// This prevents cascading deletions from causing "block not found" errors
375+
const sortedIds = [...blockIds].sort((a, b) => {
376+
const indexA = blocks.findIndex(block => block.block_id === a);
377+
const indexB = blocks.findIndex(block => block.block_id === b);
378+
// Sort in reverse order (higher index first)
379+
return indexB - indexA;
380+
});
381+
382+
const deletedBlocks: Array<{ id: string }> = [];
383+
384+
// Delete each block in reverse order
385+
for (const blockId of sortedIds) {
386+
const result = await this._deleteSingleBlock(blockId);
387+
388+
if (result.success) {
389+
deletedBlocks.push({ id: blockId });
390+
} else if (result.error?.includes('not found')) {
391+
// Block was already deleted by cascading deletion (parent collapsible removed)
392+
// This is expected behavior, skip and continue
393+
continue;
394+
} else {
395+
// Other errors should propagate
396+
return result;
397+
}
398+
}
399+
400+
return {
401+
success: true,
402+
deletedBlocks,
403+
};
404+
} catch (error) {
405+
return {
406+
success: false,
407+
error: error instanceof Error ? error.message : String(error),
408+
};
409+
}
410+
}
411+
412+
/**
413+
* Delete a single block by ID (internal helper).
345414
*/
346-
async deleteBlock(blockId: string): Promise<OperationResult> {
415+
private async _deleteSingleBlock(blockId: string): Promise<OperationResult> {
347416
return new Promise(resolve => {
348417
this._editor.update(() => {
349418
try {
@@ -414,7 +483,7 @@ export class LexicalAdapter {
414483
block: LexicalBlock,
415484
): Promise<OperationResult> {
416485
// For now, implement as delete + insert
417-
const deleteResult = await this.deleteBlock(blockId);
486+
const deleteResult = await this.deleteBlock([blockId]);
418487
if (!deleteResult.success) {
419488
return deleteResult;
420489
}
@@ -938,18 +1007,59 @@ export class LexicalAdapter {
9381007
}
9391008
});
9401009

1010+
if (!blockId) {
1011+
resolve({
1012+
success: false,
1013+
error:
1014+
'Failed to create jupyter-cell: INSERT_JUPYTER_INPUT_OUTPUT_COMMAND did not create a jupyter-input node',
1015+
});
1016+
return;
1017+
}
1018+
9411019
resolve({ success: true, blockId });
9421020
}, 20);
9431021
}, 10);
9441022
} else if (block.block_type === 'youtube') {
9451023
// Import YouTube command dynamically
9461024
const { INSERT_YOUTUBE_COMMAND } =
9471025
await import('../plugins/YouTubePlugin');
948-
// Get videoID from source field (11-character YouTube video ID)
1026+
// Get videoID from source field (11-character YouTube video ID or full URL)
9491027
const sourceText = Array.isArray(block.source)
9501028
? block.source.join('\n')
9511029
: block.source;
952-
const videoID = sourceText?.trim() || 'lO2p9LQB7ds';
1030+
1031+
// Extract video ID from various YouTube URL formats
1032+
const extractVideoID = (input: string): string => {
1033+
const trimmed = input.trim();
1034+
1035+
// If it's already just an ID (11 chars, alphanumeric + - and _)
1036+
if (/^[a-zA-Z0-9_-]{11}$/.test(trimmed)) {
1037+
return trimmed;
1038+
}
1039+
1040+
// Extract from various URL formats:
1041+
// - https://www.youtube.com/watch?v=VIDEO_ID
1042+
// - https://youtu.be/VIDEO_ID
1043+
// - https://youtube.com/watch?v=VIDEO_ID
1044+
// - https://m.youtube.com/watch?v=VIDEO_ID
1045+
1046+
// Try youtu.be format first
1047+
const shortMatch = trimmed.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
1048+
if (shortMatch) {
1049+
return shortMatch[1];
1050+
}
1051+
1052+
// Try youtube.com/watch?v= format
1053+
const watchMatch = trimmed.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
1054+
if (watchMatch) {
1055+
return watchMatch[1];
1056+
}
1057+
1058+
// Fallback to default if no valid ID found
1059+
return 'lO2p9LQB7ds';
1060+
};
1061+
1062+
const videoID = extractVideoID(sourceText || '');
9531063

9541064
// Insert using marker technique
9551065
await this.insertWithMarker(afterBlockId, () => {

packages/lexical/src/state/LexicalState.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ export type LexicalState = ILexicalsState & {
102102
metadata?: Record<string, unknown>,
103103
) => Promise<void>;
104104
deleteBlock: (id: string, blockId?: string) => Promise<void>;
105+
deleteBlocks: (
106+
id: string,
107+
blockIds?: string[],
108+
) => Promise<{ success: boolean; deletedBlocks?: Array<{ id: string }> }>;
105109
readBlock: (id: string, blockId?: string) => Promise<LexicalBlock | null>;
106110
readAllBlocks: (
107111
id: string,
@@ -224,20 +228,45 @@ export const lexicalStore = createStore<LexicalState>((set, get) => ({
224228
},
225229

226230
deleteBlock: async (id: string, blockId?: string): Promise<void> => {
227-
// Accept object from executor, destructure it
231+
// Single block deletion - delegates to deleteBlocks
228232
const params = typeof id === 'object' ? (id as any) : { id, blockId };
229233

230234
const adapter = get().lexicals.get(params.id)?.adapter;
231235
if (!adapter) {
232236
throw new Error(`Lexical document ${params.id} not found`);
233237
}
234238

235-
const result = await adapter.deleteBlock(params.blockId);
239+
const result = await adapter.deleteBlock([params.blockId]);
236240
if (!result.success) {
237241
throw new Error(result.error || 'Failed to delete block');
238242
}
239243
},
240244

245+
deleteBlocks: async (
246+
id: string,
247+
blockIds?: string[],
248+
): Promise<{ success: boolean; deletedBlocks?: Array<{ id: string }> }> => {
249+
// Multiple blocks deletion - handles array of IDs
250+
const params = typeof id === 'object' ? (id as any) : { id, blockIds };
251+
252+
const adapter = get().lexicals.get(params.id)?.adapter;
253+
if (!adapter) {
254+
throw new Error(`Lexical document ${params.id} not found`);
255+
}
256+
257+
// Ensure blockIds is an array
258+
const idsArray = Array.isArray(params.blockIds)
259+
? params.blockIds
260+
: [params.blockIds];
261+
262+
const result = await adapter.deleteBlock(idsArray);
263+
if (!result.success) {
264+
throw new Error(result.error || 'Failed to delete block');
265+
}
266+
267+
return result;
268+
},
269+
241270
readBlock: async (
242271
id: string,
243272
blockId?: string,

packages/lexical/src/tools/core/executor.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,12 @@ export class DefaultExecutor implements ToolExecutor {
5858
operationName: string,
5959
args?: unknown,
6060
): Promise<T> {
61-
console.log('[DefaultExecutor] 🚀 execute CALLED');
62-
console.log('[DefaultExecutor] Operation:', operationName);
63-
console.log('[DefaultExecutor] Args:', args);
64-
console.log('[DefaultExecutor] LexicalId:', this.lexicalId);
65-
6661
// Get the store method directly (1:1 mapping, no transformation)
6762
const method = (this.store as unknown as Record<string, unknown>)[
6863
operationName
6964
];
7065

7166
if (typeof method !== 'function') {
72-
console.error(
73-
'[DefaultExecutor] ❌ Store method not found or not a function!',
74-
);
7567
throw new Error(
7668
`Store method '${operationName}' not found or not a function`,
7769
);
@@ -83,23 +75,15 @@ export class DefaultExecutor implements ToolExecutor {
8375
? { id: this.lexicalId, ...args }
8476
: { id: this.lexicalId };
8577

86-
console.log('[DefaultExecutor] 📦 Payload to store method:', payload);
87-
8878
try {
89-
console.log('[DefaultExecutor] 📞 Calling store method...');
9079
const result = await (method as (args: unknown) => Promise<T>).call(
9180
this.store,
9281
payload,
9382
);
9483

95-
console.log('[DefaultExecutor] ✅ Store method returned:', result);
9684
return result;
9785
} catch (error) {
98-
console.error('[DefaultExecutor] ❌ ERROR calling store method:', error);
99-
console.error(
100-
'[DefaultExecutor] Error stack:',
101-
error instanceof Error ? error.stack : 'N/A',
102-
);
86+
console.error('[DefaultExecutor] Error calling store method:', error);
10387
throw error;
10488
}
10589
}

packages/lexical/src/tools/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// Re-export operations
1616
export * from '../operations/insertBlock';
1717
export * from '../operations/updateBlock';
18-
export * from '../operations/deleteBlock';
18+
export * from '../operations/deleteBlocks';
1919
export * from '../operations/readBlock';
2020
export * from '../operations/readAllBlocks';
2121
export * from '../operations/runBlock';

packages/lexical/src/tools/definitions/deleteBlock.ts renamed to packages/lexical/src/tools/definitions/deleteBlocks.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,28 @@
77
/**
88
* Tool definition for deleting blocks from Lexical documents
99
*
10-
* @module tools/definitions/tools/deleteBlock
10+
* @module tools/definitions/tools/deleteBlocks
1111
*/
1212

1313
import type { ToolDefinition } from '../core';
1414
import { zodToToolParameters } from '@datalayer/jupyter-react/tools';
15-
import { deleteBlockParamsSchema } from '../schemas/deleteBlock';
15+
import { deleteBlocksParamsSchema } from '../schemas/deleteBlocks';
1616

1717
/**
1818
* Tool definition for deleting one or more blocks from a Lexical document
1919
*
2020
* This tool permanently removes blocks by their IDs. Supports both single and multi-delete operations.
2121
*/
22-
export const deleteBlockTool: ToolDefinition = {
23-
name: 'datalayer_deleteBlock',
22+
export const deleteBlocksTool: ToolDefinition = {
23+
name: 'datalayer_deleteBlocks',
2424
displayName: 'Delete Lexical Block(s)',
25-
toolReferenceName: 'deleteBlock',
25+
toolReferenceName: 'deleteBlocks',
2626
description:
27-
'Delete one or more blocks from the currently open Lexical document by block_id. Supports single block deletion (pass string) or multi-delete (pass array of strings). WORKFLOW: 1) Call readAllBlocks to get block_id values, 2) Pass block_id(s) to delete. Validates all IDs exist before deletion. This permanently removes blocks and changes appear immediately. Works on active .lexical file.',
27+
'Delete one or more blocks from the currently open Lexical document by block_id. Pass array of block IDs to delete. WORKFLOW: 1) Call readAllBlocks to get block_id values, 2) Pass block_id array to delete. Validates all IDs exist before deletion. This permanently removes blocks and changes appear immediately. Works on active .lexical file.',
2828

29-
parameters: zodToToolParameters(deleteBlockParamsSchema),
29+
parameters: zodToToolParameters(deleteBlocksParamsSchema),
3030

31-
operation: 'deleteBlock',
31+
operation: 'deleteBlocks',
3232

3333
config: {
3434
confirmationMessage: (params: { ids: string[] }) => {

packages/lexical/src/tools/definitions/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
export * from './insertBlock';
1616
export * from './updateBlock';
17-
export * from './deleteBlock';
17+
export * from './deleteBlocks';
1818
export * from './readBlock';
1919
export * from './readAllBlocks';
2020
export * from './runBlock';

packages/lexical/src/tools/definitions/insertBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const insertBlockTool: ToolDefinition = {
3030
displayName: 'Insert Lexical Block',
3131
toolReferenceName: 'insertBlock',
3232
description:
33-
"CRITICAL: DO NOT use markdown syntax in source field. Heading blocks use PLAIN TEXT (no # symbols) - specify tag metadata (h1-h6) instead. Inline formatting like **bold** or *italic* is automatically converted. ALWAYS call listAvailableBlocks FIRST to see block types and required metadata format. Insert different types of content blocks. When inserting MULTIPLE blocks sequentially (e.g., creating a document outline with heading + paragraph + code), ALWAYS use afterId: 'BOTTOM' for each insertion to append blocks in order. For single insertions, call readAllBlocks first to see document structure. Position blocks using afterId: 'TOP' (beginning), 'BOTTOM' (end - REQUIRED for sequential inserts), or a specific block_id value from readAllBlocks. To insert blocks INSIDE a collapsible: 1) First insert collapsible (type='collapsible'), which returns a blockId, 2) Then insert nested blocks with metadata.collapsible set to that RETURNED BLOCK ID (NOT 'TOP' or 'BOTTOM'). Example: result = insertBlock({type: 'collapsible', source: 'Section', afterId: 'BOTTOM'}); insertBlock({type: 'paragraph', source: 'text', metadata: {collapsible: result.blockId}, afterId: 'BOTTOM'}). DO NOT use position markers (TOP/BOTTOM) as collapsible IDs - they are only for afterId positioning. Works on active .lexical file.",
33+
"Insert blocks into Lexical documents (.dlex files). IMPORTANT: Lexical documents support EXECUTABLE JUPYTER CELLS - use type='jupyter-cell' to insert Python/R/Julia code cells that can be executed via kernel (just like .ipynb notebooks). Other block types: paragraph, heading, code (non-executable syntax highlighting), quote, list, table, collapsible, equation, image, youtube, horizontalrule. CRITICAL: DO NOT use markdown syntax in source field. Heading blocks use PLAIN TEXT (no # symbols) - specify tag metadata (h1-h6) instead. Inline formatting like **bold** or *italic* is automatically converted. ALWAYS call listAvailableBlocks FIRST to see block types and required metadata format. When inserting MULTIPLE blocks sequentially (e.g., creating a document outline with heading + paragraph + code), ALWAYS use afterId: 'BOTTOM' for each insertion to append blocks in order. For single insertions, call readAllBlocks first to see document structure. Position blocks using afterId: 'TOP' (beginning), 'BOTTOM' (end - REQUIRED for sequential inserts), or a specific block_id value from readAllBlocks. To insert blocks INSIDE a collapsible: 1) First insert collapsible (type='collapsible'), which returns a blockId, 2) Then insert nested blocks with metadata.collapsible set to that RETURNED BLOCK ID (NOT 'TOP' or 'BOTTOM'). Example: result = insertBlock({type: 'collapsible', source: 'Section', afterId: 'BOTTOM'}); insertBlock({type: 'paragraph', source: 'text', metadata: {collapsible: result.blockId}, afterId: 'BOTTOM'}). DO NOT use position markers (TOP/BOTTOM) as collapsible IDs - they are only for afterId positioning. Works on active .lexical/.dlex file.",
3434

3535
parameters: zodToToolParameters(insertBlockParamsSchema),
3636

packages/lexical/src/tools/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// Import all tool definitions
1616
import { insertBlockTool } from './definitions/insertBlock';
1717
import { updateBlockTool } from './definitions/updateBlock';
18-
import { deleteBlockTool } from './definitions/deleteBlock';
18+
import { deleteBlocksTool } from './definitions/deleteBlocks';
1919
import { readBlockTool } from './definitions/readBlock';
2020
import { readAllBlocksTool } from './definitions/readAllBlocks';
2121
import { runBlockTool } from './definitions/runBlock';
@@ -26,7 +26,7 @@ import { executeCodeTool } from './definitions/executeCode';
2626
// Import all operations
2727
import { insertBlockOperation } from './operations/insertBlock';
2828
import { updateBlockOperation } from './operations/updateBlock';
29-
import { deleteBlockOperation } from './operations/deleteBlock';
29+
import { deleteBlocksOperation } from './operations/deleteBlocks';
3030
import { readBlockOperation } from './operations/readBlock';
3131
import { readAllBlocksOperation } from './operations/readAllBlocks';
3232
import { runBlockOperation } from './operations/runBlock';
@@ -44,7 +44,7 @@ import type { ToolOperation } from './core/interfaces';
4444
export const lexicalToolDefinitions: ToolDefinition[] = [
4545
insertBlockTool,
4646
updateBlockTool,
47-
deleteBlockTool,
47+
deleteBlocksTool,
4848
readBlockTool,
4949
readAllBlocksTool,
5050
runBlockTool,
@@ -63,7 +63,7 @@ export const lexicalToolOperations: Record<
6363
> = {
6464
insertBlock: insertBlockOperation,
6565
updateBlock: updateBlockOperation,
66-
deleteBlock: deleteBlockOperation,
66+
deleteBlocks: deleteBlocksOperation,
6767
readBlock: readBlockOperation,
6868
readAllBlocks: readAllBlocksOperation,
6969
runBlock: runBlockOperation,
@@ -85,7 +85,7 @@ export * from './core';
8585
export * from './definitions';
8686
export * from './operations/insertBlock';
8787
export * from './operations/updateBlock';
88-
export * from './operations/deleteBlock';
88+
export * from './operations/deleteBlocks';
8989
export * from './operations/readBlock';
9090
export * from './operations/readAllBlocks';
9191
export * from './operations/runBlock';

0 commit comments

Comments
 (0)