From 3213e2d18fcfd4afc76ada2442f1f8116fedf76a Mon Sep 17 00:00:00 2001 From: Mikhail Zhadanov Date: Tue, 24 Mar 2026 15:18:20 +0100 Subject: [PATCH 1/5] Add SQL Notebooks support (.exabook files) Interactive notebooks with SQL code cells and markdown documentation. SQL cells execute against the active Exasol connection with inline HTML result tables showing row counts, timing, and execution order. - NotebookSerializer: JSON-based .exabook format with transient outputs - NotebookController: executes cells via QueryExecutor with cancellation - CodeLens hidden in notebook cells (they have native run buttons) - Demo notebook in examples/ - Bump version to 1.4.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- .vscodeignore | 1 + CHANGELOG.md | 12 +- README.md | 2 + examples/demo.exabook | 32 +++ package-lock.json | 372 +----------------------------- package.json | 14 +- src/extension.ts | 14 +- src/notebooks/controller.ts | 151 ++++++++++++ src/notebooks/serializer.ts | 39 ++++ src/providers/codeLensProvider.ts | 6 + 10 files changed, 272 insertions(+), 371 deletions(-) create mode 100644 examples/demo.exabook create mode 100644 src/notebooks/controller.ts create mode 100644 src/notebooks/serializer.ts diff --git a/.vscodeignore b/.vscodeignore index 1666828..6361b74 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -19,5 +19,6 @@ esbuild.mjs CLAUDE.md .claude/** specs/** +examples/** .serena/** vendor/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9ae9e..9651e1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,19 @@ All notable changes to the "Exasol" extension will be documented in this file. -## [1.3.3] - 2026-03-31 +## [1.4.0] - 2026-03-31 + +### Added +- **SQL Notebooks** — interactive `.exabook` notebooks with SQL code cells, inline HTML result tables, markdown documentation cells, execution order tracking, and cancellation support +- CodeLens "Execute" links are now hidden in notebook cells (notebooks have their own run buttons) ### Changed -- Show individual list of errors instead of `AggregateError` when multiple error occur +- Removed deprecated `vscode-test` dependency (replaced by `@vscode/test-electron`) +## [1.3.3] - 2026-03-31 + +### Changed +- Show individual list of errors instead of `AggregateError` when multiple errors occur ## [1.3.2] - 2026-03-24 diff --git a/README.md b/README.md index 5a7bda6..15ffb38 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A Visual Studio Code extension for working with Exasol databases. Provides datab - **Object actions** — right-click to preview data, show DDL, generate SELECT, describe table - **Results viewer** — sortable, filterable grid with CSV export and cell inspection - **Query history** — automatic tracking with execution time, row counts, and error indicators +- **SQL Notebooks** — interactive `.exabook` notebooks with SQL cells, inline results, and markdown documentation - **SQL formatting** — configurable keyword case, indentation, and statement spacing - **Session management** — active schema tracking, persistent state across restarts @@ -29,6 +30,7 @@ A Visual Studio Code extension for working with Exasol databases. Provides datab |--------|---------------|-----| | Execute query | `Ctrl+Enter` | `Cmd+Enter` | | Execute selection | `Ctrl+Shift+Enter` | `Cmd+Shift+Enter` | +| Execute entire script | `Ctrl+Alt+Enter` | `Cmd+Alt+Enter` | | Find database object | `Ctrl+Shift+F` (objects view) | `Cmd+Shift+F` (objects view) | ## Configuration diff --git a/examples/demo.exabook b/examples/demo.exabook new file mode 100644 index 0000000..533adfa --- /dev/null +++ b/examples/demo.exabook @@ -0,0 +1,32 @@ +[ + { + "kind": 1, + "language": "markdown", + "value": "# Exasol SQL Notebook Demo\n\nThis notebook demonstrates interactive SQL execution against Exasol.\nMake sure you have an active connection before running cells." + }, + { + "kind": 2, + "language": "exasol-sql", + "value": "SELECT CURRENT_TIMESTAMP, CURRENT_USER, CURRENT_SCHEMA" + }, + { + "kind": 1, + "language": "markdown", + "value": "## Browse schemas" + }, + { + "kind": 2, + "language": "exasol-sql", + "value": "SELECT SCHEMA_NAME, SCHEMA_OWNER\nFROM SYS.EXA_SCHEMAS\nORDER BY SCHEMA_NAME" + }, + { + "kind": 1, + "language": "markdown", + "value": "## Check active sessions" + }, + { + "kind": 2, + "language": "exasol-sql", + "value": "SELECT SESSION_ID, USER_NAME, STATUS, CLIENT\nFROM EXA_ALL_SESSIONS\nORDER BY SESSION_ID" + } +] diff --git a/package-lock.json b/package-lock.json index 7b4a0fa..e939259 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "exasol-vscode", - "version": "1.3.1", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "exasol-vscode", - "version": "1.3.1", + "version": "1.4.0", "license": "MIT", "dependencies": { "@exasol/exasol-driver-ts": "^0.3.1", @@ -28,8 +28,7 @@ "mocha": "^11.7.1", "ovsx": "^0.10.9", "ts-node": "^10.9.2", - "typescript": "^5.9.3", - "vscode-test": "^1.6.1" + "typescript": "^5.9.3" }, "engines": { "vscode": "^1.109.0" @@ -1713,16 +1712,6 @@ "@textlint/ast-node-types": "15.5.2" } }, - "node_modules/@tootallnate/once": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.1.tgz", - "integrity": "sha512-VyMVKRrpHTT8PnotUeV8L/mDaMwD5DaAKCFLP73zAqAtvF0FCqky+Ki7BYbFCYQmqFyTe9316Ed5zS70QUR9eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -2441,30 +2430,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2523,13 +2488,6 @@ "node": ">= 6" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "dev": true, - "license": "MIT" - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2617,25 +2575,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2730,19 +2669,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dev": true, - "license": "MIT/X11", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3344,16 +3270,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3764,13 +3680,6 @@ "node": ">=14.14" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3786,83 +3695,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/fstream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fstream/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4314,18 +4146,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4883,13 +4703,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", - "dev": true, - "license": "ISC" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5173,6 +4986,7 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", + "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5187,19 +5001,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5525,6 +5326,7 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", + "optional": true, "dependencies": { "wrappy": "1" } @@ -5861,16 +5663,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6262,69 +6054,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -7273,16 +7002,6 @@ "node": ">=20" } }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "dev": true, - "license": "MIT/X11", - "engines": { - "node": "*" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -7461,25 +7180,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -7561,65 +7261,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - }, - "engines": { - "node": ">=8.9.3" - } - }, - "node_modules/vscode-test/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/vscode-test/node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/vscode-test/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -7793,7 +7434,8 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/ws": { "version": "8.20.0", diff --git a/package.json b/package.json index 29a86cd..011f158 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Exasol", "icon": "resources/exasol-icon.png", "description": "Feature-rich Exasol database extension for Visual Studio Code with IntelliSense, object browser, query execution, and more", - "version": "1.3.3", + "version": "1.4.0", "publisher": "exasol", "license": "MIT", "repository": { @@ -61,6 +61,15 @@ "path": "./syntaxes/exasol-sql.tmLanguage.json" } ], + "notebooks": [ + { + "type": "exasol-sql-notebook", + "displayName": "Exasol SQL Notebook", + "selector": [ + { "filenamePattern": "*.exabook" } + ] + } + ], "commands": [ { "command": "exasol.addConnection", @@ -511,8 +520,7 @@ "mocha": "^11.7.1", "ovsx": "^0.10.9", "ts-node": "^10.9.2", - "typescript": "^5.9.3", - "vscode-test": "^1.6.1" + "typescript": "^5.9.3" }, "dependencies": { "@exasol/exasol-driver-ts": "^0.3.1", diff --git a/src/extension.ts b/src/extension.ts index fc03464..9ee9155 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,8 @@ import { SessionManager } from './sessionManager'; import { ObjectActions } from './objectActions'; import { ObjectSearchProvider } from './providers/objectSearchProvider'; import { findStatementAtCursor, splitIntoStatements } from './utils'; +import { ExasolNotebookSerializer } from './notebooks/serializer'; +import { ExasolNotebookController } from './notebooks/controller'; import { formatError } from './connectionTypes'; // Create output channel for logging @@ -47,6 +49,14 @@ export function activate(context: vscode.ExtensionContext) { ResultsPanel.register(context); QueryStatsPanel.register(context); + // Register notebook support + const notebookSerializer = vscode.workspace.registerNotebookSerializer( + 'exasol-sql-notebook', + new ExasolNotebookSerializer(), + { transientOutputs: true } + ); + const notebookController = new ExasolNotebookController(connectionManager, queryExecutor); + // Register completion provider const completionProvider = new ExasolCompletionProvider(connectionManager); const completionDisposable = vscode.languages.registerCompletionItemProvider( @@ -431,7 +441,9 @@ export function activate(context: vscode.ExtensionContext) { statusBarItem, outputChannel, connectionsChanged, - activeConnectionChanged + activeConnectionChanged, + notebookSerializer, + notebookController ); return { diff --git a/src/notebooks/controller.ts b/src/notebooks/controller.ts new file mode 100644 index 0000000..ba9629d --- /dev/null +++ b/src/notebooks/controller.ts @@ -0,0 +1,151 @@ +import * as vscode from 'vscode'; +import { ConnectionManager } from '../connectionManager'; +import { QueryExecutor, QueryResult } from '../queryExecutor'; +import { getOutputChannel } from '../extension'; + +export class ExasolNotebookController { + private readonly controller: vscode.NotebookController; + private executionOrder = 0; + + constructor( + private connectionManager: ConnectionManager, + private queryExecutor: QueryExecutor + ) { + this.controller = vscode.notebooks.createNotebookController( + 'exasol-sql-controller', + 'exasol-sql-notebook', + 'Exasol SQL' + ); + + this.controller.supportedLanguages = ['exasol-sql', 'sql']; + this.controller.supportsExecutionOrder = true; + this.controller.executeHandler = this.executeCells.bind(this); + this.controller.interruptHandler = this.interrupt.bind(this); + } + + dispose(): void { + this.controller.dispose(); + } + + private async executeCells(cells: vscode.NotebookCell[]): Promise { + for (const cell of cells) { + await this.executeCell(cell); + } + } + + private async executeCell(cell: vscode.NotebookCell): Promise { + const execution = this.controller.createNotebookCellExecution(cell); + execution.executionOrder = ++this.executionOrder; + execution.start(Date.now()); + + const sql = cell.document.getText().trim(); + if (!sql) { + execution.replaceOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text('Empty cell — nothing to execute.', 'text/plain') + ]) + ]); + execution.end(undefined, Date.now()); + return; + } + + const activeConnection = this.connectionManager.getActiveConnection(); + if (!activeConnection) { + execution.replaceOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.stderr('No active Exasol connection. Select a connection first.') + ]) + ]); + execution.end(false, Date.now()); + return; + } + + const output = getOutputChannel(); + const cancellationTokenSource = new vscode.CancellationTokenSource(); + execution.token.onCancellationRequested(() => { + cancellationTokenSource.cancel(); + }); + + try { + const result = await this.queryExecutor.execute(sql, cancellationTokenSource.token); + const html = this.renderResult(result, sql); + + execution.replaceOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(html, 'text/html'), + ]) + ]); + execution.end(true, Date.now()); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + output.appendLine(`Notebook cell error: ${msg}`); + execution.replaceOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.stderr(msg) + ]) + ]); + execution.end(false, Date.now()); + } finally { + cancellationTokenSource.dispose(); + } + } + + private interrupt(): void { + // Cancellation is handled per-cell via execution.token + } + + private renderResult(result: QueryResult, sql: string): string { + const { columns, rows, rowCount, executionTime } = result; + + // Non-result-set queries (CREATE, INSERT, etc.) + if (columns.length === 0) { + return `
+ + Statement executed successfully. ${rowCount > 0 ? `${rowCount} row(s) affected.` : ''} + (${executionTime}ms) +
`; + } + + // Result-set queries — render HTML table + const maxDisplay = 500; + const truncated = rows.length > maxDisplay; + const displayRows = truncated ? rows.slice(0, maxDisplay) : rows; + + let html = ``; + + html += `
${rows.length} row(s) — ${executionTime}ms${truncated ? ` (showing first ${maxDisplay})` : ''}
`; + html += ''; + + for (const col of columns) { + html += ``; + } + html += ''; + + for (const row of displayRows) { + html += ''; + for (const col of columns) { + const val = row[col]; + if (val === null || val === undefined) { + html += ''; + } else { + html += ``; + } + } + html += ''; + } + + html += '
${this.escapeHtml(col)}
NULL${this.escapeHtml(String(val))}
'; + return html; + } + + private escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } +} diff --git a/src/notebooks/serializer.ts b/src/notebooks/serializer.ts new file mode 100644 index 0000000..6c6a798 --- /dev/null +++ b/src/notebooks/serializer.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; + +interface RawNotebookCell { + kind: number; // 1 = Markup, 2 = Code + language: string; + value: string; +} + +export class ExasolNotebookSerializer implements vscode.NotebookSerializer { + + deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData { + const text = new TextDecoder().decode(content); + let raw: RawNotebookCell[]; + + try { + raw = text.trim() ? JSON.parse(text) : []; + } catch { + raw = []; + } + + const cells = raw.map(cell => new vscode.NotebookCellData( + cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, + cell.value, + cell.language + )); + + return new vscode.NotebookData(cells); + } + + serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array { + const raw: RawNotebookCell[] = data.cells.map(cell => ({ + kind: cell.kind === vscode.NotebookCellKind.Markup ? 1 : 2, + language: cell.languageId, + value: cell.value + })); + + return new TextEncoder().encode(JSON.stringify(raw, null, 2)); + } +} diff --git a/src/providers/codeLensProvider.ts b/src/providers/codeLensProvider.ts index ce73ca6..b6f2916 100644 --- a/src/providers/codeLensProvider.ts +++ b/src/providers/codeLensProvider.ts @@ -15,6 +15,12 @@ export class ExasolCodeLensProvider implements vscode.CodeLensProvider { token: vscode.CancellationToken ): vscode.CodeLens[] | Thenable { const codeLenses: vscode.CodeLens[] = []; + + // Skip CodeLens in notebook cells — they have their own execute button + if (document.uri.scheme === 'vscode-notebook-cell') { + return codeLenses; + } + const text = document.getText(); const lines = text.split('\n'); From 2b9b6d47ff7926819164fcf0c20f588942b1b88f Mon Sep 17 00:00:00 2001 From: Mikhail Zhadanov Date: Tue, 24 Mar 2026 17:09:36 +0100 Subject: [PATCH 2/5] Fix changelog order, remove vscode-test, add glob override - Fix changelog entries to descending version order - Remove deprecated vscode-test (replaced by @vscode/test-electron) - Replace @tootallnate/once override with glob override ($glob) - Eliminates 9 of 11 npm deprecation warnings Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 39 ++-- package-lock.json | 478 +--------------------------------------------- package.json | 2 +- 3 files changed, 28 insertions(+), 491 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9651e1f..e44487e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the "Exasol" extension will be documented in this file. ### Changed - Removed deprecated `vscode-test` dependency (replaced by `@vscode/test-electron`) +- Eliminated deprecated transitive dependencies via npm overrides (`glob`, `serialize-javascript`) ## [1.3.3] - 2026-03-31 @@ -21,6 +22,25 @@ All notable changes to the "Exasol" extension will be documented in this file. ### Changed - Release workflow now automatically publishes to the VS Marketplace +## [1.3.1] - 2026-03-24 + +### Added +- **Separate background connection** — object tree, autocompletion, and session queries use a dedicated connection so long-running user queries no longer block the sidebar (fixes #27) +- **Disconnect command** — right-click a connection or use the command palette to close all sessions without removing the profile +- **Execute Script command** (`Cmd+Alt+Enter` / `Ctrl+Alt+Enter`) — runs all statements in the current file regardless of cursor position or selection (fixes #26) +- **Application name in sessions** — connections identify as "VSCode Exasol" in `EXA_*_SESSIONS.CLIENT` instead of the generic "Javascript client" +- Idle connection cleanup: background drivers close after 5 minutes of inactivity; old connections close 2 minutes after switching away +- Proper shutdown: `deactivate()` now closes all database sessions on extension exit + +### Fixed +- Editor no longer loses focus when query results are displayed (fixes #25) +- Background operations (tree, completion, session) have a 30s timeout so a hanging query can't freeze the extension +- User query cancellation now aborts in-flight driver calls instead of only checking between queries +- Clicking "Continue" after cancelling a batch query now properly resumes execution with a fresh cancellation token + +### Security +- Resolved 4 Dependabot alerts (3 high, 1 low) in dev dependencies via dependency updates and overrides + ## [1.3.0] - 2026-03-18 ### Added @@ -53,25 +73,6 @@ All notable changes to the "Exasol" extension will be documented in this file. - Extracted tab bar rendering into standalone `tabBarRenderer.ts` for testability - Tidied README — reduced from 357 to 72 lines -## [1.3.1] - 2026-03-24 - -### Added -- **Separate background connection** — object tree, autocompletion, and session queries use a dedicated connection so long-running user queries no longer block the sidebar (fixes #27) -- **Disconnect command** — right-click a connection or use the command palette to close all sessions without removing the profile -- **Execute Script command** (`Cmd+Alt+Enter` / `Ctrl+Alt+Enter`) — runs all statements in the current file regardless of cursor position or selection (fixes #26) -- **Application name in sessions** — connections identify as "VSCode Exasol" in `EXA_*_SESSIONS.CLIENT` instead of the generic "Javascript client" -- Idle connection cleanup: background drivers close after 5 minutes of inactivity; old connections close 2 minutes after switching away -- Proper shutdown: `deactivate()` now closes all database sessions on extension exit - -### Fixed -- Editor no longer loses focus when query results are displayed (fixes #25) -- Background operations (tree, completion, session) have a 30s timeout so a hanging query can't freeze the extension -- User query cancellation now aborts in-flight driver calls instead of only checking between queries -- Clicking "Continue" after cancelling a batch query now properly resumes execution with a fresh cancellation token - -### Security -- Resolved 4 Dependabot alerts (3 high, 1 low) in dev dependencies via dependency updates and overrides - ## [1.1.2] - 2026-02-25 ### Fixed diff --git a/package-lock.json b/package-lock.json index e939259..a87b211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -987,80 +987,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1430,17 +1356,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -1869,52 +1784,6 @@ "node": ">=18" } }, - "node_modules/@vscode/test-cli/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/test-cli/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vscode/test-cli/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@vscode/test-electron": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", @@ -2124,16 +1993,6 @@ "win32" ] }, - "node_modules/@vscode/vsce/node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@vscode/vsce/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2145,86 +2004,6 @@ "concat-map": "0.0.1" } }, - "node_modules/@vscode/vsce/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@vscode/vsce/node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/vsce/node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@vscode/vsce/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3270,13 +3049,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -4431,22 +4203,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5062,52 +4818,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/mocha/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/mocha/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mocha/node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5549,13 +5259,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -6500,45 +6203,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6578,30 +6242,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6819,68 +6459,7 @@ "node": "18 || 20 || >=22" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": { + "node_modules/test-exclude/node_modules/brace-expansion": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", @@ -6893,18 +6472,17 @@ "node": "18 || 20 || >=22" } }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7364,48 +6942,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index 011f158..f1bb50e 100644 --- a/package.json +++ b/package.json @@ -530,6 +530,6 @@ }, "overrides": { "serialize-javascript": "^7.0.3", - "@tootallnate/once": "^3.0.1" + "glob": "$glob" } } From d7e3f6aaa2efe98bbddcd42aeaa863733e512438 Mon Sep 17 00:00:00 2001 From: Mikhail Zhadanov Date: Tue, 31 Mar 2026 16:13:27 +0200 Subject: [PATCH 3/5] Fix XSS in notebook HTML rendering and harden .exabook deserializer Co-Authored-By: Claude Opus 4.6 (1M context) --- src/notebooks/controller.ts | 3 ++- src/notebooks/serializer.ts | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/notebooks/controller.ts b/src/notebooks/controller.ts index ba9629d..c4afba2 100644 --- a/src/notebooks/controller.ts +++ b/src/notebooks/controller.ts @@ -146,6 +146,7 @@ export class ExasolNotebookController { } private escapeHtml(s: string): string { - return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + return s.replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); } } diff --git a/src/notebooks/serializer.ts b/src/notebooks/serializer.ts index 6c6a798..434c34d 100644 --- a/src/notebooks/serializer.ts +++ b/src/notebooks/serializer.ts @@ -15,14 +15,22 @@ export class ExasolNotebookSerializer implements vscode.NotebookSerializer { try { raw = text.trim() ? JSON.parse(text) : []; } catch { + vscode.window.showWarningMessage('Failed to parse .exabook file — opening as empty notebook.'); raw = []; } - const cells = raw.map(cell => new vscode.NotebookCellData( - cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, - cell.value, - cell.language - )); + if (!Array.isArray(raw)) { + vscode.window.showWarningMessage('Invalid .exabook format — expected an array of cells.'); + raw = []; + } + + const cells = raw + .filter(cell => typeof cell.value === 'string') + .map(cell => new vscode.NotebookCellData( + cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, + cell.value, + typeof cell.language === 'string' ? cell.language : 'exasol-sql' + )); return new vscode.NotebookData(cells); } From d5bf584ee0b5cac3a2ff99ed6ebb410c57c467d5 Mon Sep 17 00:00:00 2001 From: Mikhail Zhadanov Date: Tue, 31 Mar 2026 16:26:04 +0200 Subject: [PATCH 4/5] Fix interrupt for Run All, remove wasteful row cap, use formatError, null-guard deserializer Co-Authored-By: Claude Opus 4.6 (1M context) --- src/notebooks/controller.ts | 24 +++++++++++++----------- src/notebooks/serializer.ts | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/notebooks/controller.ts b/src/notebooks/controller.ts index c4afba2..684326b 100644 --- a/src/notebooks/controller.ts +++ b/src/notebooks/controller.ts @@ -2,10 +2,12 @@ import * as vscode from 'vscode'; import { ConnectionManager } from '../connectionManager'; import { QueryExecutor, QueryResult } from '../queryExecutor'; import { getOutputChannel } from '../extension'; +import { formatError } from '../connectionTypes'; export class ExasolNotebookController { private readonly controller: vscode.NotebookController; private executionOrder = 0; + private interrupted = false; constructor( private connectionManager: ConnectionManager, @@ -28,7 +30,11 @@ export class ExasolNotebookController { } private async executeCells(cells: vscode.NotebookCell[]): Promise { + this.interrupted = false; for (const cell of cells) { + if (this.interrupted) { + break; + } await this.executeCell(cell); } } @@ -68,7 +74,7 @@ export class ExasolNotebookController { try { const result = await this.queryExecutor.execute(sql, cancellationTokenSource.token); - const html = this.renderResult(result, sql); + const html = this.renderResult(result); execution.replaceOutput([ new vscode.NotebookCellOutput([ @@ -77,7 +83,7 @@ export class ExasolNotebookController { ]); execution.end(true, Date.now()); } catch (error) { - const msg = error instanceof Error ? error.message : String(error); + const msg = formatError(error); output.appendLine(`Notebook cell error: ${msg}`); execution.replaceOutput([ new vscode.NotebookCellOutput([ @@ -91,10 +97,10 @@ export class ExasolNotebookController { } private interrupt(): void { - // Cancellation is handled per-cell via execution.token + this.interrupted = true; } - private renderResult(result: QueryResult, sql: string): string { + private renderResult(result: QueryResult): string { const { columns, rows, rowCount, executionTime } = result; // Non-result-set queries (CREATE, INSERT, etc.) @@ -106,11 +112,7 @@ export class ExasolNotebookController { `; } - // Result-set queries — render HTML table - const maxDisplay = 500; - const truncated = rows.length > maxDisplay; - const displayRows = truncated ? rows.slice(0, maxDisplay) : rows; - + // Result-set queries — render HTML table (row count already capped by maxResultRows setting) let html = ``; - html += `
${rows.length} row(s) — ${executionTime}ms${truncated ? ` (showing first ${maxDisplay})` : ''}
`; + html += `
${rows.length} row(s) — ${executionTime}ms
`; html += ''; for (const col of columns) { @@ -128,7 +130,7 @@ export class ExasolNotebookController { } html += ''; - for (const row of displayRows) { + for (const row of rows) { html += ''; for (const col of columns) { const val = row[col]; diff --git a/src/notebooks/serializer.ts b/src/notebooks/serializer.ts index 434c34d..240cf73 100644 --- a/src/notebooks/serializer.ts +++ b/src/notebooks/serializer.ts @@ -25,7 +25,7 @@ export class ExasolNotebookSerializer implements vscode.NotebookSerializer { } const cells = raw - .filter(cell => typeof cell.value === 'string') + .filter(cell => cell != null && typeof cell.value === 'string') .map(cell => new vscode.NotebookCellData( cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, cell.value, From 4fd226288cdcf3d319b767103c0636156238497a Mon Sep 17 00:00:00 2001 From: Mikhail Zhadanov Date: Tue, 31 Mar 2026 17:06:06 +0200 Subject: [PATCH 5/5] Extract testable notebook utils, add scrollable result container, add 22 unit tests Co-Authored-By: Claude Opus 4.6 (1M context) --- src/notebooks/controller.ts | 11 ++- src/notebooks/notebookUtils.ts | 46 +++++++++ src/notebooks/serializer.ts | 38 +++----- src/test/unit/escapeHtml.test.ts | 44 +++++++++ src/test/unit/notebookSerializer.test.ts | 113 +++++++++++++++++++++++ 5 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 src/notebooks/notebookUtils.ts create mode 100644 src/test/unit/escapeHtml.test.ts create mode 100644 src/test/unit/notebookSerializer.test.ts diff --git a/src/notebooks/controller.ts b/src/notebooks/controller.ts index 684326b..868785e 100644 --- a/src/notebooks/controller.ts +++ b/src/notebooks/controller.ts @@ -3,6 +3,7 @@ import { ConnectionManager } from '../connectionManager'; import { QueryExecutor, QueryResult } from '../queryExecutor'; import { getOutputChannel } from '../extension'; import { formatError } from '../connectionTypes'; +import { escapeHtml } from './notebookUtils'; export class ExasolNotebookController { private readonly controller: vscode.NotebookController; @@ -114,8 +115,9 @@ export class ExasolNotebookController { // Result-set queries — render HTML table (row count already capped by maxResultRows setting) let html = ``; html += `
${rows.length} row(s) — ${executionTime}ms
`; - html += '
'; + html += '
'; for (const col of columns) { html += ``; @@ -143,12 +145,11 @@ export class ExasolNotebookController { html += ''; } - html += '
${this.escapeHtml(col)}
'; + html += ''; return html; } private escapeHtml(s: string): string { - return s.replace(/&/g, '&').replace(//g, '>') - .replace(/"/g, '"').replace(/'/g, '''); + return escapeHtml(s); } } diff --git a/src/notebooks/notebookUtils.ts b/src/notebooks/notebookUtils.ts new file mode 100644 index 0000000..51b3cbd --- /dev/null +++ b/src/notebooks/notebookUtils.ts @@ -0,0 +1,46 @@ +export interface RawNotebookCell { + kind: number; // 1 = Markup, 2 = Code + language: string; + value: string; +} + +export interface ParseResult { + cells: RawNotebookCell[]; + warnings: string[]; +} + +/** Parse raw .exabook JSON text into validated cell objects. */ +export function parseExabookCells(text: string): ParseResult { + const warnings: string[] = []; + let raw: unknown; + + try { + raw = text.trim() ? JSON.parse(text) : []; + } catch { + warnings.push('Failed to parse .exabook file — opening as empty notebook.'); + return { cells: [], warnings }; + } + + if (!Array.isArray(raw)) { + warnings.push('Invalid .exabook format — expected an array of cells.'); + return { cells: [], warnings }; + } + + const cells = raw + .filter((cell: unknown): cell is Record => + cell != null && typeof cell === 'object' && typeof (cell as any).value === 'string' + ) + .map(cell => ({ + kind: cell.kind === 1 ? 1 : 2, + value: cell.value as string, + language: typeof cell.language === 'string' ? cell.language : 'exasol-sql', + })); + + return { cells, warnings }; +} + +/** Escape HTML-significant characters to prevent XSS in rendered output. */ +export function escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); +} diff --git a/src/notebooks/serializer.ts b/src/notebooks/serializer.ts index 240cf73..0d85f76 100644 --- a/src/notebooks/serializer.ts +++ b/src/notebooks/serializer.ts @@ -1,42 +1,26 @@ import * as vscode from 'vscode'; - -interface RawNotebookCell { - kind: number; // 1 = Markup, 2 = Code - language: string; - value: string; -} +import { parseExabookCells } from './notebookUtils'; export class ExasolNotebookSerializer implements vscode.NotebookSerializer { deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData { - const text = new TextDecoder().decode(content); - let raw: RawNotebookCell[]; - - try { - raw = text.trim() ? JSON.parse(text) : []; - } catch { - vscode.window.showWarningMessage('Failed to parse .exabook file — opening as empty notebook.'); - raw = []; - } + const { cells, warnings } = parseExabookCells(new TextDecoder().decode(content)); - if (!Array.isArray(raw)) { - vscode.window.showWarningMessage('Invalid .exabook format — expected an array of cells.'); - raw = []; + for (const w of warnings) { + vscode.window.showWarningMessage(w); } - const cells = raw - .filter(cell => cell != null && typeof cell.value === 'string') - .map(cell => new vscode.NotebookCellData( - cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, - cell.value, - typeof cell.language === 'string' ? cell.language : 'exasol-sql' - )); + const notebookCells = cells.map(cell => new vscode.NotebookCellData( + cell.kind === 1 ? vscode.NotebookCellKind.Markup : vscode.NotebookCellKind.Code, + cell.value, + cell.language, + )); - return new vscode.NotebookData(cells); + return new vscode.NotebookData(notebookCells); } serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array { - const raw: RawNotebookCell[] = data.cells.map(cell => ({ + const raw = data.cells.map(cell => ({ kind: cell.kind === vscode.NotebookCellKind.Markup ? 1 : 2, language: cell.languageId, value: cell.value diff --git a/src/test/unit/escapeHtml.test.ts b/src/test/unit/escapeHtml.test.ts new file mode 100644 index 0000000..fbad572 --- /dev/null +++ b/src/test/unit/escapeHtml.test.ts @@ -0,0 +1,44 @@ +import * as assert from 'assert'; +import { escapeHtml } from '../../notebooks/notebookUtils'; + +suite('escapeHtml', () => { + + test('escapes ampersand', () => { + assert.strictEqual(escapeHtml('a&b'), 'a&b'); + }); + + test('escapes less-than', () => { + assert.strictEqual(escapeHtml('