This document provides comprehensive information for developers who want to contribute to or modify the Datalayer VS Code extension.
- Node.js >= 22.0.0 and < 23.0.0 (use
.nvmrcfor version management) - VS Code >= 1.107.0
- npm (not yarn)
Important: The extension runs in VS Code's Node.js 22 runtime. Using Node.js 22 for development ensures compatibility.
# Install dependencies
# Note: Use --ignore-scripts to bypass @datalayer/core postinstall issues
npm install --ignore-scripts
# Or if postinstall works for you
npm install
# Watch for changes (development)
npm run watch
# Run linting
npm run lint
# Build extension
npm run compile
# Package extension
npm run package
# Create VSIX package
npm run vsixBy default, webview builds use hidden-source-map to keep bundle size small (~15-20 MB savings). This generates .map files but doesn't embed them in the bundle.
To enable inline source maps for easier debugging in VS Code DevTools:
# One-time compile with inline source maps
WEBVIEW_DEBUG=1 npm run compile
# Or use the convenience script
npm run compile:debug
# For watch mode with inline source maps
npm run watch:debugWhen to use inline source maps:
- Debugging webview code in Extension Development Host
- Tracing errors in React components
- Understanding webpack bundling issues
When to use hidden source maps (default):
- Creating VSIX packages for distribution
- Testing bundle size optimizations
- Production builds
How it works:
- The
WEBVIEW_DEBUGenvironment variable controls the webpackdevtoolsetting - When enabled: Full source code embedded in bundles (larger but easier debugging)
- When disabled: External
.mapfiles generated but not referenced in bundles (smaller, post-mortem debugging only)
Manual source map loading: If you have a production build with hidden source maps and need to debug:
- Open browser DevTools in the webview
- Right-click in Sources panel β "Add source map"
- Point to the
.mapfile indist/directory
The extension depends on local packages from sibling directories:
@datalayer/core- Core Datalayer library (../core)@datalayer/jupyter-lexical- Lexical editor with Jupyter integration (../jupyter-ui/packages/lexical)@datalayer/jupyter-react- React components for Jupyter notebooks (../jupyter-ui/packages/react)@datalayer/lexical-loro- Loro CRDT collaboration provider for Lexical (../lexical-loro)@datalayer/agent-runtimes- Agent runtimes UI components (../agent-runtimes)
The packages have the following dependency structure:
vscode-datalayer
βββ @datalayer/core (UI components, SDK client)
βββ @datalayer/jupyter-lexical (Lexical editor with Jupyter blocks)
β βββ @datalayer/lexical-loro (CRDT collaboration provider)
βββ @datalayer/jupyter-react (React notebook components)
βββ @datalayer/agent-runtimes (Agent runtimes UI components)
When developing features that span multiple packages, you need to sync local changes to the extension's node_modules. We use a combination of build scripts and patch-package for this workflow.
# Sync local packages (one-time)
npm run sync:jupyter
# Sync with auto-watch mode
npm run sync:jupyter:watch
# Create patches for distribution
npm run create:patches
# Apply patches manually
npm run apply:patchesEdit code in any of these locations:
../core/src/../jupyter-ui/packages/lexical/src/../jupyter-ui/packages/react/src/../lexical-loro/src/../agent-runtimes/src/
Option A: Manual Sync
npm run sync:jupyterThis script will:
- Run
npx gulp resources-to-libfor core and jupyter-lexical (copies images, examples) - Build TypeScript (
npm run build:lib) for core, jupyter-lexical, jupyter-react, and agent-runtimes - Copy
lib/,style/, andpackage.jsonfiles tonode_modules/@datalayer/ - Skip lexical-loro build (uses existing lib/ due to TypeScript issues with Yjs)
Option B: Watch Mode (Recommended for Active Development)
npm run sync:jupyter:watchThis monitors all package src/ directories and auto-syncs on changes. Requires fswatch (installed automatically on macOS via Homebrew).
# Compile extension
npm run compile
# Press F5 in VS Code to launch Extension Development HostWhen you're ready to commit your changes:
npm run create:patchesThis will:
- Run a final sync to ensure latest changes
- Generate patches in the
patches/directory usingpatch-package - Patches are for:
@datalayer/core,@datalayer/jupyter-lexical,@datalayer/jupyter-react,@datalayer/agent-runtimes,cmake-ts
Important: Commit the patches/ directory to git. These patches are automatically applied during npm install via the postinstall hook.
On CI or after a fresh npm install:
- The
postinstallscript runs automatically bash scripts/apply-patches.shapplies all patches frompatches/- No manual sync needed - patches are applied to installed packages
Key Note: After any npm install, you MUST re-run npm run sync:jupyter if you're doing local development, because install will overwrite synced packages with published versions.
If you make changes to local packages and want to test them:
New Dependencies (added January 2025):
tailwindcss- Required for processing Lexical's CSS@tailwindcss/postcss- PostCSS integrationpostcss- CSS processingautoprefixer- CSS vendor prefixing@lexical/mark- Required by CommentPlugin for text highlighting
These are needed because @datalayer/jupyter-lexical/style/index.css uses @import 'tailwindcss'.
Three critical UX issues with inline LLM completions in the Lexical editor have been fixed:
- Increased debounce delay from 200ms to 500ms for better typing experience
- Completion requests now only trigger after typing has stopped for half a second
- Prevents excessive API calls and UI flickering during fast typing
- Added inter-plugin communication via
LSP_MENU_STATE_COMMAND - LSP tab completion dropdown now properly blocks inline completions
- When LSP menu opens:
- Cancels any pending inline completion timers
- Clears visible inline completions
- Prevents new inline requests until menu closes
- Fixes issue where both completion types would appear simultaneously
- Tab acceptance now correctly moves cursor to end of inserted text
- Previously cursor stayed at insertion point, breaking typing flow
- Implementation uses explicit selection positioning after text node replacement
- LSP completion dropdown now filters results in real-time as user types
- Filter text is calculated from characters typed after menu initially opens
- Filtering happens client-side without re-querying LSP server
- Results are sorted with priority:
- Exact prefix matches: Items starting with typed text (case-insensitive)
- Partial matches: Items containing typed text anywhere
- Non-matches: Excluded from dropdown
- Menu automatically closes if no matches remain
- Mirrors VS Code's native completion behavior
Implementation Details:
- Tracks cursor offset when menu first opens (
menuOpenOffsetRef) - Monitors text changes via Lexical's
registerUpdateListener - Calculates filter text by comparing current offset to initial offset
- Uses
filterAndSortCompletions()to process cached results - When user selects completion, replaces typed filter text with full completion text
jupyter-ui/packages/lexical/src/plugins/LexicalInlineCompletionPlugin.tsxjupyter-ui/packages/lexical/src/plugins/LSPTabCompletionPlugin.tsx
- Open a
.dlexfile with Python code cells - Type code - inline completion should appear after 500ms pause
- Press Tab to trigger LSP dropdown - inline completion should disappear
- Test dynamic filtering:
- Type
sys.and press Tab to show LSP dropdown - Continue typing
p- dropdown should filter to show only items starting withp(likepath,platform) - Type more letters - dropdown updates in real-time
- Backspace to delete typed text - dropdown shows more items again
- Menu closes if no matches remain
- Type
- Select LSP completion - dropdown should close without inline interference
- Accept inline completion with Tab - cursor should move to end of inserted text
The extension uses a custom WOFF icon font for consistent Datalayer branding across the VS Code UI.
- Icon Font:
resources/datalayer-icons.woff(1KB) - Build Script:
scripts/build-icons.js(automated CLI-based generation) - Build Tool:
npm run build:icons - Source Icons:
resources/icons/*.svg - Integration: Automatically built during
compileandvscode:prepublish
SVG files β svgicons2svgfont β svg2ttf β ttf2woff β WOFF font
datalayer-logo- Main Datalayer stacked blocks logo- Used in: Notebook toolbar button, Status bar
- Unicode:
\ue900 - Source:
resources/icons/datalayer-logo.svg
The icon font is automatically generated:
# Manual build
npm run build:icons
# Automatically runs during:
npm run compile # Development builds
npm run vscode:prepublish # Production packagingCI Integration: CI automatically rebuilds the icon font when:
- SVG files in
resources/icons/are modified - Build script
scripts/build-icons.jsis updated
-
Prepare SVG:
- Create monochrome SVG (single color)
- Use
fill="currentColor"for theme adaptability - Remove strokes (convert to fills)
- Square aspect ratio recommended (e.g., 20x20 viewBox)
-
Add to Project:
# Copy SVG to icons directory cp my-icon.svg resources/icons/ -
Regenerate Font:
npm run build:icons
This generates:
resources/datalayer-icons.woff- Icon fontresources/datalayer-icons.json- Unicode mapping
-
Register in package.json: Check
resources/datalayer-icons.jsonfor the assigned unicode, then add:{ "contributes": { "icons": { "my-icon": { "description": "My custom icon", "default": { "fontPath": "./resources/datalayer-icons.woff", "fontCharacter": "\\e901" } } } } } -
Use in Code:
// In commands (package.json) "icon": "$(my-icon)" // In status bar (TypeScript) statusBarItem.text = "$(my-icon) Label";
Format: WOFF (Web Open Font Format)
Unicode Range: Private Use Area (U+E900 - U+E9FF)
Font Name: datalayer-icons
Dependencies: svgicons2svgfont, svg2ttf, ttf2woff
- Source SVGs: Excluded from
.vsixvia.vscodeignore - Generated WOFF: Included in
.vsixpackage - Build: Auto-runs before packaging
See resources/icons/README.md for complete icon font documentation.
The extension uses zeromq for direct kernel communication. Since zeromq is a native Node.js module with platform-specific binaries, we use Microsoft's @vscode/zeromq package (same approach as VS Code Jupyter extension) to download pre-built binaries for all platforms during the build.
ZeroMQ Dependencies:
zeromq@^6.0.0-beta.20- Primary ZMQ library (Electron-compatible beta)zeromqold@npm:zeromq@^6.0.0-beta.6- Fallback version for reliability@vscode/zeromq@^0.2.3- Microsoft's binary downloader (devDependency)
Universal VSIX:
The extension is built as a single universal VSIX that works on all platforms:
datalayer-jupyter-vscode-{version}.vsix- Works on macOS (Intel + ARM), Windows, and Linux
This is the same approach used by VS Code Jupyter extension. The VSIX includes native binaries for all platforms, and zeromq automatically selects the correct binary at runtime.
Build Command:
# Build universal VSIX (works on all platforms)
npm run vsixHow it works:
- During
npm install, thepostinstallscript runsscripts/downloadZmqBinaries.js - This downloads ALL platform binaries via
@vscode/zeromq.downloadZMQ() - Binaries for all platforms are placed in
node_modules/zeromq/prebuilds/ - The
npm run vsixcommand packages the extension with production dependencies (including zeromq/zeromqold modules and their binaries) - At runtime, zeromq automatically picks the correct binary for the current platform
- Fallback loader tries
zeromqfirst, thenzeromqoldif it fails (seesrc/services/kernel/rawSocket.ts)
Benefits:
- β Simpler distribution - One VSIX works everywhere
- β Faster builds - No need for matrix builds across platforms
- β 10x faster than compiling - Download vs compile with electron-rebuild
- β More reliable - Microsoft-maintained pre-built binaries
- β No build tools required - No python, make, gcc, etc.
- β Proven approach - Used by VS Code Jupyter in production
β οΈ Larger VSIX - ~100MB (contains all production dependencies including zeromq binaries for all platforms)
Important: The extension requires specific dependency versions. If you encounter ELSPROBLEMS errors during packaging, ensure:
@toon-format/toon@^1.3.0(not 1.0.0)diff@^8.0.2(not 7.0.0)- Run
npm install @toon-format/toon@^1.3.0 diff@^8.0.2to fix version mismatches
The extension uses @github/keytar for secure OS keyring access (macOS Keychain, Windows Credential Manager, Linux Secret Service). This is GitHub's actively maintained fork of the original keytar module. We use it (instead of upstream keytar) because its npm tarball ships prebuilt native bindings for every supported platform β darwin-arm64/x64, linux-{arm,arm64,armv7l,ia32,x64}, linuxmusl-{arm,arm64,x64}, and win32-{arm64,ia32,x64}. That lets a single multi-platform VSIX work everywhere without per-OS native rebuilds.
Dependency:
@github/keytar@^7.10.6- Multi-platform prebuilt native module for OS keyring access
No rebuild step required:
The prebuilds shipped in the npm tarball are loaded automatically based on the host platform/arch. There is no postinstall rebuild and no @electron/rebuild dependency.
How it's wired up:
webpack.config.jsdeclares@github/keytaras acommonjsexternal so it is not bundled.scripts/copy-external-deps.jscopiesnode_modules/@github/keytar/(including allprebuilds/) intodist/node_modules/for VSIX packaging..vscodeignoreallow-lists!node_modules/@github/keytar/**so vsce includes it in the package.webpack.config.jssetsnode: { __filename: false, __dirname: false }so the bundled@datalayer/coreNodeStoragecan usemodule.createRequire(__filename)to resolve realnode_modules/@github/keytarat runtime. Without that, webpack's default replaces__filenamewith a fake path and the require fails.@datalayer/core'sNodeStoragetries@github/keytarfirst, then@vscode/keytar, then upstreamkeytarfor backwards compatibility.
Benefits:
- Secure credential storage via OS-native keyring APIs
- Cross-tool credential sharing β the CLI and the extension write to the same keyring entry under the IAM service URL, so logging in via
dlamakes the extension see the session and vice versa - No per-OS native rebuild β the same VSIX works on every platform
- Cross-platform: macOS Intel/ARM, Windows x64/arm64/ia32, Linux x64/arm/arm64/armv7l (glibc and musl)
Important: NodeStorage from @datalayer/core is the credential backend. It is selected by default by the extension; do not pass a custom storage option to DatalayerClient in extension code, because anything other than NodeStorage (e.g. VS Code's SecretStorage) is scoped to the extension and would break credential sharing with the CLI.
The project enforces strict quality standards with zero-tolerance for errors.
# Type checking (TypeScript compilation)
npm run type-check
# Runs: tsc --noEmit && tsc --noEmit -p tsconfig.webview.json
# Linting (ESLint)
npm run lint
# Zero warnings policy in production code
# Documentation generation
npm run doc
# Must have 100% documentation coverage
# Run all checks
npm run check
# Equivalent to: format:check + lint + type-check
# Auto-fix all issues
npm run check:fix
# Equivalent to: format + lint:fix + type-check
# Tool schema generation (Copilot tools)
node scripts/generate-tool-schemas.js
# Parses TypeScript tool definitions and syncs to package.json
# Validate tool schemas
node scripts/validate-tool-schemas.js
# Checks that package.json has all expected Copilot tools- β Type Check: 0 errors (strict TypeScript)
- β
Lint: 0 errors, 28 warnings (all
no-console) (ESLint with @typescript-eslint) - β Documentation: 100% coverage (466/466 items documented)
- β Tests: 1,300+/1,300+ passing (extension + webview, 100% success rate)
- β Coverage: ~43% statements, ~89% branches, ~36% functions (extension)
- β Format: Prettier compliance
Before committing any code:
npm run type-check- Must pass with zero errorsnpm run lint- Must pass with zero warningsnpm run doc- Must generate without errorsnpm test- All tests must pass
# Run extension tests (960 tests, Mocha TDD UI)
npm test
# Run webview tests (369 tests, Vitest + jsdom)
npm run test:webview
# Run extension tests with coverage
npm run test:coverage
# Compile tests
npm run compile-tests
# Watch tests
npm run watch-testsSee TESTING.md for comprehensive testing guide.
src/test/setup.js stubs @datalayer/core and browser-only packages so that extension tests can run in the Node.js test runner without browser APIs.
scripts/fix-css-imports.jsstrips CSS imports from@primer/reactand fixes directory imports in@datalayer/icons-react(runs inpostinstall).sync:toolsusesnode --import ./scripts/ignore-css-preload.mjs --import tsxfor Node 22 compatibility.@datalayer/coreprovides asrc/node.tsentry point (Node.js-safe, no React components) for extension host usage.
- ~43% statements, ~89% branches, ~36% functions (extension only)
- Tracked via Codecov with dual flags:
extensionandwebview - Exclusions:
kernel/,pyodide/,commands/,ui/templates/,jupyter/,notebookProvider.js,lexicalProvider.js
- Extension tests: Mocha TDD UI via
@vscode/test-cli(runs in Extension Host) - Webview tests: Vitest with jsdom environment
- Assertion: Node.js built-in
assertmodule - Mock System: Custom mock factory with TypeScript interfaces
All mocks are strongly typed:
// Mock interfaces
export interface MockSDK { ... } // SDK with 24+ typed methods
export interface MockLogger extends ILogger { ... }
export interface MockSpyFunction { ... } // Spy with call tracking
// Factory functions
export function createMockSDK(): MockSDK;
export function createMockLogger(): ILogger;
export function createMockExtensionContext(): vscode.ExtensionContext;import { createMockSDK, createMockLogger } from "../utils/mockFactory";
import type { DatalayerClient } from "@datalayer/core/lib/client";
suite("My Feature Tests", () => {
let mockSDK: ReturnType<typeof createMockSDK>;
setup(() => {
mockSDK = createMockSDK();
});
test("should work correctly", async () => {
// Arrange
mockSDK.iam.getIdentity.mockResolvedValue({ uid: "test" });
// Act
const result = await myFunction(mockSDK as unknown as DatalayerClient);
// Assert
assert.strictEqual(result, "expected");
assert.strictEqual(mockSDK.iam.getIdentity.calls.length, 1);
});
});- Open the project in VS Code
- Run
npm run watchin terminal - Press
F5to launch Extension Development Host - Open any
.ipynbfile to test the extension - Set breakpoints in
src/files
- In Extension Development Host, open Command Palette
- Run "Developer: Open Webview Developer Tools"
- Use Chrome DevTools for React components
- Set breakpoints in
webview/code
- Set breakpoints in test files
- Open Run and Debug panel (Cmd+Shift+D)
- Select "Extension Tests" configuration
- Press F5 to debug tests
-
Start watch mode:
npm run watch
-
Press F5 in VS Code to launch Extension Development Host
-
Make changes - hot reload for most changes
-
Test in Extension Host - open
.ipynbfiles -
Before committing:
npm run check # Run all validations npm test # Run all tests
# Development build (with source maps)
npm run compile
# Production build (optimized)
npm run package
# Create VSIX package
npm run vsix
# Clean all artifacts
npm run clean # Removes dist/, out/, *.vsixIf you need to start fresh with dependencies:
# Remove existing dependencies
rm -rf node_modules
# Clean install (bypass postinstall issues)
npm install --ignore-scripts
# Rebuild extension
npm run compileWhy --ignore-scripts? The @datalayer/core package (version ^0.0.20 from npm) has a postinstall script that expects files only available in the development repository, not in the published npm package. Using --ignore-scripts bypasses this issue without affecting extension functionality.
- Add command to
package.jsoncontributes.commands - Create handler in
src/commands/ - Register in
src/commands/index.ts - Add tests in
src/test/commands/
- Create interface in
src/services/interfaces/ - Implement in
src/services/core/orsrc/services/notebook/ - Add to ServiceContainer if needed
- Create mock in
src/test/utils/mockFactory.ts - Write tests in
src/test/services/
All exported symbols need JSDoc:
/**
* Brief description (required).
* Additional details (optional).
*
* @param param1 Description of parameter
* @returns Description of return value
* @throws {ErrorType} When error occurs
* @example
* ```typescript
* const result = myFunction('input');
* ```
*/
export function myFunction(param1: string): string {
// ...
}Run npm run doc to verify documentation.
The extension follows a layered architecture:
-
Extension Host (
src/extension.ts)- Entry point, activation, command registration
- Runs in Node.js 22 environment
-
Commands (
src/commands/)- Thin command handlers
- Delegate to services for business logic
- Commands: auth, documents, runtimes
-
Providers (
src/providers/)- Implement VS Code APIs
- TreeDataProvider, CustomTextEditorProvider, NotebookController
- Files: spacesTreeProvider, jupyterNotebookProvider, lexicalDocumentProvider
-
Services (
src/services/)- Business logic and state management
- Some services use singleton pattern (see Service Architecture below)
- Categories:
- Core: authProvider, sdkAdapter, serviceContainer
- Notebook: documentBridge (singleton), kernelBridge, notebookRuntime (singleton), lexicalCollaboration (singleton)
- Logging: loggerManager (singleton), performanceLogger
- Cache: environmentCache (singleton)
- UI: statusBar (singleton), uiSetup
-
Models (
src/models/)- Data structures: notebookDocument, lexicalDocument, spaceItem
-
UI (
src/ui/)- Dialogs: authDialog, kernelSelector, runtimeSelector
- Templates: notebookTemplate
-
Webviews (
webview/)- React-based editors
- notebook/ - Jupyter notebook UI
- lexical/ - Rich text editor
- Theme integration with VS Code
-
Utils (
src/utils/)- Pure utility functions
- dispose, webviewSecurity, documentUtils
Services are managed through dependency injection via ServiceContainer. Some services use singleton pattern for global state management:
// Singleton services (use getInstance)
LoggerManager.getInstance(context);
EnvironmentCache.getInstance();
DocumentBridge.getInstance(context, sdk);
NotebookRuntimeService.getInstance();
LexicalCollaborationService.getInstance();
DatalayerStatusBar.getInstance(authProvider);
// Regular services (use new constructor)
new SDKAuthProvider(sdk, context, logger);
new KernelBridge(sdk, authProvider);
// Static class with initialization
ServiceLoggers.initialize(loggerManager);The extension provides automatic runtime connection when opening notebooks and lexical documents:
Location: src/services/autoConnect/
Strategy Pattern:
AutoConnectService- Main service that processes strategy arrayActiveRuntimeStrategy- Selects runtime with most time remaining (sorted by expiredAt - now)AskUserStrategy- Shows Quick Pick dialog for runtime selection
Configuration: datalayer.autoConnect.strategies (array)
- Default:
["Active Runtime", "Ask"] - Empty array
[]disables auto-connect - Tries strategies in order until one succeeds
Integration:
NotebookProvider.tryAutoConnect()- Called after webview initializationLexicalProvider.tryAutoConnect()- Called after webview initialization- Both providers get
RuntimesTreeProviderviagetRuntimesTreeProvider()export
Key Design Decisions:
- Smart Selection: ActiveRuntimeStrategy sorts runtimes by remaining time (expiredAt - now) in descending order, always selecting the runtime with the most time available to maximize session duration
- No Extra API Calls: Uses
RuntimesTreeProvider.getCachedRuntimes()to access already-loaded runtime data from the sidebar
Three-tier logging system:
- LoggerManager: Creates and manages output channels
- ServiceLoggers: Static access to categorized loggers
- Individual Loggers: auth, notebook, runtime, general
// Access loggers
ServiceLoggers.auth.info('User logged in');
ServiceLoggers.notebook.error('Failed to save', error);
ServiceLoggers.runtime.timeAsync('createRuntime', async () => {...});The editor is encapsulated within an iframe. All communications between the editor and external services involve posting messages between the extension and webview:
- Jupyter Service Interaction: The webview creates a JupyterLab
ServiceManagerwith mockedfetchandWebSocket - Message Serialization: Requests are serialized and posted to the extension
- Extension Processing: The extension deserializes and makes actual network requests
- Response Handling: Responses are serialized and posted back to the webview
The extension provides Language Server Protocol (LSP) integration for notebook cells, enabling IntelliSense features like completions, hover documentation, and diagnostics for Python (via Pylance) and Markdown cells.
Two Completion Types:
-
Inline Completions (Ghost Text): Automatic suggestions shown as faded text while typing (like GitHub Copilot)
- Provider:
LSPCompletionProviderimplementsIInlineCompletionProvider - File:
webview/services/completion/lspProvider.ts - Trigger: Automatic, continuous as you type
- Accept: Press Tab key
- Provider:
-
Tab Completions (Dropdown Menu): Traditional dropdown menu with completion options
- Provider:
LSPTabCompletionProviderimplements JupyterLabICompleterProvider - File:
webview/services/completion/lspTabProvider.ts - Trigger: Press Tab key or Ctrl+Space
- Accept: Press Enter to select, Arrow keys to navigate
- Provider:
Extension Host Components (src/services/lsp/):
-
LSPDocumentManager: Creates temporary files in
.vscode/datalayer-cells/for each notebook cell- Uses real files with
file://URIs (required by Pylance) - Python cells:
{cellId}.py - Markdown cells:
{cellId}.md - Synchronizes cell content changes to disk
- Waits 500ms after file creation for Pylance to analyze
- Uses real files with
-
LSPCompletionService: Routes completion requests to appropriate language servers
- Python cells β Pylance language server
- Markdown cells β VS Code's built-in markdown language server
- Calls
vscode.executeCompletionItemProviderwith file URI - Formats results for webview consumption
-
LSPBridge: Message routing between webview and extension host
- Handles: completion requests, hover requests, document sync
- Message types:
lsp-completion-requestβlsp-completion-responselsp-document-open/lsp-document-sync/lsp-document-closelsp-errorfor failures
Message Flow:
User types in cell
β
LSPProvider (webview) detects language (Python/Markdown)
β
postMessage('lsp-completion-request', { cellId, language, position })
β
LSPBridge routes to LSPCompletionService
β
LSPCompletionService:
1. Gets temp file URI from LSPDocumentManager
2. Ensures Pylance/Markdown LSP is active
3. Calls vscode.executeCompletionItemProvider
β
Language server returns completions (Pylance or Markdown LSP)
β
postMessage('lsp-completion-response', { completions })
β
LSPProvider formats for inline display (suffix only)
LSPTabProvider formats for dropdown (full items with metadata)
β
User sees ghost text or dropdown menu
Key Design Decisions:
- Temp Files in Workspace: Originally used
/tmp/but moved to.vscode/datalayer-cells/so Pylance can index files (Pylance only analyzes files within workspace) - Real Files, Not Virtual URIs: Pylance only supports specific URI schemes (
file://,untitled://,vscode-notebook://). Custom schemes likedatalayer-lsp://don't work. - Suffix Extraction: Inline completions show only the remaining text to type, not the full word. Extracts
textEdit.rangeto calculate already-typed characters. - 500ms Analysis Delay: After creating a Python file, wait 500ms before accepting completion requests to give Pylance time to analyze the file.
- 15 Second Timeout: LSP requests timeout after 15 seconds (increased from 5s) to accommodate Pylance's initial analysis time.
Current Configuration (as of January 2025):
The extension uses separate providers for inline completions (ghost text) and Tab completions (dropdown menu):
// In NotebookEditor.tsx
const notebook = (
<Notebook
inlineProviders={[llmProvider]} // Only LLM for inline (LSP disabled for inline)
providers={[lspTabProvider]} // Only LSP for Tab dropdown
// ...
/>
);Why LSP Inline is Disabled:
- Tab key conflict: Inline LSP ghost text would interfere with Tab key invoking dropdown menu
- User workflow: Tab key should always trigger dropdown completions immediately
- LLM inline suggestions remain active (different use case - multi-line code suggestions)
Tab Completion Without Kernel:
The LSPTabCompletionProvider implements isApplicable() to enable Tab completions even when no kernel is connected:
// In lspTabProvider.ts
export class LSPTabCompletionProvider {
readonly name = "LSP (Python & Markdown)";
readonly rank = 600; // Higher than kernel's 550
async isApplicable(context: any): Promise<boolean> {
// LSP completions work without a kernel, unlike KernelCompleterProvider
const language = this.detectCellLanguage(context);
return language === "python" || language === "markdown";
}
}This overrides the default JupyterLab behavior where Tab completions only work with kernel connections. Now Python and Markdown cells get LSP-powered Tab completions regardless of kernel state.
Tab Key Cancellation Behavior:
The Tab key handler in NotebookBase.tsx ensures Tab always invokes dropdown completions immediately:
// In NotebookBase.tsx handleKeyDown
if (event.key === "Tab") {
if (inlineCompleter?.current) {
// Visible inline suggestion β accept it
inlineCompleter.accept();
} else if (inlineCompleter?.model) {
// No visible suggestion but pending request β cancel it
inlineCompleter.model.reset();
// Let Tab propagate to invoke dropdown completer
}
}This prevents pending inline completion requests from delaying dropdown invocation. If user types and immediately presses Tab, any in-flight inline requests are cancelled and dropdown appears instantly.
Integration Points:
- jupyter-ui Modified: Added
providersprop toNotebookBaseandNotebookcomponents to accept custom Tab completion providers - ProviderReconciliator: JupyterLab's reconciliator now accepts both:
inlineProviders: Array of inline completion providers (ghost text)providers: Array of Tab completion providers (dropdown)
- NotebookEditor.tsx: Instantiates and passes both provider types to Notebook component
- Tab Key Handler: Modified in
NotebookBase.tsxto cancel pending inline requests
Cell Content Synchronization:
// In NotebookEditor.tsx
useEffect(() => {
notebook.model?.cells.forEach((cell) => {
const language = detectCellLanguage(cell); // python | markdown | unknown
if (language === "python" || language === "markdown") {
vscode.postMessage({
type: "lsp-document-open",
cellId: cell.id,
notebookId: notebookId,
content: cell.sharedModel.getSource(),
language: language,
});
}
});
// Listen for content changes
notebook.model?.contentChanged.connect(() => {
vscode.postMessage({
type: "lsp-document-sync",
cellId: cell.id,
content: cell.sharedModel.getSource(),
});
});
}, [notebook]);Files Modified:
src/services/lsp/lspDocumentManager.ts- Temp file managementsrc/services/lsp/lspCompletionService.ts- LSP request handlingsrc/services/lsp/types.ts- Type definitionssrc/services/bridges/lspBridge.ts- Message routingwebview/services/completion/lspProvider.ts- Inline completions providerwebview/services/completion/lspTabProvider.ts- Tab completions provider (NEW)webview/notebook/NotebookEditor.tsx- Provider instantiationjupyter-ui/packages/react/src/components/notebook/NotebookBase.tsx- Addedproviderspropjupyter-ui/packages/react/src/components/notebook/Notebook.tsx- Addedprovidersprop
Testing:
- Open notebook, create Python cell:
import sys - New cell: Type
sys.β Should see inline ghost text and dropdown menu with completions - Markdown cell: Type
[link](./β Should see file/folder suggestions - Verify temp files created in
.vscode/datalayer-cells/ - Check completions show suffix only (not full word) for inline completions
Known Limitations:
- Pylance needs workspace context (files outside workspace aren't analyzed)
- Initial request may timeout if Pylance hasn't finished indexing
- Temp files persist in
.vscode/datalayer-cells/until extension deactivates
The extension also provides LSP integration for Lexical editors (.dlex files), enabling the same IntelliSense features for Python and Markdown code blocks within Lexical documents.
Architecture Differences from Notebooks:
- Code Block Structure: Lexical uses
JupyterInputNode(DecoratorNode) for code blocks instead of notebook cells - No Inline Completions: Lexical editors use LLM inline completions exclusively (handled by
LexicalInlineCompletionPlugin) - Tab Dropdown Only: LSP provides Tab key dropdown completions only, not inline ghost text
- Priority System: Tab key priority ensures proper coexistence:
COMMAND_PRIORITY_CRITICAL (4): LSP Tab completionsCOMMAND_PRIORITY_HIGH (3): AutoIndent, LLM inline acceptance- LSP Tab command checks for active inline completions before triggering
Lexical-Specific Components (jupyter-ui/packages/lexical/src/plugins/):
-
lspTypes.ts: Type definitions for Lexical LSP integration
CellLanguage,LSPCompletionItem,ILSPCompletionProviderinterfaces- Message types:
LSPCompletionRequestMessage,LSPMessage
-
LSPTabCompletionProvider.ts: Provider for fetching completions from extension host
- Implements
ILSPCompletionProviderinterface - Uses postMessage to request completions (15 second timeout)
- Handles
lsp-completion-responseandlsp-errormessages - Supports Python and Markdown code blocks
- Implements
-
LSPTabCompletionPlugin.tsx: Lexical plugin for Tab dropdown completions
- Registers Tab key handler at
COMMAND_PRIORITY_CRITICAL(highest priority) - Uses
LexicalTypeaheadMenuPluginfor dropdown UI - Checks for active inline completions:
hasActiveInlineCompletion()returns false to allow LSP Tab - Extracts code content and cursor position from
JupyterInputNode - Displays completions in dropdown menu
- Registers Tab key handler at
-
LSPDocumentSyncPlugin.tsx: Syncs code block content with extension host
- Sends
lsp-document-open,lsp-document-sync,lsp-document-closemessages - Tracks Python and Markdown
JupyterInputNodeinstances only - Cleans up on unmount to prevent memory leaks
- Sends
Integration in LexicalEditor.tsx:
import {
LSPTabCompletionPlugin,
LexicalLSPCompletionProvider,
LSPDocumentSyncPlugin,
} from "@datalayer/jupyter-lexical";
const lexicalLSPProvider = React.useMemo(() => {
return new LexicalLSPCompletionProvider(
lexicalId || documentUri || "",
vscode,
);
}, [lexicalId, documentUri, vscode]);
// In plugin tree:
<LSPTabCompletionPlugin providers={[lexicalLSPProvider]} disabled={!editable} />
<LSPDocumentSyncPlugin lexicalId={lexicalId} vscodeAPI={vscode} disabled={!editable} />Message Flow for Lexical:
User presses Tab in code block
β
LSPTabCompletionPlugin checks hasActiveInlineCompletion()
β
postMessage('lsp-completion-request', {
cellId: nodeUuid,
language,
position,
source: 'lexical',
lexicalId
})
β
LSPBridge routes to LSPCompletionService (same as notebooks)
β
LSPCompletionService creates temp file and requests completions
β
postMessage('lsp-completion-response', { completions })
β
LSPTabCompletionPlugin displays dropdown menu via LexicalTypeaheadMenuPlugin
Coexistence with LLM Inline Completions:
-
Inline LLM Ghost Text: Handled by
LexicalInlineCompletionPlugin(priority: HIGH)- Multi-line code suggestions from LLM
- Accepts on Tab key when visible
- Uses
hasActiveInlineCompletion()to signal active state
-
LSP Tab Dropdown: Handled by
LSPTabCompletionPlugin(priority: CRITICAL)- Traditional dropdown completions from Pylance/Markdown LSP
- Only triggers when
hasActiveInlineCompletion()returns false - Prevents conflict by checking inline state first
Tab Key Priority Logic:
// In LSPTabCompletionPlugin.tsx
editor.registerCommand(
KEY_TAB_COMMAND,
(event) => {
// Check if inline completion is active
if (hasActiveInlineCompletion(jupyterInputNode)) {
return false; // Let inline completion handle Tab
}
if (isMenuOpen) {
return false; // Menu already open
}
event?.preventDefault();
fetchCompletions(jupyterInputNode, offset);
return true; // LSP handles Tab
},
COMMAND_PRIORITY_CRITICAL,
);Reused Infrastructure:
LSPDocumentManager: Same temp file management (.vscode/datalayer-cells/)LSPCompletionService: Same LSP request routing (Pylance/Markdown)LSPBridge: Already had message routing for Lexical (lines 517-537 inlexicalProvider.ts)- No changes needed to extension host services
Files Modified/Created:
jupyter-ui/packages/lexical/src/plugins/lspTypes.ts(NEW)jupyter-ui/packages/lexical/src/plugins/LSPTabCompletionProvider.ts(NEW)jupyter-ui/packages/lexical/src/plugins/LSPTabCompletionPlugin.tsx(NEW)jupyter-ui/packages/lexical/src/plugins/LSPDocumentSyncPlugin.tsx(NEW)jupyter-ui/packages/lexical/src/plugins/index.ts(MODIFIED - exports)vscode-datalayer/webview/lexical/LexicalEditor.tsx(MODIFIED - integration)
Testing Lexical LSP:
- Open
.dlexfile in VS Code - Create Python code block:
import sys - New line: Type
sys.and press Tab β Should see dropdown with completions - Markdown code block: Type
[link](./and press Tab β Should see file suggestions - Verify no conflict with LLM inline completions (ghost text still works)
- Check temp files created in
.vscode/datalayer-cells/
Race Condition in LSPTabCompletionProvider (LSPTabCompletionProvider.ts):
Initial implementation had a race condition where the completion response from the extension host would arrive before the resolver was registered in the pending requests map. This caused "No resolver found for requestId" errors.
// WRONG - Race condition (resolver registered AFTER sending message)
this.vscodeAPI.postMessage(message);
return new Promise((resolve) => {
this.pendingRequests.set(requestId, resolver); // Response may arrive before this!
});
// CORRECT - Resolver registered BEFORE sending message
return new Promise((resolve) => {
this.pendingRequests.set(requestId, resolver); // Register first
this.vscodeAPI.postMessage(message); // Then send
});Memory Leak in Event Listener Cleanup:
The dispose() method attempted to remove the event listener using .bind(this), which creates a new function reference each time and won't match the originally registered handler.
// WRONG - Memory leak (creates new function reference)
constructor() {
window.addEventListener('message', this.handleMessage.bind(this));
}
dispose() {
window.removeEventListener('message', this.handleMessage.bind(this)); // Won't match!
}
// CORRECT - Save bound reference
constructor() {
this.boundHandleMessage = this.handleMessage.bind(this);
window.addEventListener('message', this.boundHandleMessage);
}
dispose() {
window.removeEventListener('message', this.boundHandleMessage); // Matches!
}Menu Rendering Issue in LSPTabCompletionPlugin (LSPTabCompletionPlugin.tsx):
LexicalTypeaheadMenuPlugin only evaluates triggerFn during editor state updates. Setting isMenuOpen=true asynchronously after completions arrive doesn't trigger re-evaluation, so the menu never renders. The second Tab press was ignored because isMenuOpen was already true, causing default Tab behavior (inserting a tab character).
// WRONG - No menu rendering
if (allCompletions.length > 0) {
setIsMenuOpen(true); // Plugin never re-evaluates triggerFn
}
// CORRECT - Force editor update to trigger re-evaluation
if (allCompletions.length > 0) {
setIsMenuOpen(true);
// Force editor update to trigger typeahead plugin re-evaluation
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Trigger a no-op selection change to force typeahead re-evaluation
selection.dirty = true;
}
});
}Provider Instance Cleanup Issue (LexicalEditor.tsx):
The LSP completion provider was created with useMemo but never disposed when dependencies changed (lexicalId, documentUri, vscode). This caused multiple provider instances to accumulate, each with its own message event listener, resulting in the same completion response being received by 3 different instances.
// WRONG - No cleanup, instances accumulate
const lexicalLSPProvider = React.useMemo(() => {
return new LexicalLSPCompletionProvider(
lexicalId || documentUri || "",
vscode,
);
}, [lexicalId, documentUri, vscode]);
// CORRECT - Dispose on cleanup
const lexicalLSPProvider = React.useMemo(() => {
return new LexicalLSPCompletionProvider(
lexicalId || documentUri || "",
vscode,
);
}, [lexicalId, documentUri, vscode]);
React.useEffect(() => {
return () => {
lexicalLSPProvider.dispose(); // Clean up old instances
};
}, [lexicalLSPProvider]);Symptoms Before Fixes:
- First Tab press: Completions fetched (98 items) but no dropdown appeared
- Second Tab press: Ignored ("menu already open"), default Tab inserted tab character
- Console: "No resolver found for requestId" appeared twice before resolving (3 provider instances)
- Memory leak from uncleaned event listeners accumulating on provider re-creation
src/
βββ extension.ts # Main entry point
βββ commands/ # Command handlers
β βββ auth.ts # Authentication commands
β βββ documents.ts # Document operations
β βββ runtimes.ts # Runtime management
β βββ index.ts # Command registration
βββ providers/ # VS Code API implementations
β βββ documentsFileSystemProvider.ts # Virtual FS for Datalayer docs
β βββ jupyterNotebookProvider.ts # Notebook editor provider
β βββ lexicalDocumentProvider.ts # Lexical editor provider
β βββ smartDynamicControllerManager.ts # Runtime controller manager
β βββ spacesTreeProvider.ts # Tree view provider
βββ services/ # Business logic
β βββ core/ # Core services
β β βββ authProvider.ts # Authentication state management
β β βββ authManager.ts # Auth operations
β β βββ sdkAdapter.ts # SDK communication layer
β β βββ serviceContainer.ts # Dependency injection
β β βββ baseService.ts # Service base class
β β βββ errorHandler.ts # Error handling
β βββ notebook/ # Notebook services
β β βββ documentBridge.ts # Document sync with platform (singleton)
β β βββ kernelBridge.ts # Kernel connection routing
β β βββ notebookRuntime.ts # Runtime lifecycle (singleton)
β β βββ notebookNetwork.ts # Network communication
β β βββ lexicalCollaboration.ts # Real-time collaboration (singleton)
β βββ logging/ # Logging infrastructure
β β βββ loggerManager.ts # Logger factory (singleton)
β β βββ loggers.ts # Static logger access
β β βββ performanceLogger.ts # Performance tracking
β β βββ datalayerClientLogger.ts # SDK logging adapter
β βββ cache/ # Caching layer
β β βββ environmentCache.ts # Environment list cache (singleton)
β βββ ui/ # UI management
β β βββ statusBar.ts # Status bar updates (singleton)
β β βββ uiSetup.ts # UI initialization
β βββ interfaces/ # Service contracts
β βββ IAuthProvider.ts
β βββ IDocumentBridge.ts
β βββ IKernelBridge.ts
β βββ ILogger.ts
β βββ ILoggerManager.ts
β βββ IErrorHandler.ts
βββ models/ # Data models
β βββ notebookDocument.ts # Notebook document model
β βββ lexicalDocument.ts # Lexical document model
β βββ spaceItem.ts # Tree item model
βββ ui/ # UI components
β βββ dialogs/ # Dialog implementations
β β βββ authDialog.ts # Authentication dialog
β β βββ kernelSelector.ts # Kernel selection UI
β β βββ runtimeSelector.ts # Runtime selection UI
β β βββ confirmationDialog.ts # Confirmation prompts
β βββ templates/ # HTML templates
β βββ notebookTemplate.ts # Notebook webview HTML
βββ kernel/ # Kernel communication
β βββ clients/
β βββ websocketKernelClient.ts # WebSocket kernel protocol
βββ utils/ # Utility functions
β βββ dispose.ts # Disposable pattern utilities
β βββ webviewSecurity.ts # CSP nonce generation
β βββ webviewCollection.ts # Webview management
β βββ documentUtils.ts # Document manipulation
βββ types/ # Type definitions
β βββ errors.ts # Custom error types
β βββ vscode/
β βββ messages.ts # Message types for webviews
βββ test/ # Test suites
βββ extension.test.ts # Extension tests (1 test)
βββ services/ # Service tests (21 tests)
βββ utils-tests/ # Utility tests (19 tests)
βββ utils/ # Test utilities
βββ mockFactory.ts # Mock creators & types
βββ testHelpers.ts # Test helpers
webview/
βββ notebook/ # Jupyter notebook webview
β βββ main.ts # Entry point
β βββ NotebookEditor.tsx # Main component
β βββ NotebookToolbar.tsx # Toolbar
βββ lexical/ # Lexical editor webview
β βββ lexicalWebview.tsx # Entry point
β βββ LexicalEditor.tsx # Editor component
β βββ LexicalToolbar.tsx # Toolbar
βββ theme/ # VS Code theme integration
β βββ codemirror/ # CodeMirror themes
β βββ components/ # Themed components
β βββ mapping/ # Color mappers
β βββ providers/ # Theme providers
βββ services/ # Webview services
βββ base/ # Base manager classes (Template Method pattern)
β βββ baseKernelManager.ts # Base kernel lifecycle manager
β βββ baseSessionManager.ts # Base session lifecycle manager
β βββ index.ts # Clean exports
βββ messageHandler.ts # Extension communication
βββ mockServiceManager.ts # Mock service manager (read-only mode)
βββ localKernelServiceManager.ts # Local kernel manager (VS Code Python)
βββ serviceManager.ts # Remote service manager (Jupyter servers)
βββ mutableServiceManager.ts # Dynamic runtime switching
βββ serviceManagerFactory.ts # Type-safe factory pattern-
Separation of Concerns:
- Providers implement VS Code APIs (TreeDataProvider, CustomTextEditorProvider, etc.)
- Services handle business logic and external API communication
- Commands are thin handlers that delegate to services
- Utils are pure utility functions with no side effects
-
Dependency Injection: Services are managed through
ServiceContainerwith lazy initialization. Some services (LoggerManager, EnvironmentCache, DocumentBridge, NotebookRuntimeService, LexicalCollaborationService, DatalayerStatusBar) use singleton pattern for global state management -
Message Passing: Extension and webview communicate via structured messages with JWT tokens
-
Virtual File System: Datalayer documents are mapped to virtual URIs for seamless VS Code integration
MutableServiceManager + useKernelId Pattern: Enables seamless runtime switching without notebook re-renders.
Key Components:
- MutableServiceManager - Stable proxy that forwards to current service manager
- useRuntimeManager - React hook managing runtime selection and service manager lifecycle
- useKernelId - Jupyter UI hook that starts kernels when dependencies change
- kernelId prop - Runtime ingress passed to Notebook2 to trigger kernel startup
How It Works:
// NotebookEditor.tsx
const { selectedRuntime, serviceManager } = useRuntimeManager();
<Notebook2
serviceManager={serviceManager} // β Stable proxy (never changes)
kernelId={selectedRuntime?.ingress} // β Changes on runtime switch
startDefaultKernel={!!selectedRuntime}
/>When runtime changes:
selectedRuntime?.ingresschanges (e.g., "http://pyodide-local" β "http://local-kernel-...")- Notebook2 re-renders (cheap React VDOM diff)
useKernelIddetectskernelIdprop change β callskernels.startNew()- NotebookPanel widget NOT recreated (expensive operation avoided)
- Cells NOT re-rendered, scroll position maintained
Critical Fixes:
- Proxy method binding (
mutableServiceManager.ts:283-285) - Methods must be bound to maintainthiscontext - CORS avoidance (
useRuntimeManager.ts:85-89, 184-201) - Don't callstartNew()for remote runtimes (already running) - Force useKernelId re-run (
NotebookEditor.tsx:908) - Pass ingress as kernelId to trigger kernel startup
Result: All kernel switching scenarios work perfectly (Pyodide β Local β Remote).
The extension uses a Template Method pattern for kernel management, eliminating ~174 lines of duplicate code across different kernel types.
BaseKernelManager (webview/services/base/baseKernelManager.ts):
- Abstract base class implementing common
Kernel.IManagermethods - Template Method pattern: subclasses only implement
startNew() - Provides:
shutdown(),dispose(),running(),requestRunning(), signal management - Type discriminator:
KernelManagerType = "mock" | "pyodide" | "local" | "remote"
BaseSessionManager (webview/services/base/baseSessionManager.ts):
- Abstract base class implementing common
Session.IManagermethods - Template Method pattern: subclasses only implement
startNew() - Provides: session lifecycle, disposal, signals,
requestRunning() - Type discriminator:
SessionManagerType = "mock" | "pyodide" | "local" | "remote"
MockServiceManager (webview/services/mockServiceManager.ts):
- Extends base classes for read-only notebook viewing
- Throws helpful errors when execution is attempted
- Used when no kernel is selected
LocalKernelServiceManager (webview/services/localKernelServiceManager.ts):
- Extends base classes for direct ZMQ communication with VS Code Python environments
- Creates
LocalKernelConnectionbypassing HTTP/WebSocket flow - Detects local kernel URLs:
http://local-kernel-<kernelId>.localhost
Remote ServiceManager (webview/services/serviceManager.ts):
- Standard JupyterLab
ServiceManagerfor remote Jupyter servers - Unchanged from JupyterLab implementation
ServiceManagerFactory (webview/services/serviceManagerFactory.ts):
- Type-safe factory with discriminated unions
- Methods:
create(options),isMock(manager),getType(manager) - Includes 'pyodide' type that throws "not yet implemented" for future PR
MutableServiceManager (webview/services/mutableServiceManager.ts):
- Enables hot-swapping between kernel types without re-rendering
Notebook2 - Uses Proxy pattern to forward calls to current underlying manager
- Methods:
updateConnection(),updateServiceManager(),resetToMock() - Prevents cell flickering and scroll position loss during runtime switches
- Code Reuse: ~174 lines eliminated through base classes
- Type Safety: Discriminated unions ensure correct options per manager type
- Extensibility: Adding new kernel types only requires implementing
startNew() - Debugging: Runtime type identification via
managerTypeproperty - UX: Stable references prevent unnecessary React re-renders
The extension provides a complete datasource management system in the Settings tree view, allowing users to create and configure connections to external data sources (Athena, BigQuery, MS Sentinel, Splunk).
Components:
- Settings Tree Provider (
src/providers/settingsTreeProvider.ts) - Manages datasources and secrets sections - Datasource Tree Item (
src/models/datasourceTreeItem.ts) - Tree item with click handler and context menu - Create Dialog (
webview/datasource/DatasourceDialog.tsx) - React form for creating datasources - Edit Dialog (
webview/datasource/DatasourceEditDialog.tsx) - Separate React form for editing
Commands:
datalayer.createDatasource- Opens create dialogdatalayer.editDatasource- Opens edit dialog (triggered by click or right-click)datalayer.deleteDatasource- Deletes datasource with confirmationdatalayer.refreshDatasources- Refreshes tree view
CRITICAL: Create and edit dialogs are separate components, NOT conditional logic in one component.
// β
CORRECT - Separate components
export function DatasourceDialog({ colorMode }: Props) { ... }
export function DatasourceEditDialog({ colorMode }: Props) { ... }
// β WRONG - Conditional logic in one component
export function DatasourceDialog({ mode, colorMode }: Props) {
if (mode === 'edit') { ... } else { ... }
}Always wait for webview ready before sending data:
// Wait for webview ready
const readyPromise = new Promise<void>((resolve) => {
const disposable = panel.webview.onDidReceiveMessage((message) => {
if (message.type === "ready") {
disposable.dispose();
resolve();
}
});
});
await readyPromise;
// Now safe to send data
panel.webview.postMessage({ type: "init-edit", body: {...} });Tree items can have click commands for direct interaction:
this.command = {
command: "datalayer.editDatasource",
title: "Edit Datasource",
arguments: [this],
};WebView Panel Icons:
- WebView panels require file URIs, not
ThemeIcon - Use
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "images", "icon.png") - Cannot use
fill="currentColor"- icons need explicit colors or theme-aware variants
Tree View Icons:
- Tree items can use
ThemeIcon:this.iconPath = new vscode.ThemeIcon("database") ThemeIconautomatically adapts to VS Code theme
Extension β Webview:
panel.webview.postMessage({
type: "init-edit",
body: { datasource: {...} }
});Webview β Extension:
// Webview sends
vscode.postMessage({ type: "update-datasource", body: {...} });
// Extension receives
panel.webview.onDidReceiveMessage(async (message) => {
if (message.type === "update-datasource") { ... }
});src/commands/createDatasourceDialog.ts- Dialog command handlers and webview setupsrc/commands/datasources.ts- CRUD command registrationsrc/providers/settingsTreeProvider.ts- Tree data provider with datasources sectionsrc/models/datasourceTreeItem.ts- Tree item with click handlerwebview/datasource/DatasourceDialog.tsx- Create form componentwebview/datasource/DatasourceEditDialog.tsx- Edit form component (separate!)webview/datasource/main.tsx- Create dialog entry pointwebview/datasource/editMain.tsx- Edit dialog entry pointsrc/ui/templates/datasourceTemplate.ts- Create dialog HTML templatesrc/ui/templates/datasourceEditTemplate.ts- Edit dialog HTML templatewebpack.config.js- Separate webpack entries for create and edit dialogs
The codebase uses TypeDoc for comprehensive API documentation:
# Generate HTML documentation
npm run doc
# Generate markdown documentation
npm run doc:markdown
# Watch mode for development (rebuilds on file changes)
npm run doc:watch
# Check documentation coverage
npm run doc:coveragedocs/- HTML documentation (TypeDoc default theme)docs-markdown/- Markdown documentation for integration with other systems
Live Documentation: https://vscode-datalayer.netlify.app
The documentation is automatically built and deployed on every push to the main branch using Netlify. It includes:
- API Reference: Complete TypeScript API documentation
- Module Documentation: Detailed module and namespace documentation
- Interface Documentation: All TypeScript interfaces and types
- Code Examples: Usage examples and code snippets
- Coverage Reports: Documentation coverage metrics
The extension uses GitHub Copilot's Language Model Tools API to provide programmatic access to notebooks and lexical documents. Tool definitions are written in TypeScript and automatically synced to package.json.
Tool Definitions (src/tools/definitions/tools/*.ts):
- TypeScript interfaces with full type safety
- JSDoc descriptions for AI model understanding
- Parameter schemas with validation
Schema Generator (scripts/generate-tool-schemas.js):
- Parses TypeScript object literals without eval()
- Extracts name, description, and parameters
- Syncs to
package.jsoncontributes.languageModelTools - Handles nested objects, arrays, and complex schemas
# Generate schemas from TypeScript definitions
node scripts/generate-tool-schemas.js
# Output: β
12/12 tools parsed successfully
# Validate schemas are in sync
node scripts/validate-tool-schemas.js
# Output: β
All expected tools present- Create Tool Definition (
src/tools/definitions/tools/myTool.ts):
import type { ToolDefinition } from "../schema";
export const myTool: ToolDefinition = {
name: "datalayer_myTool",
displayName: "My Tool",
description: "Description for the AI model",
parameters: {
properties: {
myParam: {
type: "string",
description: "Parameter description",
},
},
required: ["myParam"],
},
} as const;- Export Tool (
src/tools/definitions/tools/index.ts):
export * from "./myTool";
import { myTool } from "./myTool";
export const allToolDefinitions = [, /* existing */ myTool];- Implement Operation (
src/tools/core/myOperation.ts):
export const myOperation: ToolOperation<MyParams, MyResult> = {
execute: async (params, context) => {
// Implementation
},
};- Register Operation (
src/tools/core/index.ts):
import { myOperation } from './myOperation';
export const allOperations = { /* existing */, myTool: myOperation };- Generate Schema:
node scripts/generate-tool-schemas.jsThe schema is automatically added to package.json with correct structure.
Parser Features:
- Handles TypeScript syntax (as const, single quotes, trailing commas)
- Parses nested objects and arrays
- Extracts string literals with proper escaping
- Type-safe without using eval()
- Preserves complex schemas (array items with properties)
Example Parsing:
// TypeScript tool definition
export const insertBlocksTool: ToolDefinition = {
name: 'datalayer_insertBlocks',
parameters: {
properties: {
blocks: {
type: 'array',
items: {
type: 'object',
properties: {
blockType: { type: 'string' },
source: { type: 'string' },
},
required: ['blockType', 'source'],
},
},
},
required: ['blocks'],
},
} as const;
// Parses to package.json schema (preserving nested structure)
{
"name": "datalayer_insertBlocks",
"inputSchema": {
"type": "object",
"properties": {
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"blockType": { "type": "string" },
"source": { "type": "string" }
},
"required": ["blockType", "source"]
}
}
},
"required": ["blocks"]
}
}Notebook Tools (8):
datalayer_createNotebook- Create new notebooksdatalayer_insertCell- Insert cells at specific positionsdatalayer_executeCell- Execute cells and get outputsdatalayer_readCell- Read cell contentdatalayer_updateCell- Update cell sourcedatalayer_deleteCell- Delete cellsdatalayer_getActiveDocument- Get active document info
Lexical Tools (5):
datalayer_insertBlock- Insert single blockdatalayer_insertBlocks- Batch insert multiple blocks (NEW)datalayer_readBlocks- Read blocks with formattingdatalayer_deleteBlock- Delete blocksdatalayer_listAvailableBlocks- List supported block types
Purpose: Allows Copilot to create complex documents with a single API call instead of many sequential insertBlock calls.
Benefits:
- Performance: One message instead of N messages for N blocks
- Atomicity: All blocks inserted or none (stops on first error)
- Simplicity: Cleaner code for AI model
Implementation Layers (8 layers):
- Tool Definition -
src/tools/definitions/tools/insertBlocks.ts - Operation -
src/tools/core/lexical/insertBlocks.ts - Adapter -
src/tools/adapters/vscode/VSCodeLexicalHandle.ts - Internal Command -
src/commands/internal.ts(datalayer.internal.lexical.insertBlocks) - Message Handler -
webview/lexical/plugins/InternalCommandsPlugin.tsx - Controller -
webview/utils/LexicalDocumentController.ts(insertBlocks method) - Schema - Auto-generated in
package.json - Registration -
src/tools/definitions/tools/index.ts,src/tools/core/index.ts
Usage Example (AI model prompt):
// Single call to create multi-section document
await insertBlocks({
insert_after_block_id: "TOP",
blocks: [
{ blockType: "heading", source: "# Introduction" },
{ blockType: "paragraph", source: "Overview text..." },
{ blockType: "heading", source: "## Section 1" },
{ blockType: "code", source: 'console.log("example");' },
],
});Error Handling:
- Validates each block has required fields (blockType, source)
- Stops on first failure with descriptive error
- Returns success with last inserted block ID for chaining
Pyodide packages were being re-downloaded on every kernel startup, causing:
- ~50+ MB downloads per session
- 30+ seconds startup delay
- Wasted bandwidth and poor user experience
- Despite 256 lines of cache setup code
Evidence from logs:
[PyodideKernelClient] Mounting package cache at: ~/.cache/datalayer-pyodide/0.27.3/packages
[PyodideKernelClient] Configured micropip to use persistent cache at /cache
[Pyodide/stdout] Loading micropip <-- DOWNLOADING (should be cached!)
[Pyodide/stdout] Loading Pillow, Pygments, asttokens, ... (24 packages) <-- ALL DOWNLOADING
[PyodideKernelClient] Pre-downloading 25 packages: altair, bokeh, ...
[Pyodide/stdout] Loading Jinja2, MarkupSafe, altair, ... <-- ALL DOWNLOADING AGAIN
The packageCacheDir option was missing from all loadPyodide() calls. This is the only way to enable persistent package caching in Node.js Pyodide (confirmed from Pyodide type definitions in node_modules/pyodide/pyodide.d.ts).
From Pyodide types:
/**
* The file path where packages will be cached in node. If a package
* exists in packageCacheDir it is loaded from there, otherwise it is
* downloaded from JsDelivr CDN and then cached into packageCacheDir.
* Only applies when running in node; ignored in browsers.
*/
packageCacheDir?: string;Why previous code didn't work:
- NODEFS mounting - Only affects Python file I/O, not package caching
- Environment variables - Pyodide ignores
MICROPIP_CACHE_DIRin Node.js context - micropip configuration - Only works for PyPI packages, not built-in Pyodide packages
Added packageCacheDir option to 4 files with hardcoded version "0.29.0" (npm package version):
Purpose: Main Pyodide kernel runtime for native notebooks
Before:
this._pyodide = await loadPyodide({
stdout: (text: string) => this._handleStdout(text),
stderr: (text: string) => this._handleStderr(text),
});After:
// Import Node.js modules for cache directory
const os = await import("os");
const path = await import("path");
const fs = await import("fs/promises");
// IMPORTANT: Native notebooks use npm package version (0.29.0)
// The datalayer.pyodide.version config is ONLY for webview notebooks (CDN)
const pyodideVersion = "0.29.0";
// Create cache directory path
const cacheDir = path.join(
os.homedir(),
".cache",
"datalayer-pyodide",
pyodideVersion,
"packages",
);
// Ensure cache directory exists
await fs.mkdir(cacheDir, { recursive: true });
console.log(`[PyodideKernelClient] Using package cache: ${cacheDir}`);
const { loadPyodide } = await import("pyodide");
this._pyodide = await loadPyodide({
packageCacheDir: cacheDir, // CRITICAL: Enables persistent caching
stdout: (text: string) => this._handleStdout(text),
stderr: (text: string) => this._handleStderr(text),
} as Parameters<typeof loadPyodide>[0]);Additional change: Removed _setupPersistentCache() method (90 lines of ineffective NODEFS mounting code)
Purpose: Preloads packages on extension activation
Before:
const { loadPyodide } = await import("pyodide");
pyodide = await loadPyodide({
stdout: () => {},
stderr: () => {},
});After:
// Import Node.js modules for cache directory
const os = await import("os");
const path = await import("path");
const fs = await import("fs/promises");
// IMPORTANT: Native notebooks use npm package version (0.29.0)
// The datalayer.pyodide.version config is ONLY for webview notebooks (CDN)
const pyodideVersion = "0.29.0";
// Create cache directory path (same location as runtime!)
const cacheDir = path.join(
os.homedir(),
".cache",
"datalayer-pyodide",
pyodideVersion,
"packages",
);
// Ensure cache directory exists
await fs.mkdir(cacheDir, { recursive: true });
const { loadPyodide } = await import("pyodide");
// CRITICAL FIX: Add packageCacheDir for persistent caching
// Type assertion needed - packageCacheDir exists in runtime but TypeScript may cache old types
pyodide = await loadPyodide({
packageCacheDir: cacheDir,
stdout: () => {},
stderr: () => {},
} as Parameters<typeof loadPyodide>[0]);Purpose: Background service for package preloading
Before:
const { loadPyodide } = await import("pyodide");
const pyodide = await loadPyodide({
stdout: () => {},
stderr: () => {},
} as Parameters<typeof loadPyodide>[0]);After:
// Import Node.js modules for cache directory
const os = await import("os");
const path = await import("path");
const fs = await import("fs/promises");
// IMPORTANT: Native notebooks use npm package version (0.29.0), NOT config version!
const npmPyodideVersion = "0.29.0";
// Create cache directory path (same location as runtime!)
const cacheDir = path.join(
os.homedir(),
".cache",
"datalayer-pyodide",
npmPyodideVersion,
"packages",
);
// Ensure cache directory exists
await fs.mkdir(cacheDir, { recursive: true });
// Load Pyodide using npm package (Node.js compatible)
const { loadPyodide } = await import("pyodide");
// CRITICAL: Add packageCacheDir for persistent caching
const pyodide = await loadPyodide({
packageCacheDir: cacheDir,
stdout: () => {},
stderr: () => {},
} as Parameters<typeof loadPyodide>[0]);Purpose: Cache management for webview notebooks
Before:
const { loadPyodide } = await import("pyodide");
const pyodide: PyodideInterface = await loadPyodide({
indexURL: pyodidePath,
stdout: () => {},
stderr: () => {},
});After:
const { loadPyodide } = await import("pyodide");
// Create package cache directory
const packageCache = path.join(pyodidePath, "packages");
await fs.mkdir(packageCache, { recursive: true });
// CRITICAL FIX: Add packageCacheDir for persistent caching
// Type assertion needed - packageCacheDir exists in runtime but TypeScript may cache old types
const pyodide: PyodideInterface = await loadPyodide({
indexURL: pyodidePath,
packageCacheDir: packageCache,
stdout: () => {},
stderr: () => {},
} as Parameters<typeof loadPyodide>[0]);Created scripts/validate-pyodide-version.js to auto-sync hardcoded version strings with npm package version. This ensures version consistency when upgrading Pyodide.
Purpose: Prevent version mismatches between npm package and hardcoded strings
Files Checked:
src/kernel/clients/pyodideKernelClient.ts- Main kernelsrc/services/pyodide/nativeNotebookPreloader.ts- Preloaderpackage.json- Config documentation
Script Logic:
// Read installed Pyodide version from node_modules
const pyodidePackageJson = require("../node_modules/pyodide/package.json");
const installedVersion = pyodidePackageJson.version;
// Check each file for hardcoded version strings
for (const file of filesToSync) {
const content = fs.readFileSync(filePath, "utf8");
const match = content.match(file.pattern);
if (foundVersion !== installedVersion) {
// Auto-fix the mismatch
const updatedContent = file.replace(content, installedVersion);
fs.writeFileSync(filePath, updatedContent, "utf8");
console.log(
` β
${file.description}: ${foundVersion} β ${installedVersion}`,
);
}
}Integration: Added to npm scripts as pre-build hook
{
"scripts": {
"sync:pyodide-version": "node scripts/validate-pyodide-version.js",
"precompile": "npm run sync:pyodide-version",
"pretest": "npm run sync:pyodide-version"
}
}Upgrading Pyodide:
# Update package.json
npm install pyodide@0.30.0
# Build extension (auto-syncs version)
npm run compileThe script automatically detects the new version and updates all hardcoded strings during the precompile hook.
~/.cache/datalayer-pyodide/
βββ 0.29.0/ # Version-specific cache
βββ packages/ # Persistent package storage
βββ micropip.js # Built-in packages
βββ numpy.tar
βββ pandas.tar
βββ matplotlib.tar
βββ ...
Why this location:
- Standard XDG cache directory on macOS/Linux
- Version-specific to avoid conflicts
- Shared across all extension instances
- Persists between VS Code sessions
# 1. Clear cache to force fresh download
rm -rf ~/.cache/datalayer-pyodide/
# 2. Open notebook with Pyodide kernel
# Watch console logs for "Using package cache: ..."
# 3. Close and reopen notebook
# Should see instant startup (no "Loading..." messages)
# 4. Verify cache directory
ls -lh ~/.cache/datalayer-pyodide/0.29.0/packages/Expected behavior:
- First startup: Downloads packages, saves to cache (~30 seconds)
- Second startup: Loads from cache, instant (< 5 seconds)
- Console logs: "Using package cache: ~/.cache/datalayer-pyodide/0.29.0/packages"
Use the VS Code command: "Datalayer: Clear Pyodide Cache"
This clears:
- Native notebook cache (
~/.cache/datalayer-pyodide/) - Old globalStorage files (from previous caching attempts)
- Webview notebook cache (IndexedDB)
Check 1: Verify cache directory exists and has files
ls -lh ~/.cache/datalayer-pyodide/0.29.0/packages/Should show .tar and .js files for each package.
Check 2: Check console logs for cache path
Look for: [PyodideKernelClient] Using package cache: ...
If missing, packageCacheDir option wasn't set correctly.
Check 3: Verify Pyodide version matches
# Check npm package version
npm list pyodide
# Should match hardcoded version in code files
grep 'const pyodideVersion = ' src/kernel/clients/pyodideKernelClient.tsSymptom: "Pyodide version mismatch" or incompatible package errors
Solution: Run version sync script manually
node scripts/validate-pyodide-version.jsThis auto-fixes mismatches between npm package and hardcoded strings.
Symptom: Cache directory shows old version (e.g., 0.27.3 instead of 0.29.0)
Solution:
- Clear old cache:
rm -rf ~/.cache/datalayer-pyodide/0.27.3/ - Verify version sync:
node scripts/validate-pyodide-version.js - Restart VS Code
- Reopen notebook
Before fix:
- First startup: ~30 seconds (downloads all packages)
- Second startup: ~30 seconds (downloads all packages again)
- Bandwidth: ~50+ MB per session
After fix:
- First startup: ~30 seconds (downloads and caches)
- Second startup: < 5 seconds (loads from cache)
- Bandwidth: ~50 MB first time, then 0 MB
User experience improvement: 6x faster startup after first run
-
Native vs Webview Notebooks:
- Native notebooks use npm package version (0.29.0 from package.json)
- Webview notebooks use CDN version (configurable via
datalayer.pyodide.version) - Config setting does NOT affect native notebooks
-
loadPackage() vs micropip.install():
- Use
loadPackage()for built-in Pyodide packages - respectspackageCacheDir - Avoid
micropip.install()for caching - ignorespackageCacheDirin Node.js
- Use
-
Type Assertions:
as Parameters<typeof loadPyodide>[0]needed because TypeScript types may be outdatedpackageCacheDirexists in runtime but may not be in cached type definitions
-
Version Upgrades:
- Always run
npm run compileafternpm install pyodide@X.Y.Z - Pre-build hook automatically syncs version strings
- Never manually edit version numbers in code files
- Always run
The extension provides comprehensive configuration in VS Code settings (Cmd+, β "Datalayer"):
datalayer.services.iamUrl- IAM service (default: https://prod1.datalayer.run)datalayer.services.runtimesUrl- Runtimes service (default: https://prod1.datalayer.run)datalayer.services.spacerUrl- Spacer service (default: https://prod1.datalayer.run)datalayer.services.spacerWsUrl- WebSocket URL (default: wss://prod1.datalayer.run)
datalayer.runtime.defaultMinutes- Default duration (default: 10, min: 1, max: 1440)
datalayer.logging.level- Log level (default: info)datalayer.logging.includeTimestamps- Timestamps in logs (default: true)datalayer.logging.includeContext- Context in logs (default: true)datalayer.logging.enableSDKLogging- SDK logging (default: true)datalayer.logging.enablePerformanceMonitoring- Performance tracking (default: false)
Note: Runtime environments are fetched dynamically from API and cached using EnvironmentCache singleton. Credits calculated automatically based on duration and environment burning rate.
- WebSocket Binary Support: Uses older Jupyter protocol due to serialization issues between webview and extension (cannot use v1.kernel.websocket.jupyter.org)
- Smart Controller:
SmartDynamicControllerManageris intentionally disabled (null) inuiSetup.ts:85while native controller integration is improved - Runtime Tree View Refresh: Requires 500ms delay after runtime termination to allow server-side processing before refresh
- Snapshot Creation: UI implemented in Runtimes tree view but backend implementation is pending
- Always check TypeScript compilation with
npx tsc --noEmitbefore committing - Run linting with
npm run lintto ensure code quality - Include comprehensive JSDoc documentation for all exported functions
- Test extension functionality in the Extension Development Host before submitting PRs
- Follow existing code patterns and architectural decisions
- Use
unknowninstead ofanyfor type-safe code - All test mocks must use proper TypeScript interfaces
The project maintains strict code quality through:
- TypeScript: Strong typing and compile-time checks
- ESLint: Code linting and style enforcement
- Prettier: Automated code formatting
- TypeDoc: Documentation generation and coverage
- Type Safety: No
anytypes (useunknownwith proper type guards) - Test Quality: 100% test pass rate with strongly-typed mocks
All code should include proper JSDoc comments for TypeScript interfaces, classes, and exported functions.
- Development Guide: This document
- Testing Guide: TESTING.md
- Contributing: CONTRIBUTING.md
- Release Process: RELEASE.md
- API Documentation: https://vscode-datalayer.netlify.app