From 0aa72551bb7247cb00057bb6280bba640b5c3c6f Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Mon, 1 Apr 2024 15:54:30 -0400 Subject: [PATCH 1/6] Add support for markdown cells in positron notebooks (#2563) * Add info about cell type to the PositronNotebookCell class * Add support for plain "error" notebook messages * Restructure notebook cells to differentiate between markup and code cells. * Distinguish between view and text model for cells better. * Add support for rendering (to html string) the content of a markdown cell. * Add render support for markdown. * Create two distinct classes for code and markup cells and add an observable renderedHtml field for the markup cells. * Further separate code and markup cell models and rendering * Add new button for creating markup cell next to code cell creation button * Add option for showing and hiding the editor for a markup cell * Fix overflowing action-bar border when in collapsed markup cell mode * Add message about empty markup cell so user's not super confused * Add logic to hide the cell outlines in markdown cells if the editor is hidden. * Switch off of notebook skeleton for the markup cell, too * markdown now renders in webview so we can successfully open images. * Add communication from webview to outside with info about height of content and double-click status * mend * Make action bar look a bit better when overlapping content in collapsed markup view. Also fix distracting action button outlines. * mend * Add new skeleton for extension used for converting images to data urls * Add ability to specify custom components for a given tag name when rendering HTML * Render images by converting to data-url using positron notebook helpers extension * Add placeholder image for when images are not yet loaded * Switch to using markdown-to-html rendering from the `markdown.api.render` command. * Centralize the markdown rendering logic into a single component and add a few safety checks. * Add basic styles to markdown rendered content taken from existing notebook markup style rendering * Remove unused export from extension left-over from copy-pasting * Remove unneccesary space in notebook cell component * Switch to using markdown instead of markup for... ahem, markdown cells. * Switch to simpler name for general purpose notebook cell. * Move type-guard functions into main positron notebook cell class * Make "running" a cell for markdown cells the equivalent of toggling the editor. Remove unneccesary open and close methods as well. * Update cell running method to "run" markdown cells as well as code cells * Switch to async version of readfile. * Debounce error messages and also fix bug where errored readFile calls still tried to convert to base64 * Add ability to open links using opener service * Consolidate html link clicking logic into same componentOverrides field that is used for image loading. Also update modalDialogs to use this new approach. * Update extensions/positron-notebooks/extension.webpack.config.js Co-authored-by: Jonathan Signed-off-by: Nick Strayer * Update extensions/positron-notebooks/src/extension.ts Co-authored-by: Jonathan Signed-off-by: Nick Strayer * Declare command in contributes section. * Be better about mime-type mapping * Pass responsibilities for handling bad conversions to the front-end by passing info about failure as object * Localize everything! * Make error handing handle entire markdown rendering command pipeline and also added a timeout. * Remove left-over comments in tsconfig from original copy location * Add helper function for the task of running a command with a timeout and utilize in the markdown and image rendering. * Restructure timeout command function to not allow callbacks to be called after timeout * Update timeout command to use existing cancel token class and to go back to being more typical promise format * Dont attempt to convert remote images * Only show most recent error message for conversion and clear stale messages * Don't pass source to error state to avoid failed `GET` request * Move `renderHtml()` into positron subfolder * Fix name of script containing promiseWithTimeout function * Add slightly better error message in timeout case * Add styles for rendering and error messages in markdown component * Fix utterly unneccesary string concatenation. * Fix case-sensitivity of mime-type checking. * Replace custom promiseWithTimeout() with existing functions --------- Signed-off-by: Nick Strayer Co-authored-by: Jonathan --- build/gulpfile.extensions.js | 1 + build/npm/dirs.js | 1 + .../extension.webpack.config.js | 20 + extensions/positron-notebooks/package.json | 50 + .../positron-notebooks/src/extension.ts | 82 + extensions/positron-notebooks/tsconfig.json | 17 + extensions/positron-notebooks/yarn.lock | 2133 +++++++++++++++++ .../browser/{ => positron}/renderHtml.tsx | 41 +- .../browser/ui/ExternalLink/ExternalLink.tsx | 35 + .../browser/components/activityOutputHtml.tsx | 2 +- .../browser/positronModalDialogs.tsx | 7 +- .../{AddCellButton.css => AddCellButtons.css} | 9 +- .../{AddCellButton.tsx => AddCellButtons.tsx} | 23 +- .../browser/PositronNotebookCell.ts | 191 +- .../browser/PositronNotebookComponent.tsx | 10 +- .../browser/PositronNotebookEditor.tsx | 11 +- .../browser/PositronNotebookInstance.ts | 70 +- .../browser/ServicesProvider.tsx | 18 + .../browser/getOutputContents.ts | 5 + .../CellEditorMonacoWidget.tsx} | 18 +- .../browser/notebookCells/DeferredImage.css | 7 + .../browser/notebookCells/DeferredImage.tsx | 126 + .../browser/notebookCells/Markdown.css | 161 ++ .../browser/notebookCells/Markdown.tsx | 86 + .../NodebookCodeCell.tsx} | 77 +- .../{ => notebookCells}/NotebookCell.css | 43 - .../browser/notebookCells/NotebookCell.tsx | 32 + .../notebookCells/NotebookCellActionBar.css | 47 + .../notebookCells/NotebookCellActionBar.tsx | 24 + .../notebookCells/NotebookMarkdownCell.css | 35 + .../notebookCells/NotebookMarkdownCell.tsx | 50 + .../browser/notebookCells/interfaces.ts | 120 + 32 files changed, 3311 insertions(+), 241 deletions(-) create mode 100644 extensions/positron-notebooks/extension.webpack.config.js create mode 100644 extensions/positron-notebooks/package.json create mode 100644 extensions/positron-notebooks/src/extension.ts create mode 100644 extensions/positron-notebooks/tsconfig.json create mode 100644 extensions/positron-notebooks/yarn.lock rename src/vs/base/browser/{ => positron}/renderHtml.tsx (66%) create mode 100644 src/vs/base/browser/ui/ExternalLink/ExternalLink.tsx rename src/vs/workbench/contrib/positronNotebook/browser/{AddCellButton.css => AddCellButtons.css} (75%) rename src/vs/workbench/contrib/positronNotebook/browser/{AddCellButton.tsx => AddCellButtons.tsx} (50%) rename src/vs/workbench/contrib/positronNotebook/browser/{useCellEditorWidget.tsx => notebookCells/CellEditorMonacoWidget.tsx} (88%) create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.css create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.tsx create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.css create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.tsx rename src/vs/workbench/contrib/positronNotebook/browser/{NotebookCell.tsx => notebookCells/NodebookCodeCell.tsx} (55%) rename src/vs/workbench/contrib/positronNotebook/browser/{ => notebookCells}/NotebookCell.css (77%) create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.tsx create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.css create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.tsx create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.css create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.tsx create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces.ts diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 3568c0b896c..ded5a587e43 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -35,6 +35,7 @@ const compilations = [ 'positron-connections/tsconfig.json', 'positron-javascript/tsconfig.json', 'positron-notebook-controllers/tsconfig.json', + 'positron-notebooks/tsconfig.json', 'positron-r/tsconfig.json', 'positron-rstudio-keymap/tsconfig.json', 'positron-python/tsconfig.json', diff --git a/build/npm/dirs.js b/build/npm/dirs.js index 60c49c31ca8..1a4adca7004 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -15,6 +15,7 @@ const dirs = [ 'extensions/positron-connections', 'extensions/positron-javascript', 'extensions/positron-notebook-controllers', + 'extensions/positron-notebooks', 'extensions/positron-r', 'extensions/positron-rstudio-keymap', 'extensions/positron-python', diff --git a/extensions/positron-notebooks/extension.webpack.config.js b/extensions/positron-notebooks/extension.webpack.config.js new file mode 100644 index 00000000000..3f2a2b3d7f0 --- /dev/null +++ b/extensions/positron-notebooks/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + externals: { + 'vscode': { commonjs: 'vscode' }, + 'express': { commonjs: 'express' } + } +}); diff --git a/extensions/positron-notebooks/package.json b/extensions/positron-notebooks/package.json new file mode 100644 index 00000000000..04543bdd862 --- /dev/null +++ b/extensions/positron-notebooks/package.json @@ -0,0 +1,50 @@ +{ + "name": "positron-notebooks-helpers", + "displayName": "Positron Notebooks Helpers", + "description": "Positron Notebook Helpers", + "version": "1.0.0", + "publisher": "vscode", + "engines": { + "vscode": "^1.65.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "contributes": { + "commands": [ + { + "command": "positronNotebookHelpers.convertImageToBase64", + "title": "Convert Image to Base64" + } + ] + }, + "main": "./out/extension.js", + "scripts": { + "vscode:prepublish": "yarn run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "eslint src --ext ts" + }, + "dependencies": {}, + "devDependencies": { + "@types/glob": "^7.2.0", + "@types/mocha": "^9.1.0", + "@types/node": "14.x", + "@typescript-eslint/eslint-plugin": "^5.12.1", + "@typescript-eslint/parser": "^5.12.1", + "@vscode/test-electron": "^2.1.2", + "eslint": "^8.9.0", + "glob": "^7.2.0", + "mocha": "^9.2.1", + "ts-node": "^10.9.1", + "typescript": "^4.5.5", + "vsce": "^2.11.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/posit-dev/positron" + } +} diff --git a/extensions/positron-notebooks/src/extension.ts b/extensions/positron-notebooks/src/extension.ts new file mode 100644 index 00000000000..e32f6d43d9c --- /dev/null +++ b/extensions/positron-notebooks/src/extension.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import path from 'path'; +import * as vscode from 'vscode'; +import { readFile } from 'fs'; + +// Make sure this matches the error message type defined where used +// (src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.tsx) +type CoversionErrorMsg = { + status: 'error'; + message: string; +}; + +/** + * Activates the extension. + * @param context An ExtensionContext that contains the extention context. + */ +export function activate(context: vscode.ExtensionContext) { + // Command that converts an image from the local file-system to a base64 string. + context.subscriptions.push( + vscode.commands.registerCommand( + 'positronNotebookHelpers.convertImageToBase64', + async (imageSrc: string, baseLoc: string) => new Promise((resolve) => { + const fullImagePath = path.join(baseLoc, imageSrc); + const fileExtension = path.extname(imageSrc).slice(1); + const mimeType = mimeTypeMap[fileExtension.toLowerCase()]; + if (!mimeType) { + resolve({ + status: 'error', + message: `Unsupported file type: "${fileExtension}."`, + }); + return; + } + try { + readFile(fullImagePath, (err, data) => { + if (err) { + resolve({ + status: 'error', + message: err.message, + }); + } else if (!data) { + resolve({ + status: 'error', + message: `No data found in file "${fullImagePath}."`, + }); + } else { + resolve(`data:${mimeType};base64,${data.toString('base64')}`); + } + }); + } catch (e) { + return { + type: 'error', + message: e instanceof Error ? e.message : `Error occured while converting image ${fullImagePath} to base64.`, + }; + } + }) + ) + ); +} + + +/** + * Map image file extension to MIME type. + * + * Supports all the 'image' types from [this list](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) + */ +const mimeTypeMap: Record = { + png: 'image/png', + apng: 'image/apng', + avif: 'image/avif', + ico: 'image/vnd.microsoft.icon', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + gif: 'image/gif', + bmp: 'image/bmp', + webp: 'image/webp', + svg: 'image/svg+xml', + tiff: 'image/tiff', + tif: 'image/tiff', +}; diff --git a/extensions/positron-notebooks/tsconfig.json b/extensions/positron-notebooks/tsconfig.json new file mode 100644 index 00000000000..0418934fa38 --- /dev/null +++ b/extensions/positron-notebooks/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "esModuleInterop": true, + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "typeRoots": ["./node_modules/@types"], + "paths": { + "*": ["./node_modules/*"] + } + }, + "include": ["src/**/*", "../../src/vscode-dts/vscode.d.ts"] +} diff --git a/extensions/positron-notebooks/yarn.lock b/extensions/positron-notebooks/yarn.lock new file mode 100644 index 00000000000..636843039d2 --- /dev/null +++ b/extensions/positron-notebooks/yarn.lock @@ -0,0 +1,2133 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint/eslintrc@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.0.tgz#8ec64e0df3e7a1971ee1ff5158da87389f167a63" + integrity sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/glob@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/mocha@^9.1.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*": + version "18.11.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.16.tgz#966cae211e970199559cfbd295888fca189e49af" + integrity sha512-6T7P5bDkRhqRxrQtwj7vru+bWTpelgtcETAZEUSdq0YISKz8WKdoBukQLYQQ6DFHvU9JRsbFq0JH5C51X2ZdnA== + +"@types/node@14.x": + version "14.18.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.35.tgz#879c4659cb7b3fe515844f029c75079c941bb65c" + integrity sha512-2ATO8pfhG1kDvw4Lc4C0GXIMSQFFJBCo/R1fSgTwmUlq5oy95LXyjDQinsRVgQY6gp6ghh3H91wk9ES5/5C+Tw== + +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@typescript-eslint/eslint-plugin@^5.12.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz#098abb4c9354e19f460d57ab18bff1f676a6cff0" + integrity sha512-YpzNv3aayRBwjs4J3oz65eVLXc9xx0PDbIRisHj+dYhvBn02MjYOD96P8YGiWEIFBrojaUjxvkaUpakD82phsA== + dependencies: + "@typescript-eslint/scope-manager" "5.46.1" + "@typescript-eslint/type-utils" "5.46.1" + "@typescript-eslint/utils" "5.46.1" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.12.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.1.tgz#1fc8e7102c1141eb64276c3b89d70da8c0ba5699" + integrity sha512-RelQ5cGypPh4ySAtfIMBzBGyrNerQcmfA1oJvPj5f+H4jI59rl9xxpn4bonC0tQvUKOEN7eGBFWxFLK3Xepneg== + dependencies: + "@typescript-eslint/scope-manager" "5.46.1" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/typescript-estree" "5.46.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" + integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== + dependencies: + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" + +"@typescript-eslint/type-utils@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.1.tgz#195033e4b30b51b870dfcf2828e88d57b04a11cc" + integrity sha512-V/zMyfI+jDmL1ADxfDxjZ0EMbtiVqj8LUGPAGyBkXXStWmCUErMpW873zEHsyguWCuq2iN4BrlWUkmuVj84yng== + dependencies: + "@typescript-eslint/typescript-estree" "5.46.1" + "@typescript-eslint/utils" "5.46.1" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" + integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== + +"@typescript-eslint/typescript-estree@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz#5358088f98a8f9939355e0996f9c8f41c25eced2" + integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== + dependencies: + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.1.tgz#7da3c934d9fd0eb4002a6bb3429f33298b469b4a" + integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.46.1" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/typescript-estree" "5.46.1" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" + integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== + dependencies: + "@typescript-eslint/types" "5.46.1" + eslint-visitor-keys "^3.3.0" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +"@vscode/test-electron@^2.1.2": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.2.1.tgz#6d1ac128e27c18e1d20bcb299e830b50587f74ca" + integrity sha512-DUdwSYVc9p/PbGveaq20dbAAXHfvdq4zQ24ILp6PKizOBxrOfMsOq8Vts5nMzeIo0CxtA/RxZLFyDv001PiUSg== + dependencies: + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + rimraf "^3.0.2" + unzipper "^0.10.11" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1, acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +azure-devops-node-api@^11.0.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz#bf04edbef60313117a0507415eed4790a420ad6b" + integrity sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA== + dependencies: + tunnel "0.0.6" + typed-rest-client "^1.8.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +big-integer@^1.6.17: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg== + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ== + dependencies: + traverse ">=0.3.0 <0.4" + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.9: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" + integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.1" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.9.0: + version "8.30.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.30.0.tgz#83a506125d089eef7c5b5910eeea824273a33f50" + integrity sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ== + dependencies: + "@eslint/eslintrc" "^1.4.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce" + integrity sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg== + dependencies: + reusify "^1.0.4" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0, find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.6, glob@^7.1.3, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + 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" + +globals@^13.19.0: + version "13.19.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" + integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.2: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hosted-git-info@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + +htmlparser2@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" + integrity sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + domutils "^3.0.1" + entities "^4.3.0" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keytar@^7.7.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== + dependencies: + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +"mkdirp@>=0.5 0": + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@^9.2.1: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-abi@^3.3.0: + version "3.30.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.30.0.tgz#d84687ad5d24ca81cdfa912a36f2c5c19b137359" + integrity sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw== + dependencies: + semver "^7.3.5" + +node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-semver@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-semver/-/parse-semver-1.1.1.tgz#9a4afd6df063dc4826f93fba4a99cf223f666cb8" + integrity sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ== + dependencies: + semver "^5.1.0" + +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prebuild-install@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@^6.9.1: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ== + dependencies: + mute-stream "~0.0.4" + +readable-stream@^2.0.2, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +semver@^5.1.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^7.3.5, semver@^7.3.7: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typed-rest-client@^1.8.4: + version "1.8.9" + resolved "https://registry.yarnpkg.com/typed-rest-client/-/typed-rest-client-1.8.9.tgz#e560226bcadfe71b0fb5c416b587f8da3b8f92d8" + integrity sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g== + dependencies: + qs "^6.9.1" + tunnel "0.0.6" + underscore "^1.12.1" + +typescript@^4.5.5: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +underscore@^1.12.1: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + +unzipper@^0.10.11: + version "0.10.11" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" + integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== + 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" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +vsce@^2.11.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/vsce/-/vsce-2.15.0.tgz#4a992e78532092a34a755143c6b6c2cabcb7d729" + integrity sha512-P8E9LAZvBCQnoGoizw65JfGvyMqNGlHdlUXD1VAuxtvYAaHBKLBdKPnpy60XKVDAkQCfmMu53g+gq9FM+ydepw== + dependencies: + azure-devops-node-api "^11.0.1" + chalk "^2.4.2" + cheerio "^1.0.0-rc.9" + commander "^6.1.0" + glob "^7.0.6" + hosted-git-info "^4.0.2" + keytar "^7.7.0" + leven "^3.1.0" + markdown-it "^12.3.2" + mime "^1.3.4" + minimatch "^3.0.3" + parse-semver "^1.1.1" + read "^1.0.7" + semver "^5.1.0" + tmp "^0.2.1" + typed-rest-client "^1.8.4" + url-join "^4.0.1" + xml2js "^0.4.23" + yauzl "^2.3.1" + yazl "^2.2.2" + +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yauzl@^2.3.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yazl@^2.2.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/src/vs/base/browser/renderHtml.tsx b/src/vs/base/browser/positron/renderHtml.tsx similarity index 66% rename from src/vs/base/browser/renderHtml.tsx rename to src/vs/base/browser/positron/renderHtml.tsx index efa12590987..9790492923a 100644 --- a/src/vs/base/browser/renderHtml.tsx +++ b/src/vs/base/browser/positron/renderHtml.tsx @@ -11,11 +11,11 @@ import * as React from 'react'; */ interface HTMLRendererOptions { /** - * Callback for when a link is clicked. Typically used to open the link with - * the opener service. - * @param url The URL of the link that was clicked. Pulled from the href attribute. + * Component overrides for the HTML renderer. + * Keyed by the node name (e.g. `'img'`) and the value is a component that can replace the + * default rendering of that node. */ - onLinkClick?: (url: string) => void; + componentOverrides?: Record>) => React.ReactElement>; } /** @@ -34,6 +34,12 @@ export const renderHtml = (html: string, opts: HTMLRendererOptions = {}): React. // use parsers that rely on `innerHTML` or `DOMParser`. const parsedContent = parseHtml(html); + // If there are component over-rides, use those to render the applicable elements. + function createElement(name: string, attrs: React.DOMAttributes, children?: (React.ReactNode | string)[] | React.ReactNode | string) { + const Component = opts.componentOverrides?.[name] || name; + return React.createElement(Component, attrs, children); + } + // Render the nodes into React elements. const renderNode = (node: HtmlNode): React.ReactElement | undefined => { @@ -50,46 +56,27 @@ export const renderHtml = (html: string, opts: HTMLRendererOptions = {}): React. // currently ignored. return undefined; } else if (node.type === 'tag' && node.children) { - // If we are looking at a link tag, then we want to replace the href with an onClick - // event that will call the onLinkClick callback. This typically will be used to open - // the link with the opener service. - if (node.name === 'a' && node.attrs && typeof node.attrs['href'] === 'string') { - // Note the use of `note.attrs` here. This is because not all tags have the href - // attribute and typescript doesn't like it if we look for it on the stricter - // `React.DOMAttributes` type. - const href = node.attrs['href']; - - if (opts.onLinkClick) { - // We know we wont be overwriting the onClick event here because the parser - // doesn't allow for `on*` attributes to be parsed. - nodeAttrs['onClick'] = ((e: React.MouseEvent) => { - opts.onLinkClick!(href); - e.preventDefault(); - }); - } - } - if (node.children.length === 1 && node.children[0].type === 'text') { // If this is a tag with a single text child, create a React element // for the tag and its text content. - return React.createElement(node.name!, nodeAttrs, node.children[0].content); + return createElement(node.name!, nodeAttrs, node.children[0].content); } else { if (node.children.length === 0) { // If the node has no children, create a React element for // the tag. For tags that cannot have children (such as //
), React will throw an exception if an array if // childen is supplied, even if the array is empty. - return React.createElement(node.name!, nodeAttrs); + return createElement(node.name!, nodeAttrs); } else { // Call the renderer recursively to render the children; // create a React element for the tag and its children. const children = node.children.map(renderNode); - return React.createElement(node.name!, nodeAttrs, children); + return createElement(node.name!, nodeAttrs, children); } } } else if (node.type === 'tag') { // Create a React element for the tag. - return React.createElement(node.name!, node.attrs); + return createElement(node.name!, node.attrs!); } else { // We don't render other types of nodes. return undefined; diff --git a/src/vs/base/browser/ui/ExternalLink/ExternalLink.tsx b/src/vs/base/browser/ui/ExternalLink/ExternalLink.tsx new file mode 100644 index 00000000000..fc19aab7822 --- /dev/null +++ b/src/vs/base/browser/ui/ExternalLink/ExternalLink.tsx @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import * as React from 'react'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + + +interface ExternalLinkProps extends React.ComponentPropsWithoutRef<'a'> { + /** + * The opener service to use to open the link + */ + openerService: IOpenerService; +} +/** + * Special link that opens in the opener service. Used to make links that behave like normal links + * while in the UI/React layer. + * @param props The props for the link with the opener service added. + * @returns The rendered link component that opens in the opener service. + */ +export function ExternalLink(props: ExternalLinkProps) { + // eslint-disable-next-line react/prop-types + const { href, openerService, ...otherProps } = props; + + return { + if (!href) { + return; + } + e.preventDefault(); + openerService.open(href); + }} + />; +} diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/activityOutputHtml.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/activityOutputHtml.tsx index a41bf75ea85..9d73c6d758b 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/activityOutputHtml.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/activityOutputHtml.tsx @@ -5,7 +5,7 @@ import 'vs/css!./activityOutputHtml'; import * as React from 'react'; import { ActivityItemOutputHtml } from 'vs/workbench/services/positronConsole/browser/classes/activityItemOutputHtml'; -import { renderHtml } from 'vs/base/browser/renderHtml'; +import { renderHtml } from 'vs/base/browser/positron/renderHtml'; // ActivityOutputHtml interface. export interface ActivityOutputHtmlProps { diff --git a/src/vs/workbench/contrib/positronModalDialogs/browser/positronModalDialogs.tsx b/src/vs/workbench/contrib/positronModalDialogs/browser/positronModalDialogs.tsx index 5cd6f9e917d..1b2cf7725bc 100644 --- a/src/vs/workbench/contrib/positronModalDialogs/browser/positronModalDialogs.tsx +++ b/src/vs/workbench/contrib/positronModalDialogs/browser/positronModalDialogs.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; // Other dependencies. import { Emitter } from 'vs/base/common/event'; -import { renderHtml } from 'vs/base/browser/renderHtml'; +import { renderHtml } from 'vs/base/browser/positron/renderHtml'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -22,6 +22,7 @@ import { PositronModalReactRenderer } from 'vs/workbench/browser/positronModalRe import { OKCancelActionBar } from 'vs/workbench/browser/positronComponents/positronModalDialog/components/okCancelActionBar'; import { OKCancelModalDialog } from 'vs/workbench/browser/positronComponents/positronModalDialog/positronOKCancelModalDialog'; import { IModalDialogPromptInstance, IPositronModalDialogsService, ShowConfirmationModalDialogOptions } from 'vs/workbench/services/positronModalDialogs/common/positronModalDialogs'; +import { ExternalLink } from 'vs/base/browser/ui/ExternalLink/ExternalLink'; /** * PositronModalDialogs class. @@ -186,7 +187,9 @@ export class PositronModalDialogs implements IPositronModalDialogsService { {renderHtml( message, { - onLinkClick: (href: string) => this._openerService.open(href) + componentOverrides: { + a: (props) => + } } )} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.css b/src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.css similarity index 75% rename from src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.css rename to src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.css index 5f9fa2483a0..ee9dbf6803f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.css +++ b/src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.css @@ -2,13 +2,18 @@ * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -.positron-add-cell-button { +.positron-add-cell-buttons { display: flex; justify-content: center; opacity: 0; transition: opacity 0.2s; + gap: 8px; + + .action-label { + cursor: pointer; + } } -.positron-add-cell-button:hover { +.positron-add-cell-buttons:hover { opacity: 1; } diff --git a/src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.tsx b/src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.tsx similarity index 50% rename from src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.tsx rename to src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.tsx index d1d79182a03..62229fae4d1 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/AddCellButton.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/AddCellButtons.tsx @@ -1,25 +1,36 @@ /*--------------------------------------------------------------------------------------------- * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./AddCellButton'; +import 'vs/css!./AddCellButtons'; import * as React from 'react'; import { useNotebookInstance } from 'vs/workbench/contrib/positronNotebook/browser/NotebookInstanceProvider'; import { localize } from 'vs/nls'; import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -export function AddCellButton({ index }: { index: number }) { +export function AddCellButtons({ index }: { index: number }) { const notebookInstance = useNotebookInstance(); - return
+ return
+
; diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell.ts index d1737ad3c81..49f2a0fef92 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell.ts @@ -2,108 +2,173 @@ * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ISettableObservable, observableValue } from 'vs/base/common/observableInternal/base'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IPositronNotebookInstance } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance'; +import { ExecutionStatus, IPositronNotebookCodeCell, IPositronNotebookCell, IPositronNotebookMarkdownCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; -type ExecutionStatus = 'running' | 'pending' | 'unconfirmed' | 'idle'; +abstract class PositronNotebookCellGeneral extends Disposable implements IPositronNotebookCell { + kind!: CellKind; + // Not marked as private so we can access it in subclasses + _disposableStore = new DisposableStore(); -export class PositronNotebookCell extends Disposable implements IPositronNotebookCell { + constructor( + public cellModel: NotebookCellTextModel, + public _instance: IPositronNotebookInstance, + @ITextModelService private readonly textModelResolverService: ITextModelService, + ) { + super(); + } + + get uri(): URI { + return this.cellModel.uri; + } + + get notebookUri(): URI { + return this._instance.uri; + } + + get viewModel(): ICellViewModel { + + const notebookViewModel = this._instance.viewModel; + if (!notebookViewModel) { + throw new Error('Notebook view model not found'); + } + + const viewCells = notebookViewModel.viewCells; + + const cell = viewCells.find(cell => cell.uri.toString() === this.cellModel.uri.toString()); + + if (cell) { + return cell; + } + + throw new Error('Cell view model not found'); + } + + getContent(): string { + return this.cellModel.getValue(); + } + + async getTextEditorModel(): Promise { + const modelRef = await this.textModelResolverService.createModelReference(this.uri); + return modelRef.object.textEditorModel; + } + + delete(): void { + this._instance.deleteCell(this); + } + + // Add placeholder run method to be overridden by subclasses + abstract run(): void; + + override dispose(): void { + this._disposableStore.dispose(); + super.dispose(); + } + + isMarkdownCell(): this is IPositronNotebookMarkdownCell { + return this.kind === CellKind.Markup; + } + + isCodeCell(): this is IPositronNotebookCodeCell { + return this.kind === CellKind.Code; + } +} + + +class PositronNotebookCodeCell extends PositronNotebookCellGeneral implements IPositronNotebookCodeCell { + override kind: CellKind.Code = CellKind.Code; executionStatus: ISettableObservable; outputs: ISettableObservable; constructor( - public viewModel: NotebookCellTextModel, - private _instance: IPositronNotebookInstance, - @ITextModelService private readonly textModelResolverService: ITextModelService, + cellModel: NotebookCellTextModel, + instance: IPositronNotebookInstance, + textModelResolverService: ITextModelService, ) { - super(); + super(cellModel, instance, textModelResolverService); + this.executionStatus = observableValue('cellExecutionStatus', 'idle'); - this.outputs = observableValue('cellOutputs', this.viewModel.outputs); + this.outputs = observableValue('cellOutputs', this.cellModel.outputs); // Listen for changes to the cell outputs and update the observable this._register( - this.viewModel.onDidChangeOutputs(() => { + this.cellModel.onDidChangeOutputs(() => { // By unpacking the array and repacking we make sure that // the React component will rerender when the outputs change. Probably not // great to have this leak here. - this.outputs.set([...this.viewModel.outputs], undefined); + this.outputs.set([...this.cellModel.outputs], undefined); }) ); } - get uri(): URI { - return this.viewModel.uri; + override run(): void { + this._instance.runCells([this]); } +} - getContent(): string { - return this.viewModel.getValue(); - } - run(): void { - this._instance.runCells([this]); + + +class PositronNotebookMarkdownCell extends PositronNotebookCellGeneral implements IPositronNotebookMarkdownCell { + + markdownString: ISettableObservable = observableValue('markdownString', undefined); + editorShown: ISettableObservable = observableValue('editorShown', false); + override kind: CellKind.Markup = CellKind.Markup; + + + constructor( + cellModel: NotebookCellTextModel, + instance: IPositronNotebookInstance, + textModelResolverService: ITextModelService, + ) { + super(cellModel, instance, textModelResolverService); + + // Render the markdown content and update the observable when the cell content changes + this._disposableStore.add(this.cellModel.onDidChangeContent(() => { + this.markdownString.set(this.getContent(), undefined); + })); + + this._updateContent(); } - delete(): void { - this._instance.deleteCell(this); + private _updateContent(): void { + this.markdownString.set(this.getContent(), undefined); } - async getTextEditorModel(): Promise { - const modelRef = await this.textModelResolverService.createModelReference(this.uri); - return modelRef.object.textEditorModel; + toggleEditor(): void { + this.editorShown.set(!this.editorShown.get(), undefined); } + override run(): void { + this.toggleEditor(); + } } /** - * Wrapper class for notebook cell that exposes the properties that the UI needs to render the cell. + * Instantiate a notebook cell based on the cell's kind + * @param cell Text model for the cell + * @param instance The containing Positron notebook instance that this cell resides in. + * @param instantiationService The instantiation service to use to create the cell + * @returns The instantiated notebook cell of the correct type. */ -interface IPositronNotebookCell { - - /** - * Cell specific uri for the cell within the notebook - */ - get uri(): URI; - - /** - * The content of the cell. This is the raw text of the cell. - */ - getContent(): string; - - /** - * The view model for the cell. - */ - viewModel: NotebookCellTextModel; - - /** - * Get the text editor model for use in the monaco editor widgets - */ - getTextEditorModel(): Promise; - - /** - * Current execution status for this cell - */ - executionStatus: ISettableObservable; +export function createNotebookCell(cell: NotebookCellTextModel, instance: IPositronNotebookInstance, instantiationService: IInstantiationService) { + if (cell.cellKind === CellKind.Code) { + return instantiationService.createInstance(PositronNotebookCodeCell, cell, instance); + } else { + return instantiationService.createInstance(PositronNotebookMarkdownCell, cell, instance); + } +} - /** - * Current cell outputs as an observable - */ - outputs: ISettableObservable; - /** - * Run this cell - */ - run(): void; - /** - * Delete this cell - */ - delete(): void; -} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx index 5158ac61d78..6b6b988982b 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx @@ -6,12 +6,12 @@ import 'vs/css!./PositronNotebookComponent'; import * as React from 'react'; import { useNotebookInstance } from 'vs/workbench/contrib/positronNotebook/browser/NotebookInstanceProvider'; -import { NotebookCell } from './NotebookCell'; -import { AddCellButton } from './AddCellButton'; +import { AddCellButtons } from './AddCellButtons'; import { useObservedValue } from './useObservedValue'; import { localize } from 'vs/nls'; import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; import { KernelStatusBadge } from './KernelStatusBadge'; +import { NotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell'; export function PositronNotebookComponent() { @@ -33,13 +33,13 @@ export function PositronNotebookComponent() { > {localize('runAllCells', 'Run all cells')} -
+
- + {notebookCells?.length ? notebookCells?.map((cell, index) => <> - + ) :
{localize('noCells', 'No cells')}
}
diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index 57426c9ee92..6b9a82dd6d2 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -50,6 +50,9 @@ import { } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PositronNotebookEditorInput } from './PositronNotebookEditorInput'; import { ILogService } from 'vs/platform/log/common/log'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; interface NotebookLayoutInfo { @@ -109,10 +112,11 @@ export class PositronNotebookEditor extends EditorPane { @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IWebviewService private readonly _webviewService: IWebviewService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILogService private readonly _logService: ILogService, - - + @ICommandService private readonly _commandService: ICommandService, + @IOpenerService private readonly _openerService: IOpenerService ) { // Call the base class's constructor. super( @@ -443,7 +447,10 @@ export class PositronNotebookEditor extends EditorPane { configurationService: this._configurationService, instantiationService: this._instantiationService, textModelResolverService: this._textModelResolverService, + webviewService: this._webviewService, + commandService: this._commandService, logService: this._logService, + openerService: this._openerService, sizeObservable: this._size, scopedContextKeyProviderCallback: container => scopedContextKeyService.createScoped(container) }}> diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts index 7d8cd058d8e..2ad1795cc7d 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts @@ -20,10 +20,11 @@ import { CellEditType, CellKind, ICellReplaceEdit, SelectionStateType } from 'vs import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { PositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell'; +import { createNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell'; import { PositronNotebookEditorInput } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditorInput'; import { BaseCellEditorOptions } from './BaseCellEditorOptions'; import * as DOM from 'vs/base/browser/dom'; +import { IPositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; enum KernelStatus { @@ -51,7 +52,7 @@ export interface IPositronNotebookInstance { /** * The cells that make up the notebook */ - cells: ISettableObservable; + cells: ISettableObservable; /** * Status of kernel for the notebook. @@ -61,7 +62,7 @@ export interface IPositronNotebookInstance { /** * The currently selected cells. Typically a single cell but can be multiple cells. */ - selectedCells: PositronNotebookCell[]; + selectedCells: IPositronNotebookCell[]; /** * Has the notebook instance been disposed? @@ -74,7 +75,7 @@ export interface IPositronNotebookInstance { * Run the given cells * @param cells The cells to run */ - runCells(cells: PositronNotebookCell[]): Promise; + runCells(cells: IPositronNotebookCell[]): Promise; /** * Run the selected cells @@ -89,12 +90,12 @@ export interface IPositronNotebookInstance { /** * Add a new cell of a given type to the notebook at the requested index */ - addCell(type: keyof typeof PositronNotebookInstance.cellTypeToKind, index: number): void; + addCell(type: CellKind, index: number): void; /** * Delete a cell from the notebook */ - deleteCell(cell: PositronNotebookCell): void; + deleteCell(cell: IPositronNotebookCell): void; /** * Attach a view model to this instance @@ -103,24 +104,16 @@ export interface IPositronNotebookInstance { */ attachView(viewModel: NotebookViewModel, viewState?: INotebookEditorViewState): void; + readonly viewModel: NotebookViewModel | undefined; + /** * Method called when the instance is detached from a view. This is used to cleanup * all the logic and variables related to the view/DOM. */ detachView(): void; - } export class PositronNotebookInstance extends Disposable implements IPositronNotebookInstance { - - /** - * Map from string of cell kind to the integer enum used internally. - */ - static cellTypeToKind = { - 'code': CellKind.Code, - 'markdown': CellKind.Markup, - }; - /** * Value to keep track of what instance number. * Used for keeping track in the logs. @@ -130,17 +123,17 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot private _identifier: string = `Positron Notebook | NotebookInstance(${PositronNotebookInstance.count++}) |`; - selectedCells: PositronNotebookCell[] = []; + selectedCells: IPositronNotebookCell[] = []; /** * Internal cells that we use to manage the state of the notebook */ - private _cells: PositronNotebookCell[] = []; + private _cells: IPositronNotebookCell[] = []; /** * User facing cells wrapped in an observerable for the UI to react to changes */ - cells: ISettableObservable; + cells: ISettableObservable; /** * Status of kernel for the notebook. @@ -181,6 +174,10 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot return this._input.resource; } + get viewModel(): NotebookViewModel | undefined { + return this._viewModel; + } + /** * Internal event emitter for when the editor's options change. @@ -246,7 +243,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot ) { super(); - this.cells = observableValue('positronNotebookCells', this._cells); + this.cells = observableValue('positronNotebookCells', this._cells); this.kernelStatus = observableValue('positronNotebookKernelStatus', KernelStatus.Uninitialized); this.isReadOnly = this.creationOptions?.isReadOnly ?? false; @@ -306,7 +303,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot this._cells.forEach(cell => cell.dispose()); // Update cells with new cells - this._cells = notebookModel.cells.map(cell => this._instantiationService.createInstance(PositronNotebookCell, cell, this)); + this._cells = notebookModel.cells.map(cell => createNotebookCell(cell, this, this._instantiationService)); this.language = notebookModel.cells[0].language; @@ -337,7 +334,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot } - async runCells(cells: PositronNotebookCell[]): Promise { + async runCells(cells: IPositronNotebookCell[]): Promise { if (!cells) { throw new Error(localize('noCells', "No cells to run")); @@ -358,8 +355,9 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot * @param cells Cells to run * @returns */ - private async _runCells(cells: PositronNotebookCell[]): Promise { - + private async _runCells(cells: IPositronNotebookCell[]): Promise { + // Filter so we're only working with code cells. + const codeCells = cells; this._logService.info(this._identifier, '_runCells'); if (!this._textModel) { @@ -368,24 +366,28 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot this._trySetupKernel(); - for (const cell of cells) { - cell.executionStatus.set('running', undefined); + for (const cell of codeCells) { + if (cell.isCodeCell()) { + cell.executionStatus.set('running', undefined); + } } const hasExecutions = [...cells].some(cell => Boolean(this.notebookExecutionStateService.getCellExecution(cell.uri))); if (hasExecutions) { - this.notebookExecutionService.cancelNotebookCells(this._textModel, Array.from(cells).map(c => c.viewModel)); + this.notebookExecutionService.cancelNotebookCells(this._textModel, Array.from(cells).map(c => c.cellModel)); return; } - await this.notebookExecutionService.executeNotebookCells(this._textModel, Array.from(cells).map(c => c.viewModel), this._contextKeyService); - for (const cell of cells) { - cell.executionStatus.set('idle', undefined); + await this.notebookExecutionService.executeNotebookCells(this._textModel, Array.from(cells).map(c => c.cellModel), this._contextKeyService); + for (const cell of codeCells) { + if (cell.isCodeCell()) { + cell.executionStatus.set('idle', undefined); + } } } - addCell(type: 'code' | 'markdown', index: number): void { + addCell(type: CellKind, index: number): void { if (!this._viewModel) { throw new Error(localize('noViewModel', "No view model for notebook")); } @@ -400,7 +402,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot index, '', this.language, - PositronNotebookInstance.cellTypeToKind[type], + type, undefined, [], synchronous, @@ -408,7 +410,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot ); } - deleteCell(cell: PositronNotebookCell): void { + deleteCell(cell: IPositronNotebookCell): void { if (!this._textModel) { throw new Error(localize('noModelForDelete', "No model for notebook to delete cell from")); } @@ -417,7 +419,7 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot // TODO: Hook up readOnly to the notebook actual value const readOnly = false; const computeUndoRedo = !readOnly || textModel.viewType === 'interactive'; - const cellIndex = textModel.cells.indexOf(cell.viewModel); + const cellIndex = textModel.cells.indexOf(cell.cellModel); const edits: ICellReplaceEdit = { editType: CellEditType.Replace, index: cellIndex, count: 1, cells: [] diff --git a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx index ee5b265de4d..b69009bfcd2 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx @@ -6,10 +6,13 @@ import * as React from 'react'; import { ISize } from 'vs/base/browser/positronReactRenderer'; import { ISettableObservable } from 'vs/base/common/observableInternal/base'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; @@ -38,6 +41,21 @@ interface ServiceBundle { */ logService: ILogService; + /** + * Service for creating webviews + */ + webviewService: IWebviewService; + + /** + * Servicer for opening external links + */ + openerService: IOpenerService; + + /** + * Service for envoking commands from extensions + */ + commandService: ICommandService; + /** * An observable for the size of the notebook. */ diff --git a/src/vs/workbench/contrib/positronNotebook/browser/getOutputContents.ts b/src/vs/workbench/contrib/positronNotebook/browser/getOutputContents.ts index 70eee3ae203..7c0b0fe22e7 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/getOutputContents.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/getOutputContents.ts @@ -124,6 +124,11 @@ export function parseOutputData(output: ICellOutput['outputs'][number]): ParsedO if (parsedMessage?.name === 'Runtime Error') { return { type: 'error', content: parsedMessage.message }; } + + if (mime === 'application/vnd.code.notebook.error') { + return { type: 'error', content: parsedMessage.message }; + } + } catch (e) { } diff --git a/src/vs/workbench/contrib/positronNotebook/browser/useCellEditorWidget.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx similarity index 88% rename from src/vs/workbench/contrib/positronNotebook/browser/useCellEditorWidget.tsx rename to src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx index 0f54ce37462..efab6037235 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/useCellEditorWidget.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget.tsx @@ -9,16 +9,28 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions'; import { useNotebookInstance } from 'vs/workbench/contrib/positronNotebook/browser/NotebookInstanceProvider'; -import { PositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell'; import { useServices } from 'vs/workbench/contrib/positronNotebook/browser/ServicesProvider'; +import { IPositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; import { observeValue } from 'vs/workbench/contrib/positronNotebook/common/utils/observeValue'; + +/** + * + * @param opts.cell Cell to be shown and edited in the editor widget + * @returns An editor widget for the cell + */ +export function CellEditorMonacoWidget({ cell }: { cell: IPositronNotebookCell }) { + const { editorPartRef } = useCellEditorWidget(cell); + return
; +} + + /** * Create a cell editor widget for a cell. * @param cell Cell whose editor is to be created * @returns Refs to place the editor and the wrapping div */ -export function useCellEditorWidget({ cell }: { cell: PositronNotebookCell }) { +export function useCellEditorWidget(cell: IPositronNotebookCell) { const services = useServices(); const instance = useNotebookInstance(); @@ -44,7 +56,7 @@ export function useCellEditorWidget({ cell }: { cell: PositronNotebookCell }) { const nativeContainer = DOM.$('.positron-monaco-editor-container'); editorPartRef.current.appendChild(nativeContainer); - const language = cell.viewModel.language; + const language = cell.cellModel.language; const editorContextKeyService = services.scopedContextKeyProviderCallback(editorPartRef.current); const editorInstaService = services.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService])); const editorOptions = new CellEditorOptions(instance.getBaseCellEditorOptions(language), instance.notebookOptions, services.configurationService); diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.css new file mode 100644 index 00000000000..6d45bdc15f5 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.css @@ -0,0 +1,7 @@ +.positron-notebooks-deferred-img-placeholder { + width: min(400px, 100%); + height: max(20vh, 200px); + background-color: var(--vscode-positronVariables-activeSelectionBackground); + /* Code from https://heropatterns.com/ */ + background-image: url("data:image/svg+xml,%3Csvg width='100' height='20' viewBox='0 0 100 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.184 20c.357-.13.72-.264 1.088-.402l1.768-.661C33.64 15.347 39.647 14 50 14c10.271 0 15.362 1.222 24.629 4.928.955.383 1.869.74 2.75 1.072h6.225c-2.51-.73-5.139-1.691-8.233-2.928C65.888 13.278 60.562 12 50 12c-10.626 0-16.855 1.397-26.66 5.063l-1.767.662c-2.475.923-4.66 1.674-6.724 2.275h6.335zm0-20C13.258 2.892 8.077 4 0 4V2c5.744 0 9.951-.574 14.85-2h6.334zM77.38 0C85.239 2.966 90.502 4 100 4V2c-6.842 0-11.386-.542-16.396-2h-6.225zM0 14c8.44 0 13.718-1.21 22.272-4.402l1.768-.661C33.64 5.347 39.647 4 50 4c10.271 0 15.362 1.222 24.629 4.928C84.112 12.722 89.438 14 100 14v-2c-10.271 0-15.362-1.222-24.629-4.928C65.888 3.278 60.562 2 50 2 39.374 2 33.145 3.397 23.34 7.063l-1.767.662C13.223 10.84 8.163 12 0 12v2z' fill='white' fill-opacity='0.4' fill-rule='evenodd'/%3E%3C/svg%3E"); +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.tsx new file mode 100644 index 00000000000..d6dd79ac1c7 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/DeferredImage.tsx @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./DeferredImage'; + +import * as React from 'react'; +import { useServices } from 'vs/workbench/contrib/positronNotebook/browser/ServicesProvider'; +import { useNotebookInstance } from 'vs/workbench/contrib/positronNotebook/browser/NotebookInstanceProvider'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { dirname } from 'vs/base/common/resources'; +import { localize } from 'vs/nls'; +import { createCancelablePromise, raceTimeout } from 'vs/base/common/async'; + +/** + * This should match the error message defined in the command definition + * (extensions/positron-notebooks/src/extension.ts) + */ +type CoversionErrorMsg = { + status: 'error'; + message: string; +}; + +/** + * Predicate function to allow us to be safe with our response processing from command. + * @param x: Variable of unknown type to check if it is a `CoversionErrorMsg`. + * @returns Whether the object is a `CoversionErrorMsg`. + */ +function isConversionErrorMsg(x: unknown): x is CoversionErrorMsg { + return x !== null && typeof x === 'object' && 'status' in x && x.status === 'error' && 'message' in x; +} + +type ImageDataResults = { + status: 'pending'; +} | { + status: 'success'; + data: string; +} | { + status: 'error'; + message: string; +}; + +/** + * Special image component that defers loading of the image while it converts it to a data-url using + * the `positronNotebookHelpers.convertImageToBase64` command. + * @param props: Props for `img` element. + * @returns Image tag that shows the image once it is loaded. + */ +// eslint-disable-next-line react/prop-types +export function DeferredImage({ src = 'no-source', ...props }: React.ComponentPropsWithoutRef<'img'>) { + const services = useServices(); + const notebookInstance = useNotebookInstance(); + const baseLocation = getNotebookBaseUri(notebookInstance.uri).path; + + const [results, setResults] = React.useState({ status: 'pending' }); + + React.useEffect(() => { + + // Check for prefix of http or https to avoid converting remote images + if (src.startsWith('http://') || src.startsWith('https://')) { + setResults({ status: 'success', data: src }); + return; + } + + const conversionTimeoutMs = 3000; + const errorTimeoutMs = 1000; + + let delayedErrorMsg: NodeJS.Timeout; + + const conversionCancellablePromise = createCancelablePromise(() => raceTimeout( + services.commandService.executeCommand('positronNotebookHelpers.convertImageToBase64', src, baseLocation), + conversionTimeoutMs + )); + + conversionCancellablePromise.then((payload) => { + if (typeof payload === 'string') { + setResults({ status: 'success', data: payload }); + } else if (isConversionErrorMsg(payload)) { + + delayedErrorMsg = setTimeout(() => { + services.logService.error(localize('failedToConvert', 'Failed to convert image to base64:'), src, payload.message); + }, errorTimeoutMs); + + setResults(payload); + } else { + const unexpectedResponseString = localize('unexpectedResponse', 'Unexpected response from convertImageToBase64'); + delayedErrorMsg = setTimeout(() => { + services.logService.error(unexpectedResponseString, payload); + }, errorTimeoutMs); + setResults({ status: 'error', message: unexpectedResponseString }); + } + }).catch((err) => { + setResults({ status: 'error', message: err.message }); + }); + + return () => { + clearTimeout(delayedErrorMsg); + conversionCancellablePromise.cancel(); + }; + }, [src, baseLocation, services]); + + switch (results.status) { + case 'pending': + return
; + case 'error': + // Show image tag without attempt to convert. Probably will be broken but will provide + // clue as to what's going on. + return ; + case 'success': + return ; + } +} + +function getNotebookBaseUri(notebookUri: URI) { + if (notebookUri.scheme === Schemas.untitled) { + // TODO: Use workspace context service to set the base URI to workspace root + throw new Error('Have not yet implemented untitled notebook URIs'); + } + + return dirname(notebookUri); +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.css new file mode 100644 index 00000000000..a6136b98b84 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.css @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +.positron-markdown-rendering { + text-align: center; + opacity: 0.6; + font-style: italic; +} + +.positron-markdown-error { + color: var(--vscode-editorError-foreground, orangered); + font-style: italic; +} + +/* These styles are taken from the vscode notebook markdown rendering styles found in extensions/markdown-language-features/notebook/index.ts */ +.positron-markdown-rendered { + img { + max-width: 100%; + max-height: 100%; + } + + a { + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + hr { + border: 0; + height: 2px; + border-bottom: 2px solid; + } + + h2, + h3, + h4, + h5, + h6 { + font-weight: normal; + } + + h1 { + font-size: 2.3em; + } + + h2 { + font-size: 2em; + } + + h3 { + font-size: 1.7em; + } + + h3 { + font-size: 1.5em; + } + + h4 { + font-size: 1.3em; + } + + h5 { + font-size: 1.2em; + } + + h1, + h2, + h3 { + font-weight: normal; + } + + div { + width: 100%; + } + + /* Adjust margin of first item in markdown cell */ + *:first-child { + margin-top: 0px; + } + + /* h1 tags don't need top margin */ + h1:first-child { + margin-top: 0; + } + + /* Removes bottom margin when only one item exists in markdown cell */ + #preview > *:only-child, + #preview > *:last-child { + margin-bottom: 0; + padding-bottom: 0; + } + + /* makes all markdown cells consistent */ + div { + min-height: 1rem; + } + + table { + border-collapse: collapse; + border-spacing: 0; + } + + table th, + table td { + border: 1px solid; + } + + table > thead > tr > th { + text-align: left; + border-bottom: 1px solid; + } + + table > thead > tr > th, + table > thead > tr > td, + table > tbody > tr > th, + table > tbody > tr > td { + padding: 5px 10px; + } + + table > tbody > tr + tr > td { + border-top: 1px solid; + } + + blockquote { + margin: 0 7px 0 5px; + padding: 0 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; + } + + code { + font-size: 1em; + font-family: var(--vscode-repl-font-family); + } + + pre code { + line-height: 1.357em; + white-space: pre-wrap; + padding: 0; + } + + li p { + margin-bottom: 0.7em; + } + + ul, + ol { + margin-bottom: 0.7em; + } +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.tsx new file mode 100644 index 00000000000..5300cc8e7e2 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/Markdown.tsx @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./Markdown'; + +import * as React from 'react'; +import { renderHtml } from 'vs/base/browser/positron/renderHtml'; +import { DeferredImage } from './DeferredImage'; +import { useServices } from 'vs/workbench/contrib/positronNotebook/browser/ServicesProvider'; +import { ExternalLink } from 'vs/base/browser/ui/ExternalLink/ExternalLink'; +import { localize } from 'vs/nls'; +import { createCancelablePromise, raceTimeout } from 'vs/base/common/async'; + +/** + * Component that render markdown content from a string. + * @param content: Markdown content to render in string form + * @returns React element containing the rendered markdown. + */ +export function Markdown({ content }: { content: string }) { + + const renderedHtml = useMarkdown(content); + + switch (renderedHtml.status) { + case 'error': + return
{localize('errorRenderingMd', 'Error rendering markdown:')} {renderedHtml.errorMsg}
; + case 'rendering': + return
{localize('renderingMd', "Rendering markdown...")}
; + case 'success': + return
{renderedHtml.nodes}
; + } +} + +type MarkdownRenderResults = { + status: 'rendering'; +} | { + status: 'success'; + nodes: React.ReactElement; +} | { + status: 'error'; + errorMsg: string; +}; + +function useMarkdown(content: string): MarkdownRenderResults { + + const services = useServices(); + const [renderedHtml, setRenderedHtml] = React.useState({ + status: 'rendering' + }); + + React.useEffect(() => { + + const conversionCancellablePromise = createCancelablePromise(() => raceTimeout( + services.commandService.executeCommand('markdown.api.render', content), + 5000, + )); + + conversionCancellablePromise.then((html) => { + if (typeof html !== 'string') { + setRenderedHtml({ + status: 'error', + errorMsg: localize('noHtmlResult', 'Failed to render markdown: No HTML result returned') + }); + return; + } + setRenderedHtml({ + status: 'success', + nodes: renderHtml(html, { + componentOverrides: { + img: DeferredImage, + a: (props) => + } + }) + }); + }).catch((error) => { + setRenderedHtml({ + status: 'error', + errorMsg: error.message + }); + }); + + return () => conversionCancellablePromise.cancel(); + }, [content, services]); + + return renderedHtml; +} + diff --git a/src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NodebookCodeCell.tsx similarity index 55% rename from src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.tsx rename to src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NodebookCodeCell.tsx index 5a22acc30f8..3143b8a80ed 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NodebookCodeCell.tsx @@ -1,67 +1,43 @@ /*--------------------------------------------------------------------------------------------- * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./NotebookCell'; import * as React from 'react'; import { VSBuffer } from 'vs/base/common/buffer'; import { NotebookCellOutputTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel'; import { ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { PositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/PositronNotebookCell'; +import { IPositronNotebookCodeCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; import { parseOutputData } from 'vs/workbench/contrib/positronNotebook/browser/getOutputContents'; import { useObservedValue } from 'vs/workbench/contrib/positronNotebook/browser/useObservedValue'; -import { useCellEditorWidget } from './useCellEditorWidget'; +import { CellEditorMonacoWidget } from './CellEditorMonacoWidget'; import { localize } from 'vs/nls'; import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { NotebookCellActionBar } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar'; -/** - * Logic for running a cell and handling its output. - * @param opts.cell The `PositronNotebookCell` to render - */ -export function NotebookCell(opts: { - cell: PositronNotebookCell; -}) { - - const { editorPartRef } = useCellEditorWidget(opts); - - const executionStatus = useObservedValue(opts.cell.executionStatus); - const outputContents = useObservedValue(opts.cell.outputs); +export function NodebookCodeCell({ cell }: { cell: IPositronNotebookCodeCell }) { + const outputContents = useObservedValue(cell.outputs); + const executionStatus = useObservedValue(cell.executionStatus); const isRunning = executionStatus === 'running'; - return ( -
-
- - -
-
-
-
-
- { - outputContents?.map((output) => - ) - } -
-
-
- ); -} + return
+ + + +
+ +
+ {outputContents?.map((output) => )} +
+
+
; +} function NotebookCellOutput({ cellOutput }: { cellOutput: ICellOutput }) { @@ -71,9 +47,7 @@ function NotebookCellOutput({ cellOutput }: { cellOutput: ICellOutput }) { if (cellOutput instanceof NotebookCellOutputTextModel) { return <> - { - outputs.map(({ data, mime }, i) => ) - } + {outputs.map(({ data, mime }, i) => )} ; } @@ -83,8 +57,6 @@ function NotebookCellOutput({ cellOutput }: { cellOutput: ICellOutput }) { } - - function CellOutputContents(output: { data: VSBuffer; mime: string }) { const parsed = parseOutputData(output); @@ -110,4 +82,3 @@ function CellOutputContents(output: { data: VSBuffer; mime: string }) { } } - diff --git a/src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.css similarity index 77% rename from src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.css rename to src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.css index a1a556908b6..42ff3b1b407 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/NotebookCell.css +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.css @@ -15,49 +15,6 @@ position: relative; - .action-bar { - --bar-h: 1rem; - position: absolute; - top: calc((var(--bar-h) + 0.25rem) * -1); - right: 0; - border: var(--positron-notebooks-border); - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; - display: flex; - gap: 0.25rem; - - .action-button { - aspect-ratio: 1; - display: grid; - place-content: center; - padding: 2px; - - &:hover { - scale: 1.2; - - .button-icon::before { - color: blue; - } - } - - .button-icon { - align-items: center; - justify-content: center; - display: flex; - width: 16px; - aspect-ratio: 1; - } - - .button-icon::before { - color: inherit; - } - - &.disabled { - color: grey; - } - } - } - .cell-contents { border: var(--positron-notebooks-border); diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.tsx new file mode 100644 index 00000000000..f7efe91b156 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCell.tsx @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./NotebookCell'; + +import * as React from 'react'; +import { IPositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; +import { NodebookCodeCell } from './NodebookCodeCell'; +import { NotebookMarkdownCell } from './NotebookMarkdownCell'; + + +/** + * Logic for running a cell and handling its output. + * @param opts.cell The `PositronNotebookCell` to render + */ +export function NotebookCell({ cell }: { + cell: IPositronNotebookCell; +}) { + + if (cell.isCodeCell()) { + return ; + } + + if (cell.isMarkdownCell()) { + return ; + } + + throw new Error('Unknown cell type'); +} + + + diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.css new file mode 100644 index 00000000000..5444317dcb1 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.css @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +.positron-notebooks-cell-action-bar { + --bar-h: 1rem; + position: absolute; + top: calc((var(--bar-h) + 0.25rem) * -1); + right: 0; + border: var(--positron-notebooks-border); + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + border-bottom: transparent; + background-color: var(--vscode-editor-background, white); + display: flex; + gap: 0.25rem; + + .action-button { + aspect-ratio: 1; + display: grid; + place-content: center; + padding: 2px; + /* The added outline here is distracting */ + outline: revert; + + &:hover { + .button-icon::before { + color: blue; + } + } + + .button-icon { + align-items: center; + justify-content: center; + display: flex; + width: 16px; + aspect-ratio: 1; + } + + .button-icon::before { + color: inherit; + } + + &.disabled { + color: grey; + } + } +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.tsx new file mode 100644 index 00000000000..d61b061e5e4 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar.tsx @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./NotebookCellActionBar'; + +import * as React from 'react'; +import { localize } from 'vs/nls'; +import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { IPositronNotebookCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; + + +export function NotebookCellActionBar({ cell, children }: { cell: IPositronNotebookCell; children: React.ReactNode }) { + + return
+ {children} + +
; +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.css new file mode 100644 index 00000000000..af6cb116c08 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.css @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +.positron-notebook-markup-rendered { + padding-inline: 1rem; + padding-block: 0.5rem; + + .empty-output-msg { + opacity: 0.75; + font-style: italic; + text-align: center; + cursor: pointer; + } + + img { + max-width: 100%; + } +} + +.positron-notebook-cell.editor-hidden { + --positron-notebooks-border-color: transparent; +} + +.positron-notebook-cell.editor-hidden:not(:hover) + .positron-notebooks-cell-action-bar { + display: none; +} + +/* Put the actions bar halfway down the cell so it's more clearly associated with the content that +doesn't have borders to deliniate where it sits */ +.positron-notebook-cell.editor-hidden .positron-notebooks-cell-action-bar { + top: calc(50% - (var(--bar-h) / 2)); + border-radius: 0.25rem; +} diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.tsx new file mode 100644 index 00000000000..4c0313ef3f9 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookMarkdownCell.tsx @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./NotebookMarkdownCell'; +import * as React from 'react'; + +import { IPositronNotebookMarkdownCell } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces'; + +import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { CellEditorMonacoWidget } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/CellEditorMonacoWidget'; +import { NotebookCellActionBar } from 'vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellActionBar'; +import { useObservedValue } from 'vs/workbench/contrib/positronNotebook/browser/useObservedValue'; +import { Markdown } from './Markdown'; +import { localize } from 'vs/nls'; + +export function NotebookMarkdownCell({ cell }: { cell: IPositronNotebookMarkdownCell }) { + + const markdownString = useObservedValue(cell.markdownString); + const editorShown = useObservedValue(cell.editorShown); + + return ( +
+ + + +
+ {editorShown ? : null} +
{ + cell.toggleEditor(); + }}> + { + markdownString ? + + :
+ Empty markup cell. {editorShown ? '' : 'Double click to edit'} +
+ } +
+
+
+ ); +} + + + diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces.ts b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces.ts new file mode 100644 index 00000000000..48a46c86d61 --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/interfaces.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ISettableObservable } from 'vs/base/common/observableInternal/base'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellKind, ICellOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export type ExecutionStatus = 'running' | 'pending' | 'unconfirmed' | 'idle'; + +/** + * Wrapper class for notebook cell that exposes the properties that the UI needs to render the cell. + * This interface is extended to provide the specific properties for code and markdown cells. + */ +export interface IPositronNotebookCell extends Disposable { + + /** + * The kind of cell + */ + kind: CellKind; + + /** + * Cell specific uri for the cell within the notebook + */ + get uri(): URI; + + /** + * URI for the notebook that contains this cell + */ + get notebookUri(): URI; + + /** + * The content of the cell. This is the raw text of the cell. + */ + getContent(): string; + + /** + * The notebook text model for the cell. + */ + cellModel: NotebookCellTextModel; + + /** + * Get the view model for the cell + */ + get viewModel(): ICellViewModel; + + /** + * Get the text editor model for use in the monaco editor widgets + */ + getTextEditorModel(): Promise; + + /** + * Delete this cell + */ + delete(): void; + + /** + * Run this cell + */ + run(): void; + + /** + * Type guard for checking if cell is a markdown cell + */ + isMarkdownCell(): this is IPositronNotebookMarkdownCell; + + /** + * Type guard for checking if cell is a code cell + */ + isCodeCell(): this is IPositronNotebookCodeCell; +} + + +/** + * Cell that contains code that can be executed + */ +export interface IPositronNotebookCodeCell extends IPositronNotebookCell { + kind: CellKind.Code; + + /** + * Current execution status for this cell + */ + executionStatus: ISettableObservable; + + /** + * Current cell outputs as an observable + */ + outputs: ISettableObservable; + + +} + + + +/** + * Cell that contains markdown content + */ +export interface IPositronNotebookMarkdownCell extends IPositronNotebookCell { + kind: CellKind.Markup; + + /** + * Observable content of cell. Equivalent to the cell's content, but as an observable + */ + markdownString: ISettableObservable; + + /** + * Observable that indicates whether the editor is currently shown + */ + editorShown: ISettableObservable; + + /** + * Toggle the editor for this cell + */ + toggleEditor(): void; +} + From 899dced5f45deb8695480d23359df5bafea612d8 Mon Sep 17 00:00:00 2001 From: Brian Lambert Date: Mon, 1 Apr 2024 14:34:07 -0700 Subject: [PATCH 2/6] Feature/add edit filters through filter bar (#2591) --- .../browser/ui/codicons/codicon/codicon.ttf | Bin 93516 -> 93656 bytes .../ui/positronComponents/button/button.css | 1 + src/vs/base/common/codicons.ts | 1 + .../components/okActionBar.tsx | 2 +- .../positronModalDialog.tsx | 2 +- .../positronModalPopup/positronModalPopup.tsx | 8 + .../addEditRowFilterModalPopup.css | 3 +- .../addEditRowFilterModalPopup.tsx | 143 +++++++------ .../components/columnSearch.tsx | 2 +- .../components/columnSelectorCell.css | 1 + .../components/dropDownColumnSelector.css | 7 +- .../components/dropDownColumnSelector.tsx | 9 +- .../components/rowFilterParameter.tsx | 5 +- .../addEditRowFilterModalPopup/rowFilter.ts | 88 +++++++- .../components/filterBar.tsx | 173 ---------------- .../components/rowFilterWidget.css | 88 ++++++++ .../components/rowFilterWidget.tsx | 122 +++++++++++ .../rowFilterBar.css} | 57 +++--- .../components/rowFilterBar/rowFilterBar.tsx | 191 ++++++++++++++++++ .../dataExplorerPanel/dataExplorerPanel.tsx | 6 +- 20 files changed, 619 insertions(+), 290 deletions(-) delete mode 100644 src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/filterBar.tsx create mode 100644 src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.css create mode 100644 src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.tsx rename src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/{filterBar.css => rowFilterBar/rowFilterBar.css} (86%) create mode 100644 src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.tsx diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index ba853082102e3a8653f625c2e0647ad027938f5e..de74fdbf7444a54914a76974f9ce9e04af335e05 100644 GIT binary patch delta 4364 zcmYk=d0bTG9>?+D_c)*uD3+pG0s)$t8|J>JrpYFP3xcwP>>wbbqN5-#xO0li%B;+m zYi42YrJ0qP8Q09r%*@Pd47rdQmD>G2{q7%EU+?+MnK?7VdCv395s#g5JN>QOxAwe0r#ibcv|J}s*YY=HMGABPJDQN52jUrtyi#P&ulS&Jk1!FOv@4)^2^OADY z?B6@?2^EBe@xi8o{G#H4K`X`xBF2dLRj=P4=;bZozEsexrPum~V0hcUcpD|Pb?|hn zwj8x*-4mL1I`Yq6;y5JD{<+tk^$>A8A`VB59pmAL61&u6V#+G4kXp>e*YL-7cwnc5 zTW)v>Tk$VhC;d<;mGZgVl9BjIO2x+#*&xeMiF@IVHmH|RKEM~X-S2u3&H~Ir5sEPzC76R!EWkoIu?UM%jtVTnQY=RmR$?{QU>(+@ z8aAH5vv>~AV`9Bat#eB7&r?)X6s4E-^9_8{}n4lXh}YK0tu@iNE|M_uvovh*u}K({c>+?0T=x z&P@`Em+_7qM+^Lf|HxQulPwmcHy%JMG{-WTC|9r+Z%es2u@|q)PT3*vTFop=#^4#$ zNVM#gt#TiFU<4*21*r%{IHn^GlQ9gBBNBI+2d!`_`xXkYLi zg0F1fHUS)BdsLf1RNCondZFH~Y17Mjfx9kMTvAfMOi2~HT;WUBY!mjeD;18i=Aa2j z*tH7$DymraDZJS6*#!AM-@20u2BlB+0hDX#3?R*FB{+pyB<^E2)o8A zaD-js6gar9#}zohuJH=bvLOo3v7rjjvtbInXt+4>47|Wb7#s$?$UBh=TiA&Tud-1J zud&ezZ?KaT-ee~$yv0sYsAZ=r>|kRQ-espL>|&j2mcNHl7!kU#pxWX1HTxE+Cer1aleq(1V++@wF zApFkGQTUTJtAg+sJ6GWr&%bM)!X0+L!ryF}vfSAP%4*IoR8|Ytyq>I` zk=2SdtB81I6jz1fHBwwl$e!G(fEUM)J5}I@9pXFA>nT3?g2G9Aqi>+sE8KcnVY7YQ zxBb0)xMe0Pp$q$}!hHLVZ+qOfef*+PW@q_D2cP0IW@ZtZSo2DP`S`sG*Vugu*X=8Q z?Y*AlmYH{ir|rA_1D$tq>yWa%*bkNE$(k8QILn$DNBEBYSm7LNW*^~u_K3oH_EUu) z*rSRUaB+R6cqtbbC#b!{{~-MFfUct?{)lKp8B@GWN8N+s0c!uqvP-BnnJjBVZ z>kfqxteXPAs;;{$q_E8uQbihjEAZGGgB8NrJ_`JXX?#Q>j_s>3neC@AjP1{>gt@`v z+!&w`$qrOVWCtl^vx5~zvO^Ri*r5s&So6k&N$hY14tFCD&cHO*yq++M-x7_Fa)U61 z9i|t!c5kT03m}lBSSFf-FyhaocBZpbKa8_%&9OVM(D40G@1`5n7Ls-oM7gL`EY`n8)h^JW^S0#Aegyf<_lq{9p5!DsW-RGi6NN7EKo2f zf0lyzQ419wW{VVhvc(Ef>}&-yrX>p9YfpELvz*(;p9~oqvL@tssDEgm(1_5S&>f+t z!V<$ahdaV!!Z(IDhTn7i|HG)A?D_^vD21Mdnwik!v9 zZeZNH>E6@JoYODQ=r-f{j9c-e;^)Oz$M20llQ1|TFQF-MO5*Os`lP{0=}E_uZY2jK z&r7aJ-kcJjayaF3YI5qiw4k&n(vGD&(x<0arSF{?GIMi=WW;8aXKcwhni-HeDl{2+UFu5qQsJ!UuqW6ni6h{>26>loOIJ$+rD!NpZR&1$gT2iy5E^Nv9CD$sY(z~*ta(m^qrGu9)T6%3+$7LDIt}L%wzITP^ zis38ft*EQIyDGkFVb!&jeOH#OtXp|)RnJwit9Gn9vASlB`&PZ5IthphxE?bzGcq$X zQwwuJGfGn{GAb*xjdo(ljLgimzWeR*x_9%;oabM4l0=}1idd6$whL(ZoXnW&j6x1}Jy?ae#+n6?8 zPyhE8ajTOy|2^x=dWg867B{y|c9cgq6xhWcGZHGXN@`GqPvC?7@W87QV!7cNyof!r zUWQ#Aqi-Z zY{`{OiIiS)Kis8CHpnUb&YAU+L}|f2*o=?RQ;uRMo`pA#pcfX)T-=cHXe(>vbs2vX zcVdY=06%n<02wM%@iG3yZ}=6LWj{9IaoiypmWS2ex(%P>3w(*M@HHB7Q94T)y2&=H zjlIRQtJf3qtR%@DQYUYqhjbGk`CI;nYj(Y7SNBh)9*gWto;{sUOAvP8WjTj-_#QvX zB)lZMED?WnM+e-3a+x8QuoefTM4UK^-Ev3{%Bz-TK_*}e9+PmXmA!H=`eHOf5sw4} zA{g@!iJ2IQ$(W89q+<+1Fb(0DhzN|sECgXbq7WB}smSJsGw=u|Arbz_LMqZQ0sRq; z@tA`!OhFQok%EC3goiK+eUOR47=i!{#V`!V!x+IWJb(e1i?N8sgXo7Dm|b(JgC}ln z;`--5tBdW{$#-g{NOOYXa+c;qB{gi4LJ^y+@Clou;KQaW?6)1AyhgpsQHGKbHd9${ zY?i_^Y_`ISY>vVn`&1`iEU=q8jlg-^uX7Iz|JqYJ`=ZRw>^uNJ+uJ%1aDKx#mn)t( z(!5eh1#5mj;X~Gp3AL=*MZ()GFH8o`u*TJu1}HdKe}!Cjph6xyNFkqnNTGlotWd}fQCP?ZC=|0p6_&EY z6qd2WMV#gVP7ZhtHSj1qLYeENYotO6J4&IH9j&l}9izahb&XY6&5l#xw7MQq<~r{h zudqg(;+nt#fphDcsKB{(O;X_8x+W`dZe3FpIIXU!3R~Ggg(uk{g>7uG!c#04n1Su= zG=qDNYCZ{7c#fT+u!{{-c%BVcc!8a%u$P^su#cUs@CrLep@xl6ILOXbIK(<5`9L_# z&Qsv6%oU~Z8arR%1RJgJIvb<#CL618l8sY1#l|a~W_i*YILjs~C*I|gB!&0bWQF(H z6b0TIU8xG6v1tn5vgr!nu^9?Ku$c-!u{@6rG_gF74K%Yk=J@jrfQtjO`pMk5T?-U` zVRIEOv3UxutT_`1S6Fi<5dL7znLxP8nlpj$7t2eMf$Qudg}-_HU5gcNvP%^HVT+aJ z&Ms9}TXvbUZeh*m$!f=%Gl#5OS##!)<;j*P-t5Fxs(9NIR~gy!x;oHc*LN+&?{=v7 zWY5if@o9yR?491evvzXytb(Z-6TDfoTnR^5vs?+iShHM-cSUjSR$`W&S-ON9_I=&L zvBY+C3!n8dUo)R4{K+0w_>HYq_?11TcmoxeS)nBRS+hb3o7fWykK1?o_&Pl}GHa8p z_UuVz-Nv3$_?&%9;S2V(!k6qBg|FDN3SYBtD>Sn2DBifmWtKAW7A~%O5xdss0kpBd z_o=}X_LlCYylvmtqXwVa4L!Vg<8JElT~E$*3lEDSL2O5b9c(9sm+i~GzMkK6bi2Zj zwvV6J-7j(ErG)d|a+k8i4)q&>a=XgU*K;jj>#lIXKF(J-yDdExcH1p}0|r_g^-=uq zfV4cQWCGh)VGG+&;W0b7=LjeN@|&B&Xx3dJlx?dJ&vKC(NDyiDR|sSWDg?8G6nOn< zeMlja9jq{u9ilLj4d8vlJYX^hLlvg8!xUoJ;R@;O!wO^A5egx!*%)COJ4%69*;ek5 zfr+g73?YIYt1yn&jn;7-5N5HDDDW7zj#rq^at;kdv1Vh0IChf4RMzYS@qbX#Iz`F7 ztoa)ugEccoFyCXoBgn~t*@)Q-LLzJSf?%fE%myKg-@nHeCM*PJ|?TM1#TIaUPo>&-DFB(Y|%3CXOP4MGZQHcT*UAV$Hgfmj8z z2FxrG%wgd=GSG)L>xkl!$tPy731*>~FC>_SV!n{THPULnkYLt{nE`@XCuRl+W}TQd zLNM#ZOgCYKo!HxV&H#>b6wEF=6wFFkpkRJ>uEK+CoGN>$QSFnF@e(?E_-XYl`Z-$%;xiKwZ+Melsr#q(an_e4gg^mr4 z4c!-damKP44Pk@Bc7=O{$A@nWZ<#rA=H6MMvo6dIp1o_1%o#mrS%h0edc^6ucg~HN zn>+Vnq<>_3m)(t~y3Kb}#T>a5eY#+(CIhd0}~}d2917-nYN7YSu!YMOo+>IX+O??HqWDGii>@s0zj){3#wC7Bl9%jQ(o{USxT^R-@%5z} zmo___msc;ZUw*kHs-&#sK*`yXtEE#)3ro+IC6`r~9hg#9SJqJ0xMJ*zM_1I9dz6Qk z*OfP~9Jcbrs>oFptG-y>VRhi@>eb&?^sks!QCI0!IjOR`vaa% +// CSS. import 'vs/css!./okActionBar'; // React. diff --git a/src/vs/workbench/browser/positronComponents/positronModalDialog/positronModalDialog.tsx b/src/vs/workbench/browser/positronComponents/positronModalDialog/positronModalDialog.tsx index df8cefcfe11..4231a62c1fb 100644 --- a/src/vs/workbench/browser/positronComponents/positronModalDialog/positronModalDialog.tsx +++ b/src/vs/workbench/browser/positronComponents/positronModalDialog/positronModalDialog.tsx @@ -110,7 +110,7 @@ export const PositronModalDialog = (props: PropsWithChildren void; } /** @@ -195,6 +196,13 @@ export const PositronModalPopup = (props: PropsWithChildren { @@ -38,13 +38,13 @@ const validateRowFilterValue = (columnSchema: ColumnSchema, value: string) => { /** * Checks whether the value is a boolean. - * @returns true if the value is a number; otherwise, false. + * @returns true if the value is a boolean; otherwise, false. */ const isBoolean = () => /^(true|false)$/i.test(value); /** - * Checks whether the value is a boolean. - * @returns true if the value is a number; otherwise, false. + * Checks whether the value is a date. + * @returns true if the value is a date; otherwise, false. */ const isDate = () => !Number.isNaN(Date.parse(value)); @@ -74,24 +74,6 @@ const validateRowFilterValue = (columnSchema: ColumnSchema, value: string) => { } }; -/** - * RowFilterCondition enumeration. - */ -enum RowFilterCondition { - // Conditions with no parameters. - CONDITION_IS_EMPTY = 'is-empty', - CONDITION_IS_NOT_EMPTY = 'is-not-empty', - - // Conditions with one parameter. - CONDITION_IS_LESS_THAN = 'is-less-than', - CONDITION_IS_GREATER_THAN = 'is-greater-than', - CONDITION_IS_EQUAL = 'is-equal', - - // Conditions with two parameters. - CONDITION_IS_BETWEEN = 'is-between', - CONDITION_IS_NOT_BETWEEN = 'is-not-between' -} - /** * AddEditRowFilterModalPopupProps interface. */ @@ -99,8 +81,8 @@ interface AddEditRowFilterModalPopupProps { dataExplorerClientInstance: DataExplorerClientInstance; renderer: PositronModalReactRenderer; anchor: HTMLElement; - rowFilter?: RowFilter; - onAddRowFilter: (rowFilter: RowFilter) => void; + editRowFilter?: RowFilter; + onApplyRowFilter: (rowFilter: RowFilter) => void; } /** @@ -114,11 +96,28 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp const secondRowFilterParameterRef = useRef(undefined!); // State hooks. - const [selectedColumnSchema, setSelectedColumnSchema] = - useState(undefined); - const [selectedCondition, setSelectedCondition] = useState(undefined); - const [firstRowFilterValue, setFirstRowFilterValue] = useState(''); - const [secondRowFilterValue, setSecondRowFilterValue] = useState(''); + const [selectedColumnSchema, setSelectedColumnSchema] = useState( + props.editRowFilter?.columnSchema + ); + const [selectedCondition, setSelectedCondition] = useState( + props.editRowFilter?.rowFilterCondition + ); + const [firstRowFilterValue, setFirstRowFilterValue] = useState(() => { + if (props.editRowFilter instanceof SingleValueRowFilter) { + return props.editRowFilter.value; + } else if (props.editRowFilter instanceof RangeRowFilter) { + return props.editRowFilter.lowerLimit; + } else { + return ''; + } + }); + const [secondRowFilterValue, setSecondRowFilterValue] = useState(() => { + if (props.editRowFilter instanceof RangeRowFilter) { + return props.editRowFilter.upperLimit; + } else { + return ''; + } + }); const [errorText, setErrorText] = useState(undefined); // useEffect for when the selectedCondition changes. @@ -182,7 +181,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp break; } - // Add is exactly condition. + // Add is equal to condition. switch (selectedColumnSchema.type_display) { case ColumnSchemaTypeDisplay.Number: case ColumnSchemaTypeDisplay.Boolean: @@ -191,10 +190,10 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp case ColumnSchemaTypeDisplay.Datetime: case ColumnSchemaTypeDisplay.Time: conditionEntries.push(new DropDownListBoxItem({ - identifier: RowFilterCondition.CONDITION_IS_EQUAL, + identifier: RowFilterCondition.CONDITION_IS_EQUAL_TO, title: localize( - 'positron.addEditRowFilter.conditionIsExactly', - "is exactly" + 'positron.addEditRowFilter.conditionIsEqualTo', + "is equal to" ) })); break; @@ -241,7 +240,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp // Render the first row filter parameter component in single-value mode. case RowFilterCondition.CONDITION_IS_LESS_THAN: case RowFilterCondition.CONDITION_IS_GREATER_THAN: - case RowFilterCondition.CONDITION_IS_EQUAL: + case RowFilterCondition.CONDITION_IS_EQUAL_TO: placeholderText = localize( 'positron.addEditRowFilter.valuePlaceholder', "value" @@ -263,6 +262,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp { // Set the first row filter value. setFirstRowFilterValue(text); @@ -275,7 +275,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp })(); // Set the second row filter parameter component. - const secondRowFilterParameter = (() => { + const secondRowFilterParameterComponent = (() => { let placeholderText: string | undefined = undefined; switch (selectedCondition) { // Do not render the second row filter parameter component. @@ -284,7 +284,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp case RowFilterCondition.CONDITION_IS_NOT_EMPTY: case RowFilterCondition.CONDITION_IS_LESS_THAN: case RowFilterCondition.CONDITION_IS_GREATER_THAN: - case RowFilterCondition.CONDITION_IS_EQUAL: + case RowFilterCondition.CONDITION_IS_EQUAL_TO: return null; // Render the second row filter parameter component in two-value mode. @@ -302,6 +302,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp { // Set the second row filter value. setSecondRowFilterValue(text); @@ -314,9 +315,9 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp })(); /** - * Apply button onPressed handler. + * Applies the row filter, if it is valid. */ - const applyButtonPressed = () => { + const applyRowFilter = () => { // Ensure that the user has selected a column schema. if (!selectedColumnSchema) { setErrorText(localize( @@ -438,54 +439,57 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp }; /** - * Adds a row filter. + * Applies a row filter. * @param rowFilter The row filter to add. */ - const addRowFilter = (rowFilter: RowFilter) => { + const applyRowFilter = (rowFilter: RowFilter) => { setErrorText(undefined); props.renderer.dispose(); - props.onAddRowFilter(rowFilter); + props.onApplyRowFilter(rowFilter); }; // Validate the condition and row filter values. If things are valid, add the row filter. switch (selectedCondition) { - // Add the is empty row filter. + // Apply the is empty row filter. case RowFilterCondition.CONDITION_IS_EMPTY: { - addRowFilter(new RowFilterIsEmpty(selectedColumnSchema)); + applyRowFilter(new RowFilterIsEmpty(selectedColumnSchema)); break; } - // Add the is not empty row filter. + // Apply the is not empty row filter. case RowFilterCondition.CONDITION_IS_NOT_EMPTY: { - addRowFilter(new RowFilterIsNotEmpty(selectedColumnSchema)); + applyRowFilter(new RowFilterIsNotEmpty(selectedColumnSchema)); break; } - // Add the is less than row filter. + // Apply the is less than row filter. case RowFilterCondition.CONDITION_IS_LESS_THAN: { if (!validateFirstRowFilterValue()) { return; } - addRowFilter(new RowFilterIsLessThan(selectedColumnSchema, firstRowFilterValue)); + applyRowFilter(new RowFilterIsLessThan(selectedColumnSchema, firstRowFilterValue)); break; } + // Apply the is greater than row filter. case RowFilterCondition.CONDITION_IS_GREATER_THAN: { if (!validateFirstRowFilterValue()) { return; } - addRowFilter(new RowFilterIsGreaterThan(selectedColumnSchema, firstRowFilterValue)); + applyRowFilter(new RowFilterIsGreaterThan(selectedColumnSchema, firstRowFilterValue)); break; } - case RowFilterCondition.CONDITION_IS_EQUAL: { + // Apply the is equal to row filter. + case RowFilterCondition.CONDITION_IS_EQUAL_TO: { if (!validateFirstRowFilterValue()) { return; } - addRowFilter(new RowFilterIsEqualTo(selectedColumnSchema, firstRowFilterValue)); + applyRowFilter(new RowFilterIsEqualTo(selectedColumnSchema, firstRowFilterValue)); break; } + // Apply the is between row filter. case RowFilterCondition.CONDITION_IS_BETWEEN: { if (!validateFirstRowFilterValue()) { return; @@ -493,7 +497,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp if (!validateSecondRowFilterValue()) { return; } - addRowFilter(new RowFilterIsBetween( + applyRowFilter(new RowFilterIsBetween( selectedColumnSchema, firstRowFilterValue, secondRowFilterValue @@ -501,6 +505,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp break; } + // Apply the is not between row filter. case RowFilterCondition.CONDITION_IS_NOT_BETWEEN: { if (!validateFirstRowFilterValue()) { return; @@ -508,7 +513,7 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp if (!validateSecondRowFilterValue()) { return; } - addRowFilter(new RowFilterIsNotBetween( + applyRowFilter(new RowFilterIsNotBetween( selectedColumnSchema, firstRowFilterValue, secondRowFilterValue @@ -518,6 +523,15 @@ export const AddEditRowFilterModalPopup = (props: AddEditRowFilterModalPopupProp } }; + /** + * Clears the filter values and error text. + */ + const clearFilterValuesAndErrorText = () => { + setFirstRowFilterValue(''); + setSecondRowFilterValue(''); + setErrorText(undefined); + }; + // Render. return (
{ + selectedColumnSchema={selectedColumnSchema} + onSelectedColumnSchemaChanged={columnSchema => { // Set the selected column schema. setSelectedColumnSchema(columnSchema); // Reset the selected condition. setSelectedCondition(undefined); - // Clear the state. - setFirstRowFilterValue(''); - setSecondRowFilterValue(''); - setErrorText(undefined); + // Clear the filter values and error text. + clearFilterValuesAndErrorText(); }} /> {firstRowFilterParameterComponent} - {secondRowFilterParameter} + {secondRowFilterParameterComponent} {errorText && (
{errorText}
)} - -
-
- {!filtersHidden && filters.map((filter, index) => -
{filter.name}
- )} - -
-
- ); -}; diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.css b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.css new file mode 100644 index 00000000000..d2ed6f9b90e --- /dev/null +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.css @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +.row-filter-widget { + padding: 0; + height: 25px; + display: flex; + cursor: pointer; + border-radius: 3px; + align-items: center; + box-sizing: border-box; + justify-content: center; + color: var(--vscode-positronDataExplorer-foreground); + border: 1px solid var(--vscode-positronDataExplorer-border); + background-color: var(--vscode-positronDataExplorer-background); +} + +.row-filter-widget:hover { + background-color: var(--vscode-positronDataExplorer-contrastBackground); +} + +.row-filter-widget +.boolean-operator { + height: 100%; + padding: 0 6px; + display: flex; + align-items: center; + justify-content: center; + border-right: 1px solid var(--vscode-positronDataExplorer-border); +} + +.row-filter-widget +.title { + margin: 0 2px 0 6px; +} + +.row-filter-widget +.title +.column-name { + font-weight: 600; +} + +.row-filter-widget +.title +.space-before::before { + content: " "; + white-space: pre; +} + +.row-filter-widget +.title +.space-after::after { + content: " "; + white-space: pre; +} + +.row-filter-widget +.clear-filter-button { + width: 18px; + height: 18px; + display: flex; + opacity: 80%; + cursor: pointer; + margin-right: 3px; + border-radius: 3px; + align-items: center; + box-sizing: border-box; + justify-content: center; +} + +.row-filter-widget +.clear-filter-button:hover { + border: 1px solid var(--vscode-positronDataExplorer-border); + + /* outline: 1px solid var(--vscode-focusBorder); */ + background-color: var(--vscode-positronDataExplorer-contrastBackground); +} + +.row-filter-widget +.clear-button:focus { + outline: none !important; +} + +.row-filter-widget +.clear-button:focus-visible { + outline: 1px solid var(--vscode-focusBorder); +} diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.tsx new file mode 100644 index 00000000000..277f09f7a69 --- /dev/null +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget.tsx @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// CSS. +import 'vs/css!./rowFilterWidget'; + +// React. +import * as React from 'react'; +import { forwardRef } from 'react'; // eslint-disable-line no-duplicate-imports + +// Other dependencies. +import { localize } from 'vs/nls'; +import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { RowFilter, RowFilterIsBetween, RowFilterIsEmpty, RowFilterIsEqualTo, RowFilterIsGreaterThan, RowFilterIsLessThan, RowFilterIsNotBetween, RowFilterIsNotEmpty } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/rowFilter'; + +/** + * RowFilterWidgetProps interface. + */ +interface RowFilterWidgetProps { + rowFilter: RowFilter; + booleanOperator?: 'and'; + onEdit: () => void; + onClear: () => void; +} + +/** + * RowFilterWidget component. + * @param props A RowFilterWidgetProps that contains the component properties. + * @returns The rendered component. + */ +export const RowFilterWidget = forwardRef((props, ref) => { + // Compute the title. + const title = (() => { + if (props.rowFilter instanceof RowFilterIsEmpty) { + return <> + {props.rowFilter.columnSchema.column_name} + + {localize('positron.dataExplorer.rowFilterWidget.isEmpty', "is empty")} + + ; + } else if (props.rowFilter instanceof RowFilterIsNotEmpty) { + return <> + {props.rowFilter.columnSchema.column_name} + + {localize('positron.dataExplorer.rowFilterWidget.isNotEmpty', "is not empty")} + + ; + } else if (props.rowFilter instanceof RowFilterIsLessThan) { + return <> + {props.rowFilter.columnSchema.column_name} + < + {props.rowFilter.value} + ; + } else if (props.rowFilter instanceof RowFilterIsGreaterThan) { + return <> + {props.rowFilter.columnSchema.column_name} + > + {props.rowFilter.value} + ; + } else if (props.rowFilter instanceof RowFilterIsEqualTo) { + return <> + {props.rowFilter.columnSchema.column_name} + = + {props.rowFilter.value} + ; + } else if (props.rowFilter instanceof RowFilterIsBetween) { + return <> + {props.rowFilter.columnSchema.column_name} + >= + {props.rowFilter.lowerLimit} + + {localize('positron.dataExplorer.rowFilterWidget.and', "and")} + + {props.rowFilter.columnSchema.column_name} + <= + {props.rowFilter.upperLimit} + ; + } else if (props.rowFilter instanceof RowFilterIsNotBetween) { + return <> + {props.rowFilter.columnSchema.column_name} + < + {props.rowFilter.lowerLimit} + + {localize('positron.dataExplorer.rowFilterWidget.and', "and")} + + {props.rowFilter.columnSchema.column_name} + > + {props.rowFilter.upperLimit} + ; + } else { + // This indicates a bug. + return null; + } + })(); + + // Render. + return ( + + + ); +}); + +// Set the display name. +RowFilterWidget.displayName = 'RowFilterWidget'; diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/filterBar.css b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.css similarity index 86% rename from src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/filterBar.css rename to src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.css index 2964dc0aa90..dcd7f1cf7aa 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/filterBar.css +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.css @@ -3,7 +3,7 @@ *--------------------------------------------------------------------------------------------*/ .data-explorer-panel -.filter-bar { +.row-filter-bar { padding: 8px; display: grid; grid-row: filter-bar / data-explorer; @@ -13,15 +13,8 @@ } .data-explorer-panel -.filter-bar -.filter { - grid-column: icon / gutter; -} - -.data-explorer-panel -.filter-bar -.filter -.filter-button { +.row-filter-bar +.row-filter-button { width: 25px; height: 25px; display: flex; @@ -31,47 +24,45 @@ align-items: center; box-sizing: border-box; justify-content: center; + grid-column: icon / gutter; border: 1px solid var(--vscode-positronDataExplorer-border); background-color: var(--vscode-positronDataExplorer-background); } .data-explorer-panel -.filter-bar -.filter -.filter-button:focus { +.row-filter-bar +.row-filter-button:focus { outline: none !important; } .data-explorer-panel -.filter-bar -.filter -.filter-button:focus-visible { +.row-filter-bar +.row-filter-button:focus-visible { border-radius: 3px; outline: 1px solid var(--vscode-focusBorder) !important; } .data-explorer-panel -.filter-bar -.filter -.filter-button +.row-filter-bar +.row-filter-button .counter { top: -6px; right: -6px; - font-size: 60%; - font-weight: 600; - position: absolute; - background-color: var(--vscode-positronDataGrid-sortIndexForeground); width: 14px; height: 14px; display: flex; + font-size: 60%; + color: white; + font-weight: 600; + border-radius: 7px; + position: absolute; align-items: center; justify-content: center; - border-radius: 7px; - color: white; + background-color: var(--vscode-positronDataGrid-sortIndexForeground); } .data-explorer-panel -.filter-bar +.row-filter-bar .filter-entries { row-gap: 8px; column-gap: 4px; @@ -82,9 +73,9 @@ } .data-explorer-panel -.filter-bar +.row-filter-bar .filter-entries -.add-filter-button { +.add-row-filter-button { width: 25px; height: 25px; display: flex; @@ -98,22 +89,22 @@ } .data-explorer-panel -.filter-bar +.row-filter-bar .filter-entries -.add-filter-button:focus { +.add-row-filter-button:focus { outline: none !important; } .data-explorer-panel -.filter-bar +.row-filter-bar .filter-entries -.add-filter-button:focus-visible { +.add-row-filter-button:focus-visible { border-radius: 3px; outline: 1px solid var(--vscode-focusBorder) !important; } .data-explorer-panel -.filter-bar +.row-filter-bar .filter-entries .filter { width: 90px; diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.tsx new file mode 100644 index 00000000000..2774c3da843 --- /dev/null +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar.tsx @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// CSS. +import 'vs/css!./rowFilterBar'; + +// React. +import * as React from 'react'; +import { useRef, useState } from 'react'; // eslint-disable-line no-duplicate-imports + +// Other dependencies. +import { localize } from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { Button } from 'vs/base/browser/ui/positronComponents/button/button'; +import { showContextMenu } from 'vs/workbench/browser/positronComponents/contextMenu/contextMenu'; +import { ContextMenuItem } from 'vs/workbench/browser/positronComponents/contextMenu/contextMenuItem'; +import { ContextMenuSeparator } from 'vs/workbench/browser/positronComponents/contextMenu/contextMenuSeparator'; +import { usePositronDataExplorerContext } from 'vs/workbench/browser/positronDataExplorer/positronDataExplorerContext'; +import { PositronModalReactRenderer } from 'vs/workbench/browser/positronModalReactRenderer/positronModalReactRenderer'; +import { RowFilter } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/rowFilter'; +import { RowFilterWidget } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/components/rowFilterWidget'; +import { AddEditRowFilterModalPopup } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/addEditRowFilterModalPopup'; + +/** + * RowFilterBar component. + * @returns The rendered component. + */ +export const RowFilterBar = () => { + // Context hooks. + const context = usePositronDataExplorerContext(); + + // Reference hooks. + const ref = useRef(undefined!); + const filterButtonRef = useRef(undefined!); + const rowFilterWidgetRefs = useRef<(HTMLButtonElement)[]>([]); + const addFilterButtonRef = useRef(undefined!); + + // State hooks. + const [rowFilters, setRowFilters] = useState([]); + const [filtersHidden, setFiltersHidden] = useState(false); + + /** + * Shows the add / edit row filter modal popup. + * @param rowFilterToEdit The row filter to edit, or undefined, to add a row filter. + */ + const showAddEditRowFilterModalPopup = (anchor: HTMLElement, rowFilterToEdit?: RowFilter) => { + // Create the renderer. + const renderer = new PositronModalReactRenderer({ + keybindingService: context.keybindingService, + layoutService: context.layoutService, + container: context.layoutService.getContainer(DOM.getWindow(ref.current)) + }); + + /** + * onApplyRowFilter event handler. + * @param rowFilterToApply The row filter to apply. + */ + const applyRowFilterHandler = (rowFilterToApply: RowFilter) => { + // If this is a new row filter, append it to the array of row filters. Otherwise, + // replace the row filter that was edited. + if (!rowFilterToEdit) { + setRowFilters(rowFilters => [...rowFilters, rowFilterToApply]); + } else { + // Find the index of the row filter to edit. + const index = rowFilters.findIndex(rowFilter => + rowFilterToEdit.identifier === rowFilter.identifier + ); + + setRowFilters(rowFilters => + [ + ...rowFilters.slice(0, index), + rowFilterToApply, + ...rowFilters.slice(index + 1) + ] + ); + } + }; + + // Show the add /edit row filter modal popup. + renderer.render( + + ); + }; + + /** + * Filter button pressed handler. + */ + const filterButtonPressedHandler = async () => { + // Build the context menu entries. + const entries: (ContextMenuItem | ContextMenuSeparator)[] = []; + entries.push(new ContextMenuItem({ + label: localize('positron.dataExplorer.addFilter', "Add filter"), + icon: 'positron-add-filter', + onSelected: () => showAddEditRowFilterModalPopup(filterButtonRef.current) + })); + entries.push(new ContextMenuSeparator()); + if (!filtersHidden) { + entries.push(new ContextMenuItem({ + label: localize('positron.dataExplorer.hideFilters', "Hide filters"), + icon: 'positron-hide-filters', + disabled: rowFilters.length === 0, + onSelected: () => setFiltersHidden(true) + })); + } else { + entries.push(new ContextMenuItem({ + label: localize('positron.dataExplorer.showFilters', "Show filters"), + icon: 'positron-show-filters', + onSelected: () => setFiltersHidden(false) + })); + } + entries.push(new ContextMenuSeparator()); + entries.push(new ContextMenuItem({ + label: localize('positron.dataExplorer.clearFilters', "Clear filters"), + icon: 'positron-clear-row-filters', + disabled: rowFilters.length === 0, + onSelected: () => setRowFilters([]) + })); + + // Show the context menu. + await showContextMenu( + context.keybindingService, + context.layoutService, + filterButtonRef.current, + 'left', + 200, + entries + ); + }; + + /** + * Clears the row filter at the specified row filter index. + * @param rowFilterIndex The row filter index. + */ + const clearRowFilter = (identifier: string) => { + setRowFilters(rowFilters => rowFilters.filter(rowFilter => + identifier !== rowFilter.identifier + )); + }; + + // Render. + return ( +
+ +
+ {!filtersHidden && rowFilters.map((rowFilter, index) => + { + if (ref) { + rowFilterWidgetRefs.current[index] = ref; + } + }} + key={index} + rowFilter={rowFilter} + booleanOperator={index ? 'and' : undefined} + onEdit={() => { + if (rowFilterWidgetRefs.current[index]) { + showAddEditRowFilterModalPopup( + rowFilterWidgetRefs.current[index], + rowFilter + ); + } + }} + onClear={() => clearRowFilter(rowFilter.identifier)} /> + )} + +
+
+ ); +}; diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/dataExplorerPanel.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/dataExplorerPanel.tsx index 253ef41dd94..31bc3e7d879 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/dataExplorerPanel.tsx +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/dataExplorerPanel.tsx @@ -2,16 +2,16 @@ * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -// CSS> +// CSS. import 'vs/css!./dataExplorerPanel'; // React. import * as React from 'react'; // Other dependencies. -import { FilterBar } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/filterBar'; import { StatusBar } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/statusBar'; import { DataExplorer } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/dataExplorer'; +import { RowFilterBar } from 'vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/rowFilterBar/rowFilterBar'; /** * DataExplorerPanel component. @@ -21,7 +21,7 @@ export const DataExplorerPanel = () => { // Render. return (
- +
From 31ea67f4c6f626bb5db9a148b833436323a5e204 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Mon, 1 Apr 2024 18:05:57 -0500 Subject: [PATCH 3/6] Try --no-dependencies instead --- .github/workflows/positron-python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/positron-python-ci.yml b/.github/workflows/positron-python-ci.yml index 498137d59a6..007d4f810dc 100644 --- a/.github/workflows/positron-python-ci.yml +++ b/.github/workflows/positron-python-ci.yml @@ -235,7 +235,7 @@ jobs: run: | python -m pip config set global.index-url https://packagemanager.posit.co/pypi/${{ env.SNAPSHOT_DATE }}/simple python -m pip config set global.trusted-host packagemanager.posit.co - python -m pip install --prefer-binary --force-reinstall -r pythonFiles/positron/data-science-requirements.txt + python -m pip install --prefer-binary --force-reinstall --no-dependencies -r pythonFiles/positron/data-science-requirements.txt - name: Run Positron IPyKernel unit tests run: pytest pythonFiles/positron From 4ead84be6af0a7ead1ec3a16eb15df786b3b568e Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Mon, 1 Apr 2024 18:40:14 -0500 Subject: [PATCH 4/6] Temporarily disable snapshot CI tests (#2596) polars forces a downgrade of the typing_extensions library which breaks newer IPython in CI. This temporarily disables these tests until we can sort out a proper fix to unblock other PRs. It also makes sure that these tests run when the CI configuration changes. --- .github/workflows/positron-python-ci.yml | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/positron-python-ci.yml b/.github/workflows/positron-python-ci.yml index 007d4f810dc..cb86336b1c8 100644 --- a/.github/workflows/positron-python-ci.yml +++ b/.github/workflows/positron-python-ci.yml @@ -5,11 +5,13 @@ on: branches: - main paths: + - '.github/workflows/positron-python-ci.yml' - 'extensions/positron-python/**' pull_request: branches: - main paths: + - '.github/workflows/positron-python-ci.yml' - 'extensions/positron-python/**' @@ -196,18 +198,18 @@ jobs: - os: 'ubuntu-latest' python: '3.12' time-elapsed: '' - - os: 'ubuntu-latest' - python: '3.10' - time-elapsed: '3 months' - - os: 'ubuntu-latest' - python: '3.10' - time-elapsed: '6 months' - - os: 'ubuntu-latest' - python: '3.10' - time-elapsed: '9 months' - - os: 'ubuntu-latest' - python: '3.10' - time-elapsed: '1 year' + # - os: 'ubuntu-latest' + # python: '3.10' + # time-elapsed: '3 months' + # - os: 'ubuntu-latest' + # python: '3.10' + # time-elapsed: '6 months' + # - os: 'ubuntu-latest' + # python: '3.10' + # time-elapsed: '9 months' + # - os: 'ubuntu-latest' + # python: '3.10' + # time-elapsed: '1 year' steps: - name: Checkout @@ -235,7 +237,7 @@ jobs: run: | python -m pip config set global.index-url https://packagemanager.posit.co/pypi/${{ env.SNAPSHOT_DATE }}/simple python -m pip config set global.trusted-host packagemanager.posit.co - python -m pip install --prefer-binary --force-reinstall --no-dependencies -r pythonFiles/positron/data-science-requirements.txt + python -m pip install --prefer-binary --force-reinstall -r pythonFiles/positron/data-science-requirements.txt - name: Run Positron IPyKernel unit tests run: pytest pythonFiles/positron From 50b92a92781c50f0ffa029d788786de5387ceb8e Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Mon, 1 Apr 2024 19:12:50 -0500 Subject: [PATCH 5/6] Implement accumulated improvements to Data Explorer backend protocol, add batch profile requests. In Python implement between, search filter types, prototype search_schema RPC, improve filter API (#2585) * Change column profiles to come in batches Work on search_schema RPC Ported changes from positron-python Add column_index Drafting more APIs per discussions Initial fixes for protocol changes format * Refactor profile results to use pseudo-union-like pattern * Stub more things out, normalize profile type names * Rename column_filter to row_filter * Implement initial search_schema, fix some unit tests * Add tests for search_schema and fix issues * Add tests for between * Basic implementations of text search filter type * Hack in batch null counts * Revert changes to ui_comm.py * Fix ruff flakes * Avoid unwanted side effects when directly modifying shell.user_ns for unit tests --- .../positron_ipykernel/data_explorer.py | 376 ++++++++++---- .../positron_ipykernel/data_explorer_comm.py | 482 ++++++++++++------ .../tests/test_data_explorer.py | 438 +++++++++++++--- .../comms/data_explorer-backend-openrpc.json | 429 +++++++++++----- .../languageRuntimeDataExplorerClient.ts | 13 +- .../common/positronDataExplorerComm.ts | 385 +++++++++----- .../browser/components/columnSummaryCell.tsx | 2 +- .../browser/tableSummaryDataGridInstance.tsx | 7 + .../common/dataExplorerCache.ts | 57 ++- .../common/positronDataExplorerMocks.ts | 49 +- .../common/positronDataExplorerMocks.test.ts | 43 +- 11 files changed, 1656 insertions(+), 625 deletions(-) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py index 8b3ddb6576b..7fe12776473 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer.py @@ -5,6 +5,7 @@ # flake8: ignore E203 # pyright: reportOptionalMemberAccess=false +import abc import logging import operator import uuid @@ -23,21 +24,29 @@ from .access_keys import decode_access_key from .data_explorer_comm import ( - ColumnFilter, - ColumnFilterCompareOp, + ColumnFrequencyTable, + ColumnHistogram, + ColumnSummaryStats, + CompareFilterParamsOp, + ColumnProfileRequestType, + ColumnProfileResult, ColumnSchema, ColumnSchemaTypeDisplay, ColumnSortKey, DataExplorerBackendMessageContent, DataExplorerFrontendEvent, FilterResult, - GetColumnProfileProfileType, - GetColumnProfileRequest, + GetColumnProfilesRequest, GetDataValuesRequest, GetSchemaRequest, GetStateRequest, + RowFilter, + RowFilterFilterType, SchemaUpdateParams, - SetColumnFiltersRequest, + SearchFilterParamsType, + SearchSchemaRequest, + SearchSchemaResult, + SetRowFiltersRequest, SetSortColumnsRequest, TableData, TableSchema, @@ -60,7 +69,7 @@ PathKey = Tuple[str, ...] -class DataExplorerTableView: +class DataExplorerTableView(abc.ABC): """ Interface providing a consistent wrapper around different data frame / table types for the data explorer for serving requests from @@ -71,7 +80,7 @@ class DataExplorerTableView: def __init__( self, table, - filters: Optional[List[ColumnFilter]], + filters: Optional[List[RowFilter]], sort_keys: Optional[List[ColumnSortKey]], ): # Note: we must not ever modify the user's data @@ -82,13 +91,8 @@ def __init__( self._need_recompute = len(self.filters) > 0 or len(self.sort_keys) > 0 - def invalidate_computations(self): - raise NotImplementedError - - def ui_should_update_schema(self, new_table): - raise NotImplementedError - - def ui_should_update_data(self, new_table): + @abc.abstractmethod + def _recompute(self): raise NotImplementedError def _recompute_if_needed(self) -> bool: @@ -99,12 +103,16 @@ def _recompute_if_needed(self) -> bool: else: return False - def _recompute(self): - raise NotImplementedError - def get_schema(self, request: GetSchemaRequest): return self._get_schema(request.params.start_index, request.params.num_columns).dict() + def search_schema(self, request: SearchSchemaRequest): + return self._search_schema( + request.params.search_term, + request.params.start_index, + request.params.max_results, + ).dict() + def get_data_values(self, request: GetDataValuesRequest): self._recompute_if_needed() return self._get_data_values( @@ -113,8 +121,8 @@ def get_data_values(self, request: GetDataValuesRequest): request.params.column_indices, ).dict() - def set_column_filters(self, request: SetColumnFiltersRequest): - return self._set_column_filters(request.params.filters) + def set_row_filters(self, request: SetRowFiltersRequest): + return self._set_row_filters(request.params.filters) def set_sort_columns(self, request: SetSortColumnsRequest): self.sort_keys = request.params.sort_keys @@ -124,39 +132,90 @@ def set_sort_columns(self, request: SetSortColumnsRequest): # trigger a sort self._sort_data() - def get_column_profile(self, request: GetColumnProfileRequest): + def get_column_profiles(self, request: GetColumnProfilesRequest): self._recompute_if_needed() - return self._get_column_profile(request.params.profile_type, request.params.column_index) + results = [] + + for req in request.params.profiles: + if req.type == ColumnProfileRequestType.NullCount: + count = self._prof_null_count(req.column_index) + result = ColumnProfileResult(null_count=count) + elif req.type == ColumnProfileRequestType.SummaryStats: + stats = self._prof_summary_stats(req.column_index) + result = ColumnProfileResult(summary_stats=stats) + elif req.type == ColumnProfileRequestType.FrequencyTable: + freq_table = self._prof_freq_table(req.column_index) + result = ColumnProfileResult(frequency_table=freq_table) + elif req.type == ColumnProfileRequestType.Histogram: + histogram = self._prof_histogram(req.column_index) + result = ColumnProfileResult(histogram=histogram) + else: + raise NotImplementedError(req.type) + results.append(result.dict()) + + return results def get_state(self, request: GetStateRequest): return self._get_state().dict() + @abc.abstractmethod + def invalidate_computations(self): + pass + + @abc.abstractmethod + def ui_should_update_schema(self, new_table) -> Tuple[bool, bool]: + pass + + @abc.abstractmethod + def ui_should_update_data(self, new_table) -> bool: + pass + + @abc.abstractmethod def _get_schema(self, column_start: int, num_columns: int) -> TableSchema: - raise NotImplementedError + pass + @abc.abstractmethod + def _search_schema( + self, search_term: str, start_index: int, max_results: int + ) -> SearchSchemaResult: + pass + + @abc.abstractmethod def _get_data_values( self, row_start: int, num_rows: int, column_indices: Sequence[int], ) -> TableData: - raise NotImplementedError + pass - def _set_column_filters(self, filters: List[ColumnFilter]) -> FilterResult: - raise NotImplementedError + @abc.abstractmethod + def _set_row_filters(self, filters: List[RowFilter]) -> FilterResult: + pass - def _sort_data(self) -> None: - raise NotImplementedError + @abc.abstractmethod + def _sort_data(self): + pass - def _get_column_profile( - self, - profile_type: GetColumnProfileProfileType, - column_index: int, - ) -> None: - raise NotImplementedError + @abc.abstractmethod + def _prof_null_count(self, column_index: int) -> int: + pass + + @abc.abstractmethod + def _prof_summary_stats(self, column_index: int) -> ColumnSummaryStats: + pass + + @abc.abstractmethod + def _prof_freq_table(self, column_index: int) -> ColumnFrequencyTable: + pass + + @abc.abstractmethod + def _prof_histogram(self, column_index: int) -> ColumnHistogram: + pass + @abc.abstractmethod def _get_state(self) -> TableState: - raise NotImplementedError + pass def _pandas_format_values(col): @@ -203,7 +262,7 @@ class PandasView(DataExplorerTableView): def __init__( self, table, - filters: Optional[List[ColumnFilter]], + filters: Optional[List[RowFilter]], sort_keys: Optional[List[ColumnSortKey]], ): super().__init__(table, filters, sort_keys) @@ -227,6 +286,14 @@ def __init__( # self.filtered_indices self.view_indices = None + # We store a tuple of (last_search_term, matches) + # here so that we can support scrolling through the search + # results without having to recompute the search. If the + # search term changes, we discard the last search result. We + # might add an LRU cache here or something if it helps + # performance. + self._search_schema_last_result: Optional[Tuple[str, List[ColumnSchema]]] = None + def invalidate_computations(self): self.filtered_indices = self.view_indices = None self._need_recompute = True @@ -255,7 +322,7 @@ def ui_should_update_data(self, new_table): def _recompute(self): # Resetting the column filters will trigger filtering AND # sorting - self._set_column_filters(self.filters) + self._set_row_filters(self.filters) @property def dtypes(self): @@ -264,36 +331,82 @@ def dtypes(self): return self._dtypes def _get_schema(self, column_start: int, num_columns: int) -> TableSchema: + column_schemas = [] + + for column_index in range( + column_start, + min(column_start + num_columns, len(self.table.columns)), + ): + column_raw_name = self.table.columns[column_index] + column_name = str(column_raw_name) + + col_schema = self._get_single_column_schema(column_index, column_name) + column_schemas.append(col_schema) + + return TableSchema(columns=column_schemas) + + def _search_schema( + self, search_term: str, start_index: int, max_results: int + ) -> SearchSchemaResult: + # Sanitize user input here for now, possibly remove this later + search_term = search_term.lower() + + if self._search_schema_last_result is not None: + last_search_term, matches = self._search_schema_last_result + if last_search_term != search_term: + matches = self._search_schema_get_matches(search_term) + self._search_schema_last_result = (search_term, matches) + else: + matches = self._search_schema_get_matches(search_term) + self._search_schema_last_result = (search_term, matches) + + matches_slice = matches[start_index : start_index + max_results] + return SearchSchemaResult( + matches=TableSchema(columns=matches_slice), + total_num_matches=len(matches), + ) + + def _search_schema_get_matches(self, search_term: str) -> List[ColumnSchema]: + matches = [] + for column_index in range(len(self.table.columns)): + column_raw_name = self.table.columns[column_index] + column_name = str(column_raw_name) + + # Do a case-insensitive search + if search_term not in column_name.lower(): + continue + + col_schema = self._get_single_column_schema(column_index, column_name) + matches.append(col_schema) + + return matches + + def _get_inferred_dtype(self, column_index: int): from pandas.api.types import infer_dtype + if column_index not in self._inferred_dtypes: + self._inferred_dtypes[column_index] = infer_dtype(self.table.iloc[:, column_index]) + return self._inferred_dtypes[column_index] + + def _get_single_column_schema(self, column_index: int, column_name: str): # TODO: pandas MultiIndex columns # TODO: time zone for datetimetz datetime64[ns] types - columns_slice = self.table.columns[column_start : column_start + num_columns] - dtypes_slice = self.dtypes.iloc[column_start : column_start + num_columns] - column_schemas = [] + dtype = self.dtypes.iloc[column_index] - for i, (c, dtype) in enumerate(zip(columns_slice, dtypes_slice)): - if dtype == object: - column_index = i + column_start - if column_index not in self._inferred_dtypes: - self._inferred_dtypes[column_index] = infer_dtype( - self.table.iloc[:, column_index] - ) - type_name = self._inferred_dtypes[column_index] - else: - # TODO: more sophisticated type mapping - type_name = str(dtype) - - type_display = self.TYPE_DISPLAY_MAPPING.get(type_name, "unknown") + if dtype == object: + type_name = self._get_inferred_dtype(column_index) + else: + # TODO: more sophisticated type mapping + type_name = str(dtype) - col_schema = ColumnSchema( - column_name=str(c), - type_name=type_name, - type_display=ColumnSchemaTypeDisplay(type_display), - ) - column_schemas.append(col_schema) + type_display = self.TYPE_DISPLAY_MAPPING.get(type_name, "unknown") - return TableSchema(columns=column_schemas) + return ColumnSchema( + column_name=column_name, + column_index=column_index, + type_name=type_name, + type_display=ColumnSchemaTypeDisplay(type_display), + ) def _get_data_values( self, row_start: int, num_rows: int, column_indices: Sequence[int] @@ -345,7 +458,7 @@ def _update_view_indices(self): # reflect the filtered_indices that have just been updated self._sort_data() - def _set_column_filters(self, filters) -> FilterResult: + def _set_row_filters(self, filters) -> FilterResult: self.filters = filters if len(filters) == 0: @@ -357,7 +470,7 @@ def _set_column_filters(self, filters) -> FilterResult: # Evaluate all the filters and AND them together combined_mask = None for filt in filters: - single_mask = _pandas_eval_filter(self.table, filt) + single_mask = self._eval_filter(filt) if combined_mask is None: combined_mask = single_mask else: @@ -369,6 +482,74 @@ def _set_column_filters(self, filters) -> FilterResult: self._update_view_indices() return FilterResult(selected_num_rows=len(self.filtered_indices)) + def _eval_filter(self, filt: RowFilter): + col = self.table.iloc[:, filt.column_index] + mask = None + if filt.filter_type in ( + RowFilterFilterType.Between, + RowFilterFilterType.NotBetween, + ): + params = filt.between_params + assert params is not None + left_value = _coerce_value_param(params.left_value, col.dtype) + right_value = _coerce_value_param(params.right_value, col.dtype) + if filt.filter_type == RowFilterFilterType.Between: + mask = (col >= left_value) & (col <= right_value) + else: + # NotBetween + mask = (col < left_value) | (col > right_value) + elif filt.filter_type == RowFilterFilterType.Compare: + params = filt.compare_params + assert params is not None + + if params.op not in COMPARE_OPS: + raise ValueError(f"Unsupported filter type: {params.op}") + op = COMPARE_OPS[params.op] + # pandas comparison filters return False for null values + mask = op(col, _coerce_value_param(params.value, col.dtype)) + elif filt.filter_type == RowFilterFilterType.IsNull: + mask = col.isnull() + elif filt.filter_type == RowFilterFilterType.NotNull: + mask = col.notnull() + elif filt.filter_type == RowFilterFilterType.SetMembership: + params = filt.set_membership_params + assert params is not None + boxed_values = pd_.Series(params.values).astype(col.dtype) + # IN + mask = col.isin(boxed_values) + if not params.inclusive: + # NOT-IN + mask = ~mask + elif filt.filter_type == RowFilterFilterType.Search: + params = filt.search_params + assert params is not None + + col_inferred_type = self._get_inferred_dtype(filt.column_index) + + if col_inferred_type != "string": + col = col.astype(str) + + term = params.term + + if params.type == SearchFilterParamsType.RegexMatch: + mask = col.str.match(term, case=params.case_sensitive) + else: + if not params.case_sensitive: + col = col.str.lower() + term = term.lower() + if params.type == SearchFilterParamsType.Contains: + mask = col.str.contains(term) + elif params.type == SearchFilterParamsType.StartsWith: + mask = col.str.startswith(term) + elif params.type == SearchFilterParamsType.EndsWith: + mask = col.str.endswith(term) + + # Nulls are possible in the mask, so we just fill them if any + if mask.dtype != bool: + mask = mask.fillna(False) + + return mask.to_numpy() + def _sort_data(self) -> None: from pandas.core.sorting import lexsort_indexer, nargsort @@ -396,10 +577,8 @@ def _sort_data(self) -> None: cols_to_sort = [] directions = [] for key in self.sort_keys: - column = self.table.iloc[:, key.column_index] - if self.filtered_indices is not None: - column = column.take(self.filtered_indices) - cols_to_sort.append(column) + col = self._get_column(key.column_index) + cols_to_sort.append(col) directions.append(key.ascending) # lexsort_indexer uses np.lexsort and so is always stable @@ -413,57 +592,46 @@ def _sort_data(self) -> None: # This will be None if the data is unfiltered self.view_indices = self.filtered_indices - def _get_column_profile( - self, profile_type: GetColumnProfileProfileType, column_index: int - ) -> None: - pass + def _get_column(self, column_index: int) -> "pd.Series": + column = self.table.iloc[:, column_index] + if self.filtered_indices is not None: + column = column.take(self.filtered_indices) + return column + + def _prof_null_count(self, column_index: int): + return self._get_column(column_index).isnull().sum() + + def _prof_summary_stats(self, column_index: int): + raise NotImplementedError + + def _prof_freq_table(self, column_index: int): + raise NotImplementedError + + def _prof_histogram(self, column_index: int): + raise NotImplementedError def _get_state(self) -> TableState: return TableState( table_shape=TableShape(num_rows=self.table.shape[0], num_columns=self.table.shape[1]), - filters=self.filters, + row_filters=self.filters, sort_keys=self.sort_keys, ) COMPARE_OPS = { - ColumnFilterCompareOp.Gt: operator.gt, - ColumnFilterCompareOp.GtEq: operator.ge, - ColumnFilterCompareOp.Lt: operator.lt, - ColumnFilterCompareOp.LtEq: operator.le, - ColumnFilterCompareOp.Eq: operator.eq, - ColumnFilterCompareOp.NotEq: operator.ne, + CompareFilterParamsOp.Gt: operator.gt, + CompareFilterParamsOp.GtEq: operator.ge, + CompareFilterParamsOp.Lt: operator.lt, + CompareFilterParamsOp.LtEq: operator.le, + CompareFilterParamsOp.Eq: operator.eq, + CompareFilterParamsOp.NotEq: operator.ne, } -def _pandas_eval_filter(df: "pd.DataFrame", filt: ColumnFilter): - col = df.iloc[:, filt.column_index] - mask = None - if filt.filter_type == "compare": - if filt.compare_op not in COMPARE_OPS: - raise ValueError(f"Unsupported filter type: {filt.compare_op}") - op = COMPARE_OPS[filt.compare_op] - # Let pandas decide how to coerce the string we got from the UI - dummy = pd_.Series([filt.compare_value]).astype(col.dtype) - - # pandas comparison filters return False for null values - mask = op(col, dummy.iloc[0]) - elif filt.filter_type == "isnull": - mask = col.isnull() - elif filt.filter_type == "notnull": - mask = col.notnull() - elif filt.filter_type == "set_membership": - boxed_values = pd_.Series(filt.set_member_values).astype(col.dtype) - # IN - mask = col.isin(boxed_values) - if not filt.set_member_inclusive: - # NOT-IN - mask = ~mask - elif filt.filter_type == "search": - raise NotImplementedError - - # TODO(wesm): is it possible for there to be null values in the mask? - return mask.to_numpy() +def _coerce_value_param(value, dtype): + # Let pandas decide how to coerce the string we got from the UI + dummy = pd_.Series([value]).astype(dtype) + return dummy.iloc[0] class PolarsView(DataExplorerTableView): diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py index 25d1133d4d9..5b832565a3f 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/data_explorer_comm.py @@ -17,17 +17,6 @@ from ._vendor.pydantic import BaseModel, Field -@enum.unique -class GetColumnProfileProfileType(str, enum.Enum): - """ - Possible values for ProfileType in GetColumnProfile - """ - - Freqtable = "freqtable" - - Histogram = "histogram" - - @enum.unique class ColumnSchemaTypeDisplay(str, enum.Enum): """ @@ -54,26 +43,30 @@ class ColumnSchemaTypeDisplay(str, enum.Enum): @enum.unique -class ColumnFilterFilterType(str, enum.Enum): +class RowFilterFilterType(str, enum.Enum): """ - Possible values for FilterType in ColumnFilter + Possible values for FilterType in RowFilter """ - Isnull = "isnull" - - Notnull = "notnull" + Between = "between" Compare = "compare" - SetMembership = "set_membership" + IsNull = "is_null" + + NotBetween = "not_between" + + NotNull = "not_null" Search = "search" + SetMembership = "set_membership" + @enum.unique -class ColumnFilterCompareOp(str, enum.Enum): +class CompareFilterParamsOp(str, enum.Enum): """ - Possible values for CompareOp in ColumnFilter + Possible values for Op in CompareFilterParams """ Eq = "=" @@ -90,27 +83,47 @@ class ColumnFilterCompareOp(str, enum.Enum): @enum.unique -class ColumnFilterSearchType(str, enum.Enum): +class SearchFilterParamsType(str, enum.Enum): """ - Possible values for SearchType in ColumnFilter + Possible values for Type in SearchFilterParams """ Contains = "contains" - Startswith = "startswith" + StartsWith = "starts_with" - Endswith = "endswith" + EndsWith = "ends_with" - Regex = "regex" + RegexMatch = "regex_match" -class TableSchema(BaseModel): +@enum.unique +class ColumnProfileRequestType(str, enum.Enum): """ - The schema for a table-like object + Possible values for Type in ColumnProfileRequest """ - columns: List[ColumnSchema] = Field( - description="Schema for each column in the table", + NullCount = "null_count" + + SummaryStats = "summary_stats" + + FrequencyTable = "frequency_table" + + Histogram = "histogram" + + +class SearchSchemaResult(BaseModel): + """ + Result in Methods + """ + + matches: Optional[TableSchema] = Field( + default=None, + description="A schema containing matching columns up to the max_results limit", + ) + + total_num_matches: int = Field( + description="The total number of columns matching the search term", ) @@ -139,70 +152,6 @@ class FilterResult(BaseModel): ) -class ProfileResult(BaseModel): - """ - Result of computing column profile - """ - - null_count: int = Field( - description="Number of null values in column", - ) - - min_value: Optional[str] = Field( - default=None, - description="Minimum value as string computed as part of histogram", - ) - - max_value: Optional[str] = Field( - default=None, - description="Maximum value as string computed as part of histogram", - ) - - mean_value: Optional[str] = Field( - default=None, - description="Average value as string computed as part of histogram", - ) - - histogram_bin_sizes: Optional[List[int]] = Field( - default=None, - description="Absolute count of values in each histogram bin", - ) - - histogram_bin_width: Optional[float] = Field( - default=None, - description="Absolute floating-point width of a histogram bin", - ) - - histogram_quantiles: Optional[List[ColumnQuantileValue]] = Field( - default=None, - description="Quantile values computed from histogram bins", - ) - - freqtable_counts: Optional[List[FreqtableCounts]] = Field( - default=None, - description="Counts of distinct values in column", - ) - - freqtable_other_count: Optional[int] = Field( - default=None, - description="Number of other values not accounted for in counts", - ) - - -class FreqtableCounts(BaseModel): - """ - Items in FreqtableCounts - """ - - value: str = Field( - description="Stringified value", - ) - - count: int = Field( - description="Number of occurrences of value", - ) - - class TableState(BaseModel): """ The current backend table state @@ -212,8 +161,9 @@ class TableState(BaseModel): description="Provides number of rows and columns in table", ) - filters: List[ColumnFilter] = Field( - description="The set of currently applied filters", + row_filters: Optional[List[RowFilter]] = Field( + default=None, + description="The set of currently applied row filters", ) sort_keys: List[ColumnSortKey] = Field( @@ -244,6 +194,10 @@ class ColumnSchema(BaseModel): description="Name of column as UTF-8 string", ) + column_index: int = Field( + description="The position of the column within the schema", + ) + type_name: str = Field( description="Exact name of data type used by underlying table", ) @@ -283,16 +237,26 @@ class ColumnSchema(BaseModel): ) -class ColumnFilter(BaseModel): +class TableSchema(BaseModel): + """ + The schema for a table-like object + """ + + columns: List[ColumnSchema] = Field( + description="Schema for each column in the table", + ) + + +class RowFilter(BaseModel): """ - Specifies a table row filter based on a column's values + Specifies a table row filter based on a single column's values """ filter_id: str = Field( description="Unique identifier for this filter", ) - filter_type: ColumnFilterFilterType = Field( + filter_type: RowFilterFilterType = Field( description="Type of filter to apply", ) @@ -300,42 +264,203 @@ class ColumnFilter(BaseModel): description="Column index to apply filter to", ) - compare_op: Optional[ColumnFilterCompareOp] = Field( + between_params: Optional[BetweenFilterParams] = Field( default=None, - description="String representation of a binary comparison", + description="Parameters for the 'between' and 'not_between' filter types", ) - compare_value: Optional[str] = Field( + compare_params: Optional[CompareFilterParams] = Field( default=None, - description="A stringified column value for a comparison filter", + description="Parameters for the 'compare' filter type", ) - set_member_values: Optional[List[str]] = Field( + search_params: Optional[SearchFilterParams] = Field( default=None, - description="Array of column values for a set membership filter", + description="Parameters for the 'search' filter type", ) - set_member_inclusive: Optional[bool] = Field( + set_membership_params: Optional[SetMembershipFilterParams] = Field( default=None, + description="Parameters for the 'set_membership' filter type", + ) + + +class BetweenFilterParams(BaseModel): + """ + Parameters for the 'between' and 'not_between' filter types + """ + + left_value: str = Field( + description="The lower limit for filtering", + ) + + right_value: str = Field( + description="The upper limit for filtering", + ) + + +class CompareFilterParams(BaseModel): + """ + Parameters for the 'compare' filter type + """ + + op: CompareFilterParamsOp = Field( + description="String representation of a binary comparison", + ) + + value: str = Field( + description="A stringified column value for a comparison filter", + ) + + +class SetMembershipFilterParams(BaseModel): + """ + Parameters for the 'set_membership' filter type + """ + + values: List[str] = Field( + description="Array of column values for a set membership filter", + ) + + inclusive: bool = Field( description="Filter by including only values passed (true) or excluding (false)", ) - search_type: Optional[ColumnFilterSearchType] = Field( - default=None, + +class SearchFilterParams(BaseModel): + """ + Parameters for the 'search' filter type + """ + + type: SearchFilterParamsType = Field( description="Type of search to perform", ) - search_term: Optional[str] = Field( - default=None, + term: str = Field( description="String value/regex to search for in stringified data", ) - search_case_sensitive: Optional[bool] = Field( - default=None, + case_sensitive: bool = Field( description="If true, do a case-sensitive search, otherwise case-insensitive", ) +class ColumnProfileRequest(BaseModel): + """ + A single column profile request + """ + + column_index: int = Field( + description="The ordinal column index to profile", + ) + + type: ColumnProfileRequestType = Field( + description="The type of analytical column profile", + ) + + +class ColumnProfileResult(BaseModel): + """ + Result of computing column profile + """ + + null_count: Optional[int] = Field( + default=None, + description="Result from null_count request", + ) + + summary_stats: Optional[ColumnSummaryStats] = Field( + default=None, + description="Results from summary_stats request", + ) + + histogram: Optional[ColumnHistogram] = Field( + default=None, + description="Results from summary_stats request", + ) + + frequency_table: Optional[ColumnFrequencyTable] = Field( + default=None, + description="Results from frequency_table request", + ) + + +class ColumnSummaryStats(BaseModel): + """ + ColumnSummaryStats in Schemas + """ + + min_value: str = Field( + description="Minimum value as string", + ) + + max_value: str = Field( + description="Maximum value as string", + ) + + mean_value: Optional[str] = Field( + default=None, + description="Average value as string", + ) + + median: Optional[str] = Field( + default=None, + description="Sample median (50% value) value as string", + ) + + q25: Optional[str] = Field( + default=None, + description="25th percentile value as string", + ) + + q75: Optional[str] = Field( + default=None, + description="75th percentile value as string", + ) + + +class ColumnHistogram(BaseModel): + """ + Result from a histogram profile request + """ + + bin_sizes: List[int] = Field( + description="Absolute count of values in each histogram bin", + ) + + bin_width: float = Field( + description="Absolute floating-point width of a histogram bin", + ) + + +class ColumnFrequencyTable(BaseModel): + """ + Result from a frequency_table profile request + """ + + counts: List[ColumnFrequencyTableItem] = Field( + description="Counts of distinct values in column", + ) + + other_count: int = Field( + description="Number of other values not accounted for in counts. May be 0", + ) + + +class ColumnFrequencyTableItem(BaseModel): + """ + Entry in a column's frequency table + """ + + value: str = Field( + description="Stringified value", + ) + + count: int = Field( + description="Number of occurrences of value", + ) + + class ColumnQuantileValue(BaseModel): """ An exact or approximate quantile value from a column @@ -377,17 +502,20 @@ class DataExplorerBackendRequest(str, enum.Enum): # Request schema GetSchema = "get_schema" + # Search schema by column name + SearchSchema = "search_schema" + # Get a rectangle of data values GetDataValues = "get_data_values" - # Set column filters - SetColumnFilters = "set_column_filters" + # Set row filters based on column values + SetRowFilters = "set_row_filters" # Set or clear sort-by-column(s) SetSortColumns = "set_sort_columns" - # Get a column profile - GetColumnProfile = "get_column_profile" + # Request a batch of column profiles + GetColumnProfiles = "get_column_profiles" # Get the state GetState = "get_state" @@ -426,6 +554,43 @@ class GetSchemaRequest(BaseModel): ) +class SearchSchemaParams(BaseModel): + """ + Search schema for column names matching a passed substring + """ + + search_term: str = Field( + description="Substring to match for (currently case insensitive", + ) + + start_index: int = Field( + description="Index (starting from zero) of first result to fetch", + ) + + max_results: int = Field( + description="Maximum number of resulting column schemas to fetch from the start index", + ) + + +class SearchSchemaRequest(BaseModel): + """ + Search schema for column names matching a passed substring + """ + + params: SearchSchemaParams = Field( + description="Parameters to the SearchSchema method", + ) + + method: Literal[DataExplorerBackendRequest.SearchSchema] = Field( + description="The JSON-RPC method name (search_schema)", + ) + + jsonrpc: str = Field( + default="2.0", + description="The JSON-RPC version specifier", + ) + + class GetDataValuesParams(BaseModel): """ Request a rectangular subset of data with values formatted as strings @@ -463,27 +628,27 @@ class GetDataValuesRequest(BaseModel): ) -class SetColumnFiltersParams(BaseModel): +class SetRowFiltersParams(BaseModel): """ - Set or clear column filters on table, replacing any previous filters + Set or clear row filters on table, replacing any previous filters """ - filters: List[ColumnFilter] = Field( + filters: List[RowFilter] = Field( description="Zero or more filters to apply", ) -class SetColumnFiltersRequest(BaseModel): +class SetRowFiltersRequest(BaseModel): """ - Set or clear column filters on table, replacing any previous filters + Set or clear row filters on table, replacing any previous filters """ - params: SetColumnFiltersParams = Field( - description="Parameters to the SetColumnFilters method", + params: SetRowFiltersParams = Field( + description="Parameters to the SetRowFilters method", ) - method: Literal[DataExplorerBackendRequest.SetColumnFilters] = Field( - description="The JSON-RPC method name (set_column_filters)", + method: Literal[DataExplorerBackendRequest.SetRowFilters] = Field( + description="The JSON-RPC method name (set_row_filters)", ) jsonrpc: str = Field( @@ -523,31 +688,27 @@ class SetSortColumnsRequest(BaseModel): ) -class GetColumnProfileParams(BaseModel): +class GetColumnProfilesParams(BaseModel): """ - Requests a statistical summary or data profile for a column + Requests a statistical summary or data profile for batch of columns """ - profile_type: GetColumnProfileProfileType = Field( - description="The type of analytical column profile", - ) - - column_index: int = Field( - description="Column index to compute profile for", + profiles: List[ColumnProfileRequest] = Field( + description="Array of requested profiles", ) -class GetColumnProfileRequest(BaseModel): +class GetColumnProfilesRequest(BaseModel): """ - Requests a statistical summary or data profile for a column + Requests a statistical summary or data profile for batch of columns """ - params: GetColumnProfileParams = Field( - description="Parameters to the GetColumnProfile method", + params: GetColumnProfilesParams = Field( + description="Parameters to the GetColumnProfiles method", ) - method: Literal[DataExplorerBackendRequest.GetColumnProfile] = Field( - description="The JSON-RPC method name (get_column_profile)", + method: Literal[DataExplorerBackendRequest.GetColumnProfiles] = Field( + description="The JSON-RPC method name (get_column_profiles)", ) jsonrpc: str = Field( @@ -575,10 +736,11 @@ class DataExplorerBackendMessageContent(BaseModel): comm_id: str data: Union[ GetSchemaRequest, + SearchSchemaRequest, GetDataValuesRequest, - SetColumnFiltersRequest, + SetRowFiltersRequest, SetSortColumnsRequest, - GetColumnProfileRequest, + GetColumnProfilesRequest, GetStateRequest, ] = Field(..., discriminator="method") @@ -606,23 +768,41 @@ class SchemaUpdateParams(BaseModel): ) -TableSchema.update_forward_refs() +SearchSchemaResult.update_forward_refs() TableData.update_forward_refs() FilterResult.update_forward_refs() -ProfileResult.update_forward_refs() - -FreqtableCounts.update_forward_refs() - TableState.update_forward_refs() TableShape.update_forward_refs() ColumnSchema.update_forward_refs() -ColumnFilter.update_forward_refs() +TableSchema.update_forward_refs() + +RowFilter.update_forward_refs() + +BetweenFilterParams.update_forward_refs() + +CompareFilterParams.update_forward_refs() + +SetMembershipFilterParams.update_forward_refs() + +SearchFilterParams.update_forward_refs() + +ColumnProfileRequest.update_forward_refs() + +ColumnProfileResult.update_forward_refs() + +ColumnSummaryStats.update_forward_refs() + +ColumnHistogram.update_forward_refs() + +ColumnFrequencyTable.update_forward_refs() + +ColumnFrequencyTableItem.update_forward_refs() ColumnQuantileValue.update_forward_refs() @@ -632,21 +812,25 @@ class SchemaUpdateParams(BaseModel): GetSchemaRequest.update_forward_refs() +SearchSchemaParams.update_forward_refs() + +SearchSchemaRequest.update_forward_refs() + GetDataValuesParams.update_forward_refs() GetDataValuesRequest.update_forward_refs() -SetColumnFiltersParams.update_forward_refs() +SetRowFiltersParams.update_forward_refs() -SetColumnFiltersRequest.update_forward_refs() +SetRowFiltersRequest.update_forward_refs() SetSortColumnsParams.update_forward_refs() SetSortColumnsRequest.update_forward_refs() -GetColumnProfileParams.update_forward_refs() +GetColumnProfilesParams.update_forward_refs() -GetColumnProfileRequest.update_forward_refs() +GetColumnProfilesRequest.update_forward_refs() GetStateRequest.update_forward_refs() diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py index 9cfeba367e5..64d0bab6e3d 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/tests/test_data_explorer.py @@ -13,7 +13,8 @@ from ..access_keys import encode_access_key from ..data_explorer import COMPARE_OPS, DataExplorerService from ..data_explorer_comm import ( - ColumnFilter, + RowFilter, + ColumnProfileResult, ColumnSchema, ColumnSortKey, FilterResult, @@ -115,11 +116,11 @@ def test_explorer_open_close_delete( de_service: DataExplorerService, variables_comm: DummyComm, ): - shell.user_ns.update( - { - "x": SIMPLE_PANDAS_DF, - "y": {"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, - } + _assign_variables( + shell, + variables_comm, + x=SIMPLE_PANDAS_DF, + y={"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, ) path = _open_viewer(variables_comm, ["x"]) @@ -142,16 +143,25 @@ def test_explorer_open_close_delete( assert len(de_service.table_views) == 0 +def _assign_variables(shell: PositronShell, variables_comm: DummyComm, **variables): + # A hack to make sure that change events are fired when we + # manipulate user_ns + shell.kernel.variables_service.snapshot_user_ns() + shell.user_ns.update(**variables) + shell.kernel.variables_service.poll_variables() + variables_comm.messages.clear() + + def test_explorer_delete_variable( shell: PositronShell, de_service: DataExplorerService, variables_comm: DummyComm, ): - shell.user_ns.update( - { - "x": SIMPLE_PANDAS_DF, - "y": {"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, - } + _assign_variables( + shell, + variables_comm, + x=SIMPLE_PANDAS_DF, + y={"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, ) # Open multiple data viewers @@ -214,12 +224,13 @@ def test_explorer_variable_updates( ): x = pd.DataFrame({"a": [1, 0, 3, 4]}) big_x = pd.DataFrame({"a": np.arange(BIG_ARRAY_LENGTH)}) - shell.user_ns.update( - { - "x": x, - "big_x": big_x, - "y": {"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, - } + + _assign_variables( + shell, + variables_comm, + x=x, + big_x=big_x, + y={"key1": SIMPLE_PANDAS_DF, "key2": SIMPLE_PANDAS_DF}, ) # Check updates @@ -247,8 +258,8 @@ def test_explorer_variable_updates( assert tv.sort_keys == [ColumnSortKey(**k) for k in x_sort_keys] assert tv._need_recompute - pf = PandasFixture(de_service) - new_state = pf.get_state("x") + dxf = DataExplorerFixture(de_service) + new_state = dxf.get_state("x") assert new_state["table_shape"]["num_rows"] == 5 assert new_state["table_shape"]["num_columns"] == 1 assert new_state["sort_keys"] == [ColumnSortKey(**k) for k in x_sort_keys] @@ -301,7 +312,7 @@ def test_shutdown(de_service: DataExplorerService): JsonRecords = List[Dict[str, Any]] -class PandasFixture: +class DataExplorerFixture: def __init__(self, de_service: DataExplorerService): self.de_service = de_service @@ -349,25 +360,37 @@ def get_schema(self, table_name, start_index, num_columns): num_columns=num_columns, ) + def search_schema(self, table_name, search_term, start_index, max_results): + return self.do_json_rpc( + table_name, + "search_schema", + search_term=search_term, + start_index=start_index, + max_results=max_results, + ) + def get_state(self, table_name): return self.do_json_rpc(table_name, "get_state") def get_data_values(self, table_name, **params): return self.do_json_rpc(table_name, "get_data_values", **params) - def set_column_filters(self, table_name, filters=None): - return self.do_json_rpc(table_name, "set_column_filters", filters=filters) + def set_row_filters(self, table_name, filters=None): + return self.do_json_rpc(table_name, "set_row_filters", filters=filters) def set_sort_columns(self, table_name, sort_keys=None): return self.do_json_rpc(table_name, "set_sort_columns", sort_keys=sort_keys) + def get_column_profiles(self, table_name, profiles): + return self.do_json_rpc(table_name, "get_column_profiles", profiles=profiles) + def check_filter_case(self, table, filter_set, expected_table): table_id = guid() ex_id = guid() self.register_table(table_id, table) self.register_table(ex_id, expected_table) - response = self.set_column_filters(table_id, filters=filter_set) + response = self.set_row_filters(table_id, filters=filter_set) assert response == FilterResult(selected_num_rows=len(expected_table)) self.compare_tables(table_id, ex_id, table.shape) @@ -378,7 +401,7 @@ def check_sort_case(self, table, sort_keys, expected_table, filters=None): self.register_table(ex_id, expected_table) if filters is not None: - self.set_column_filters(table_id, filters) + self.set_row_filters(table_id, filters) response = self.set_sort_columns(table_id, sort_keys=sort_keys) assert response is None @@ -403,16 +426,16 @@ def compare_tables(self, table_id: str, expected_id: str, table_shape: tuple): @pytest.fixture() -def pandas_fixture(de_service: DataExplorerService): - return PandasFixture(de_service) +def de_fixture(de_service: DataExplorerService): + return DataExplorerFixture(de_service) def _wrap_json(model: Type[BaseModel], data: JsonRecords): return [model(**d).dict() for d in data] -def test_pandas_get_state(pandas_fixture: PandasFixture): - result = pandas_fixture.get_state("simple") +def test_pandas_get_state(de_fixture: DataExplorerFixture): + result = de_fixture.get_state("simple") assert result["table_shape"]["num_rows"] == 5 assert result["table_shape"]["num_columns"] == 6 @@ -421,94 +444,155 @@ def test_pandas_get_state(pandas_fixture: PandasFixture): {"column_index": 1, "ascending": False}, ] filters = [_compare_filter(0, ">", 0), _compare_filter(0, "<", 5)] - pandas_fixture.set_sort_columns("simple", sort_keys=sort_keys) - pandas_fixture.set_column_filters("simple", filters=filters) + de_fixture.set_sort_columns("simple", sort_keys=sort_keys) + de_fixture.set_row_filters("simple", filters=filters) - result = pandas_fixture.get_state("simple") + result = de_fixture.get_state("simple") assert result["sort_keys"] == sort_keys - assert result["filters"] == [ColumnFilter(**f) for f in filters] + assert result["row_filters"] == [RowFilter(**f) for f in filters] -def test_pandas_get_schema(pandas_fixture: PandasFixture): - result = pandas_fixture.get_schema("simple", 0, 100) +def test_pandas_get_schema(de_fixture: DataExplorerFixture): + dxf = de_fixture + + result = dxf.get_schema("simple", 0, 100) full_schema = [ { "column_name": "a", + "column_index": 0, "type_name": "int64", "type_display": "number", }, { "column_name": "b", + "column_index": 1, "type_name": "boolean", "type_display": "boolean", }, { "column_name": "c", + "column_index": 2, "type_name": "string", "type_display": "string", }, { "column_name": "d", + "column_index": 3, "type_name": "float64", "type_display": "number", }, { "column_name": "e", + "column_index": 4, "type_name": "datetime64[ns]", "type_display": "datetime", }, - {"column_name": "f", "type_name": "mixed", "type_display": "unknown"}, + { + "column_name": "f", + "column_index": 5, + "type_name": "mixed", + "type_display": "unknown", + }, ] assert result["columns"] == _wrap_json(ColumnSchema, full_schema) - result = pandas_fixture.get_schema("simple", 2, 100) + result = dxf.get_schema("simple", 2, 100) assert result["columns"] == _wrap_json(ColumnSchema, full_schema[2:]) - result = pandas_fixture.get_schema("simple", 6, 100) + result = dxf.get_schema("simple", 6, 100) assert result["columns"] == [] # Make a really big schema bigger_df = pd.concat([SIMPLE_PANDAS_DF] * 100, axis="columns") bigger_name = guid() bigger_schema = full_schema * 100 - pandas_fixture.register_table(bigger_name, bigger_df) - result = pandas_fixture.get_schema(bigger_name, 0, 100) + # Fix the column indexes + for i, c in enumerate(bigger_schema): + c = c.copy() + c["column_index"] = i + bigger_schema[i] = c + + dxf.register_table(bigger_name, bigger_df) + + result = dxf.get_schema(bigger_name, 0, 100) assert result["columns"] == _wrap_json(ColumnSchema, bigger_schema[:100]) - result = pandas_fixture.get_schema(bigger_name, 10, 10) + result = dxf.get_schema(bigger_name, 10, 10) assert result["columns"] == _wrap_json(ColumnSchema, bigger_schema[10:20]) -def test_pandas_wide_schemas(pandas_fixture: PandasFixture): +def test_pandas_wide_schemas(de_fixture: DataExplorerFixture): + dxf = de_fixture + arr = np.arange(10).astype(object) ncols = 10000 df = pd.DataFrame({f"col_{i}": arr for i in range(ncols)}) - pandas_fixture.register_table("wide_df", df) + dxf.register_table("wide_df", df) chunk_size = 100 for chunk_index in range(ncols // chunk_size): start_index = chunk_index * chunk_size - pandas_fixture.register_table( + dxf.register_table( f"wide_df_{chunk_index}", df.iloc[:, start_index : (chunk_index + 1) * chunk_size], ) - schema_slice = pandas_fixture.get_schema("wide_df", start_index, chunk_size) - expected = pandas_fixture.get_schema(f"wide_df_{chunk_index}", 0, chunk_size) - assert schema_slice["columns"] == expected["columns"] + schema_slice = dxf.get_schema("wide_df", start_index, chunk_size) + expected = dxf.get_schema(f"wide_df_{chunk_index}", 0, chunk_size) + + for left, right in zip(schema_slice["columns"], expected["columns"]): + right["column_index"] = right["column_index"] + start_index + assert left == right + + +def test_pandas_search_schema(de_fixture: DataExplorerFixture): + dxf = de_fixture + + # Make a few thousand column names we can search for + column_names = [ + f"{prefix}_{i}" + for prefix in ["aaa", "bbb", "ccc", "ddd"] + for i in range({"aaa": 1000, "bbb": 100, "ccc": 50, "ddd": 10}[prefix]) + ] + + # Make a data frame with those column names + arr = np.arange(10) + df = pd.DataFrame({name: arr for name in column_names}, columns=pd.Index(column_names)) + + dxf.register_table("df", df) + + full_schema = dxf.get_schema("df", 0, len(column_names))["columns"] + + # (search_term, start_index, max_results, ex_total, ex_matches) + cases = [ + ("aaa", 0, 100, 1000, full_schema[:100]), + ("aaa", 100, 100, 1000, full_schema[100:200]), + ("aaa", 950, 100, 1000, full_schema[950:1000]), + ("aaa", 1000, 100, 1000, []), + ("bbb", 0, 10, 100, full_schema[1000:1010]), + ("ccc", 0, 10, 50, full_schema[1100:1110]), + ("ddd", 0, 10, 10, full_schema[1150:1160]), + ] + + for search_term, start_index, max_results, ex_total, ex_matches in cases: + result = dxf.search_schema("df", search_term, start_index, max_results) + + assert result["total_num_matches"] == ex_total + matches = result["matches"]["columns"] + assert matches == ex_matches def _trim_whitespace(columns): return [[x.strip() for x in column] for column in columns] -def test_pandas_get_data_values(pandas_fixture: PandasFixture): - result = pandas_fixture.get_data_values( +def test_pandas_get_data_values(de_fixture: DataExplorerFixture): + result = de_fixture.get_data_values( "simple", row_start_index=0, num_rows=20, @@ -537,14 +621,14 @@ def test_pandas_get_data_values(pandas_fixture: PandasFixture): assert result["row_labels"] == [["0", "1", "2", "3", "4"]] # Edge cases: request beyond end of table - response = pandas_fixture.get_data_values( + response = de_fixture.get_data_values( "simple", row_start_index=5, num_rows=10, column_indices=[0] ) assert response["columns"] == [[]] # Issue #2149 -- return empty result when requesting non-existent # column indices - response = pandas_fixture.get_data_values( + response = de_fixture.get_data_values( "simple", row_start_index=0, num_rows=5, column_indices=[2, 3, 4, 5] ) assert _trim_whitespace(response["columns"]) == expected_columns[2:] @@ -554,7 +638,7 @@ def test_pandas_get_data_values(pandas_fixture: PandasFixture): # to request non-existent column indices, disable this test # with pytest.raises(IndexError): - # pandas_fixture.get_data_values( + # de_fixture.get_data_values( # "simple", row_start_index=0, num_rows=10, column_indices=[4] # ) @@ -570,12 +654,31 @@ def _filter(filter_type, column_index, **kwargs): return kwargs -def _compare_filter(column_index, compare_op, compare_value): +def _compare_filter(column_index, op, value): + return _filter("compare", column_index, compare_params={"op": op, "value": value}) + + +def _between_filter(column_index, left_value, right_value, op="between"): + return _filter( + op, + column_index, + between_params={"left_value": left_value, "right_value": right_value}, + ) + + +def _not_between_filter(column_index, left_value, right_value): + return _between_filter(column_index, left_value, right_value, op="not_between") + + +def _search_filter(column_index, term, case_sensitive=False, search_type="contains"): return _filter( - "compare", + "search", column_index, - compare_op=compare_op, - compare_value=compare_value, + search_params={ + "type": search_type, + "term": term, + "case_sensitive": case_sensitive, + }, ) @@ -583,12 +686,40 @@ def _set_member_filter(column_index, values, inclusive=True): return _filter( "set_membership", column_index, - set_member_inclusive=inclusive, - set_member_values=values, + set_membership_params={"values": values, "inclusive": inclusive}, ) -def test_pandas_filter_compare(pandas_fixture: PandasFixture): +def test_pandas_filter_between(de_fixture: DataExplorerFixture): + dxf = de_fixture + df = SIMPLE_PANDAS_DF + column = "a" + column_index = df.columns.get_loc(column) + + cases = [ + (0, 2, 4), # a column + (3, 0, 2), # d column + ] + + for column_index, left_value, right_value in cases: + col = df.iloc[:, column_index] + + ex_between = df[(col >= left_value) & (col <= right_value)] + ex_not_between = df[(col < left_value) | (col > right_value)] + + dxf.check_filter_case( + df, + [_between_filter(column_index, str(left_value), str(right_value))], + ex_between, + ) + dxf.check_filter_case( + df, + [_not_between_filter(column_index, str(left_value), str(right_value))], + ex_not_between, + ) + + +def test_pandas_filter_compare(de_fixture: DataExplorerFixture): # Just use the 'a' column to smoke test comparison filters on # integers table_name = "simple" @@ -600,37 +731,39 @@ def test_pandas_filter_compare(pandas_fixture: PandasFixture): for op, op_func in COMPARE_OPS.items(): filt = _compare_filter(column_index, op, str(compare_value)) expected_df = df[op_func(df[column], compare_value)] - pandas_fixture.check_filter_case(df, [filt], expected_df) + de_fixture.check_filter_case(df, [filt], expected_df) + + # TODO(wesm): move these tests to their own test case # Test that passing empty filter set resets to unfiltered state filt = _compare_filter(column_index, "<", str(compare_value)) - _ = pandas_fixture.set_column_filters(table_name, filters=[filt]) - response = pandas_fixture.set_column_filters(table_name, filters=[]) + _ = de_fixture.set_row_filters(table_name, filters=[filt]) + response = de_fixture.set_row_filters(table_name, filters=[]) assert response == FilterResult(selected_num_rows=len(df)) # register the whole table to make sure the filters are really cleared ex_id = guid() - pandas_fixture.register_table(ex_id, df) - pandas_fixture.compare_tables(table_name, ex_id, df.shape) + de_fixture.register_table(ex_id, df) + de_fixture.compare_tables(table_name, ex_id, df.shape) -def test_pandas_filter_isnull_notnull(pandas_fixture: PandasFixture): +def test_pandas_filter_is_null_not_null(de_fixture: DataExplorerFixture): df = SIMPLE_PANDAS_DF - b_isnull = _filter("isnull", 1) - b_notnull = _filter("notnull", 1) - c_notnull = _filter("notnull", 2) + b_is_null = _filter("is_null", 1) + b_not_null = _filter("not_null", 1) + c_not_null = _filter("not_null", 2) cases = [ - [[b_isnull], df[df["b"].isnull()]], - [[b_notnull], df[df["b"].notnull()]], - [[b_notnull, c_notnull], df[df["b"].notnull() & df["c"].notnull()]], + [[b_is_null], df[df["b"].isnull()]], + [[b_not_null], df[df["b"].notnull()]], + [[b_not_null, c_not_null], df[df["b"].notnull() & df["c"].notnull()]], ] for filter_set, expected_df in cases: - pandas_fixture.check_filter_case(df, filter_set, expected_df) + de_fixture.check_filter_case(df, filter_set, expected_df) -def test_pandas_filter_set_membership(pandas_fixture: PandasFixture): +def test_pandas_filter_set_membership(de_fixture: DataExplorerFixture): df = SIMPLE_PANDAS_DF cases = [ @@ -645,10 +778,85 @@ def test_pandas_filter_set_membership(pandas_fixture: PandasFixture): ] for filter_set, expected_df in cases: - pandas_fixture.check_filter_case(df, filter_set, expected_df) + de_fixture.check_filter_case(df, filter_set, expected_df) + +def test_pandas_filter_search(de_fixture: DataExplorerFixture): + dxf = de_fixture + df = pd.DataFrame( + { + "a": ["foo1", "foo2", None, "2FOO", "FOO3", "bar1", "2BAR"], + "b": [1, 11, 31, 22, 24, 62, 89], + } + ) + + dxf.register_table("df", df) + + # (search_type, column_index, term, case_sensitive, boolean mask) + cases = [ + ("contains", 0, "foo", False, df["a"].str.lower().str.contains("foo")), + ("contains", 0, "foo", True, df["a"].str.contains("foo")), + ( + "starts_with", + 0, + "foo", + False, + df["a"].str.lower().str.startswith("foo"), + ), + ( + "starts_with", + 0, + "foo", + True, + df["a"].str.startswith("foo"), + ), + ( + "ends_with", + 0, + "foo", + False, + df["a"].str.lower().str.endswith("foo"), + ), + ( + "ends_with", + 0, + "foo", + True, + df["a"].str.endswith("foo"), + ), + ( + "regex_match", + 0, + "f[o]+", + False, + df["a"].str.match("f[o]+", case=False), + ), + ( + "regex_match", + 0, + "f[o]+[^o]*", + True, + df["a"].str.match("f[o]+[^o]*", case=True), + ), + ] -def test_pandas_set_sort_columns(pandas_fixture: PandasFixture): + for search_type, column_index, term, cs, mask in cases: + ex_table = df[mask.fillna(False)] + dxf.check_filter_case( + df, + [ + _search_filter( + column_index, + term, + case_sensitive=cs, + search_type=search_type, + ) + ], + ex_table, + ) + + +def test_pandas_set_sort_columns(de_fixture: DataExplorerFixture): tables = { "df1": SIMPLE_PANDAS_DF, # Just some random data to test multiple keys, different sort @@ -694,18 +902,18 @@ def test_pandas_set_sort_columns(pandas_fixture: PandasFixture): expected_df = df.sort_values(**expected_params) - pandas_fixture.check_sort_case(df, wrapped_keys, expected_df) + de_fixture.check_sort_case(df, wrapped_keys, expected_df) for filter_f, filters in filter_cases.get(df_name, []): expected_filtered = filter_f(df).sort_values(**expected_params) - pandas_fixture.check_sort_case(df, wrapped_keys, expected_filtered, filters=filters) + de_fixture.check_sort_case(df, wrapped_keys, expected_filtered, filters=filters) def test_pandas_change_schema_after_sort( shell: PositronShell, de_service: DataExplorerService, variables_comm: DummyComm, - pandas_fixture: PandasFixture, + de_fixture: DataExplorerFixture, ): df = pd.DataFrame( { @@ -716,30 +924,90 @@ def test_pandas_change_schema_after_sort( "e": np.arange(10), } ) - shell.user_ns.update({"df": df}) + _assign_variables(shell, variables_comm, df=df) _open_viewer(variables_comm, ["df"]) # Sort a column that is out of bounds for the table after the # schema change below - pandas_fixture.set_sort_columns("df", [{"column_index": 4, "ascending": True}]) + de_fixture.set_sort_columns("df", [{"column_index": 4, "ascending": True}]) expected_df = df[["a", "b"]] - pandas_fixture.register_table("expected_df", df) + de_fixture.register_table("expected_df", df) # Sort last column, and we will then change the schema shell.run_cell("df = df[['a', 'b']]") _check_update_variable(de_service, "df", update_type="schema", discard_state=True) # Call get_data_values and make sure it works - pandas_fixture.compare_tables("df", "expected_df", expected_df.shape) + de_fixture.compare_tables("df", "expected_df", expected_df.shape) + + +def _profile_request(column_index, profile_type): + return {"column_index": column_index, "type": profile_type} + + +def test_pandas_profile_null_counts(de_fixture: DataExplorerFixture): + dxf = de_fixture + + df1 = pd.DataFrame( + { + "a": [0, np.nan, 2, np.nan, 4, 5, 6], + "b": ["zero", None, None, None, "four", "five", "six"], + "c": [False, False, False, None, None, None, None], + "d": [0, 1, 2, 3, 4, 5, 6], + } + ) + tables = {"df1": df1} + + for name, df in tables.items(): + dxf.register_table(name, df) + def _null_count(column_index): + return _profile_request(column_index, "null_count") + + # tuples like (table_name, [ColumnProfileRequest], [results]) + all_profiles = [ + _null_count(0), + _null_count(1), + _null_count(2), + _null_count(3), + ] + cases = [ + ("df1", [], []), + ( + "df1", + [_null_count(3)], + [0], + ), + ( + "df1", + [_null_count(0), _null_count(1), _null_count(2), _null_count(3)], + [2, 3, 4, 0], + ), + ] + + for table_name, profiles, ex_results in cases: + results = dxf.get_column_profiles(table_name, profiles) + + ex_results = [ColumnProfileResult(null_count=count) for count in ex_results] + + assert results == ex_results + + # Test profiling with filter + # format: (table, filters, filtered_table, profiles) + filter_cases = [(df1, [_filter("not_null", 0)], df1[df1["a"].notnull()], all_profiles)] + for table, filters, filtered_table, profiles in filter_cases: + table_id = guid() + dxf.register_table(table_id, table) + dxf.set_row_filters(table_id, filters) -# def test_pandas_get_column_profile(pandas_fixture: PandasFixture): -# pass + filtered_id = guid() + dxf.register_table(filtered_id, filtered_table) + results = dxf.get_column_profiles(table_id, profiles) + ex_results = dxf.get_column_profiles(filtered_id, profiles) -# def test_pandas_get_state(pandas_fixture: PandasFixture): -# pass + assert results == ex_results # ---------------------------------------------------------------------- diff --git a/positron/comms/data_explorer-backend-openrpc.json b/positron/comms/data_explorer-backend-openrpc.json index 5333b578f2a..09eb728a433 100644 --- a/positron/comms/data_explorer-backend-openrpc.json +++ b/positron/comms/data_explorer-backend-openrpc.json @@ -29,19 +29,56 @@ ], "result": { "schema": { + "$ref": "#/components/schemas/table_schema" + } + } + }, + { + "name": "search_schema", + "summary": "Search schema by column name", + "description": "Search schema for column names matching a passed substring", + "params": [ + { + "name": "search_term", + "description": "Substring to match for (currently case insensitive)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "start_index", + "description": "Index (starting from zero) of first result to fetch", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "max_results", + "description": "Maximum number of resulting column schemas to fetch from the start index", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "result": { + "schema": { + "name": "search_schema_result", "type": "object", - "name": "table_schema", - "description": "The schema for a table-like object", "required": [ - "columns" + "schema", + "total_num_matches" ], "properties": { - "columns": { - "type": "array", - "description": "Schema for each column in the table", - "items": { - "$ref": "#/components/schemas/column_schema" - } + "matches": { + "description": "A schema containing matching columns up to the max_results limit", + "$ref": "#/components/schemas/table_schema" + }, + "total_num_matches": { + "description": "The total number of columns matching the search term", + "type": "integer" } } } @@ -116,9 +153,9 @@ } }, { - "name": "set_column_filters", - "summary": "Set column filters", - "description": "Set or clear column filters on table, replacing any previous filters", + "name": "set_row_filters", + "summary": "Set row filters based on column values", + "description": "Set or clear row filters on table, replacing any previous filters", "params": [ { "name": "filters", @@ -127,7 +164,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/column_filter" + "$ref": "#/components/schemas/row_filter" } } } @@ -169,99 +206,28 @@ "result": {} }, { - "name": "get_column_profile", - "summary": "Get a column profile", - "description": "Requests a statistical summary or data profile for a column", + "name": "get_column_profiles", + "summary": "Request a batch of column profiles", + "description": "Requests a statistical summary or data profile for batch of columns", "params": [ { - "name": "profile_type", - "description": "The type of analytical column profile", + "name": "profiles", + "description": "Array of requested profiles", "required": true, "schema": { - "type": "string", - "enum": [ - "freqtable", - "histogram" - ] - } - }, - { - "name": "column_index", - "description": "Column index to compute profile for", - "required": true, - "schema": { - "type": "integer" + "type": "array", + "items": { + "$ref": "#/components/schemas/column_profile_request" + } } } ], "result": { "schema": { - "type": "object", - "name": "profile_result", - "description": "Result of computing column profile", - "required": [ - "null_count" - ], - "properties": { - "null_count": { - "type": "integer", - "description": "Number of null values in column" - }, - "min_value": { - "type": "string", - "description": "Minimum value as string computed as part of histogram" - }, - "max_value": { - "type": "string", - "description": "Maximum value as string computed as part of histogram" - }, - "mean_value": { - "type": "string", - "description": "Average value as string computed as part of histogram" - }, - "histogram_bin_sizes": { - "type": "array", - "description": "Absolute count of values in each histogram bin", - "items": { - "type": "integer" - } - }, - "histogram_bin_width": { - "type": "number", - "description": "Absolute floating-point width of a histogram bin" - }, - "histogram_quantiles": { - "type": "array", - "description": "Quantile values computed from histogram bins", - "items": { - "$ref": "#/components/schemas/column_quantile_value" - } - }, - "freqtable_counts": { - "type": "array", - "description": "Counts of distinct values in column", - "items": { - "type": "object", - "required": [ - "value", - "count" - ], - "properties": { - "value": { - "type": "string", - "description": "Stringified value" - }, - "count": { - "type": "integer", - "description": "Number of occurrences of value" - } - } - } - }, - "freqtable_other_count": { - "type": "integer", - "description": "Number of other values not accounted for in counts" - } + "name": "column_profile_results", + "type": "array", + "items": { + "$ref": "#/components/schemas/column_profile_result" } } } @@ -301,11 +267,11 @@ } } }, - "filters": { + "row_filters": { "type": "array", - "description": "The set of currently applied filters", + "description": "The set of currently applied row filters", "items": { - "$ref": "#/components/schemas/column_filter" + "$ref": "#/components/schemas/row_filter" } }, "sort_keys": { @@ -328,6 +294,7 @@ "description": "Schema for a column in a table", "required": [ "column_name", + "column_index", "type_name", "type_display" ], @@ -336,6 +303,10 @@ "type": "string", "description": "Name of column as UTF-8 string" }, + "column_index": { + "type": "integer", + "description": "The position of the column within the schema" + }, "type_name": { "type": "string", "description": "Exact name of data type used by underlying table" @@ -384,9 +355,25 @@ } } }, - "column_filter": { + "table_schema": { + "type": "object", + "description": "The schema for a table-like object", + "required": [ + "columns" + ], + "properties": { + "columns": { + "type": "array", + "description": "Schema for each column in the table", + "items": { + "$ref": "#/components/schemas/column_schema" + } + } + } + }, + "row_filter": { "type": "object", - "description": "Specifies a table row filter based on a column's values", + "description": "Specifies a table row filter based on a single column's values", "required": [ "filter_id", "filter_type", @@ -401,18 +388,64 @@ "type": "string", "description": "Type of filter to apply", "enum": [ - "isnull", - "notnull", + "between", "compare", - "set_membership", - "search" + "is_null", + "not_between", + "not_null", + "search", + "set_membership" ] }, "column_index": { "type": "integer", "description": "Column index to apply filter to" }, - "compare_op": { + "between_params": { + "description": "Parameters for the 'between' and 'not_between' filter types", + "$ref": "#/components/schemas/between_filter_params" + }, + "compare_params": { + "description": "Parameters for the 'compare' filter type", + "$ref": "#/components/schemas/compare_filter_params" + }, + "search_params": { + "description": "Parameters for the 'search' filter type", + "$ref": "#/components/schemas/search_filter_params" + }, + "set_membership_params": { + "description": "Parameters for the 'set_membership' filter type", + "$ref": "#/components/schemas/set_membership_filter_params" + } + } + }, + "between_filter_params": { + "type": "object", + "description": "Parameters for the 'between' and 'not_between' filter types", + "required": [ + "left_value", + "right_value" + ], + "properties": { + "left_value": { + "type": "string", + "description": "The lower limit for filtering" + }, + "right_value": { + "type": "string", + "description": "The upper limit for filtering" + } + } + }, + "compare_filter_params": { + "type": "object", + "description": "Parameters for the 'compare' filter type", + "required": [ + "op", + "value" + ], + "properties": { + "op": { "type": "string", "description": "String representation of a binary comparison", "enum": [ @@ -424,41 +457,201 @@ ">=" ] }, - "compare_value": { + "value": { "type": "string", "description": "A stringified column value for a comparison filter" - }, - "set_member_values": { + } + } + }, + "set_membership_filter_params": { + "type": "object", + "description": "Parameters for the 'set_membership' filter type", + "required": [ + "values", + "inclusive" + ], + "properties": { + "values": { "type": "array", "description": "Array of column values for a set membership filter", "items": { "type": "string" } }, - "set_member_inclusive": { + "inclusive": { "type": "boolean", "description": "Filter by including only values passed (true) or excluding (false)" - }, - "search_type": { + } + } + }, + "search_filter_params": { + "type": "object", + "description": "Parameters for the 'search' filter type", + "required": [ + "type", + "term", + "case_sensitive" + ], + "properties": { + "type": { "type": "string", "description": "Type of search to perform", "enum": [ "contains", - "startswith", - "endswith", - "regex" + "starts_with", + "ends_with", + "regex_match" ] }, - "search_term": { + "term": { "type": "string", "description": "String value/regex to search for in stringified data" }, - "search_case_sensitive": { + "case_sensitive": { "type": "boolean", "description": "If true, do a case-sensitive search, otherwise case-insensitive" } } }, + "column_profile_request": { + "type": "object", + "description": "A single column profile request", + "required": [ + "column_index", + "type" + ], + "properties": { + "column_index": { + "type": "integer", + "description": "The ordinal column index to profile" + }, + "type": { + "type": "string", + "description": "The type of analytical column profile", + "enum": [ + "null_count", + "summary_stats", + "frequency_table", + "histogram" + ] + } + } + }, + "column_profile_result": { + "type": "object", + "description": "Result of computing column profile", + "properties": { + "null_count": { + "description": "Result from null_count request", + "type": "integer" + }, + "summary_stats": { + "description": "Results from summary_stats request", + "$ref": "#/components/schemas/column_summary_stats" + }, + "histogram": { + "description": "Results from summary_stats request", + "$ref": "#/components/schemas/column_histogram" + }, + "frequency_table": { + "description": "Results from frequency_table request", + "$ref": "#/components/schemas/column_frequency_table" + } + } + }, + "column_summary_stats": { + "type": "object", + "required": [ + "min_value", + "max_value" + ], + "properties": { + "min_value": { + "type": "string", + "description": "Minimum value as string" + }, + "max_value": { + "type": "string", + "description": "Maximum value as string" + }, + "mean_value": { + "type": "string", + "description": "Average value as string" + }, + "median": { + "type": "string", + "description": "Sample median (50% value) value as string" + }, + "q25": { + "type": "string", + "description": "25th percentile value as string" + }, + "q75": { + "type": "string", + "description": "75th percentile value as string" + } + } + }, + "column_histogram": { + "type": "object", + "description": "Result from a histogram profile request", + "required": [ + "bin_sizes", + "bin_width" + ], + "properties": { + "bin_sizes": { + "type": "array", + "description": "Absolute count of values in each histogram bin", + "items": { + "type": "integer" + } + }, + "bin_width": { + "type": "number", + "description": "Absolute floating-point width of a histogram bin" + } + } + }, + "column_frequency_table": { + "type": "object", + "description": "Result from a frequency_table profile request", + "required": [ + "counts", + "other_count" + ], + "properties": { + "counts": { + "type": "array", + "description": "Counts of distinct values in column", + "items": { + "$ref": "#/components/schemas/column_frequency_table_item" + } + }, + "other_count": { + "type": "integer", + "description": "Number of other values not accounted for in counts. May be 0" + } + } + }, + "column_frequency_table_item": { + "type": "object", + "description": "Entry in a column's frequency table", + "required": [ + "value", + "count" + ], + "properties": { + "value": { + "type": "string", + "description": "Stringified value" + }, + "count": { + "type": "integer", + "description": "Number of occurrences of value" + } + } + }, "column_quantile_value": { "type": "object", "description": "An exact or approximate quantile value from a column", diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient.ts index 4356439005e..50aff031bd2 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; -import { ColumnSchema, ColumnSortKey, PositronDataExplorerComm, SchemaUpdateEvent, TableData, TableSchema, TableState } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; +import { ColumnProfileRequest, ColumnProfileResult, ColumnSchema, ColumnSortKey, PositronDataExplorerComm, SchemaUpdateEvent, TableData, TableSchema, TableState } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; /** * TableSchemaSearchResult interface. This is here temporarily until searching the tabe schema @@ -163,6 +163,17 @@ export class DataExplorerClientInstance extends Disposable { return this._positronDataExplorerComm.getDataValues(rowStartIndex, numRows, columnIndices); } + /** + * Request a batch of column profiles + * @param profiles An array of profile types and colum indexes + * @returns A Promise> that resolves when the operation is complete. + */ + async getColumnProfiles( + profiles: Array + ): Promise> { + return this._positronDataExplorerComm.getColumnProfiles(profiles); + } + /** * Set or clear the columns(s) to sort by, replacing any previous sort columns. * @param sortKeys Pass zero or more keys to sort by. Clears any existing keys. diff --git a/src/vs/workbench/services/languageRuntime/common/positronDataExplorerComm.ts b/src/vs/workbench/services/languageRuntime/common/positronDataExplorerComm.ts index d5b8fefd574..8af3e7e49be 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronDataExplorerComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronDataExplorerComm.ts @@ -11,13 +11,18 @@ import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/p import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; /** - * The schema for a table-like object + * Result in Methods */ -export interface TableSchema { +export interface SearchSchemaResult { /** - * Schema for each column in the table + * A schema containing matching columns up to the max_results limit */ - columns: Array; + matches?: TableSchema; + + /** + * The total number of columns matching the search term + */ + total_num_matches: number; } @@ -48,73 +53,6 @@ export interface FilterResult { } -/** - * Result of computing column profile - */ -export interface ProfileResult { - /** - * Number of null values in column - */ - null_count: number; - - /** - * Minimum value as string computed as part of histogram - */ - min_value?: string; - - /** - * Maximum value as string computed as part of histogram - */ - max_value?: string; - - /** - * Average value as string computed as part of histogram - */ - mean_value?: string; - - /** - * Absolute count of values in each histogram bin - */ - histogram_bin_sizes?: Array; - - /** - * Absolute floating-point width of a histogram bin - */ - histogram_bin_width?: number; - - /** - * Quantile values computed from histogram bins - */ - histogram_quantiles?: Array; - - /** - * Counts of distinct values in column - */ - freqtable_counts?: Array; - - /** - * Number of other values not accounted for in counts - */ - freqtable_other_count?: number; - -} - -/** - * Items in FreqtableCounts - */ -export interface FreqtableCounts { - /** - * Stringified value - */ - value: string; - - /** - * Number of occurrences of value - */ - count: number; - -} - /** * The current backend table state */ @@ -125,9 +63,9 @@ export interface TableState { table_shape: TableShape; /** - * The set of currently applied filters + * The set of currently applied row filters */ - filters: Array; + row_filters?: Array; /** * The set of currently applied sorts @@ -161,6 +99,11 @@ export interface ColumnSchema { */ column_name: string; + /** + * The position of the column within the schema + */ + column_index: number; + /** * Exact name of data type used by underlying table */ @@ -204,9 +147,20 @@ export interface ColumnSchema { } /** - * Specifies a table row filter based on a column's values + * The schema for a table-like object */ -export interface ColumnFilter { +export interface TableSchema { + /** + * Schema for each column in the table + */ + columns: Array; + +} + +/** + * Specifies a table row filter based on a single column's values + */ +export interface RowFilter { /** * Unique identifier for this filter */ @@ -215,47 +169,227 @@ export interface ColumnFilter { /** * Type of filter to apply */ - filter_type: ColumnFilterFilterType; + filter_type: RowFilterFilterType; /** * Column index to apply filter to */ column_index: number; + /** + * Parameters for the 'between' and 'not_between' filter types + */ + between_params?: BetweenFilterParams; + + /** + * Parameters for the 'compare' filter type + */ + compare_params?: CompareFilterParams; + + /** + * Parameters for the 'search' filter type + */ + search_params?: SearchFilterParams; + + /** + * Parameters for the 'set_membership' filter type + */ + set_membership_params?: SetMembershipFilterParams; + +} + +/** + * Parameters for the 'between' and 'not_between' filter types + */ +export interface BetweenFilterParams { + /** + * The lower limit for filtering + */ + left_value: string; + + /** + * The upper limit for filtering + */ + right_value: string; + +} + +/** + * Parameters for the 'compare' filter type + */ +export interface CompareFilterParams { /** * String representation of a binary comparison */ - compare_op?: ColumnFilterCompareOp; + op: CompareFilterParamsOp; /** * A stringified column value for a comparison filter */ - compare_value?: string; + value: string; +} + +/** + * Parameters for the 'set_membership' filter type + */ +export interface SetMembershipFilterParams { /** * Array of column values for a set membership filter */ - set_member_values?: Array; + values: Array; /** * Filter by including only values passed (true) or excluding (false) */ - set_member_inclusive?: boolean; + inclusive: boolean; +} + +/** + * Parameters for the 'search' filter type + */ +export interface SearchFilterParams { /** * Type of search to perform */ - search_type?: ColumnFilterSearchType; + type: SearchFilterParamsType; /** * String value/regex to search for in stringified data */ - search_term?: string; + term: string; /** * If true, do a case-sensitive search, otherwise case-insensitive */ - search_case_sensitive?: boolean; + case_sensitive: boolean; + +} + +/** + * A single column profile request + */ +export interface ColumnProfileRequest { + /** + * The ordinal column index to profile + */ + column_index: number; + + /** + * The type of analytical column profile + */ + type: ColumnProfileRequestType; + +} + +/** + * Result of computing column profile + */ +export interface ColumnProfileResult { + /** + * Result from null_count request + */ + null_count?: number; + + /** + * Results from summary_stats request + */ + summary_stats?: ColumnSummaryStats; + + /** + * Results from summary_stats request + */ + histogram?: ColumnHistogram; + + /** + * Results from frequency_table request + */ + frequency_table?: ColumnFrequencyTable; + +} + +/** + * ColumnSummaryStats in Schemas + */ +export interface ColumnSummaryStats { + /** + * Minimum value as string + */ + min_value: string; + + /** + * Maximum value as string + */ + max_value: string; + + /** + * Average value as string + */ + mean_value?: string; + + /** + * Sample median (50% value) value as string + */ + median?: string; + + /** + * 25th percentile value as string + */ + q25?: string; + + /** + * 75th percentile value as string + */ + q75?: string; + +} + +/** + * Result from a histogram profile request + */ +export interface ColumnHistogram { + /** + * Absolute count of values in each histogram bin + */ + bin_sizes: Array; + + /** + * Absolute floating-point width of a histogram bin + */ + bin_width: number; + +} + +/** + * Result from a frequency_table profile request + */ +export interface ColumnFrequencyTable { + /** + * Counts of distinct values in column + */ + counts: Array; + + /** + * Number of other values not accounted for in counts. May be 0 + */ + other_count: number; + +} + +/** + * Entry in a column's frequency table + */ +export interface ColumnFrequencyTableItem { + /** + * Stringified value + */ + value: string; + + /** + * Number of occurrences of value + */ + count: number; } @@ -297,14 +431,6 @@ export interface ColumnSortKey { } -/** - * Possible values for ProfileType in GetColumnProfile - */ -export enum GetColumnProfileProfileType { - Freqtable = 'freqtable', - Histogram = 'histogram' -} - /** * Possible values for TypeDisplay in ColumnSchema */ @@ -321,20 +447,22 @@ export enum ColumnSchemaTypeDisplay { } /** - * Possible values for FilterType in ColumnFilter + * Possible values for FilterType in RowFilter */ -export enum ColumnFilterFilterType { - Isnull = 'isnull', - Notnull = 'notnull', +export enum RowFilterFilterType { + Between = 'between', Compare = 'compare', - SetMembership = 'set_membership', - Search = 'search' + IsNull = 'is_null', + NotBetween = 'not_between', + NotNull = 'not_null', + Search = 'search', + SetMembership = 'set_membership' } /** - * Possible values for CompareOp in ColumnFilter + * Possible values for Op in CompareFilterParams */ -export enum ColumnFilterCompareOp { +export enum CompareFilterParamsOp { Eq = '=', NotEq = '!=', Lt = '<', @@ -344,13 +472,23 @@ export enum ColumnFilterCompareOp { } /** - * Possible values for SearchType in ColumnFilter + * Possible values for Type in SearchFilterParams */ -export enum ColumnFilterSearchType { +export enum SearchFilterParamsType { Contains = 'contains', - Startswith = 'startswith', - Endswith = 'endswith', - Regex = 'regex' + StartsWith = 'starts_with', + EndsWith = 'ends_with', + RegexMatch = 'regex_match' +} + +/** + * Possible values for Type in ColumnProfileRequest + */ +export enum ColumnProfileRequestType { + NullCount = 'null_count', + SummaryStats = 'summary_stats', + FrequencyTable = 'frequency_table', + Histogram = 'histogram' } /** @@ -391,12 +529,28 @@ export class PositronDataExplorerComm extends PositronBaseComm { * @param numColumns Number of column schemas to fetch from start index. * May extend beyond end of table * - * @returns The schema for a table-like object + * @returns undefined */ getSchema(startIndex: number, numColumns: number): Promise { return super.performRpc('get_schema', ['start_index', 'num_columns'], [startIndex, numColumns]); } + /** + * Search schema by column name + * + * Search schema for column names matching a passed substring + * + * @param searchTerm Substring to match for (currently case insensitive + * @param startIndex Index (starting from zero) of first result to fetch + * @param maxResults Maximum number of resulting column schemas to fetch + * from the start index + * + * @returns undefined + */ + searchSchema(searchTerm: string, startIndex: number, maxResults: number): Promise { + return super.performRpc('search_schema', ['search_term', 'start_index', 'max_results'], [searchTerm, startIndex, maxResults]); + } + /** * Get a rectangle of data values * @@ -415,16 +569,16 @@ export class PositronDataExplorerComm extends PositronBaseComm { } /** - * Set column filters + * Set row filters based on column values * - * Set or clear column filters on table, replacing any previous filters + * Set or clear row filters on table, replacing any previous filters * * @param filters Zero or more filters to apply * * @returns The result of applying filters to a table */ - setColumnFilters(filters: Array): Promise { - return super.performRpc('set_column_filters', ['filters'], [filters]); + setRowFilters(filters: Array): Promise { + return super.performRpc('set_row_filters', ['filters'], [filters]); } /** @@ -442,17 +596,16 @@ export class PositronDataExplorerComm extends PositronBaseComm { } /** - * Get a column profile + * Request a batch of column profiles * - * Requests a statistical summary or data profile for a column + * Requests a statistical summary or data profile for batch of columns * - * @param profileType The type of analytical column profile - * @param columnIndex Column index to compute profile for + * @param profiles Array of requested profiles * - * @returns Result of computing column profile + * @returns undefined */ - getColumnProfile(profileType: GetColumnProfileProfileType, columnIndex: number): Promise { - return super.performRpc('get_column_profile', ['profile_type', 'column_index'], [profileType, columnIndex]); + getColumnProfiles(profiles: Array): Promise> { + return super.performRpc('get_column_profiles', ['profiles'], [profiles]); } /** diff --git a/src/vs/workbench/services/positronDataExplorer/browser/components/columnSummaryCell.tsx b/src/vs/workbench/services/positronDataExplorer/browser/components/columnSummaryCell.tsx index 9b694e795d2..691003a554b 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/components/columnSummaryCell.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/components/columnSummaryCell.tsx @@ -140,7 +140,7 @@ export const ColumnSummaryCell = (props: ColumnSummaryCellProps) => { {props.columnSchema.column_name}
- 29% + {props.instance.getColumnNullPercent(props.columnIndex)}%
diff --git a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx index cb2883cbb4b..502957a3d54 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx @@ -223,6 +223,13 @@ export class TableSummaryDataGridInstance extends DataGridInstance { ); } + getColumnNullPercent(columnIndex: number): number | undefined { + const nullCount = this._dataExplorerCache.getColumnNullCount(columnIndex); + // TODO: Is floor what we want? + return nullCount === undefined ? undefined : Math.floor( + nullCount * 100 / this._dataExplorerCache.rows); + } + //#endregion DataGridInstance Methods //#region Public Events diff --git a/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts b/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts index 23d6eaa52ca..f3c7961ab8d 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/dataExplorerCache.ts @@ -4,7 +4,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ColumnSchema, TableData } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; +import { ColumnProfileRequestType, ColumnSchema, TableData } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; import { DataExplorerClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeDataExplorerClient'; /** @@ -67,6 +67,11 @@ export class DataExplorerCache extends Disposable { */ private readonly _columnSchemaCache = new Map(); + /** + * Gets the column schema cache. + */ + private readonly _columnNullCountCache = new Map(); + /** * Gets the row label cache. */ @@ -101,6 +106,7 @@ export class DataExplorerCache extends Disposable { this._register(this._dataExplorerClientInstance.onDidSchemaUpdate(async () => { // Clear the column schema cache, row label cache, and data cell cache. this._columnSchemaCache.clear(); + this._columnNullCountCache.clear(); this._rowLabelCache.clear(); this._dataCellCache.clear(); })); @@ -110,6 +116,7 @@ export class DataExplorerCache extends Disposable { // Clear the row label cache and data cell cache. this._rowLabelCache.clear(); this._dataCellCache.clear(); + this._columnNullCountCache.clear(); })); } @@ -150,6 +157,7 @@ export class DataExplorerCache extends Disposable { invalidateDataCache() { this._rowLabelCache.clear(); this._dataCellCache.clear(); + this._columnNullCountCache.clear(); } /** @@ -173,6 +181,15 @@ export class DataExplorerCache extends Disposable { return this._columnSchemaCache.get(columnIndex); } + /** + * Gets the null count for the specified column index. + * @param columnIndex The column index. + * @returns The number of nulls in the specified column index. + */ + getColumnNullCount(columnIndex: number) { + return this._columnNullCountCache.get(columnIndex); + } + /** * Gets the row label for the specified row index. * @param rowIndex The row index. @@ -236,11 +253,11 @@ export class DataExplorerCache extends Disposable { this._columns - 1 ); - // Build an array of the column indicies to cache. - const columnIndicies = arrayFromIndexRange(startColumnIndex, endColumnIndex); + // Build an array of the column indices to cache. + const columnIndices = arrayFromIndexRange(startColumnIndex, endColumnIndex); // Build an array of the column schema indices that need to be cached. - const columnSchemaIndices = columnIndicies.filter(columnIndex => + const columnSchemaIndices = columnIndices.filter(columnIndex => !this._columnSchemaCache.has(columnIndex) ); @@ -264,6 +281,32 @@ export class DataExplorerCache extends Disposable { cacheUpdated = true; } + // Build an array of the column schema indices that need to be cached. + const columnNullCountIndices = columnIndices.filter(columnIndex => + !this._columnNullCountCache.has(columnIndex) + ); + + // If there are null counts that need to be cached, cache them. + if (columnNullCountIndices.length) { + // Request the profiles + const results = await this._dataExplorerClientInstance.getColumnProfiles( + columnNullCountIndices.map(column_index => { + return { + column_index, + type: ColumnProfileRequestType.NullCount + }; + }) + ); + + // Update the column schema cache, overwriting any entries we already have cached. + for (let i = 0; i < results.length; i++) { + this._columnNullCountCache.set(columnNullCountIndices[i], results[i].null_count!); + } + + // Update the cache updated flag. + cacheUpdated = true; + } + // If data is also being cached, update the data cache. if (firstRowIndex !== undefined && visibleRows !== undefined) { // Set the start row index and the end row index of the rows to cache. @@ -296,7 +339,7 @@ export class DataExplorerCache extends Disposable { const tableData: TableData = await this._dataExplorerClientInstance.getDataValues( rowIndices[0], rows, - columnIndicies + columnIndices ); // Update the data cell cache, overwriting any entries we already have cached. @@ -311,9 +354,9 @@ export class DataExplorerCache extends Disposable { } // Cache the data cells. - for (let column = 0; column < columnIndicies.length; column++) { + for (let column = 0; column < columnIndices.length; column++) { const value = tableData.columns[column][row]; - const columnIndex = columnIndicies[column]; + const columnIndex = columnIndices[column]; const rowIndex = rowIndices[row]; this._dataCellCache.set(`${columnIndex},${rowIndex}`, value); } diff --git a/src/vs/workbench/services/positronDataExplorer/common/positronDataExplorerMocks.ts b/src/vs/workbench/services/positronDataExplorer/common/positronDataExplorerMocks.ts index bf7872abf14..c8e8cb50259 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/positronDataExplorerMocks.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/positronDataExplorerMocks.ts @@ -4,12 +4,12 @@ import { generateUuid } from 'vs/base/common/uuid'; import { - ColumnFilter, - ColumnFilterCompareOp, - ColumnFilterFilterType, - ColumnFilterSearchType, + CompareFilterParamsOp, + SearchFilterParamsType, ColumnSchema, - ProfileResult, + ColumnProfileResult, + RowFilter, + RowFilterFilterType, TableData, TableSchema } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; @@ -87,7 +87,7 @@ export function getColumnSchema(colName: string, typeName: string, typeDisplay: } as ColumnSchema; } -export function getExampleHistogram(): ProfileResult { +export function getExampleHistogram(): ColumnProfileResult { // This example is basically made up. return { null_count: 10, @@ -101,10 +101,10 @@ export function getExampleHistogram(): ProfileResult { { q: 50, value: '70', exact: true }, { q: 75, value: '82', exact: true } ], - } as ProfileResult; + } as ColumnProfileResult; } -export function getExampleFreqtable(): ProfileResult { +export function getExampleFreqtable(): ColumnProfileResult { return { null_count: 10, freqtable_counts: [ @@ -114,49 +114,44 @@ export function getExampleFreqtable(): ProfileResult { { value: 'qux444444', count: 2 } ], freqtable_other_count: 12 - } as ProfileResult; + } as ColumnProfileResult; } // For filtering -function _getFilterWithProps(columnIndex: number, filterType: ColumnFilterFilterType, - props: Partial = {}): ColumnFilter { +function _getFilterWithProps(columnIndex: number, filterType: RowFilterFilterType, + props: Partial = {}): RowFilter { return { filter_id: generateUuid(), filter_type: filterType, column_index: columnIndex, ...props - } as ColumnFilter; + } as RowFilter; } -export function getCompareFilter(columnIndex: number, op: ColumnFilterCompareOp, +export function getCompareFilter(columnIndex: number, op: CompareFilterParamsOp, value: string) { - return _getFilterWithProps(columnIndex, ColumnFilterFilterType.Compare, { - compare_op: op, - compare_value: value - }); + return _getFilterWithProps(columnIndex, RowFilterFilterType.Compare, + { compare_params: { op, value } }); } export function getIsNullFilter(columnIndex: number) { - return _getFilterWithProps(columnIndex, ColumnFilterFilterType.Isnull); + return _getFilterWithProps(columnIndex, RowFilterFilterType.IsNull); } export function getNotNullFilter(columnIndex: number) { - return _getFilterWithProps(columnIndex, ColumnFilterFilterType.Notnull); + return _getFilterWithProps(columnIndex, RowFilterFilterType.NotNull); } export function getSetMemberFilter(columnIndex: number, values: string[], inclusive: boolean) { - return _getFilterWithProps(columnIndex, ColumnFilterFilterType.SetMembership, { - set_member_values: values, - set_member_inclusive: inclusive + return _getFilterWithProps(columnIndex, RowFilterFilterType.SetMembership, { + set_membership_params: { values, inclusive } }); } export function getTextSearchFilter(columnIndex: number, searchTerm: string, - searchType: ColumnFilterSearchType, caseSensitive: boolean) { - return _getFilterWithProps(columnIndex, ColumnFilterFilterType.Search, { - search_term: searchTerm, - search_type: searchType, - search_case_sensitive: caseSensitive + searchType: SearchFilterParamsType, caseSensitive: boolean) { + return _getFilterWithProps(columnIndex, RowFilterFilterType.Search, { + search_params: { term: searchTerm, type: searchType, case_sensitive: caseSensitive } }); } diff --git a/src/vs/workbench/services/positronDataExplorer/test/common/positronDataExplorerMocks.test.ts b/src/vs/workbench/services/positronDataExplorer/test/common/positronDataExplorerMocks.test.ts index 579b94b116b..7fcead87985 100644 --- a/src/vs/workbench/services/positronDataExplorer/test/common/positronDataExplorerMocks.test.ts +++ b/src/vs/workbench/services/positronDataExplorer/test/common/positronDataExplorerMocks.test.ts @@ -5,9 +5,9 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { - ColumnFilterCompareOp, - ColumnFilterFilterType, - ColumnFilterSearchType + CompareFilterParamsOp, + RowFilterFilterType, + SearchFilterParamsType } from 'vs/workbench/services/languageRuntime/common/positronDataExplorerComm'; import * as mocks from "vs/workbench/services/positronDataExplorer/common/positronDataExplorerMocks"; @@ -39,39 +39,48 @@ suite('DataExplorerMocks', () => { }); test('Test getCompareFilter', () => { - const filter = mocks.getCompareFilter(2, ColumnFilterCompareOp.Gt, '1234'); - assert.equal(filter.filter_type, ColumnFilterFilterType.Compare); + const filter = mocks.getCompareFilter(2, CompareFilterParamsOp.Gt, '1234'); + assert.equal(filter.filter_type, RowFilterFilterType.Compare); assert.equal(filter.column_index, 2); - assert.equal(filter.compare_op, ColumnFilterCompareOp.Gt); - assert.equal(filter.compare_value, '1234'); + + const params = filter.compare_params!; + + assert.equal(params.op, CompareFilterParamsOp.Gt); + assert.equal(params.value, '1234'); }); test('Test getIsNullFilter', () => { let filter = mocks.getIsNullFilter(3); assert.equal(filter.column_index, 3); - assert.equal(filter.filter_type, ColumnFilterFilterType.Isnull); + assert.equal(filter.filter_type, RowFilterFilterType.IsNull); filter = mocks.getNotNullFilter(3); - assert.equal(filter.filter_type, ColumnFilterFilterType.Notnull); + assert.equal(filter.filter_type, RowFilterFilterType.NotNull); }); test('Test getTextSearchFilter', () => { const filter = mocks.getTextSearchFilter(5, 'needle', - ColumnFilterSearchType.Contains, false); + SearchFilterParamsType.Contains, false); assert.equal(filter.column_index, 5); - assert.equal(filter.filter_type, ColumnFilterFilterType.Search); - assert.equal(filter.search_term, 'needle'); - assert.equal(filter.search_type, ColumnFilterSearchType.Contains); - assert.equal(filter.search_case_sensitive, false); + assert.equal(filter.filter_type, RowFilterFilterType.Search); + + const params = filter.search_params!; + + assert.equal(params.term, 'needle'); + assert.equal(params.type, SearchFilterParamsType.Contains); + assert.equal(params.case_sensitive, false); }); test('Test getSetMemberFilter', () => { const set_values = ['need1', 'need2']; const filter = mocks.getSetMemberFilter(6, set_values, true); assert.equal(filter.column_index, 6); - assert.equal(filter.filter_type, ColumnFilterFilterType.SetMembership); - assert.equal(filter.set_member_values, set_values); - assert.equal(filter.set_member_inclusive, true); + assert.equal(filter.filter_type, RowFilterFilterType.SetMembership); + + const params = filter.set_membership_params!; + + assert.equal(params.values, set_values); + assert.equal(params.inclusive, true); }); }); From aa2727f5cdd98ad55dd9b670e1c057ee2a4563b6 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Mon, 1 Apr 2024 20:00:10 -0600 Subject: [PATCH 6/6] OpenRPC for cursors/ranges rstudioapi shims (#2582) * Add `set_editor_selections` and `modify_editor_selections` OpenRPC methods * Bump ark to 0.1.73 --- .../positron/positron_ipykernel/ui_comm.py | 97 +++++++++++++++++++ extensions/positron-r/package.json | 2 +- positron/comms/ui-frontend-openrpc.json | 63 ++++++++++++ .../positron/mainThreadLanguageRuntime.ts | 11 ++- .../api/common/positron/extHostMethods.ts | 30 +++++- .../common/languageRuntimeUiClient.ts | 4 +- .../languageRuntime/common/positronUiComm.ts | 54 +++++++++++ .../runtimeSession/common/runtimeSession.ts | 9 ++ 8 files changed, 266 insertions(+), 4 deletions(-) diff --git a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py index cbbaa0a3fd2..c21823933fd 100644 --- a/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py +++ b/extensions/positron-python/pythonFiles/positron/positron_ipykernel/ui_comm.py @@ -116,6 +116,20 @@ class Selection(BaseModel): ) +class Range(BaseModel): + """ + Selection range + """ + + start: Position = Field( + description="Start position of the selection", + ) + + end: Position = Field( + description="End position of the selection", + ) + + @enum.unique class UiBackendRequest(str, enum.Enum): """ @@ -195,6 +209,15 @@ class UiFrontendEvent(str, enum.Enum): # Execute a Positron command ExecuteCommand = "execute_command" + # Open a workspace + OpenWorkspace = "open_workspace" + + # Set the selections in the editor + SetEditorSelections = "set_editor_selections" + + # Show a URL in Positron's Viewer pane + ShowUrl = "show_url" + class BusyParams(BaseModel): """ @@ -256,6 +279,20 @@ class ShowQuestionParams(BaseModel): ) +class ShowDialogParams(BaseModel): + """ + Show a dialog + """ + + title: str = Field( + description="The title of the dialog", + ) + + message: str = Field( + description="The message to display in the dialog", + ) + + class PromptStateParams(BaseModel): """ New state of the primary and secondary prompts @@ -300,6 +337,54 @@ class ExecuteCommandParams(BaseModel): ) +class OpenWorkspaceParams(BaseModel): + """ + Open a workspace + """ + + path: str = Field( + description="The path for the workspace to be opened", + ) + + new_window: bool = Field( + description="Should the workspace be opened in a new window?", + ) + + +class SetEditorSelectionsParams(BaseModel): + """ + Set the selections in the editor + """ + + selections: List[Range] = Field( + description="The selections (really, ranges) to set in the document", + ) + + +class ModifyEditorSelectionsParams(BaseModel): + """ + Modify selections in the editor with a text edit + """ + + selections: List[Range] = Field( + description="The selections (really, ranges) to set in the document", + ) + + values: List[str] = Field( + description="The text values to insert at the selections", + ) + + +class ShowUrlParams(BaseModel): + """ + Show a URL in Positron's Viewer pane + """ + + url: str = Field( + description="The URL to display", + ) + + EditorContext.update_forward_refs() TextDocument.update_forward_refs() @@ -308,6 +393,8 @@ class ExecuteCommandParams(BaseModel): Selection.update_forward_refs() +Range.update_forward_refs() + CallMethodParams.update_forward_refs() CallMethodRequest.update_forward_refs() @@ -320,6 +407,8 @@ class ExecuteCommandParams(BaseModel): ShowQuestionParams.update_forward_refs() +ShowDialogParams.update_forward_refs() + PromptStateParams.update_forward_refs() WorkingDirectoryParams.update_forward_refs() @@ -327,3 +416,11 @@ class ExecuteCommandParams(BaseModel): DebugSleepParams.update_forward_refs() ExecuteCommandParams.update_forward_refs() + +OpenWorkspaceParams.update_forward_refs() + +SetEditorSelectionsParams.update_forward_refs() + +ModifyEditorSelectionsParams.update_forward_refs() + +ShowUrlParams.update_forward_refs() diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index 0928e7c1086..85191ee8fe2 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -533,7 +533,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.72" + "ark": "0.1.73" } } } diff --git a/positron/comms/ui-frontend-openrpc.json b/positron/comms/ui-frontend-openrpc.json index 9af706bd604..13e9b703e10 100644 --- a/positron/comms/ui-frontend-openrpc.json +++ b/positron/comms/ui-frontend-openrpc.json @@ -229,6 +229,51 @@ } } }, + { + "name": "set_editor_selections", + "summary": "Set the selections in the editor", + "description": "Use this to set the selection ranges/cursor in the editor", + "params": [ + { + "name": "selections", + "description": "The selections (really, ranges) to set in the document", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/range" + } + } + } + ] + }, + { + "name": "modify_editor_selections", + "summary": "Modify selections in the editor with a text edit", + "description": "Use this to edit a set of selection ranges/cursor in the editor", + "params": [ + { + "name": "selections", + "description": "The selections (really, ranges) to set in the document", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/range" + } + } + }, + { + "name": "values", + "description": "The text values to insert at the selections", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "result": {} + }, { "name": "last_active_editor_context", "summary": "Context metadata for the last editor", @@ -383,6 +428,24 @@ "end", "text" ] + }, + "range": { + "type": "object", + "description": "Selection range", + "properties": { + "start": { + "description": "Start position of the selection", + "$ref": "#/components/schemas/position" + }, + "end": { + "description": "End position of the selection", + "$ref": "#/components/schemas/position" + } + }, + "required": [ + "start", + "end" + ] } } } diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index bc4ef78fe4f..56429f264b1 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -28,8 +28,10 @@ import { IPositronHelpService } from 'vs/workbench/contrib/positronHelp/browser/ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IRuntimeClientEvent } from 'vs/workbench/services/languageRuntime/common/languageRuntimeUiClient'; import { URI } from 'vs/base/common/uri'; -import { BusyEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, WorkingDirectoryEvent, ShowMessageEvent, ExecuteCommandEvent } from 'vs/workbench/services/languageRuntime/common/positronUiComm'; +import { BusyEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, WorkingDirectoryEvent, ShowMessageEvent, ExecuteCommandEvent, SetEditorSelectionsEvent } from 'vs/workbench/services/languageRuntime/common/positronUiComm'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { Selection } from 'vs/editor/common/core/selection'; import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IPositronDataExplorerService } from 'vs/workbench/services/positronDataExplorer/browser/interfaces/positronDataExplorerService'; import { ObservableValue } from 'vs/base/common/observableInternal/base'; @@ -190,6 +192,13 @@ class ExtHostLanguageRuntimeSessionAdapter implements ILanguageRuntimeSession { // Update busy state const busy = ev.data as BusyEvent; this.dynState.busy = busy.busy; + } else if (ev.name === UiFrontendEvent.SetEditorSelections) { + // Set the editor selections + const sel = ev.data as SetEditorSelectionsEvent; + const selections = sel.selections.map(s => + new Selection(s.start.line, s.start.character, s.end.line, s.end.character)); + const editor = this._editorService.activeTextEditorControl as IEditor; + editor.setSelections(selections); } else if (ev.name === UiFrontendEvent.OpenEditor) { // Open an editor const ed = ev.data as OpenEditorEvent; diff --git a/src/vs/workbench/api/common/positron/extHostMethods.ts b/src/vs/workbench/api/common/positron/extHostMethods.ts index 974a8cfca85..925cee80cf2 100644 --- a/src/vs/workbench/api/common/positron/extHostMethods.ts +++ b/src/vs/workbench/api/common/positron/extHostMethods.ts @@ -6,7 +6,8 @@ import * as extHostProtocol from './extHost.positron.protocol'; import { ExtHostEditors } from '../extHostTextEditors'; import { ExtHostModalDialogs } from '../positron/extHostModalDialogs'; import { ExtHostWorkspace } from '../extHostWorkspace'; -import { UiFrontendRequest, EditorContext } from 'vs/workbench/services/languageRuntime/common/positronUiComm'; +import { Range } from 'vs/workbench/api/common/extHostTypes'; +import { UiFrontendRequest, EditorContext, Range as UIRange } from 'vs/workbench/services/languageRuntime/common/positronUiComm'; import { JsonRpcErrorCode } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; import { EndOfLine } from '../extHostTypeConverters'; @@ -64,6 +65,18 @@ export class ExtHostMethods implements extHostProtocol.ExtHostMethodsShape { result = await this.lastActiveEditorContext(); break; } + case UiFrontendRequest.ModifyEditorSelections: { + if (!params || + !Object.keys(params).includes('selections') || + !Object.keys(params).includes('values')) { + return newInvalidParamsError(method); + } + const sel = params.selections as UIRange[]; + const selections = sel.map(s => + new Range(s.start.line, s.start.character, s.end.line, s.end.character)); + result = await this.modifyEditorLocations(selections, params.values as string[]); + break; + } case UiFrontendRequest.WorkspaceFolder: { if (params && Object.keys(params).length > 0) { return newInvalidParamsError(method); @@ -179,6 +192,21 @@ export class ExtHostMethods implements extHostProtocol.ExtHostMethodsShape { }; } + async modifyEditorLocations(locations: Range[], values: string[]): Promise { + const editor = this.editors.getActiveTextEditor(); + if (!editor) { + return null; + } + + editor.edit(editBuilder => { + locations.map((location, i) => { + editBuilder.replace(location, values[i]); + }); + }); + + return null; + } + async workspaceFolder(): Promise { const folders = this.workspace.getWorkspaceFolders(); if (folders && folders.length > 0) { diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts index a5ed2f1ee22..8e5aaed4d5b 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; -import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PositronUiComm, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ExecuteCommandEvent, ShowUrlEvent } from './positronUiComm'; +import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PositronUiComm, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ExecuteCommandEvent, ShowUrlEvent, SetEditorSelectionsEvent } from './positronUiComm'; /** @@ -63,6 +63,7 @@ export class UiClientInstance extends Disposable { /** Emitters for events forwarded from the UI comm */ onDidBusy: Event; onDidClearConsole: Event; + onDidSetEditorSelections: Event; onDidOpenEditor: Event; onDidOpenWorkspace: Event; onDidShowMessage: Event; @@ -86,6 +87,7 @@ export class UiClientInstance extends Disposable { this._comm = new PositronUiComm(this._client); this.onDidBusy = this._comm.onDidBusy; this.onDidClearConsole = this._comm.onDidClearConsole; + this.onDidSetEditorSelections = this._comm.onDidSetEditorSelections; this.onDidOpenEditor = this._comm.onDidOpenEditor; this.onDidOpenWorkspace = this._comm.onDidOpenWorkspace; this.onDidShowMessage = this._comm.onDidShowMessage; diff --git a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts index 4704b5105b1..6775dcc567c 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts @@ -138,6 +138,22 @@ export interface Selection { } +/** + * Selection range + */ +export interface Range { + /** + * Start position of the selection + */ + start: Position; + + /** + * End position of the selection + */ + end: Position; + +} + /** * Event: Change in backend's busy/idle status */ @@ -241,6 +257,17 @@ export interface OpenWorkspaceEvent { } +/** + * Event: Set the selections in the editor + */ +export interface SetEditorSelectionsEvent { + /** + * The selections (really, ranges) to set in the document + */ + selections: Array; + +} + /** * Event: Show a URL in Positron's Viewer pane */ @@ -320,6 +347,24 @@ export interface DebugSleepRequest { export interface WorkspaceFolderRequest { } +/** + * Request: Modify selections in the editor with a text edit + * + * Use this to edit a set of selection ranges/cursor in the editor + */ +export interface ModifyEditorSelectionsRequest { + /** + * The selections (really, ranges) to set in the document + */ + selections: Array; + + /** + * The text values to insert at the selections + */ + values: Array; + +} + /** * Request: Context metadata for the last editor * @@ -338,6 +383,7 @@ export enum UiFrontendEvent { WorkingDirectory = 'working_directory', ExecuteCommand = 'execute_command', OpenWorkspace = 'open_workspace', + SetEditorSelections = 'set_editor_selections', ShowUrl = 'show_url' } @@ -346,6 +392,7 @@ export enum UiFrontendRequest { ShowDialog = 'show_dialog', DebugSleep = 'debug_sleep', WorkspaceFolder = 'workspace_folder', + ModifyEditorSelections = 'modify_editor_selections', LastActiveEditorContext = 'last_active_editor_context' } @@ -360,6 +407,7 @@ export class PositronUiComm extends PositronBaseComm { this.onDidWorkingDirectory = super.createEventEmitter('working_directory', ['directory']); this.onDidExecuteCommand = super.createEventEmitter('execute_command', ['command']); this.onDidOpenWorkspace = super.createEventEmitter('open_workspace', ['path', 'new_window']); + this.onDidSetEditorSelections = super.createEventEmitter('set_editor_selections', ['selections']); this.onDidShowUrl = super.createEventEmitter('show_url', ['url']); } @@ -434,6 +482,12 @@ export class PositronUiComm extends PositronBaseComm { * Use this to open a workspace in Positron */ onDidOpenWorkspace: Event; + /** + * Set the selections in the editor + * + * Use this to set the selection ranges/cursor in the editor + */ + onDidSetEditorSelections: Event; /** * Show a URL in Positron's Viewer pane * diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 09079b90ef6..acc4775b12c 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -1051,6 +1051,15 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession } }); })); + this._register(uiClient.onDidSetEditorSelections(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + session_id: session.sessionId, + event: { + name: UiFrontendEvent.SetEditorSelections, + data: event + } + }); + })); this._register(uiClient.onDidOpenEditor(event => { this._onDidReceiveRuntimeEventEmitter.fire({ session_id: session.sessionId,