diff --git a/.github/workflows/positron-ci.yml b/.github/workflows/positron-ci.yml index 7aa4ef2b4ef..bbdacd657b2 100644 --- a/.github/workflows/positron-ci.yml +++ b/.github/workflows/positron-ci.yml @@ -13,7 +13,7 @@ jobs: linux: name: Tests on Linux runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 45 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} POSITRON_BUILD_NUMBER: 0 # CI skips building releases @@ -55,14 +55,17 @@ jobs: - name: Compile Integration Tests run: yarn --cwd test/integration/browser compile + - name: Compile Smoke Tests + run: yarn --cwd test/smoke compile + - name: Run Unit Tests (node.js) id: nodejs-unit-tests run: yarn test-node - - name: Run Unit Tests (Browser, Chromium) - id: browser-unit-tests - run: DISPLAY=:10 yarn test-browser-no-install --browser chromium - - name: Run Integration Tests (Electron) id: electron-integration-tests run: DISPLAY=:10 ./scripts/test-integration.sh + + - name: Run Smoke Tests (Electron) + id: electron-smoke-tests + run: DISPLAY=:10 yarn smoketest-no-compile --tracing diff --git a/.github/workflows/positron-full-test.yml b/.github/workflows/positron-full-test.yml new file mode 100644 index 00000000000..d99383c3e0d --- /dev/null +++ b/.github/workflows/positron-full-test.yml @@ -0,0 +1,86 @@ +name: "Positron: Full Test Suite" + +# Run tests daily at 4am UTC (11p EST) on weekdays for now, or manually +on: + schedule: + - cron: "0 4 * * 1-5" + workflow_dispatch: + +jobs: + + linux: + name: Tests on Linux + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSITRON_BUILD_NUMBER: 0 # CI skips building releases + steps: + - uses: actions/checkout@v4 + + - name: Setup Build Environment + run: | + sudo apt-get update + sudo apt-get install -y vim curl build-essential clang make cmake git r-base-dev python3-pip python-is-python3 libsodium-dev libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 libnss3 libnspr4 libasound2 libkrb5-dev + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Execute yarn + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + POSITRON_GITHUB_PAT: ${{ secrets.POSITRON_GITHUB_PAT }} + run: | + # Install Yarn + npm install -g yarn + + # Install node-gyp; this is required by some packages, and yarn + # sometimes fails to automatically install it. + yarn global add node-gyp + + # Perform the main yarn command; this installs all Node packages and + # dependencies + yarn --immutable --network-timeout 120000 + + - name: Compile and Download + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + + - name: Compile Integration Tests + run: yarn --cwd test/integration/browser compile + + - name: Compile Smoke Tests + run: yarn --cwd test/smoke compile + + - name: Run Unit Tests (Electron) + id: electron-unit-tests + run: DISPLAY=:10 ./scripts/test.sh + + - name: Run Unit Tests (node.js) + id: nodejs-unit-tests + run: yarn test-node + + - name: Run Unit Tests (Browser, Chromium) + id: browser-unit-tests + run: DISPLAY=:10 yarn test-browser-no-install --browser chromium + + - name: Run Integration Tests (Electron) + id: electron-integration-tests + run: DISPLAY=:10 ./scripts/test-integration.sh + + - name: Run Integration Tests (Remote) + id: electron-remote-integration-tests + timeout-minutes: 15 + run: DISPLAY=:10 ./scripts/test-remote-integration.sh + + - name: Run Integration Tests (Browser, Chromium) + id: browser-integration-tests + run: DISPLAY=:10 ./scripts/test-web-integration.sh --browser chromium + + - name: Run Smoke Tests (Electron) + id: electron-smoke-tests + run: DISPLAY=:10 yarn smoketest-no-compile --tracing diff --git a/extensions/jupyter-adapter/src/LanguageRuntimeAdapter.ts b/extensions/jupyter-adapter/src/LanguageRuntimeAdapter.ts index efa5563c486..b22d58d169b 100644 --- a/extensions/jupyter-adapter/src/LanguageRuntimeAdapter.ts +++ b/extensions/jupyter-adapter/src/LanguageRuntimeAdapter.ts @@ -117,11 +117,16 @@ export class LanguageRuntimeAdapter // Create the request. This uses a JSON-RPC 2.0 format, with an // additional `msg_type` field to indicate that this is a request type // for the frontend comm. + // + // NOTE: Currently using nested RPC messages for convenience but + // we'd like to do better const request = { - msg_type: 'rpc_request', jsonrpc: '2.0', - method: method, - params: args, + method: 'call_method', + params: { + method, + params: args + }, }; // Return a promise that resolves when the server side of the frontend diff --git a/extensions/positron-python b/extensions/positron-python index 197c2666e71..24e8f52bb0c 160000 --- a/extensions/positron-python +++ b/extensions/positron-python @@ -1 +1 @@ -Subproject commit 197c2666e71248a2ff686eed8de431173de075d2 +Subproject commit 24e8f52bb0c663a23ab2ecc8e366d7298d30c0c1 diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index ec52c3b199e..483b8219c65 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -503,7 +503,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.38" + "ark": "0.1.40" } } } diff --git a/positron/comms/data_tool-backend-openrpc.json b/positron/comms/data_tool-backend-openrpc.json new file mode 100644 index 00000000000..0d2408e4b55 --- /dev/null +++ b/positron/comms/data_tool-backend-openrpc.json @@ -0,0 +1,433 @@ +{ + "openrpc": "1.3.0", + "info": { + "title": "Data Tool Backend", + "version": "1.0.0" + }, + "methods": [ + { + "name": "get_schema", + "summary": "Request schema", + "description": "Request full schema for a table-like object", + "params": [], + "result": { + "schema": { + "type": "object", + "name": "table_schema", + "description": "The schema for a table-like object", + "properties": { + "columns": { + "type": "array", + "description": "Schema for each column in the table", + "items": { + "$ref": "#/components/schemas/column_schema" + } + }, + "num_rows": { + "type": "integer", + "description": "Numbers of rows in the unfiltered dataset" + } + } + } + } + }, + { + "name": "get_data_values", + "summary": "Get a rectangle of data values", + "description": "Request a rectangular subset of data with values formatted as strings", + "params": [ + { + "name": "row_start_index", + "description": "First row to fetch (inclusive)", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "row_end_index", + "description": "Last row to fetch (inclusive). May be beyond end", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "column_start_index", + "description": "First column to fetch (inclusive)", + "schema": { + "type": "integer" + } + }, + { + "name": "column_end_index", + "description": "Last column to fetch (inclusive). May extend beyond end", + "schema": { + "type": "integer" + } + } + ], + "result": { + "schema": { + "type": "object", + "name": "table_data", + "description": "Table values formatted as strings", + "required": [ + "columns" + ], + "properties": { + "columns": { + "type": "array", + "description": "The columns of data", + "items": { + "$ref": "#/components/schemas/column_formatted_data" + } + }, + "row_labels": { + "type": "array", + "description": "Zero or more arrays of row labels", + "items": { + "$ref": "#/components/schemas/column_formatted_data" + } + } + } + } + } + }, + { + "name": "set_column_filters", + "summary": "Set column filters", + "description": "Set or clear column filters on table, replacing any previous filters", + "params": [ + { + "name": "filters", + "description": "Zero or more filters to apply", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/column_filter" + } + } + } + ], + "result": { + "schema": { + "type": "object", + "name": "filter_result", + "description": "The result of applying filters to a table", + "properties": { + "selected_num_rows": { + "type": "integer", + "description": "Number of rows in table after applying filters" + } + } + } + } + }, + { + "name": "set_sort_columns", + "summary": "Set or clear sort-by-column(s)", + "description": "Set or clear the columns(s) to sort by, replacing any previous sort columns", + "params": [ + { + "name": "sort_keys", + "description": "Pass zero or more keys to sort by. Clears any existing keys", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/column_sort_key" + } + } + } + ], + "result": {} + }, + { + "name": "get_column_profile", + "summary": "Get a column profile", + "description": "Requests a statistical summary or data profile for a column", + "params": [ + { + "name": "profile_id", + "description": "Unique identifier for the requested profile", + "schema": { + "type": "string" + } + }, + { + "name": "profile_type", + "description": "The type of analytical column profile", + "schema": { + "type": "string", + "enum": [ + "freqtable", + "histogram" + ] + } + }, + { + "name": "column", + "description": "Column name to compute profile for", + "schema": { + "type": "string" + } + } + ], + "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", + "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": "get_state", + "summary": "Get the state", + "description": "Request the current backend state (applied filters and sort columns)", + "params": [], + "result": { + "schema": { + "type": "object", + "name": "backend_state", + "description": "The current backend state", + "properties": { + "filters": { + "type": "array", + "description": "The set of currently applied filters", + "items": { + "$ref": "#/components/schemas/column_filter" + } + }, + "sort_keys": { + "type": "array", + "description": "The set of currently applied sorts", + "items": { + "$ref": "#/components/schemas/column_sort_key" + } + } + } + } + } + } + ], + "components": { + "contentDescriptors": {}, + "schemas": { + "column_schema": { + "type": "object", + "description": "Schema for a column in a table", + "required": [ + "name", + "type_name" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of column as UTF-8 string" + }, + "type_name": { + "type": "string", + "description": "Canonical name of data type class" + }, + "description": { + "type": "string", + "description": "Column annotation / description" + }, + "children": { + "type": "array", + "description": "Schema of nested child types", + "items": { + "$ref": "#/components/schemas/column_schema" + } + }, + "precision": { + "type": "integer", + "description": "Precision for decimal types" + }, + "scale": { + "type": "integer", + "description": "Scale for decimal types" + }, + "timezone": { + "type": "string", + "description": "Time zone for timestamp with time zone" + }, + "type_size": { + "type": "integer", + "description": "Size parameter for fixed-size types (list, binary)" + } + } + }, + "column_formatted_data": { + "type": "array", + "description": "Column values formatted as strings", + "items": { + "type": "string" + } + }, + "column_filter": { + "type": "object", + "description": "Specifies a table row filter based on a column's values", + "required": [ + "filter_id", + "filter_type" + ], + "properties": { + "filter_id": { + "type": "string", + "description": "Unique identifier for this filter" + }, + "filter_type": { + "type": "string", + "description": "Type of filter to apply", + "enum": [ + "isnull", + "notnull", + "compare", + "set_membership", + "search" + ] + }, + "compare_op": { + "type": "string", + "description": "String representation of a binary comparison", + "enum": [ + "==", + "!=", + "<", + "<=", + ">", + ">=" + ] + }, + "compare_value": { + "type": "string", + "description": "A stringified column value for a comparison filter" + }, + "set_member_values": { + "type": "array", + "description": "Array of column values for a set membership filter", + "items": { + "type": "string" + } + }, + "set_member_inclusive": { + "type": "boolean", + "description": "Filter by including only values passed (true) or excluding (false)" + }, + "search_type": { + "type": "string", + "description": "Type of search to perform", + "enum": [ + "contains", + "startswith", + "endswith", + "regex" + ] + }, + "search_term": { + "type": "string", + "description": "String value/regex to search for in stringified data" + }, + "search_case_sensitive": { + "type": "boolean", + "description": "If true, do a case-sensitive search, otherwise case-insensitive" + } + } + }, + "column_quantile_value": { + "type": "object", + "description": "An exact or approximate quantile value from a column", + "properties": { + "q": { + "type": "number", + "description": "Quantile number (percentile). E.g. 1 for 1%, 50 for median" + }, + "value": { + "type": "string", + "description": "Stringified quantile value" + }, + "exact": { + "type": "boolean", + "description": "Whether value is exact or approximate (computed from binned data or sketches)" + } + } + }, + "column_sort_key": { + "type": "object", + "description": "Specifies a column to sort by", + "properties": { + "column": { + "type": "string", + "description": "Column name to sort by" + }, + "ascending": { + "type": "boolean", + "description": "Sort order, ascending (true) or descending (false)" + } + } + } + } + } +} diff --git a/positron/comms/data_tool.json b/positron/comms/data_tool.json new file mode 100644 index 00000000000..94d8d74e869 --- /dev/null +++ b/positron/comms/data_tool.json @@ -0,0 +1,9 @@ +{ + "name": "data_tool", + "initiator": "frontend", + "initial_data": { + "schema": { + "type": "null" + } + } +} diff --git a/positron/comms/frontend-backend-openrpc.json b/positron/comms/frontend-backend-openrpc.json new file mode 100644 index 00000000000..73dad97ed96 --- /dev/null +++ b/positron/comms/frontend-backend-openrpc.json @@ -0,0 +1,45 @@ +{ + "openrpc": "1.3.0", + "info": { + "title": "Frontend Backend", + "version": "1.0.0" + }, + "methods": [ + { + "name": "call_method", + "summary": "Run a method in the interpreter and return the result to the frontend", + "description": "Unlike other RPC methods, `call_method` calls into methods implemented in the interpreter and returns the result back to the frontend using an implementation-defined serialization scheme.", + "params": [ + { + "name": "method", + "description": "The method to call inside the interpreter", + "schema": { + "type": "string" + } + }, + { + "name": "params", + "description": "The parameters for `method`", + "schema": { + "type": "array", + "items": { + "name": "param", + "type": "object", + "properties": {}, + "additionalProperties": true + } + } + } + ], + "result": { + "schema": { + "name": "call_method_result", + "description": "The method result", + "type": "object", + "properties": {}, + "additionalProperties": true + } + } + } + ] +} diff --git a/positron/comms/frontend-frontend-openrpc.json b/positron/comms/frontend-frontend-openrpc.json new file mode 100644 index 00000000000..bf628ca6b6b --- /dev/null +++ b/positron/comms/frontend-frontend-openrpc.json @@ -0,0 +1,106 @@ +{ + "openrpc": "1.3.0", + "info": { + "title": "Frontend Frontend", + "version": "1.0.0" + }, + "methods": [ + { + "name": "busy", + "summary": "Change in backend's busy/idle status", + "description": "This represents the busy state of the underlying computation engine, not the busy state of the kernel. The kernel is busy when it is processing a request, but the runtime is busy only when a computation is running.", + "params": [ + { + "name": "busy", + "description": "Whether the backend is busy", + "schema": { + "type": "boolean" + } + } + ] + }, + { + "name": "clear_console", + "summary": "Clear the console", + "description": "Use this to clear the console.", + "params": [] + }, + { + "name": "open_editor", + "summary": "Open an editor", + "description": "This event is used to open an editor with a given file and selection.", + "params": [ + { + "name": "file", + "description": "The path of the file to open", + "schema": { + "type": "string" + } + }, + { + "name": "line", + "description": "The line number to jump to", + "schema": { + "type": "integer" + } + }, + { + "name": "column", + "description": "The column number to jump to", + "schema": { + "type": "integer" + } + } + ] + }, + { + "name": "show_message", + "summary": "Show a message", + "description": "Use this for messages that require immediate attention from the user", + "params": [ + { + "name": "message", + "description": "The message to show to the user.", + "schema": { + "type": "string" + } + } + ] + }, + { + "name": "prompt_state", + "summary": "New state of the primary and secondary prompts", + "description": "Languages like R allow users to change the way their prompts look. This event signals a change in the prompt configuration.", + "params": [ + { + "name": "input_prompt", + "description": "Prompt for primary input.", + "schema": { + "type": "string" + } + }, + { + "name": "continuation_prompt", + "description": "Prompt for incomplete input.", + "schema": { + "type": "string" + } + } + ] + }, + { + "name": "working_directory", + "summary": "Change the displayed working directory", + "description": "This event signals a change in the working direcotry of the interpreter", + "params": [ + { + "name": "directory", + "description": "The new working directory", + "schema": { + "type": "string" + } + } + ] + } + ] +} diff --git a/positron/comms/frontend.json b/positron/comms/frontend.json new file mode 100644 index 00000000000..46e2c26d460 --- /dev/null +++ b/positron/comms/frontend.json @@ -0,0 +1,9 @@ +{ + "name": "frontend", + "initiator": "frontend", + "initial_data": { + "schema": { + "type": "null" + } + } +} diff --git a/positron/comms/generate-comms.ts b/positron/comms/generate-comms.ts index 54d2ffeae87..edd977d2b20 100644 --- a/positron/comms/generate-comms.ts +++ b/positron/comms/generate-comms.ts @@ -11,9 +11,8 @@ */ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'; -import { compile } from 'json-schema-to-typescript'; import { execSync } from 'child_process'; -import path, { format } from 'path'; +import path from 'path'; const commsDir = `${__dirname}`; const commsFiles = readdirSync(commsDir); @@ -48,7 +47,8 @@ const TypescriptTypeMap: Record = { 'number': 'number', 'string': 'string', 'null': 'null', - 'array': 'Array', + 'array-begin': 'Array<', + 'array-end': '>', 'object': 'object', }; @@ -59,7 +59,8 @@ const RustTypeMap: Record = { 'number': 'f64', 'string': 'String', 'null': 'null', - 'array': 'Vec', + 'array-begin': 'Vec<', + 'array-end': '>', 'object': 'HashMap', }; @@ -70,17 +71,22 @@ const PythonTypeMap: Record = { 'number': 'float', 'string': 'str', 'null': 'null', - 'array': 'List', + 'array-begin': 'List[', + 'array-end': ']', 'object': 'Dict', }; /** - * Converter from snake_case to camelCase + * Converter from snake_case to camelCase. Also replaces some special characters * * @param name A snake_case name * @returns A camelCase name */ function snakeCaseToCamelCase(name: string) { + name = name.replace(/=/g, 'Eq'); + name = name.replace(/!/g, 'Not'); + name = name.replace(//g, 'Gt'); return name.replace(/_([a-z])/g, (m) => m[1].toUpperCase()); } @@ -94,8 +100,97 @@ function snakeCaseToSentenceCase(name: string) { return snakeCaseToCamelCase(name).replace(/^[a-z]/, (m) => m[0].toUpperCase()); } -// Breaks a single line of text into multiple lines, each of which is no longer than -// 70 characters. +/** + * Parse a ref tag to get the name of the object referred to by the ref. + * + * @param ref The ref to parse + * @param contract The OpenRPC contract that the ref is part of + * @returns The name of the object referred to by the ref, as a SentenceCase + * string, or undefined if the ref could not be parsed or found. + */ +function parseRefFromContract(ref: string, contract: any): string | undefined { + // Split the ref into parts, and then walk the contract to find the + // referenced object + const parts = ref.split('/'); + let target = contract; + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '#') { + continue; + } + if (Object.keys(target).includes(parts[i])) { + target = target[parts[i]]; + } else { + return undefined; + } + } + return snakeCaseToSentenceCase(parts[parts.length - 1]); +} + +/** + * Parse a ref tag to get the name of the object referred to by the ref. + * Searches all the given contracts for the ref; throws if the ref cannot be + * found in any of the contracts. + * + * @param ref The ref to parse + * @param contracts The OpenRPC contracts to search for the ref. + * @returns The name of the object referred to by the ref. + */ +function parseRef(ref: string, contracts: Array): string { + for (const contract of contracts) { + if (!contract) { + continue; + } + const name = parseRefFromContract(ref, contract); + if (name) { + return name; + } + } + throw new Error(`Could not find ref: ${ref}`); +} + +/** + * Generic function for deriving a type from a schema. + * + * @param contract The OpenRPC contracts + * @param typeMap A map from schema types to language types + * @param key The key beneath which this schema is defined + * @param schema The schema to derive a type from + * + * @returns A string containing the derived type + */ +function deriveType(contracts: Array, + typeMap: Record, + key: string, + schema: any): string { + if (schema.type === 'array') { + // If the array has a ref, use that to derive an array type + return typeMap['array-begin'] + + deriveType(contracts, typeMap, key, schema.items) + + typeMap['array-end']; + } else if (schema.$ref) { + return parseRef(schema.$ref, contracts); + } else if (schema.type === 'object') { + if (schema.name) { + return snakeCaseToSentenceCase(schema.name); + } else { + return snakeCaseToSentenceCase(key); + } + } else { + if (Object.keys(typeMap).includes(schema.type)) { + return typeMap[schema.type]; + } else { + throw new Error(`Unknown type: ${schema.type}`); + } + } +} + +/** + * Breaks a single line of text into multiple lines, each of which is no longer + * than 70 characters. + * + * @param line The line to break + * @returns An array of lines + */ function formatLines(line: string): string[] { const words = line.split(' '); const lines = new Array(); @@ -132,6 +227,109 @@ function formatComment(leader: string, comment: string): string { return result; } +/** + * Visitor function for enums in an OpenRPC contract. Recursively discovers all + * enum values and calls the callback function for each enum. + * + * @param context The current context stack (names of objects leading to the enum) + * @param contract The OpenRPC contract to visit + * @param callback The callback function to call for each enum + * + * @returns An generator that yields the results of the callback function, + * invoked for each enum + */ +function* enumVisitor( + context: Array, + contract: any, + callback: (context: Array, e: Array) => Generator +): Generator { + if (contract.enum) { + // If this object has an enum, call the callback function and yield the + // result + yield* callback(context, contract.enum); + } else if (Array.isArray(contract)) { + // If this object is an array, recurse into each item + for (const item of contract) { + yield* enumVisitor(context, item, callback); + } + } else if (typeof contract === 'object') { + // If this object is an object, recurse into each property + for (const key of Object.keys(contract)) { + if (contract['name']) { + // If this is a named object, push the name onto the context + // and recurse + yield* enumVisitor( + [contract['name'], ...context], contract[key], callback); + } else if (key === 'properties' || key === 'params') { + // If this is a properties or params object, recurse into each + // property, but don't push the parent name onto the context + yield* enumVisitor( + context, contract[key], callback); + } else { + // For all other objects, push the key onto the context and + // recurse + yield* enumVisitor( + [key, ...context], contract[key], callback); + } + + } + } +} + + +/** + * Visitor function for object definitions (i.e. schema type = "object") in an + * OpenRPC contract. Recursively discovers all object definitions and invokes + * the callback for each one. + * + * @param context The current context stack (names of keys leading to the object) + * @param contract The OpenRPC contract to visit + * @param callback The callback function to call for each object + * + * @returns An generator that yields the results of the callback function, + * invoked for each object definition + */ +function* objectVisitor( + context: Array, + contract: any, + callback: (context: Array, o: Record) => Generator +): Generator { + if (contract.type === 'object') { + // This is an object definition, so call the callback function and yield + // the result + yield* callback(context, contract); + + // Keep recursing into the object definition to discover any nested + // object definitions + yield* objectVisitor(context, contract.properties, callback); + } else if (Array.isArray(contract)) { + // If this object is an array, recurse into each item + for (const item of contract) { + yield* objectVisitor(context, item, callback); + } + } else if (typeof contract === 'object') { + // If this object is an object, recurse into each property + for (const key of Object.keys(contract)) { + if (key === 'schema') { + yield* objectVisitor(context, contract[key], callback); + } + else { + yield* objectVisitor( + [key, ...context], contract[key], callback); + } + } + } +} + +/** + * Create a Rust comm for a given OpenRPC contract. + * + * @param name The name of the comm + * @param frontend The OpenRPC contract for the frontend + * @param backend The OpenRPC contract for the backend + * + * @returns A generator that yields the Rust code for the comm + */ function* createRustComm(name: string, frontend: any, backend: any): Generator { yield `/*--------------------------------------------------------------------------------------------- * Copyright (C) ${year} Posit Software, PBC. All rights reserved. @@ -146,63 +344,95 @@ use serde::Serialize; `; - if (backend) { - // Create objects for all the object schemas first - for (const method of backend.methods) { - if (method.result && - method.result.schema && - method.result.schema.type === 'object') { - yield '#[derive(Debug, Serialize, Deserialize, PartialEq)]\n'; - yield `pub struct ${snakeCaseToSentenceCase(method.result.schema.name)} {\n`; - const props = Object.keys(method.result.schema.properties); - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const schema = method.result.schema.properties[prop]; - if (schema.description) { - yield formatComment('\t/// ', schema.description); - } - yield `\tpub ${prop}: ${RustTypeMap[schema.type]},\n`; - if (i < props.length - 1) { - yield '\n'; - } - } - yield '}\n\n'; - } - } - } + const contracts = [backend, frontend]; - // Create enums for all enum types - for (const source of [backend, frontend]) { + for (const source of contracts) { if (!source) { continue; } - for (const method of source.methods) { - for (const param of method.params) { - if (param.schema.enum) { - yield formatComment(`/// `, - `Possible values for the ` + - snakeCaseToSentenceCase(param.name) + ` ` + - `parameter of the ` + - snakeCaseToSentenceCase(method.name) + ` ` + - `method.`); - yield '#[derive(Debug, Serialize, Deserialize, PartialEq)]\n'; - yield `pub enum ${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)} {\n`; - for (let i = 0; i < param.schema.enum.length; i++) { - const value = param.schema.enum[i]; - yield `\t#[serde(rename = "${value}")]\n`; - yield `\t${snakeCaseToSentenceCase(value)}`; - if (i < param.schema.enum.length - 1) { - yield ',\n\n'; - } else { - yield '\n'; - } - } - yield '}\n\n'; + + // Create type aliases for all the shared types + if (source.components && source.components.schemas) { + for (const key of Object.keys(backend.components.schemas)) { + const schema = backend.components.schemas[key]; + if (schema.type !== 'object') { + yield formatComment('/// ', schema.description); + yield `type ${snakeCaseToSentenceCase(key)} = `; + yield deriveType(contracts, RustTypeMap, + schema.name ? schema.name : key, + schema); + yield ';\n\n'; } } } + + // Create structs for all object types + yield* objectVisitor([], source, function* (context: Array, o: Record) { + if (o.description) { + yield formatComment('/// ', o.description); + } else { + yield formatComment('/// ', + snakeCaseToSentenceCase(context[0]) + ' in ' + + snakeCaseToSentenceCase(context[1])); + } + const name = o.name ? o.name : context[0] === 'items' ? context[1] : context[0]; + const props = Object.keys(o.properties); + + // Map "any" type to `Value` + if (props.length === 0 && o.additionalProperties === true) { + return yield `pub type ${snakeCaseToSentenceCase(name)} = serde_json::Value;\n\n`; + } + + yield '#[derive(Debug, Serialize, Deserialize, PartialEq)]\n'; + yield `pub struct ${snakeCaseToSentenceCase(name)} {\n`; + + for (let i = 0; i < props.length; i++) { + const key = props[i]; + const prop = o.properties[key]; + if (prop.description) { + yield formatComment('\t/// ', prop.description); + } + yield `\tpub ${key}: `; + if (!o.required || !o.required.includes(key)) { + yield 'Option<'; + yield deriveType(contracts, RustTypeMap, key, prop); + yield '>'; + + } else { + yield deriveType(contracts, RustTypeMap, key, prop); + } + if (i < props.length - 1) { + yield ',\n'; + } + yield '\n'; + } + yield '}\n\n'; + }); + + + // Create enums for all enum types + yield* enumVisitor([], source, function* (context: Array, values: Array) { + yield formatComment(`/// `, + `Possible values for ` + + snakeCaseToSentenceCase(context[0]) + ` in ` + + snakeCaseToSentenceCase(context[1])); + yield '#[derive(Debug, Serialize, Deserialize, PartialEq)]\n'; + yield `pub enum ${snakeCaseToSentenceCase(context[1])}${snakeCaseToSentenceCase(context[0])} {\n`; + for (let i = 0; i < values.length; i++) { + const value = values[i]; + yield `\t#[serde(rename = "${value}")]\n`; + yield `\t${snakeCaseToSentenceCase(value)}`; + if (i < values.length - 1) { + yield ',\n\n'; + } else { + yield '\n'; + } + } + yield '}\n\n'; + }); } + // Create parameter objects for each method for (const source of [backend, frontend]) { if (!source) { continue; @@ -223,9 +453,12 @@ use serde::Serialize; if (param.schema.enum) { // Use an enum type if the schema has an enum yield `\tpub ${param.name}: ${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)},\n`; + } else if (param.schema.type === 'object' && Object.keys(param.schema.properties).length === 0) { + // Handle the "any" type + yield `\tpub ${param.name}: serde_json::Value,\n`; } else { // Otherwise use the type directly - yield `\tpub ${param.name}: ${RustTypeMap[param.schema.type]},\n`; + yield `\tpub ${param.name}: ${deriveType(contracts, RustTypeMap, param.name, param.schema)},\n`; } if (i < method.params.length - 1) { yield '\n'; @@ -236,6 +469,7 @@ use serde::Serialize; } } + // Create the RPC request and reply enums if (backend) { yield '/**\n'; yield ` * RPC request types for the ${name} comm\n`; @@ -254,9 +488,9 @@ use serde::Serialize; yield `\t#[serde(rename = "${method.name}")]\n`; yield `\t${snakeCaseToSentenceCase(method.name)}`; if (method.params.length > 0) { - yield `(${snakeCaseToSentenceCase(method.name)}Params),\n`; + yield `(${snakeCaseToSentenceCase(method.name)}Params),\n\n`; } else { - yield ',\n'; + yield ',\n\n'; } } yield `}\n\n`; @@ -275,15 +509,16 @@ use serde::Serialize; } yield `\t${snakeCaseToSentenceCase(method.name)}Reply`; if (schema.type === 'object') { - yield `(${snakeCaseToSentenceCase(schema.name)}),\n`; + yield `(${snakeCaseToSentenceCase(schema.name)}),\n\n`; } else { - yield `(${RustTypeMap[schema.type]}),\n`; + yield `(${RustTypeMap[schema.type]}),\n\n`; } } } yield `}\n\n`; } + // Create the event enum if (frontend) { yield '/**\n'; yield ` * Front-end events for the ${name} comm\n`; @@ -295,18 +530,29 @@ use serde::Serialize; yield `\t#[serde(rename = "${method.name}")]\n`; yield `\t${snakeCaseToSentenceCase(method.name)}`; if (method.params.length > 0) { - yield `(${snakeCaseToSentenceCase(method.name)}Params),\n`; + yield `(${snakeCaseToSentenceCase(method.name)}Params),\n\n`; } else { - yield ',\n'; + yield ',\n\n'; } } yield `}\n\n`; } } -function* createPythonComm(name: string, frontend: any, backend: any): Generator { +/** + * Create a Python comm for a given OpenRPC contract. + * + * @param name The name of the comm + * @param frontend The OpenRPC contract for the frontend + * @param backend The OpenRPC contract for the backend + * + * @returns A generator that yields the Python code for the comm + */ +function* createPythonComm(name: string, + frontend: any, + backend: any): Generator { yield `# -# Copyright (C) ${year} Posit Software, PBC. All rights reserved. +# Copyright (C) ${year} Posit Software, PBC. All rights reserved. # # @@ -315,70 +561,94 @@ function* createPythonComm(name: string, frontend: any, backend: any): Generator import enum from dataclasses import dataclass, field +from typing import Dict, List, Union +JsonData = Union[Dict[str, "JsonData"], List["JsonData"], str, int, float, bool, None] `; + const contracts = [backend, frontend]; + for (const source of contracts) { + if (!source) { + continue; + } + // Create dataclasses for all object types + yield* objectVisitor([], source, function* ( + context: Array, + o: Record) { + + let name = o.name ? o.name : context[0] === 'items' ? context[1] : context[0]; + name = snakeCaseToSentenceCase(name); + + // Empty object specs map to `JsonData` + const props = Object.keys(o.properties); + if ((!props || !props.length) && o.additionalProperties === true) { + return yield `${name} = JsonData\n`; + } - if (backend) { - // Create classes for all the object schemas first - for (const method of backend.methods) { - if (method.result && - method.result.schema && - method.result.schema.type === 'object') { - yield '@dataclass\n'; - yield `class ${snakeCaseToSentenceCase(method.result.schema.name)}:\n`; - if (method.result.schema.description) { - yield ' """\n'; - yield formatComment(' ', method.result.schema.description); - yield ' """\n'; - yield '\n'; + // Preamble + yield '@dataclass\n'; + yield `class ${name}:\n`; + + // Docstring + if (o.description) { + yield ' """\n'; + yield formatComment(' ', o.description); + yield ' """\n'; + yield '\n'; + } else { + yield ' """\n'; + yield formatComment(' ', snakeCaseToSentenceCase(context[0]) + ' in ' + + snakeCaseToSentenceCase(context[1])); + yield ' """\n'; + yield '\n'; + } + + // Fields + for (const prop of Object.keys(o.properties)) { + const schema = o.properties[prop]; + yield ` ${prop}: `; + if (!o.required || !o.required.includes(prop)) { + yield 'Optional['; + yield deriveType(contracts, PythonTypeMap, prop, schema); + yield ']'; + } else { + yield deriveType(contracts, PythonTypeMap, prop, schema); } - for (const prop of Object.keys(method.result.schema.properties)) { - const schema = method.result.schema.properties[prop]; - yield ` ${prop}: ${PythonTypeMap[schema.type]}`; - yield ' = field(\n'; - yield ` metadata={\n`; - yield ` "description": "${schema.description}",\n`; - yield ` }\n`; - yield ` )\n\n`; + yield ' = field(\n'; + yield ` metadata={\n`; + yield ` "description": "${schema.description}",\n`; + if (!o.required || !o.required.includes(prop)) { + yield ` "default": None,\n`; } - yield '\n\n'; + yield ` }\n`; + yield ` )\n\n`; } - } - } - - // Create enums for all enum types - for (const source of [backend, frontend]) { - if (!source) { - continue; - } - for (const method of source.methods) { - for (const param of method.params) { - if (param.schema.enum) { - yield '@enum.unique\n'; - yield `class ${snakeCaseToSentenceCase(method.name)}`; - yield `${snakeCaseToSentenceCase(param.name)}(str, enum.Enum):\n`; - yield ' """\n'; - yield formatComment(` `, - `Possible values for the ` + - snakeCaseToSentenceCase(param.name) + ` ` + - `parameter of the ` + - snakeCaseToSentenceCase(method.name) + ` ` + - `method.`); - yield ' """\n'; - yield '\n'; - for (let i = 0; i < param.schema.enum.length; i++) { - const value = param.schema.enum[i]; - yield ` ${snakeCaseToSentenceCase(value)} = "${value}"`; - if (i < param.schema.enum.length - 1) { - yield '\n\n'; - } else { - yield '\n'; - } - } + yield '\n\n'; + }); + + // Create enums for all enum types + yield* enumVisitor([], source, function* (context: Array, values: Array) { + yield '@enum.unique\n'; + yield `class ${snakeCaseToSentenceCase(context[1])}`; + yield `${snakeCaseToSentenceCase(context[0])}(str, enum.Enum):\n`; + yield ' """\n'; + yield formatComment(` `, + `Possible values for ` + + snakeCaseToSentenceCase(context[0]) + + ` in ` + + snakeCaseToSentenceCase(context[1])); + yield ' """\n'; + yield '\n'; + for (let i = 0; i < values.length; i++) { + const value = values[i]; + yield ` ${snakeCaseToSentenceCase(value)} = "${value}"`; + if (i < values.length - 1) { yield '\n\n'; + } else { + yield '\n'; } } - } + yield '\n\n'; + }); } if (backend) { @@ -399,6 +669,9 @@ from dataclasses import dataclass, field if (backend) { for (const method of backend.methods) { + if (!method.description) { + throw new Error(`No description for '${method.name}'; please add a description to the schema`); + } yield `@dataclass\n`; yield `class ${snakeCaseToSentenceCase(method.name)}Params:\n`; yield ` """\n`; @@ -409,7 +682,7 @@ from dataclasses import dataclass, field if (param.schema.enum) { yield ` ${param.name}: ${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)}`; } else { - yield ` ${param.name}: ${PythonTypeMap[param.schema.type]}`; + yield ` ${param.name}: ${deriveType(contracts, PythonTypeMap, param.name, param.schema)}`; } yield ' = field(\n'; yield ` metadata={\n`; @@ -480,7 +753,7 @@ from dataclasses import dataclass, field if (param.schema.enum) { yield ` ${param.name}: ${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)}`; } else { - yield ` ${param.name}: ${PythonTypeMap[param.schema.type]}`; + yield ` ${param.name}: ${deriveType(contracts, PythonTypeMap, param.name, param.schema)}`; } yield ' = field(\n'; yield ` metadata={\n`; @@ -493,7 +766,77 @@ from dataclasses import dataclass, field } } -async function* createTypescriptComm(name: string, frontend: any, backend: any): AsyncGenerator { +/** + * Generates a Typescript interface for a given object schema. + * + * @param contract The OpenRPC contracts that the schema is part of + * @param name The name of the schema + * @param description The description of the schema + * @param properties The properties of the schema + * @param required An array of required properties + * @param additionalProperties Whether additional properties are allowed. + * Currently only used for "any" objects. + * + * @returns A generator that yields the Typescript code for an interface + * representing the schema + */ +function* createTypescriptInterface( + contracts: Array, + name: string, + description: string, + properties: Record, + required: Array, + additionalProperties?: boolean, +): Generator { + + if (!description) { + throw new Error(`No description for '${name}'; please add a description to the schema`); + } + yield '/**\n'; + yield formatComment(' * ', description); + yield ' */\n'; + yield `export interface ${snakeCaseToSentenceCase(name)} {\n`; + if (!properties || Object.keys(properties).length === 0) { + if (!additionalProperties) { + throw new Error(`No properties for '${name}'; please add properties to the schema`); + } + + // If `additionalProperties` is true, treat empty object specs as an "any" object + yield '\t[k: string]: unknown;\n'; + } + for (const prop of Object.keys(properties)) { + const schema = properties[prop]; + if (!schema.description) { + throw new Error(`No description for the '${name}.${prop}' value; please add a description to the schema`); + } + yield '\t/**\n'; + yield formatComment('\t * ', schema.description); + yield '\t */\n'; + yield `\t${prop}`; + if (!required.includes(prop)) { + yield '?'; + } + yield `: `; + if (schema.type === 'object') { + yield snakeCaseToSentenceCase(schema.name); + } else if (schema.type === 'string' && schema.enum) { + yield `${snakeCaseToSentenceCase(name)}${snakeCaseToSentenceCase(prop)}`; + } else { + yield deriveType(contracts, TypescriptTypeMap, prop, schema); + } + yield `;\n\n`; + } + yield '}\n\n'; +} + +/** + * Create a Typescript comm for a given OpenRPC contract. + * + * @param name The name of the comm + * @param frontend The OpenRPC contract for the frontend + * @param backend The OpenRPC contract for the backend + */ +function* createTypescriptComm(name: string, frontend: any, backend: any): Generator { // Read the metadata file const metadata: CommMetadata = JSON.parse( readFileSync(path.join(commsDir, `${name}.json`), { encoding: 'utf-8' })); @@ -505,55 +848,107 @@ async function* createTypescriptComm(name: string, frontend: any, backend: any): // AUTO-GENERATED from ${name}.json; do not edit. // -import { Event } from 'vs/base/common/event'; -import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; +`; + // If there are frontend events, import the Event class + if (frontend) { + yield `import { Event } from 'vs/base/common/event';\n`; + } + yield `import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; `; + const contracts = [backend, frontend]; + for (const source of contracts) { + if (!source) { + continue; + } + yield* objectVisitor([], source, + function* (context: Array, o: Record): Generator { + const name = o.name ? o.name : context[0] === 'items' ? context[1] : context[0]; + const description = o.description ? o.description : + snakeCaseToSentenceCase(context[0]) + ' in ' + + snakeCaseToSentenceCase(context[1]); + const additionalProperties = o.additionalProperties ? o.additionalProperties : false; + yield* createTypescriptInterface(contracts, name, description, o.properties, + o.required ? o.required : [], additionalProperties); + }); + + // Create enums for all enum types + yield* enumVisitor([], source, function* (context: Array, values: Array) { + yield '/**\n'; + yield formatComment(` * `, + `Possible values for ` + + snakeCaseToSentenceCase(context[0]) + ` in ` + + snakeCaseToSentenceCase(context[1])); + yield ' */\n'; + yield `export enum ${snakeCaseToSentenceCase(context[1])}${snakeCaseToSentenceCase(context[0])} {\n`; + for (let i = 0; i < values.length; i++) { + const value = values[i]; + yield `\t${snakeCaseToSentenceCase(value)} = '${value}'`; + if (i < values.length - 1) { + yield ',\n'; + } else { + yield '\n'; + } + } + yield '}\n\n'; + }); + } - if (backend) { - // Create interfaces for all the object schemas first - for (const method of backend.methods) { - if (method.result && - method.result.schema && - method.result.schema.type === 'object') { - yield await compile(method.result.schema, - method.result.schema.name, { - bannerComment: '', - additionalProperties: false, - style: { - useTabs: true - } - }); - yield '\n'; + for (const source of [backend, frontend]) { + if (!source) { + continue; + } + if (source.components && source.components.schemas) { + for (const key of Object.keys(backend.components.schemas)) { + const schema = backend.components.schemas[key]; + if (schema.type !== 'object') { + yield `/**\n`; + yield formatComment(' * ', schema.description); + yield ' */\n'; + yield `export type ${snakeCaseToSentenceCase(key)} = `; + yield deriveType(contracts, TypescriptTypeMap, key, schema); + yield ';\n\n'; + } } } } if (frontend) { + const events: string[] = []; + for (const method of frontend.methods) { // Ignore methods that have a result; we're generating event types here if (method.result) { continue; } + + // Collect enum fields + const sentenceName = snakeCaseToSentenceCase(method.name); + events.push(`\t${sentenceName} = '${method.name}'`); + yield '/**\n'; yield formatComment(' * ', `Event: ${method.summary}`); yield ' */\n'; - yield `export interface ${snakeCaseToSentenceCase(method.name)}Event {\n`; + yield `export interface ${sentenceName}Event {\n`; for (const param of method.params) { yield '\t/**\n'; yield formatComment('\t * ', `${param.description}`); yield '\t */\n'; - yield `\t${snakeCaseToCamelCase(param.name)}: `; + yield `\t${param.name}: `; if (param.schema.type === 'string' && param.schema.enum) { - yield param.schema.enum.map((value: string) => `'${value}'`).join(' | '); + yield `${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)}`; } else { - yield TypescriptTypeMap[param.schema.type as string]; + yield deriveType(contracts, TypescriptTypeMap, param.name, param.schema); } yield `;\n\n`; } yield '}\n\n'; } + + yield `export enum ${snakeCaseToSentenceCase(name)}Event {\n`; + yield events.join(',\n'); + yield '\n}\n\n'; } yield `export class Positron${snakeCaseToSentenceCase(name)}Comm extends PositronBaseComm {\n`; @@ -599,7 +994,7 @@ import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/co `@param ${snakeCaseToCamelCase(param.name)} ${param.description}`); } yield `\t *\n`; - if (method.result) { + if (method.result && method.result.schema) { yield formatComment('\t * ', `@returns ${method.result.schema.description}`); } @@ -607,20 +1002,29 @@ import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/co yield '\t' + snakeCaseToCamelCase(method.name) + '('; for (let i = 0; i < method.params.length; i++) { const param = method.params[i]; - yield snakeCaseToCamelCase(param.name) + - ': ' + - TypescriptTypeMap[param.schema.type as string]; + if (!param.schema) { + throw new Error(`No schema for '${method.name}' parameter '${param.name}'`); + } + yield snakeCaseToCamelCase(param.name) + ': '; + const schema = param.schema; + if (schema.type === 'string' && schema.enum) { + yield `${snakeCaseToSentenceCase(method.name)}${snakeCaseToSentenceCase(param.name)}`; + } else { + yield deriveType(contracts, TypescriptTypeMap, param.name, schema); + } if (i < method.params.length - 1) { yield ', '; } } yield '): Promise<'; - if (method.result) { + if (method.result && method.result.schema) { if (method.result.schema.type === 'object') { yield snakeCaseToSentenceCase(method.result.schema.name); } else { - yield TypescriptTypeMap[method.result.schema.type as string]; + yield deriveType(contracts, TypescriptTypeMap, method.name, method.result.schema); } + } else { + yield 'void'; } yield '> {\n'; yield '\t\treturn super.performRpc(\'' + method.name + '\', ['; @@ -638,7 +1042,7 @@ import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/co } } yield ']);\n'; - yield `\t}\n`; + yield `\t}\n\n`; } } diff --git a/positron/comms/plot-backend-openrpc.json b/positron/comms/plot-backend-openrpc.json index 60d360f0b8b..ae607cc8cc9 100644 --- a/positron/comms/plot-backend-openrpc.json +++ b/positron/comms/plot-backend-openrpc.json @@ -46,7 +46,11 @@ "description": "The MIME type of the plot data", "type": "string" } - } + }, + "required": [ + "data", + "mime_type" + ] } } } diff --git a/positron/comms/variables-backend-openrpc.json b/positron/comms/variables-backend-openrpc.json new file mode 100644 index 00000000000..3a52a09a3b9 --- /dev/null +++ b/positron/comms/variables-backend-openrpc.json @@ -0,0 +1,221 @@ +{ + "openrpc": "1.3.0", + "info": { + "title": "Variables Backend", + "version": "1.0.0" + }, + "methods": [ + { + "name": "clear", + "summary": "Clear all variables", + "description": "Clears (deletes) all variables in the current session.", + "params": [ + { + "name": "include_hidden_objects", + "description": "Whether to clear hidden objects in addition to normal variables", + "schema": { + "type": "boolean" + } + } + ], + "result": {} + }, + { + "name": "delete", + "summary": "Deletes a set of named variables", + "description": "Deletes the named variables from the current session.", + "params": [ + { + "name": "names", + "description": "The names of the variables to delete.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "result": {} + }, + { + "name": "inspect", + "summary": "Inspect a variable", + "description": "Returns the children of a variable, as an array of variables.", + "params": [ + { + "name": "path", + "description": "The path to the variable to inspect, as an array of access keys.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "result": { + "schema": { + "type": "object", + "name": "inspected_variable", + "description": "An inspected variable.", + "properties": { + "children": { + "type": "array", + "description": "The children of the inspected variable.", + "items": { + "$ref": "#/components/schemas/variable" + } + }, + "length": { + "type": "integer", + "description": "The total number of children. This may be greater than the number of children in the 'children' array if the array is truncated." + } + }, + "required": [ + "children", + "length" + ] + } + } + }, + { + "name": "clipboard_format", + "summary": "Format for clipboard", + "description": "Requests a formatted representation of a variable for copying to the clipboard.", + "params": [ + { + "name": "path", + "description": "The path to the variable to format, as an array of access keys.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "format", + "description": "The requested format for the variable, as a MIME type", + "schema": { + "type": "string" + } + } + ], + "result": { + "schema": { + "type": "object", + "name": "formatted_variable", + "description": "An object formatted for copying to the clipboard.", + "properties": { + "format": { + "type": "string", + "description": "The format returned, as a MIME type; matches the MIME type of the format named in the request." + }, + "content": { + "type": "string", + "description": "The formatted content of the variable." + } + }, + "required": [ + "format", + "content" + ] + } + } + }, + { + "name": "view", + "summary": "Request a viewer for a variable", + "description": "Request that the runtime open a data viewer to display the data in a variable.", + "params": [ + { + "name": "path", + "description": "The path to the variable to view, as an array of access keys.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "result": {} + } + ], + "components": { + "schemas": { + "variable": { + "type": "object", + "name": "variable", + "description": "A single variable in the runtime.", + "properties": { + "access_key": { + "type": "string", + "description": "A key that uniquely identifies the variable within the runtime and can be used to access the variable in `inspect` requests" + }, + "display_name": { + "type": "string", + "description": "The name of the variable, formatted for display" + }, + "display_value": { + "type": "string", + "description": "A string representation of the variable's value, formatted for display and possibly truncated" + }, + "display_type": { + "type": "string", + "description": "The variable's type, formatted for display" + }, + "type_info": { + "type": "string", + "description": "Extended information about the variable's type" + }, + "kind": { + "type": "string", + "enum": [ + "boolean", + "bytes", + "collection", + "empty", + "function", + "map", + "number", + "other", + "string", + "table" + ], + "description": "The kind of value the variable represents, such as 'string' or 'number'" + }, + "length": { + "type": "integer", + "description": "The number of elements in the variable, if it is a collection" + }, + "has_children": { + "type": "boolean", + "description": "Whether the variable has child variables" + }, + "has_viewer": { + "type": "boolean", + "description": "True if there is a viewer available for this variable (i.e. the runtime can handle a 'view' request for this variable)" + }, + "is_truncated": { + "type": "boolean", + "description": "True the 'value' field is a truncated representation of the variable's value" + } + }, + "required": [ + "access_key", + "display_name", + "display_value", + "display_type", + "type_info", + "kind", + "length", + "has_children", + "has_viewer", + "is_truncated" + ] + } + } + } +} diff --git a/positron/comms/variables-frontend-openrpc.json b/positron/comms/variables-frontend-openrpc.json new file mode 100644 index 00000000000..4492bc5a246 --- /dev/null +++ b/positron/comms/variables-frontend-openrpc.json @@ -0,0 +1,36 @@ +{ + "openrpc": "1.3.0", + "info": { + "title": "Variables Frontend", + "version": "1.0.0" + }, + "methods": [ + { + "name": "update", + "summary": "Update variables", + "description": "Updates the variables in the current session.", + "params": [ + { + "name": "assigned", + "description": "An array of variables that have been newly assigned.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/variable" + } + } + }, + { + "name": "removed", + "description": "An array of variable names that have been removed.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ] + } + ] +} diff --git a/positron/comms/variables.json b/positron/comms/variables.json new file mode 100644 index 00000000000..680410d9e97 --- /dev/null +++ b/positron/comms/variables.json @@ -0,0 +1,9 @@ +{ + "name": "variables", + "initiator": "frontend", + "initial_data": { + "schema": { + "type": "null" + } + } +} diff --git a/positron/events.yaml b/positron/events.yaml deleted file mode 100644 index 431d00e93ab..00000000000 --- a/positron/events.yaml +++ /dev/null @@ -1,54 +0,0 @@ -- name: Busy - comment: > - Represents a change in the runtime's busy state. - - Note that this represents the busy state of the underlying computation engine, - not the busy state of the kernel. - - The kernel is busy when it is processing a request, - but the runtime is busy only when a computation is running. - params: - - name: busy - type: boolean - comment: Whether the runtime is busy. - -- name: ShowMessage - comment: > - Use this event to show a message to the user. - params: - - name: message - type: string - comment: The message to show to the user. - -- name: ShowHelp - comment: > - Show help content in the Help pane. - params: - - name: content - type: string - comment: The help content to be shown. - - name: kind - type: string - comment: The content help type. Must be one of 'html', 'markdown', or 'url'. - - name: focus - type: boolean - comment: Focus the Help pane after the Help content has been rendered? - -- name: PromptState - comment: > - Update strings of future input and continuation prompts. - params: - - name: inputPrompt - type: string - comment: String for future input prompts. - - name: continuationPrompt - type: string - comment: String for future continuation prompts. - -- name: WorkingDirectory - comment: > - Change the displayed working directory for the interpreter. - params: - - name: directory - type: string - comment: The new working directory. diff --git a/positron/scripts/generate-events.ts b/positron/scripts/generate-events.ts deleted file mode 100644 index 21ff94830cb..00000000000 --- a/positron/scripts/generate-events.ts +++ /dev/null @@ -1,228 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { events, PositronEventDefinition } from './positron-events'; - -type TypeMap = { - [key: string]: string; -}; - -const rustTypesMap = { - boolean: 'bool', - string: 'String', - integer: 'i32', -}; - -const tsTypesMap = { - boolean: 'boolean', - string: 'string', - integer: 'integer' -}; - -function camel(value: string): string { - - let snakeCased = value.replace(/[A-Z]/g, (letter) => { - return `_${letter.toLowerCase()}`; - }); - - if (snakeCased.startsWith('_')) { - snakeCased = snakeCased.substring(1); - } - - return snakeCased.replace(/_event$/, ''); - -} - -function snake(str: string) { - return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); -} - -function indent(value: string, indent: string): string { - return value.replace(/(^|\n)/g, `$1${indent}`); -} - -function generateRustEvent(event: PositronEventDefinition) { - - const lines: string[] = []; - - const comment = event.comment.trimEnd().replace(/(^|\n)/g, '$1/// '); - lines.push(comment); - lines.push(`#[positron::event("${camel(event.name)}")]`); - lines.push(`pub struct ${event.name}Event {`); - lines.push(''); - - for (const param of event.params) { - lines.push(` /// ${param.comment}`); - lines.push(` pub ${snake(param.name)}: ${rustTypesMap[param.type]},`); - lines.push(''); - } - - lines.push('}'); - lines.push(''); - - return lines.join('\n'); - -} - -function generateRustPositronEventEnum() { - - const lines: string[] = []; - - lines.push('#[derive(Debug, Clone)]'); - lines.push('pub enum PositronEvent {'); - for (const event of events) { - lines.push(` ${event.name}(${event.name}Event),`); - } - lines.push('}'); - - return lines.join('\n'); - -} - -function generateRustClientEventHelper() { - - const dispatchLines = events.map((event) => { - return `PositronEvent::${event.name}(data) => Self::as_evt(data),`; - }).join('\n'); - - return `impl From for ClientEvent { - fn from(event: PositronEvent) -> Self { - match event { -${indent(dispatchLines, ' ')} - } - } -}`; - -} - -function updateRustEventsFile(rustEventsFile: string) { - - // Generate event definitions for Ark - const rustEvents = events.map(generateRustEvent); - - const currentYear = (new Date()).getFullYear(); - const rustHeader = `// -// mod.rs -// -// Copyright (C) ${currentYear} Posit Software, PBC. All rights reserved. -// -// -// Auto-generated by 'positron/scripts/generate-events.ts'. -// Please do not modify this file directly. -// - -use crate::positron; - -pub trait PositronEventType { - fn event_type(&self) -> String; -} -`; - - rustEvents.unshift(rustHeader); - - const eventsEnum = generateRustPositronEventEnum(); - rustEvents.push(eventsEnum); - rustEvents.push(''); - - writeFileSync(rustEventsFile, rustEvents.join('\n')); - -} - -function updateRustClientEventsFile(path: string) { - - const contents = readFileSync(path, { encoding: 'utf-8' }); - const lines = contents.split(/\r?\n/); - - const startIndex = lines.findIndex((line) => { - return line.endsWith('/** begin rust-client-event */'); - }); - - const endIndex = lines.findIndex((line) => { - return line.endsWith('/** end rust-client-event */'); - }); - - const replacement = generateRustClientEventHelper(); - lines.splice(startIndex + 1, endIndex - startIndex - 1, replacement); - - const replacedContents = lines.join('\n'); - writeFileSync(path, replacedContents); - - -} - -function generateLanguageRuntimeEventTypeEnum() { - - const lines: string[] = []; - lines.push('export enum LanguageRuntimeEventType {'); - for (const event of events) { - const lhs = event.name; - const rhs = camel(lhs); - lines.push(`\t${lhs} = '${rhs}',`); - } - lines.push('}'); - - return lines.join('\n'); - -} - -function generateLanguageRuntimeEventDefinitions() { - - const lines: string[] = []; - - for (const event of events) { - - const comment = event.comment.trimEnd().replace(/(^|\n)/g, '$1// '); - lines.push(comment); - lines.push(`export interface ${event.name}Event extends LanguageRuntimeEventData {`); - lines.push(''); - for (const param of event.params) { - lines.push(`\t/** ${param.comment} */`); - lines.push(`\t${param.name}: ${tsTypesMap[param.type]};`); - lines.push(''); - } - lines.push('}'); - lines.push(''); - } - - return lines.join('\n'); - -} - -function generateLanguageRuntimeEventsFile(languageRuntimeEventsFile: string) { - - const languageRuntimeEventEnum = generateLanguageRuntimeEventTypeEnum(); - const languageRuntimeEventDefinitions = generateLanguageRuntimeEventDefinitions(); - const currentYear = (new Date()).getFullYear(); - const generatedContents = `/*--------------------------------------------------------------------------------------------- - * Copyright (C) ${currentYear} Posit Software, PBC. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// This file was automatically generated by 'positron/scripts/generate-events.ts'. -// Please do not modify this file directly. - -export interface LanguageRuntimeEventData { } - -${languageRuntimeEventEnum} - -${languageRuntimeEventDefinitions} -`; - - writeFileSync(languageRuntimeEventsFile, generatedContents); - -} - -// Move to project root directory. -while (!existsSync(`${process.cwd()}/.git`)) { - process.chdir('..'); -} - -const rustEventsFile = '../amalthea/crates/amalthea/src/events/mod.rs'; -updateRustEventsFile(rustEventsFile); - -const rustClientEventsFile = '../amalthea/crates/amalthea/src/wire/client_event.rs'; -updateRustClientEventsFile(rustClientEventsFile); - -const languageRuntimeEventsFile = 'src/vs/workbench/services/languageRuntime/common/languageRuntimeEvents.ts'; -generateLanguageRuntimeEventsFile(languageRuntimeEventsFile); diff --git a/positron/scripts/positron-events.ts b/positron/scripts/positron-events.ts deleted file mode 100644 index 32614c2ccae..00000000000 --- a/positron/scripts/positron-events.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2022 Posit Software, PBC. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -import { readFileSync } from 'fs'; -import yaml from 'js-yaml'; - -export interface PositronEventDefinitionParam { - name: string; - type: string; - comment: string; -} - -export interface PositronEventDefinition { - name: string; - comment: string; - params: PositronEventDefinitionParam[]; -} - -const eventsYamlFile = `${__dirname}/../events.yaml`; -const eventsYamlContents = readFileSync(eventsYamlFile, { encoding: 'utf-8' }); -export const events = yaml.load(eventsYamlContents) as PositronEventDefinition[]; diff --git a/scripts/test-remote-integration.sh b/scripts/test-remote-integration.sh index d3e9f2a5265..07d2753172b 100755 --- a/scripts/test-remote-integration.sh +++ b/scripts/test-remote-integration.sh @@ -124,6 +124,14 @@ echo "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/configuration-editing --extensionTestsPath=$REMOTE_VSCODE/configuration-editing/out/test $API_TESTS_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS kill_app +# Positron Extensions + +echo +echo "### Positron Code Cells tests" +echo +yarn test-extension -l positron-code-cells +kill_app + # Cleanup if [[ "$3" == "" ]]; then diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 77ed3cf0c49..3ed3c8bfa91 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -197,8 +197,6 @@ declare module 'positron' { type: LanguageRuntimeMessageType; } - export interface LanguageRuntimeEventData { } - /** LanguageRuntimeOutput is a LanguageRuntimeMessage representing output (text, plots, etc.) */ export interface LanguageRuntimeOutput extends LanguageRuntimeMessage { /** A record of data MIME types to the associated data, e.g. `text/plain` => `'hello world'` */ diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index 70f09760253..4779b9cab71 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -20,11 +20,13 @@ import { DeferredPromise } from 'vs/base/common/async'; import { generateUuid } from 'vs/base/common/uuid'; import { IPositronPlotsService } from 'vs/workbench/services/positronPlots/common/positronPlots'; import { IPositronIPyWidgetsService, MIME_TYPE_WIDGET_STATE, MIME_TYPE_WIDGET_VIEW } from 'vs/workbench/services/positronIPyWidgets/common/positronIPyWidgetsService'; -import { BusyEvent, LanguageRuntimeEventType, PromptStateEvent, WorkingDirectoryEvent } from 'vs/workbench/services/languageRuntime/common/languageRuntimeEvents'; import { IPositronHelpService } from 'vs/workbench/contrib/positronHelp/browser/positronHelpService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IRuntimeClientEvent } from 'vs/workbench/services/languageRuntime/common/languageRuntimeFrontEndClient'; import { URI } from 'vs/base/common/uri'; +import { BusyEvent, FrontendEvent, OpenEditorEvent, PromptStateEvent, WorkingDirectoryEvent } from 'vs/workbench/services/languageRuntime/common/positronFrontendComm'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; /** * Represents a language runtime event (for example a message or state change) @@ -104,6 +106,7 @@ class ExtHostLanguageRuntimeAdapter implements ILanguageRuntime { private readonly _languageRuntimeService: ILanguageRuntimeService, private readonly _logService: ILogService, private readonly _notebookService: INotebookService, + private readonly _editorService: IEditorService, private readonly _proxy: ExtHostLanguageRuntimeShape) { // Bind events to emitters @@ -141,14 +144,14 @@ class ExtHostLanguageRuntimeAdapter implements ILanguageRuntime { } const ev = globalEvent.event; - if (ev.name === LanguageRuntimeEventType.PromptState) { + if (ev.name === FrontendEvent.PromptState) { // Update config before propagating event const state = ev.data as PromptStateEvent; // Runtimes might supply prompts with trailing whitespace (e.g. R, // Python) that we trim here because we add our own whitespace later on - const inputPrompt = state.inputPrompt?.trimEnd(); - const continuationPrompt = state.continuationPrompt?.trimEnd(); + const inputPrompt = state.input_prompt?.trimEnd(); + const continuationPrompt = state.continuation_prompt?.trimEnd(); if (inputPrompt) { this.dynState.inputPrompt = inputPrompt; @@ -160,11 +163,19 @@ class ExtHostLanguageRuntimeAdapter implements ILanguageRuntime { // Don't include new state in event, clients should // inspect the runtime's dyn state instead this.emitDidReceiveRuntimeMessagePromptConfig(); - } else if (ev.name === LanguageRuntimeEventType.Busy) { + } else if (ev.name === FrontendEvent.Busy) { // Update busy state const busy = ev.data as BusyEvent; this.dynState.busy = busy.busy; - } else if (ev.name === LanguageRuntimeEventType.WorkingDirectory) { + } else if (ev.name === FrontendEvent.OpenEditor) { + // Open an editor + const ed = ev.data as OpenEditorEvent; + const editor: ITextResourceEditorInput = { + resource: URI.file(ed.file), + options: { selection: { startLineNumber: ed.line, startColumn: ed.column } } + }; + this._editorService.openEditor(editor); + } else if (ev.name === FrontendEvent.WorkingDirectory) { // Update current working directory const dir = ev.data as WorkingDirectoryEvent; this.dynState.currentWorkingDirectory = dir.directory; @@ -938,7 +949,8 @@ export class MainThreadLanguageRuntime implements MainThreadLanguageRuntimeShape @IPositronPlotsService private readonly _positronPlotService: IPositronPlotsService, @IPositronIPyWidgetsService private readonly _positronIPyWidgetsService: IPositronIPyWidgetsService, @ILogService private readonly _logService: ILogService, - @INotebookService private readonly _notebookService: INotebookService + @INotebookService private readonly _notebookService: INotebookService, + @IEditorService private readonly _editorService: IEditorService, ) { // TODO@softwarenerd - We needed to find a central place where we could ensure that certain // Positron services were up and running early in the application lifecycle. For now, this @@ -982,6 +994,7 @@ export class MainThreadLanguageRuntime implements MainThreadLanguageRuntimeShape this._languageRuntimeService, this._logService, this._notebookService, + this._editorService, this._proxy ); this._runtimes.set(handle, adapter); diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx index cbc5e540969..0d2f57eedca 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/actionBar.tsx @@ -13,12 +13,12 @@ import { PositronActionBar } from 'vs/platform/positronActionBar/browser/positro import { ActionBarRegion } from 'vs/platform/positronActionBar/browser/components/actionBarRegion'; import { ActionBarButton } from 'vs/platform/positronActionBar/browser/components/actionBarButton'; import { ActionBarSeparator } from 'vs/platform/positronActionBar/browser/components/actionBarSeparator'; -import { LanguageRuntimeEventType } from 'vs/workbench/services/languageRuntime/common/languageRuntimeEvents'; import { usePositronConsoleContext } from 'vs/workbench/contrib/positronConsole/browser/positronConsoleContext'; import { PositronActionBarContextProvider } from 'vs/platform/positronActionBar/browser/positronActionBarContext'; import { ILanguageRuntime, RuntimeState } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; import { PositronConsoleState } from 'vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService'; import { ConsoleInstanceMenuButton } from 'vs/workbench/contrib/positronConsole/browser/components/consoleInstanceMenuButton'; +import { FrontendEvent } from 'vs/workbench/services/languageRuntime/common/positronFrontendComm'; /** * Constants. @@ -188,7 +188,7 @@ export const ActionBar = (props: ActionBarProps) => { // Listen for changes to the working directory. disposableRuntimeStore.add(runtime.onDidReceiveRuntimeClientEvent((event) => { - if (event.name === LanguageRuntimeEventType.WorkingDirectory) { + if (event.name === FrontendEvent.WorkingDirectory) { setDirectoryLabel(runtime.dynState.currentWorkingDirectory); } })); diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx index ef862510224..87cb05043bd 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInput.tsx @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./consoleInput'; +import * as DOM from 'vs/base/browser/dom'; import * as React from 'react'; import { FocusEvent, useEffect, useRef } from 'react'; // eslint-disable-line no-duplicate-imports import { URI } from 'vs/base/common/uri'; @@ -96,6 +97,7 @@ export const ConsoleInput = (props: ConsoleInputProps) => { /** * Executes the code editor widget's code, if possible. + * @returns A Promise that indicates whether the code was executed. */ const executeCodeEditorWidgetCodeIfPossible = async () => { // Get the code from the code editor widget. @@ -110,13 +112,8 @@ export const ConsoleInput = (props: ConsoleInputProps) => { // If the code fragment is incomplete, don't do anything. The user will just see a new // line in the input area. case RuntimeCodeFragmentStatus.Incomplete: { - // For the moment, this works. Ideally, we'd like to have the current code fragment - // prettied up by the runtime and updated. - const updatedCodeFragment = code + '\n'; - setCurrentCodeFragment(updatedCodeFragment); - codeEditorWidgetRef.current.setValue(updatedCodeFragment); - updateCodeEditorWidgetPosition(Position.Last, Position.Last); - return; + // Don't execute the code, let the code editor widget handle the key event. + return false; } // If the code fragment is invalid (contains syntax errors), log a warning but execute @@ -142,6 +139,8 @@ export const ConsoleInput = (props: ConsoleInputProps) => { // Reset the code input state. setCurrentCodeFragment(undefined); codeEditorWidgetRef.current.setValue(''); + + return true; }; // Key down event handler. @@ -165,7 +164,7 @@ export const ConsoleInput = (props: ConsoleInputProps) => { // 'scoped' contexts makes that challenging to access here, and I // haven't figured out the 'right' way to get access to those contexts. if (!cmdOrCtrlKey) { - const suggestWidgets = document.getElementsByClassName('suggest-widget'); + const suggestWidgets = DOM.getActiveWindow().document.getElementsByClassName('suggest-widget'); for (const suggestWidget of suggestWidgets) { if (suggestWidget.classList.contains('visible')) { return; @@ -298,16 +297,9 @@ export const ConsoleInput = (props: ConsoleInputProps) => { // Enter processing. case KeyCode.Enter: { - // Consume the event. - consumeEvent(); - // If the shift key is pressed, do not process the event because the user is // entering multiple lines. if (e.shiftKey) { - codeEditorWidgetRef.current.setValue( - codeEditorWidgetRef.current.getValue() + '\n' - ); - updateCodeEditorWidgetPosition(Position.Last, Position.Last); return; } @@ -317,7 +309,12 @@ export const ConsoleInput = (props: ConsoleInputProps) => { } // Try to execute the code editor widget's code. - await executeCodeEditorWidgetCodeIfPossible(); + if (await executeCodeEditorWidgetCodeIfPossible()) { + // Only consume the event if the code was executed. Otherwise, let the code + // editor widget handle the key event. + consumeEvent(); + } + break; } } diff --git a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx index 156b670dbb0..31730c7e059 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx +++ b/src/vs/workbench/contrib/positronConsole/browser/components/consoleInstance.tsx @@ -6,6 +6,7 @@ import 'vs/css!./consoleInstance'; import * as React from 'react'; import { KeyboardEvent, MouseEvent, UIEvent, useEffect, useLayoutEffect, useRef, useState, WheelEvent } from 'react'; // eslint-disable-line no-duplicate-imports import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; import { generateUuid } from 'vs/base/common/uuid'; import { PixelRatio } from 'vs/base/browser/browser'; import { isMacintosh } from 'vs/base/common/platform'; @@ -98,7 +99,7 @@ export const ConsoleInstance = (props: ConsoleInstanceProps) => { */ const getSelection = () => { // Get the selection. - const selection = document.getSelection(); + const selection = DOM.getActiveWindow().document.getSelection(); if (selection) { // If the selection is outside the element, return null. for (let i = 0; i < selection.rangeCount; i++) { @@ -201,7 +202,7 @@ export const ConsoleInstance = (props: ConsoleInstanceProps) => { * selectAllRuntimeItems selects all runtime items. */ const selectAllRuntimeItems = () => { - const selection = document.getSelection(); + const selection = DOM.getActiveWindow().document.getSelection(); if (selection) { selection.selectAllChildren(runtimeItemsRef.current); } @@ -492,7 +493,8 @@ export const ConsoleInstance = (props: ConsoleInstanceProps) => { const scrollPosition = Math.abs( consoleInstanceRef.current.scrollHeight - consoleInstanceRef.current.clientHeight - - consoleInstanceRef.current.scrollTop); + consoleInstanceRef.current.scrollTop + ); // Update scroll lock. setScrollLock(scrollPosition >= 1); diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/actionBarComponents/layoutMenuButton.tsx b/src/vs/workbench/contrib/positronDataTool/browser/components/actionBarComponents/layoutMenuButton.tsx index 417a97e5a23..d0054131f8a 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/actionBarComponents/layoutMenuButton.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/actionBarComponents/layoutMenuButton.tsx @@ -7,8 +7,8 @@ import * as React from 'react'; import { localize } from 'vs/nls'; import { IAction, Separator } from 'vs/base/common/actions'; import { ActionBarMenuButton } from 'vs/platform/positronActionBar/browser/components/actionBarMenuButton'; -import { PositronDataToolLayout } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; import { usePositronDataToolContext } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolContext'; +import { PositronDataToolLayout } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; /** * Localized strings. @@ -25,12 +25,12 @@ const columnsHidden = localize('positron.columnsHidden', "Columns Hidden"); */ export const LayoutMenuButton = () => { // Context hooks. - const positronDataToolContext = usePositronDataToolContext(); + const context = usePositronDataToolContext(); // Builds the actions. const actions = () => { // Get the current layout. - const layout = positronDataToolContext.layout; + const layout = context.instance.layout; // Build the actions. const actions: IAction[] = []; @@ -44,7 +44,7 @@ export const LayoutMenuButton = () => { enabled: true, checked: layout === PositronDataToolLayout.ColumnsLeft, run: () => { - positronDataToolContext.setLayout(PositronDataToolLayout.ColumnsLeft); + context.instance.layout = PositronDataToolLayout.ColumnsLeft; } }); @@ -57,7 +57,7 @@ export const LayoutMenuButton = () => { enabled: true, checked: layout === PositronDataToolLayout.ColumnsRight, run: () => { - positronDataToolContext.setLayout(PositronDataToolLayout.ColumnsRight); + context.instance.layout = PositronDataToolLayout.ColumnsRight; } }); @@ -73,7 +73,7 @@ export const LayoutMenuButton = () => { enabled: true, checked: layout === PositronDataToolLayout.ColumnsHidden, run: () => { - positronDataToolContext.setLayout(PositronDataToolLayout.ColumnsHidden); + context.instance.layout = PositronDataToolLayout.ColumnsHidden; } }); @@ -86,7 +86,7 @@ export const LayoutMenuButton = () => { * @returns The icon ID for the layout. */ const selectIconId = () => { - switch (positronDataToolContext.layout) { + switch (context.instance.layout) { // Columns left. case PositronDataToolLayout.ColumnsLeft: return 'positron-data-tool-columns-left'; diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.css b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.css index 4081b658953..55cffae99b3 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.css +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.css @@ -3,5 +3,8 @@ *--------------------------------------------------------------------------------------------*/ .data-tool-panel .columns-panel .column-controller { - + display: flex; + box-sizing: border-box; /* Draw the borders inside the box. */ + align-items: center; + height: 26px; } diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.tsx b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.tsx index 9134a3fc7cb..a8b218ace2c 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController.tsx @@ -4,11 +4,15 @@ import 'vs/css!./columnController'; import * as React from 'react'; +import { CSSProperties } from 'react'; // eslint-disable-line no-duplicate-imports +import { DummyColumnInfo } from 'vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel'; /** * ColumnControllerProps interface. */ interface ColumnControllerProps { + dummyColumnInfo: DummyColumnInfo; + style: CSSProperties; } /** @@ -18,8 +22,8 @@ interface ColumnControllerProps { */ export const ColumnController = (props: ColumnControllerProps) => { return ( -
-
Name
+
+ {props.dummyColumnInfo.name}
); }; diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.css b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.css index 7b0d4a2b4c2..196db9abc48 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.css +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.css @@ -3,8 +3,11 @@ *--------------------------------------------------------------------------------------------*/ .data-tool-panel .columns-panel { + /* This is the scroll container. */ + /* overflow-y: scroll; */ } .data-tool-panel .columns-panel .title { - padding: 10px; + /* display: flex; */ + height: 26px; } diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.tsx b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.tsx index 7a16960da65..292dac1f498 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel.tsx @@ -4,18 +4,28 @@ import 'vs/css!./columnsPanel'; import * as React from 'react'; +import { useEffect, useRef, useState } from 'react'; // eslint-disable-line no-duplicate-imports import { generateUuid } from 'vs/base/common/uuid'; +import { FixedSizeList as List, ListChildComponentProps, ListOnItemsRenderedProps, ListOnScrollProps } from 'react-window'; +import { usePositronDataToolContext } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolContext'; +import { ColumnController } from 'vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnController'; + +/** + * Constants. + */ +const LINE_HEIGHT = 26; /** * ColumnsPanelProps interface. */ interface ColumnsPanelProps { + height: number; } /** * DummyColumnInfo interface. */ -interface DummyColumnInfo { +export interface DummyColumnInfo { key: string; name: string; } @@ -28,7 +38,7 @@ const dummyColumns: DummyColumnInfo[] = []; /** * Fill the dummy columns. */ -for (let i = 0; i < 100; i++) { +for (let i = 0; i < 64; i++) { dummyColumns.push({ key: generateUuid(), name: `This is column ${i + 1}` @@ -41,11 +51,85 @@ for (let i = 0; i < 100; i++) { * @returns The rendered component. */ export const ColumnsPanel = (props: ColumnsPanelProps) => { + // Context hooks. + const context = usePositronDataToolContext(); + + // Reference hooks. + const columnsPanel = useRef(undefined!); + const listRef = useRef(undefined!); + const innerRef = useRef(undefined!); + + // State hooks. + const [firstRender, setFirstRender] = useState(true); + + useEffect(() => { + if (!firstRender) { + listRef.current.scrollTo(20 * LINE_HEIGHT); + } + }, [firstRender]); + + const itemsRenderedHandler = ({ + visibleStartIndex, + visibleStopIndex + }: ListOnItemsRenderedProps) => { + console.log(context); + setFirstRender(true); + console.log(`-----------------> LIST height ${props.height} itemsRenderedHandler: visibleStartIndex ${visibleStartIndex} visibleStopIndex ${visibleStopIndex}`); + }; + + const scrollHandler = ({ + scrollDirection, + scrollOffset, + }: ListOnScrollProps) => { + console.log(`-----------------> LIST height ${props.height} scrollHandler: scrollDirection ${scrollDirection} scrollOffset ${scrollOffset}`); + // context.instance.columnsScrollOffset = props.scrollOffset; + + }; + + /** + * ColumnEntry component. + * @param index The index of the column entry. + * @param style The style (positioning) at which to render the column entry. + * @param isScrolling A value which indicates whether the list is scrolling. + * @returns The rendered column entry. + */ + const ColumnEntry = (props: ListChildComponentProps) => { + // Get the entry being rendered. + const column = dummyColumns[props.index]; + + console.log(`Render ColumnEntry ${props.index}`); + + if (!firstRender) { + return ( +
+ ); + } + + // Render. + return ( + + ); + }; + + // Render. return ( -
- {dummyColumns.map(column => -
{column.name}
- )} +
+ dummyColumns[index].key} + width='100%' + height={props.height - 2} + itemSize={LINE_HEIGHT} + overscanCount={10} + onItemsRendered={itemsRenderedHandler} + onScroll={scrollHandler} + > + {ColumnEntry} +
); }; diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.css b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.css index 929ae3730ed..bc232a761d5 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.css +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.css @@ -3,6 +3,8 @@ *--------------------------------------------------------------------------------------------*/ .data-tool-panel .rows-panel { + /* This is the scroll container. */ + overflow-y: scroll; } .data-tool-panel .rows-panel .title { diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.tsx b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.tsx index bef16e4335d..1b9e370bde1 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel.tsx @@ -4,12 +4,14 @@ import 'vs/css!./rowsPanel'; import * as React from 'react'; +import { UIEvent, useEffect, useRef } from 'react'; // eslint-disable-line no-duplicate-imports import { generateUuid } from 'vs/base/common/uuid'; +// import { usePositronDataToolContext } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolContext'; /** * RowsPanelProps interface. */ -interface ColumnsPanelProps { +interface RowsPanelProps { } /** @@ -40,9 +42,28 @@ for (let i = 0; i < 100; i++) { * @param props A RowsPanelProps that contains the component properties. * @returns The rendered component. */ -export const RowsPanel = (props: ColumnsPanelProps) => { +export const RowsPanel = (props: RowsPanelProps) => { + // Context hooks. + // const context = usePositronDataToolContext(); + + // Reference hooks. + const rowsPanel = useRef(undefined!); + + // Main useEffect. + useEffect(() => { + // console.log(`rowsPanel is ${rowsPanel} and rowsScrollPosition is ${context.instance.rowsScrollOffset}`); + }, []); + + /** + * onScroll event handler. + * @param e A UIEvent that describes a user interaction with the mouse. + */ + const scrollHandler = (e: UIEvent) => { + }; + + // Render. return ( -
+
{dummyRows.map(row =>
{row.name}
)} diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.css b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.css index 7489278a9dc..4edf8107d7e 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.css +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.css @@ -25,8 +25,8 @@ border-radius: 4px; border: 1px solid var(--vscode-positronDataTool-border); - /* This is the scroll container. */ - overflow-y: scroll; + /* Prevent 1fr from exceeding the parent height. */ + min-height: 0; /* Initially hidden. Layout happens in code. */ display: none; @@ -42,7 +42,9 @@ border-radius: 4px; border: 1px solid var(--vscode-positronDataTool-border); + /* Prevent 1fr from exceeding the parent height. */ + min-height: 0; + /* Initially hidden. Layout happens in code. */ display: none; - overflow-y: scroll; } diff --git a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.tsx b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.tsx index 089db37c3d8..7d724a6c1f7 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel.tsx @@ -5,12 +5,13 @@ import 'vs/css!./dataToolPanel'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; // eslint-disable-line no-duplicate-imports +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IReactComponentContainer } from 'vs/base/browser/positronReactRenderer'; import { PositronDataToolProps } from 'vs/workbench/contrib/positronDataTool/browser/positronDataTool'; -import { PositronDataToolLayout } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; import { RowsPanel } from 'vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/rowsPanel'; import { usePositronDataToolContext } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolContext'; import { ColumnsPanel } from 'vs/workbench/contrib/positronDataTool/browser/components/dataToolComponents/columnsPanel'; +import { PositronDataToolLayout } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; import { PositronColumnSplitter, PositronColumnSplitterResizeResult } from 'vs/base/browser/ui/positronComponents/positronColumnSplitter'; /** @@ -34,7 +35,7 @@ interface DataToolPanelProps extends PositronDataToolProps { */ export const DataToolPanel = (props: DataToolPanelProps) => { // Context hooks. - const positronDataToolContext = usePositronDataToolContext(); + const context = usePositronDataToolContext(); // Reference hooks. const dataToolPanel = useRef(undefined!); @@ -43,19 +44,36 @@ export const DataToolPanel = (props: DataToolPanelProps) => { const column2 = useRef(undefined!); // State hooks. - const [columnWidth, setColumnWidth] = useState(200); + const [layout, setLayout] = useState(context.instance.layout); + const [columnsWidth, setColumnsWidth] = useState( + Math.max(context.instance.columnsWidthPercent * props.width, MIN_COLUMN_WIDTH) + ); + + // Main useEffect. + useEffect(() => { + // Create the disposable store for cleanup. + const disposableStore = new DisposableStore(); + + // Add the onDidChangeLayout event handler. + disposableStore.add(context.instance.onDidChangeLayout(layout => { + setLayout(layout); + })); + + // Return the cleanup function that will dispose of the event handlers. + return () => disposableStore.dispose(); + }, []); // Layout effect. useEffect(() => { - switch (positronDataToolContext.layout) { + switch (layout) { // Columns left. case PositronDataToolLayout.ColumnsLeft: dataToolPanel.current.style.gridTemplateRows = '[main] 1fr [end]'; - dataToolPanel.current.style.gridTemplateColumns = `[column-1] ${columnWidth}px [splitter] 8px [column-2] 1fr [end]`; + dataToolPanel.current.style.gridTemplateColumns = `[column-1] ${columnsWidth}px [splitter] 8px [column-2] 1fr [end]`; column1.current.style.gridRow = 'main / end'; column1.current.style.gridColumn = 'column-1 / splitter'; - column1.current.style.display = 'inline'; + column1.current.style.display = 'grid'; splitter.current.style.gridRow = 'main / end'; splitter.current.style.gridColumn = 'splitter / column-2'; @@ -63,17 +81,17 @@ export const DataToolPanel = (props: DataToolPanelProps) => { column2.current.style.gridRow = 'main / end'; column2.current.style.gridColumn = 'column-2 / end'; - column2.current.style.display = 'inline'; + column2.current.style.display = 'grid'; break; // Columns right. case PositronDataToolLayout.ColumnsRight: dataToolPanel.current.style.gridTemplateRows = '[main] 1fr [end]'; - dataToolPanel.current.style.gridTemplateColumns = `[column-1] 1fr [splitter] 8px [column-2] ${columnWidth}px [end]`; + dataToolPanel.current.style.gridTemplateColumns = `[column-1] 1fr [splitter] 8px [column-2] ${columnsWidth}px [end]`; column1.current.style.gridRow = 'main / end'; column1.current.style.gridColumn = 'column-2 / end'; - column1.current.style.display = 'inline'; + column1.current.style.display = 'grid'; splitter.current.style.gridRow = 'main / end'; splitter.current.style.gridColumn = 'splitter / column-2'; @@ -81,7 +99,7 @@ export const DataToolPanel = (props: DataToolPanelProps) => { column2.current.style.gridRow = 'main / end'; column2.current.style.gridColumn = 'column-1 / splitter'; - column2.current.style.display = 'inline'; + column2.current.style.display = 'grid'; break; // Columns hidden. @@ -99,15 +117,10 @@ export const DataToolPanel = (props: DataToolPanelProps) => { column2.current.style.gridRow = 'main / end'; column2.current.style.gridColumn = 'column / end'; - column2.current.style.display = 'inline'; + column2.current.style.display = 'grid'; break; } - }, [positronDataToolContext.layout, columnWidth]); - - // Width effect. - useEffect(() => { - console.log(`Width changed useEffect is running width is now ${props.width}`); - }, [props.width]); + }, [layout, columnsWidth]); /** * onResize handler. @@ -115,27 +128,27 @@ export const DataToolPanel = (props: DataToolPanelProps) => { */ const resizeHandler = (x: number) => { // Calculate the new column width. - let newColumnWidth: number; - switch (positronDataToolContext.layout) { + let newColumnWidth = -1; + switch (layout) { // Columns left. case PositronDataToolLayout.ColumnsLeft: - newColumnWidth = columnWidth + x; + newColumnWidth = columnsWidth + x; break; // Columns right. case PositronDataToolLayout.ColumnsRight: - newColumnWidth = columnWidth - x; + newColumnWidth = columnsWidth - x; break; // Columns hidden. This can't happen. case PositronDataToolLayout.ColumnsHidden: - throw new Error('Sizer should not be available.'); + throw new Error('Resize is unavailable'); } // If the new column width is too small, pin it at the minimum column width and return // ColumnSplitterResizeResult.TooSmall to get the cursor updated. if (newColumnWidth < MIN_COLUMN_WIDTH) { - setColumnWidth(MIN_COLUMN_WIDTH); + setColumnsWidth(MIN_COLUMN_WIDTH); return PositronColumnSplitterResizeResult.TooSmall; } @@ -143,13 +156,14 @@ export const DataToolPanel = (props: DataToolPanelProps) => { // ColumnSplitterResizeResult.TooLarge to get the cursor updated. const maxColumnWidth = props.width - (MIN_COLUMN_WIDTH + 24); if (newColumnWidth > maxColumnWidth) { - setColumnWidth(maxColumnWidth); + setColumnsWidth(maxColumnWidth); return PositronColumnSplitterResizeResult.TooLarge; } - // Set the column width and return ColumnSplitterResizeResult.Resizing to get the cursor - // updated. - setColumnWidth(newColumnWidth); + // Update the the column width and return ColumnSplitterResizeResult.Resizing to get the + // cursor updated. + setColumnsWidth(newColumnWidth); + context.instance.columnsWidthPercent = newColumnWidth / props.width; return PositronColumnSplitterResizeResult.Resizing; }; @@ -164,7 +178,7 @@ export const DataToolPanel = (props: DataToolPanelProps) => { className='data-tool-panel' >
- +
diff --git a/src/vs/workbench/contrib/positronDataTool/browser/positronDataTool.tsx b/src/vs/workbench/contrib/positronDataTool/browser/positronDataTool.tsx index 1e99b382419..2af405140d3 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/positronDataTool.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/positronDataTool.tsx @@ -9,13 +9,13 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IReactComponentContainer } from 'vs/base/browser/positronReactRenderer'; import { ActionBar } from 'vs/workbench/contrib/positronDataTool/browser/components/actionBar'; import { DataToolPanel } from 'vs/workbench/contrib/positronDataTool/browser/components/dataToolPanel'; -import { PositronDataToolServices } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; +import { PositronDataToolConfiguration } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; import { PositronDataToolContextProvider } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolContext'; /** * PositronDataToolProps interface. */ -export interface PositronDataToolProps extends PositronDataToolServices { +export interface PositronDataToolProps extends PositronDataToolConfiguration { readonly reactComponentContainer: IReactComponentContainer; } @@ -29,7 +29,7 @@ export const PositronDataTool = (props: PropsWithChildren const [width, setWidth] = useState(props.reactComponentContainer.width); const [height, setHeight] = useState(props.reactComponentContainer.height); - // Add IReactComponentContainer event handlers. + // Main useEffect. useEffect(() => { // Create the disposable store for cleanup. const disposableStore = new DisposableStore(); diff --git a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolActions.ts b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolActions.ts index fdfe27b8c73..f3688da8069 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolActions.ts +++ b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolActions.ts @@ -3,16 +3,13 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { generateUuid } from 'vs/base/common/uuid'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IPositronDataToolService } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; /** * Positron data tool action category. @@ -70,9 +67,10 @@ class OpenPositronDataTool extends Action2 { */ async run(accessor: ServicesAccessor) { // Access services. - const editorService = accessor.get(IEditorService); - const resource = URI.from({ scheme: Schemas.positronDataTool, path: `data-tool-${generateUuid()}` }); - await editorService.openEditor({ resource }); + const positronDataToolService = accessor.get(IPositronDataToolService); + + // Test opening a positron data tool. + await positronDataToolService.testOpen('b809490e-1801-4f2f-911c-7b9539c78204'); } } diff --git a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolContext.tsx b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolContext.tsx index 611063954b3..dccf061d858 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolContext.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolContext.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { PropsWithChildren, createContext, useContext } from 'react'; // eslint-disable-line no-duplicate-imports -import { PositronDataToolServices, PositronDataToolState, usePositronDataToolState } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; +import { PositronDataToolConfiguration, PositronDataToolState, usePositronDataToolState } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolState'; /** * Create the Positron data tool context. @@ -15,14 +15,14 @@ const PositronDataToolContext = createContext(undefined!) * Export the PositronDataToolContextProvider. */ export const PositronDataToolContextProvider = ( - props: PropsWithChildren + props: PropsWithChildren ) => { // State hooks. - const positronDataToolState = usePositronDataToolState(props); + const state = usePositronDataToolState(props); // Render. return ( - + {props.children} ); diff --git a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolEditor.tsx b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolEditor.tsx index 50bd1efc995..00676de63b9 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolEditor.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolEditor.tsx @@ -6,6 +6,8 @@ import 'vs/css!./positronDataToolEditor'; import * as React from 'react'; import * as DOM from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; +import { IEditorOpenContext } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,8 +21,10 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { PositronDataTool } from 'vs/workbench/contrib/positronDataTool/browser/positronDataTool'; +import { PositronDataToolUri } from 'vs/workbench/services/positronDataTool/common/positronDataToolUri'; import { IReactComponentContainer, ISize, PositronReactRenderer } from 'vs/base/browser/positronReactRenderer'; import { PositronDataToolEditorInput } from 'vs/workbench/contrib/positronDataTool/browser/positronDataToolEditorInput'; +import { IPositronDataToolService } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; // Temporary instance counter. let instance = 0; @@ -91,7 +95,9 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen /** * Gets the instance. This is a temporary property. */ - private instance = `${++instance}`; + private _instance = `${++instance}`; + + private _identifier?: string; //#endregion Private Properties @@ -160,21 +166,27 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen //#endregion IReactComponentContainer //#region Constructor & Dispose - /** * Constructor. - * @param clipboardService The clipboard service. + * @param _clipboardService The clipboard service. + * @param _commandService The command service. + * @param _configurationService The configuration service. + * @param _contextKeyService The context key service. + * @param _contextMenuService The context menu service. + * @param _keybindingService The keybinding service. + * @param _positronDataToolService The Positron data tool service. * @param storageService The storage service. * @param telemetryService The telemetry service. * @param themeService The theme service. */ constructor( - @IClipboardService readonly clipboardService: IClipboardService, - @ICommandService private readonly commandService: ICommandService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IClipboardService readonly _clipboardService: IClipboardService, + @ICommandService private readonly _commandService: ICommandService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IPositronDataToolService private readonly _positronDataToolService: IPositronDataToolService, @IStorageService storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @@ -183,18 +195,21 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen super(PositronDataToolEditorInput.EditorID, telemetryService, themeService, storageService); // Logging. - console.log(`PositronDataEditor ${this.instance} constructor`); + console.log(`PositronDataEditor ${this._instance} created`); } /** * dispose override method. */ public override dispose(): void { + // Logging. + console.log(`PositronDataEditor ${this._instance} dispose`); + + // Dispose the PositronReactRenderer for the PositronDataTool. + this.disposePositronReactRenderer(); + // Call the base class's dispose method. super.dispose(); - - // Logging. - console.log(`PositronDataEditor ${this.instance} dispose`); } //#endregion Constructor & Dispose @@ -207,26 +222,52 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen */ protected override createEditor(parent: HTMLElement): void { // Logging. - console.log(`PositronDataEditor ${this.instance} createEdtitor`); + console.log(`PositronDataEditor ${this._instance} createEditor`); // Create and append the Positron data tool container. this._positronDataToolsContainer = DOM.$('.positron-data-tool-container'); parent.appendChild(this._positronDataToolsContainer); + } - // Create the PositronReactRenderer for the PositronDataTool component and render it. - this._positronReactRenderer = new PositronReactRenderer(this._positronDataToolsContainer); - this._register(this._positronReactRenderer); - this._positronReactRenderer.render( - - ); + /** + * Sets the editor input. + * @param input The Positron data tool editor input. + * @param options The Positron data tool editor options. + * @param context The editor open context. + * @param token The cancellation token. + */ + override async setInput( + input: PositronDataToolEditorInput, + options: IPositronDataToolEditorOptions, + context: IEditorOpenContext, + token: CancellationToken + ): Promise { + // Call the base class's method. + await super.setInput(input, options, context, token); + + // Logging. + console.log(`PositronDataEditor ${this._instance} setInput ${input.resource}`); + + // Parse the Positron data tool URI. + this._identifier = PositronDataToolUri.parse(input.resource); + + // TODO: Render some kind of an error. + } + + /** + * Clears the input. + */ + override clearInput(): void { + // Logging. + console.log(`PositronDataEditor ${this._instance} clearInput`); + + // Dispose the PositronReactRenderer for the PositronDataTool. + this.disposePositronReactRenderer(); + + this._identifier = undefined; + + // Call the base class's method. + super.clearInput(); } /** @@ -235,11 +276,11 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen * @param group The editor group. */ protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + // Logging. + console.log(`PositronDataEditor ${this._instance} setEditorVisible ${visible} group ${group?.id}`); + // Call the base class's method. super.setEditorVisible(visible, group); - - // Logging. - console.log(`PositronDataEditor ${this.instance} setEditorVisible ${visible} group ${group}`); } //#endregion Protected Overrides @@ -251,6 +292,9 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen * @param dimension The layout dimension. */ override layout(dimension: DOM.Dimension): void { + // Logging. + console.log(`PositronDataEditor ${this._instance} layout ${dimension.width},${dimension.height}`); + // Size the container. DOM.size(this._positronDataToolsContainer, dimension.width, dimension.height); @@ -261,7 +305,57 @@ export class PositronDataToolEditor extends EditorPane implements IReactComponen width: this._width, height: this._height }); + + if (this._identifier && !this._positronReactRenderer) { + // Get the Positron data tool instance. + const positronDataToolInstance = this._positronDataToolService.getInstance(this._identifier); + + // If the Positron data tool instance was found, render the Positron data tool. + if (positronDataToolInstance) { + // Create the PositronReactRenderer for the PositronDataTool component and render it. + this._positronReactRenderer = new PositronReactRenderer(this._positronDataToolsContainer); + this._positronReactRenderer.render( + + ); + + // Logging. + console.log(`PositronDataEditor ${this._instance} create PositronReactRenderer`); + + + // Success. + return; + } + } } //#endregion Protected Overrides + + //#region Private Methods + + /** + * Disposes of the PositronReactRenderer for the PositronDataTool. + */ + private disposePositronReactRenderer() { + // If the PositronReactRenderer for the PositronDataTool is exists, dispose it. This removes + // the PositronDataTool from the DOM. + if (this._positronReactRenderer) { + // Logging. + console.log(`PositronDataEditor ${this._instance} dispose PositronReactRenderer`); + + // Dispose of the PositronReactRenderer for the PositronDataTool. + this._positronReactRenderer.dispose(); + this._positronReactRenderer = undefined; + } + } + + //#endregion Private Methods } diff --git a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolState.tsx b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolState.tsx index 1172586d481..315f0adec69 100644 --- a/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolState.tsx +++ b/src/vs/workbench/contrib/positronDataTool/browser/positronDataToolState.tsx @@ -2,43 +2,39 @@ * Copyright (C) 2023 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { useEffect, useState } from 'react'; // eslint-disable-line no-duplicate-imports +import { useEffect } from 'react'; // eslint-disable-line no-duplicate-imports import { DisposableStore } from 'vs/base/common/lifecycle'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { PositronActionBarServices } from 'vs/platform/positronActionBar/browser/positronActionBarState'; +import { IPositronDataToolInstance } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; /** - * PositronDataToolLayout enumeration. + * PositronDataToolServices interface. */ -export enum PositronDataToolLayout { - ColumnsLeft = 'ColumnsLeft', - ColumnsRight = 'ColumnsRight', - ColumnsHidden = 'ColumnsHidden', +export interface PositronDataToolServices extends PositronActionBarServices { + readonly clipboardService: IClipboardService; } /** - * PositronDataToolServices interface. + * PositronDataToolConfiguration interface. */ -export interface PositronDataToolServices extends PositronActionBarServices { - readonly clipboardService: IClipboardService; +export interface PositronDataToolConfiguration extends PositronDataToolServices { + readonly instance: IPositronDataToolInstance; } /** * PositronDataToolState interface. */ -export interface PositronDataToolState extends PositronDataToolServices { - layout: PositronDataToolLayout; - setLayout(layout: PositronDataToolLayout): void; +export interface PositronDataToolState extends PositronDataToolConfiguration { } /** * The usePositronDataToolState custom hook. * @returns The hook. */ -export const usePositronDataToolState = (services: PositronDataToolServices): PositronDataToolState => { - // Hooks. - const [layout, setLayout] = useState(PositronDataToolLayout.ColumnsLeft); - +export const usePositronDataToolState = ( + configuration: PositronDataToolConfiguration +): PositronDataToolState => { // Add event handlers. useEffect(() => { // Create a disposable store for the event handlers we'll add. @@ -50,8 +46,6 @@ export const usePositronDataToolState = (services: PositronDataToolServices): Po // Return the Positron data tool state. return { - ...services, - layout, - setLayout + ...configuration, }; }; diff --git a/src/vs/workbench/contrib/positronHelp/browser/positronHelpService.ts b/src/vs/workbench/contrib/positronHelp/browser/positronHelpService.ts index 490abed7497..d39fd04b2a7 100644 --- a/src/vs/workbench/contrib/positronHelp/browser/positronHelpService.ts +++ b/src/vs/workbench/contrib/positronHelp/browser/positronHelpService.ts @@ -18,7 +18,6 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t import { HelpEntry, IHelpEntry } from 'vs/workbench/contrib/positronHelp/browser/helpEntry'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { LanguageRuntimeEventData, LanguageRuntimeEventType } from 'vs/workbench/services/languageRuntime/common/languageRuntimeEvents'; import { HelpClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeHelpClient'; import { ILanguageRuntime, ILanguageRuntimeService, IRuntimeClientInstance, RuntimeClientType, RuntimeState } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; import { ShowHelpEvent } from 'vs/workbench/services/languageRuntime/common/positronHelpComm'; @@ -210,47 +209,6 @@ class PositronHelpService extends Disposable implements IPositronHelpService { } }) ); - - // Register onDidReceiveRuntimeEvent handler. - // - // *** - // TEMPORARY: Currently, some runtimes deliver the ShowHelp event as a - // global event. This code can go away as soon as all runtimes send this - // event over the `positron.help` comm channel instead. - // *** - this._register( - this._languageRuntimeService.onDidReceiveRuntimeEvent(async languageRuntimeGlobalEvent => { - /** - * Custom custom type guard for ShowHelpEvent. - * @param _ The LanguageRuntimeEventData that should be a ShowHelpEvent. - * @returns true if the LanguageRuntimeEventData is a ShowHelpEvent; otherwise, false. - */ - const isShowHelpEvent = (_: LanguageRuntimeEventData): _ is ShowHelpEvent => { - return (_ as ShowHelpEvent).kind !== undefined; - }; - - // Show help event types are supported. - if (languageRuntimeGlobalEvent.event.name !== LanguageRuntimeEventType.ShowHelp) { - return; - } - - // Ensure that the right event data was supplied. - if (!isShowHelpEvent(languageRuntimeGlobalEvent.event.data)) { - this._logService.error(`ShowHelp event supplied unsupported event data.`); - return; - } - - // Get the show help event. - const showHelpEvent = languageRuntimeGlobalEvent.event.data as ShowHelpEvent; - const runtime = this._languageRuntimeService.getRuntime( - languageRuntimeGlobalEvent.runtime_id); - if (runtime) { - this.handleShowHelpEvent(runtime, showHelpEvent); - } else { - this._logService.error(`PositronHelpService could not find runtime ${languageRuntimeGlobalEvent.runtime_id}.`); - } - }) - ); } /** diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index a49ba3c9d4f..40fa9e2e143 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -20,6 +20,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IModalDialogPromptInstance, IPositronModalDialogsService } from 'vs/workbench/services/positronModalDialogs/common/positronModalDialogs'; import { IOpener, IOpenerService, OpenExternalOptions, OpenInternalOptions } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; +import { FrontendEvent } from './positronFrontendComm'; /** * LanguageRuntimeInfo class. @@ -713,16 +714,64 @@ export class LanguageRuntimeService extends Disposable implements ILanguageRunti (RuntimeClientType.FrontEnd, {}).then(client => { // Create the frontend client instance wrapping the client instance. const frontendClient = new FrontEndClientInstance(client); + this._register(frontendClient); // When the frontend client instance emits an event, broadcast - // it to Positron. - this._register(frontendClient.onDidEmitEvent(event => { + // it to Positron with the corresponding runtime ID. + this._register(frontendClient.onDidBusy(event => { this._onDidReceiveRuntimeEventEmitter.fire({ runtime_id: runtime.metadata.runtimeId, - event + event: { + name: FrontendEvent.Busy, + data: event + } + }); + })); + this._register(frontendClient.onDidClearConsole(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + runtime_id: runtime.metadata.runtimeId, + event: { + name: FrontendEvent.ClearConsole, + data: event + } + }); + })); + this._register(frontendClient.onDidOpenEditor(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + runtime_id: runtime.metadata.runtimeId, + event: { + name: FrontendEvent.OpenEditor, + data: event + } + }); + })); + this._register(frontendClient.onDidShowMessage(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + runtime_id: runtime.metadata.runtimeId, + event: { + name: FrontendEvent.ShowMessage, + data: event + } + }); + })); + this._register(frontendClient.onDidPromptState(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + runtime_id: runtime.metadata.runtimeId, + event: { + name: FrontendEvent.PromptState, + data: event + } + }); + })); + this._register(frontendClient.onDidWorkingDirectory(event => { + this._onDidReceiveRuntimeEventEmitter.fire({ + runtime_id: runtime.metadata.runtimeId, + event: { + name: FrontendEvent.WorkingDirectory, + data: event + } }); })); - this._register(frontendClient); }); } diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeEvents.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeEvents.ts deleted file mode 100644 index f88935dd1d3..00000000000 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeEvents.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// This file was automatically generated by 'positron/scripts/generate-events.ts'. -// Please do not modify this file directly. - -export interface LanguageRuntimeEventData { } - -export enum LanguageRuntimeEventType { - Busy = 'busy', - ShowMessage = 'show_message', - ShowHelp = 'show_help', - PromptState = 'prompt_state', - WorkingDirectory = 'working_directory', -} - -// Represents a change in the runtime's busy state. -// Note that this represents the busy state of the underlying computation engine, not the busy state of the kernel. -// The kernel is busy when it is processing a request, but the runtime is busy only when a computation is running. -export interface BusyEvent extends LanguageRuntimeEventData { - - /** Whether the runtime is busy. */ - busy: boolean; - -} - -// Use this event to show a message to the user. -export interface ShowMessageEvent extends LanguageRuntimeEventData { - - /** The message to show to the user. */ - message: string; - -} - -// Show help content in the Help pane. -export interface ShowHelpEvent extends LanguageRuntimeEventData { - - /** The help content to be shown. */ - content: string; - - /** The content help type. Must be one of 'html', 'markdown', or 'url'. */ - kind: string; - - /** Focus the Help pane after the Help content has been rendered? */ - focus: boolean; - -} - -// Update strings of future input and continuation prompts. -export interface PromptStateEvent extends LanguageRuntimeEventData { - - /** String for future input prompts. */ - inputPrompt: string; - - /** String for future continuation prompts. */ - continuationPrompt: string; - -} - -// Change the displayed working directory for the interpreter. -export interface WorkingDirectoryEvent extends LanguageRuntimeEventData { - - /** The new working directory. */ - directory: string; - -} - diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeFrontEndClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeFrontEndClient.ts index 558ad544426..caf817e2b25 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeFrontEndClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeFrontEndClient.ts @@ -3,9 +3,9 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; -import { LanguageRuntimeEventData, LanguageRuntimeEventType } from 'vs/workbench/services/languageRuntime/common/languageRuntimeEvents'; +import { BusyEvent, ClearConsoleEvent, FrontendEvent, OpenEditorEvent, PositronFrontendComm, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent } from './positronFrontendComm'; /** @@ -39,8 +39,8 @@ export interface IFrontEndClientMessageOutput { * An event from the backend. */ export interface IRuntimeClientEvent { - name: LanguageRuntimeEventType; - data: LanguageRuntimeEventData; + name: FrontendEvent; + data: any; } /** @@ -58,9 +58,15 @@ export interface IFrontEndClientMessageOutputEvent * the backend know when Positron is connected. */ export class FrontEndClientInstance extends Disposable { + private _comm: PositronFrontendComm; - /** The emitter for runtime client events. */ - private readonly _onDidEmitEvent = this._register(new Emitter()); + /** Emitters for events forwarded from the frontend comm */ + onDidBusy: Event; + onDidClearConsole: Event; + onDidOpenEditor: Event; + onDidShowMessage: Event; + onDidPromptState: Event; + onDidWorkingDirectory: Event; /** * Creates a new frontend client instance. @@ -69,27 +75,17 @@ export class FrontEndClientInstance extends Disposable { * instance and will dispose it when it is disposed. */ constructor( - private readonly _client: - IRuntimeClientInstance, + private readonly _client: IRuntimeClientInstance, ) { super(); this._register(this._client); - this._register(this._client.onDidReceiveData(data => this.handleData(data))); - this.onDidEmitEvent = this._onDidEmitEvent.event; - } - - onDidEmitEvent: Event; - /** - * Handles data received from the backend. - * - * @param data Data received from the backend. - */ - private handleData(data: IFrontEndClientMessageOutput): void { - switch (data.msg_type) { - case FrontEndMessageTypeOutput.Event: - this._onDidEmitEvent.fire(data as IFrontEndClientMessageOutputEvent); - break; - } + this._comm = new PositronFrontendComm(this._client); + this.onDidBusy = this._comm.onDidBusy; + this.onDidClearConsole = this._comm.onDidClearConsole; + this.onDidOpenEditor = this._comm.onDidOpenEditor; + this.onDidShowMessage = this._comm.onDidShowMessage; + this.onDidPromptState = this._comm.onDidPromptState; + this.onDidWorkingDirectory = this._comm.onDidWorkingDirectory; } } diff --git a/src/vs/workbench/services/languageRuntime/common/positronDataToolComm.ts b/src/vs/workbench/services/languageRuntime/common/positronDataToolComm.ts new file mode 100644 index 00000000000..727b7b54dc3 --- /dev/null +++ b/src/vs/workbench/services/languageRuntime/common/positronDataToolComm.ts @@ -0,0 +1,408 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// +// AUTO-GENERATED from data_tool.json; do not edit. +// + +import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; +import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; + +/** + * The schema for a table-like object + */ +export interface TableSchema { + /** + * Schema for each column in the table + */ + columns?: Array; + + /** + * Numbers of rows in the unfiltered dataset + */ + num_rows?: number; + +} + +/** + * Table values formatted as strings + */ +export interface TableData { + /** + * The columns of data + */ + columns: Array; + + /** + * Zero or more arrays of row labels + */ + row_labels?: Array; + +} + +/** + * The result of applying filters to a table + */ +export interface FilterResult { + /** + * Number of rows in table after applying filters + */ + selected_num_rows?: number; + +} + +/** + * 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 state + */ +export interface BackendState { + /** + * The set of currently applied filters + */ + filters?: Array; + + /** + * The set of currently applied sorts + */ + sort_keys?: Array; + +} + +/** + * Schema for a column in a table + */ +export interface ColumnSchema { + /** + * Name of column as UTF-8 string + */ + name: string; + + /** + * Canonical name of data type class + */ + type_name: string; + + /** + * Column annotation / description + */ + description?: string; + + /** + * Schema of nested child types + */ + children?: Array; + + /** + * Precision for decimal types + */ + precision?: number; + + /** + * Scale for decimal types + */ + scale?: number; + + /** + * Time zone for timestamp with time zone + */ + timezone?: string; + + /** + * Size parameter for fixed-size types (list, binary) + */ + type_size?: number; + +} + +/** + * Specifies a table row filter based on a column's values + */ +export interface ColumnFilter { + /** + * Unique identifier for this filter + */ + filter_id: string; + + /** + * Type of filter to apply + */ + filter_type: ColumnFilterFilterType; + + /** + * String representation of a binary comparison + */ + compare_op?: ColumnFilterCompareOp; + + /** + * A stringified column value for a comparison filter + */ + compare_value?: string; + + /** + * Array of column values for a set membership filter + */ + set_member_values?: Array; + + /** + * Filter by including only values passed (true) or excluding (false) + */ + set_member_inclusive?: boolean; + + /** + * Type of search to perform + */ + search_type?: ColumnFilterSearchType; + + /** + * String value/regex to search for in stringified data + */ + search_term?: string; + + /** + * If true, do a case-sensitive search, otherwise case-insensitive + */ + search_case_sensitive?: boolean; + +} + +/** + * An exact or approximate quantile value from a column + */ +export interface ColumnQuantileValue { + /** + * Quantile number (percentile). E.g. 1 for 1%, 50 for median + */ + q?: number; + + /** + * Stringified quantile value + */ + value?: string; + + /** + * Whether value is exact or approximate (computed from binned data or + * sketches) + */ + exact?: boolean; + +} + +/** + * Specifies a column to sort by + */ +export interface ColumnSortKey { + /** + * Column name to sort by + */ + column?: string; + + /** + * Sort order, ascending (true) or descending (false) + */ + ascending?: boolean; + +} + +/** + * Possible values for ProfileType in GetColumnProfile + */ +export enum GetColumnProfileProfileType { + Freqtable = 'freqtable', + Histogram = 'histogram' +} + +/** + * Possible values for FilterType in ColumnFilter + */ +export enum ColumnFilterFilterType { + Isnull = 'isnull', + Notnull = 'notnull', + Compare = 'compare', + SetMembership = 'set_membership', + Search = 'search' +} + +/** + * Possible values for CompareOp in ColumnFilter + */ +export enum ColumnFilterCompareOp { + EqEq = '==', + NotEq = '!=', + Lt = '<', + LtEq = '<=', + Gt = '>', + GtEq = '>=' +} + +/** + * Possible values for SearchType in ColumnFilter + */ +export enum ColumnFilterSearchType { + Contains = 'contains', + Startswith = 'startswith', + Endswith = 'endswith', + Regex = 'regex' +} + +/** + * Column values formatted as strings + */ +export type ColumnFormattedData = Array; + +export class PositronDataToolComm extends PositronBaseComm { + constructor(instance: IRuntimeClientInstance) { + super(instance); + } + + /** + * Request schema + * + * Request full schema for a table-like object + * + * + * @returns The schema for a table-like object + */ + getSchema(): Promise { + return super.performRpc('get_schema', [], []); + } + + /** + * Get a rectangle of data values + * + * Request a rectangular subset of data with values formatted as strings + * + * @param rowStartIndex First row to fetch (inclusive) + * @param rowEndIndex Last row to fetch (inclusive). May be beyond end + * @param columnStartIndex First column to fetch (inclusive) + * @param columnEndIndex Last column to fetch (inclusive). May extend + * beyond end + * + * @returns Table values formatted as strings + */ + getDataValues(rowStartIndex: number, rowEndIndex: number, columnStartIndex: number, columnEndIndex: number): Promise { + return super.performRpc('get_data_values', ['row_start_index', 'row_end_index', 'column_start_index', 'column_end_index'], [rowStartIndex, rowEndIndex, columnStartIndex, columnEndIndex]); + } + + /** + * Set column filters + * + * Set or clear column 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]); + } + + /** + * Set or clear sort-by-column(s) + * + * 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 + * + */ + setSortColumns(sortKeys: Array): Promise { + return super.performRpc('set_sort_columns', ['sort_keys'], [sortKeys]); + } + + /** + * Get a column profile + * + * Requests a statistical summary or data profile for a column + * + * @param profileId Unique identifier for the requested profile + * @param profileType The type of analytical column profile + * @param column Column name to compute profile for + * + * @returns Result of computing column profile + */ + getColumnProfile(profileId: string, profileType: GetColumnProfileProfileType, column: string): Promise { + return super.performRpc('get_column_profile', ['profile_id', 'profile_type', 'column'], [profileId, profileType, column]); + } + + /** + * Get the state + * + * Request the current backend state (applied filters and sort columns) + * + * + * @returns The current backend state + */ + getState(): Promise { + return super.performRpc('get_state', [], []); + } + +} + diff --git a/src/vs/workbench/services/languageRuntime/common/positronFrontendComm.ts b/src/vs/workbench/services/languageRuntime/common/positronFrontendComm.ts new file mode 100644 index 00000000000..209d717cef7 --- /dev/null +++ b/src/vs/workbench/services/languageRuntime/common/positronFrontendComm.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// +// AUTO-GENERATED from frontend.json; do not edit. +// + +import { Event } from 'vs/base/common/event'; +import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; +import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; + +/** + * Items in Params + */ +export interface Param { + [k: string]: unknown; +} + +/** + * The method result + */ +export interface CallMethodResult { + [k: string]: unknown; +} + +/** + * Event: Change in backend's busy/idle status + */ +export interface BusyEvent { + /** + * Whether the backend is busy + */ + busy: boolean; + +} + +/** + * Event: Clear the console + */ +export interface ClearConsoleEvent { +} + +/** + * Event: Open an editor + */ +export interface OpenEditorEvent { + /** + * The path of the file to open + */ + file: string; + + /** + * The line number to jump to + */ + line: number; + + /** + * The column number to jump to + */ + column: number; + +} + +/** + * Event: Show a message + */ +export interface ShowMessageEvent { + /** + * The message to show to the user. + */ + message: string; + +} + +/** + * Event: New state of the primary and secondary prompts + */ +export interface PromptStateEvent { + /** + * Prompt for primary input. + */ + input_prompt: string; + + /** + * Prompt for incomplete input. + */ + continuation_prompt: string; + +} + +/** + * Event: Change the displayed working directory + */ +export interface WorkingDirectoryEvent { + /** + * The new working directory + */ + directory: string; + +} + +export enum FrontendEvent { + Busy = 'busy', + ClearConsole = 'clear_console', + OpenEditor = 'open_editor', + ShowMessage = 'show_message', + PromptState = 'prompt_state', + WorkingDirectory = 'working_directory' +} + +export class PositronFrontendComm extends PositronBaseComm { + constructor(instance: IRuntimeClientInstance) { + super(instance); + this.onDidBusy = super.createEventEmitter('busy', ['busy']); + this.onDidClearConsole = super.createEventEmitter('clear_console', []); + this.onDidOpenEditor = super.createEventEmitter('open_editor', ['file', 'line', 'column']); + this.onDidShowMessage = super.createEventEmitter('show_message', ['message']); + this.onDidPromptState = super.createEventEmitter('prompt_state', ['input_prompt', 'continuation_prompt']); + this.onDidWorkingDirectory = super.createEventEmitter('working_directory', ['directory']); + } + + /** + * Run a method in the interpreter and return the result to the frontend + * + * Unlike other RPC methods, `call_method` calls into methods implemented + * in the interpreter and returns the result back to the frontend using + * an implementation-defined serialization scheme. + * + * @param method The method to call inside the interpreter + * @param params The parameters for `method` + * + * @returns The method result + */ + callMethod(method: string, params: Array): Promise { + return super.performRpc('call_method', ['method', 'params'], [method, params]); + } + + + /** + * Change in backend's busy/idle status + * + * This represents the busy state of the underlying computation engine, + * not the busy state of the kernel. The kernel is busy when it is + * processing a request, but the runtime is busy only when a computation + * is running. + */ + onDidBusy: Event; + /** + * Clear the console + * + * Use this to clear the console. + */ + onDidClearConsole: Event; + /** + * Open an editor + * + * This event is used to open an editor with a given file and selection. + */ + onDidOpenEditor: Event; + /** + * Show a message + * + * Use this for messages that require immediate attention from the user + */ + onDidShowMessage: Event; + /** + * New state of the primary and secondary prompts + * + * Languages like R allow users to change the way their prompts look. + * This event signals a change in the prompt configuration. + */ + onDidPromptState: Event; + /** + * Change the displayed working directory + * + * This event signals a change in the working direcotry of the + * interpreter + */ + onDidWorkingDirectory: Event; +} + diff --git a/src/vs/workbench/services/languageRuntime/common/positronHelpComm.ts b/src/vs/workbench/services/languageRuntime/common/positronHelpComm.ts index 3facbec7aee..6b080ce8eeb 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronHelpComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronHelpComm.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ // @@ -10,6 +10,15 @@ import { Event } from 'vs/base/common/event'; import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; +/** + * Possible values for Kind in ShowHelp + */ +export enum ShowHelpKind { + Html = 'html', + Markdown = 'markdown', + Url = 'url' +} + /** * Event: Request to show help in the frontend */ @@ -22,7 +31,7 @@ export interface ShowHelpEvent { /** * The type of content to show */ - kind: 'html' | 'markdown' | 'url'; + kind: ShowHelpKind; /** * Whether to focus the Help pane when the content is displayed. @@ -31,6 +40,10 @@ export interface ShowHelpEvent { } +export enum HelpEvent { + ShowHelp = 'show_help' +} + export class PositronHelpComm extends PositronBaseComm { constructor(instance: IRuntimeClientInstance) { super(instance); @@ -54,6 +67,7 @@ export class PositronHelpComm extends PositronBaseComm { return super.performRpc('show_help_topic', ['topic'], [topic]); } + /** * Request to show help in the frontend */ diff --git a/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts b/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts index ca1463eb74b..d513a5e2b33 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronPlotComm.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ // @@ -17,11 +17,13 @@ export interface PlotResult { /** * The plot data, as a base64-encoded string */ - data?: string; + data: string; + /** * The MIME type of the plot data */ - mime_type?: string; + mime_type: string; + } /** @@ -30,6 +32,10 @@ export interface PlotResult { export interface UpdateEvent { } +export enum PlotEvent { + Update = 'update' +} + export class PositronPlotComm extends PositronBaseComm { constructor(instance: IRuntimeClientInstance) { super(instance); @@ -52,6 +58,7 @@ export class PositronPlotComm extends PositronBaseComm { return super.performRpc('render', ['height', 'width', 'pixel_ratio'], [height, width, pixelRatio]); } + /** * Notification that a plot has been updated on the backend. */ diff --git a/src/vs/workbench/services/languageRuntime/common/positronVariablesComm.ts b/src/vs/workbench/services/languageRuntime/common/positronVariablesComm.ts new file mode 100644 index 00000000000..d175b82cf4f --- /dev/null +++ b/src/vs/workbench/services/languageRuntime/common/positronVariablesComm.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// +// AUTO-GENERATED from variables.json; do not edit. +// + +import { Event } from 'vs/base/common/event'; +import { PositronBaseComm } from 'vs/workbench/services/languageRuntime/common/positronBaseComm'; +import { IRuntimeClientInstance } from 'vs/workbench/services/languageRuntime/common/languageRuntimeClientInstance'; + +/** + * An inspected variable. + */ +export interface InspectedVariable { + /** + * The children of the inspected variable. + */ + children: Array; + + /** + * The total number of children. This may be greater than the number of + * children in the 'children' array if the array is truncated. + */ + length: number; + +} + +/** + * An object formatted for copying to the clipboard. + */ +export interface FormattedVariable { + /** + * The format returned, as a MIME type; matches the MIME type of the + * format named in the request. + */ + format: string; + + /** + * The formatted content of the variable. + */ + content: string; + +} + +/** + * A single variable in the runtime. + */ +export interface Variable { + /** + * A key that uniquely identifies the variable within the runtime and can + * be used to access the variable in `inspect` requests + */ + access_key: string; + + /** + * The name of the variable, formatted for display + */ + display_name: string; + + /** + * A string representation of the variable's value, formatted for display + * and possibly truncated + */ + display_value: string; + + /** + * The variable's type, formatted for display + */ + display_type: string; + + /** + * Extended information about the variable's type + */ + type_info: string; + + /** + * The kind of value the variable represents, such as 'string' or + * 'number' + */ + kind: VariableKind; + + /** + * The number of elements in the variable, if it is a collection + */ + length: number; + + /** + * Whether the variable has child variables + */ + has_children: boolean; + + /** + * True if there is a viewer available for this variable (i.e. the + * runtime can handle a 'view' request for this variable) + */ + has_viewer: boolean; + + /** + * True the 'value' field is a truncated representation of the variable's + * value + */ + is_truncated: boolean; + +} + +/** + * Possible values for Kind in Variable + */ +export enum VariableKind { + Boolean = 'boolean', + Bytes = 'bytes', + Collection = 'collection', + Empty = 'empty', + Function = 'function', + Map = 'map', + Number = 'number', + Other = 'other', + String = 'string', + Table = 'table' +} + +/** + * Event: Update variables + */ +export interface UpdateEvent { + /** + * An array of variables that have been newly assigned. + */ + assigned: Array; + + /** + * An array of variable names that have been removed. + */ + removed: Array; + +} + +export enum VariablesEvent { + Update = 'update' +} + +export class PositronVariablesComm extends PositronBaseComm { + constructor(instance: IRuntimeClientInstance) { + super(instance); + this.onDidUpdate = super.createEventEmitter('update', ['assigned', 'removed']); + } + + /** + * Clear all variables + * + * Clears (deletes) all variables in the current session. + * + * @param includeHiddenObjects Whether to clear hidden objects in + * addition to normal variables + * + */ + clear(includeHiddenObjects: boolean): Promise { + return super.performRpc('clear', ['include_hidden_objects'], [includeHiddenObjects]); + } + + /** + * Deletes a set of named variables + * + * Deletes the named variables from the current session. + * + * @param names The names of the variables to delete. + * + */ + delete(names: Array): Promise { + return super.performRpc('delete', ['names'], [names]); + } + + /** + * Inspect a variable + * + * Returns the children of a variable, as an array of variables. + * + * @param path The path to the variable to inspect, as an array of access + * keys. + * + * @returns An inspected variable. + */ + inspect(path: Array): Promise { + return super.performRpc('inspect', ['path'], [path]); + } + + /** + * Format for clipboard + * + * Requests a formatted representation of a variable for copying to the + * clipboard. + * + * @param path The path to the variable to format, as an array of access + * keys. + * @param format The requested format for the variable, as a MIME type + * + * @returns An object formatted for copying to the clipboard. + */ + clipboardFormat(path: Array, format: string): Promise { + return super.performRpc('clipboard_format', ['path', 'format'], [path, format]); + } + + /** + * Request a viewer for a variable + * + * Request that the runtime open a data viewer to display the data in a + * variable. + * + * @param path The path to the variable to view, as an array of access + * keys. + * + */ + view(path: Array): Promise { + return super.performRpc('view', ['path'], [path]); + } + + + /** + * Update variables + * + * Updates the variables in the current session. + */ + onDidUpdate: Event; +} + diff --git a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts index 58d83b12f20..fdd9b8b1ca4 100644 --- a/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService.ts @@ -33,7 +33,9 @@ export const enum PositronConsoleState { * IPositronConsoleService interface. */ export interface IPositronConsoleService { - // Needed for service branding in dependency injector. + /** + * Needed for service branding in dependency injector. + */ readonly _serviceBrand: undefined; /** diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index c98fe0bf47e..3895f065fde 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -5,10 +5,15 @@ import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; +import { PixelRatio } from 'vs/base/browser/browser'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ILogService } from 'vs/platform/log/common/log'; import { IViewsService } from 'vs/workbench/common/views'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -35,11 +40,7 @@ import { ActivityItemInput, ActivityItemInputState } from 'vs/workbench/services import { ActivityItemErrorStream, ActivityItemOutputStream } from 'vs/workbench/services/positronConsole/browser/classes/activityItemStream'; import { IPositronConsoleInstance, IPositronConsoleService, POSITRON_CONSOLE_VIEW_ID, PositronConsoleState } from 'vs/workbench/services/positronConsole/browser/interfaces/positronConsoleService'; import { formatLanguageRuntime, ILanguageRuntime, ILanguageRuntimeExit, ILanguageRuntimeMessage, ILanguageRuntimeService, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeExitReason, RuntimeOnlineState, RuntimeState } from 'vs/workbench/services/languageRuntime/common/languageRuntimeService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; -import { PixelRatio } from 'vs/base/browser/browser'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { FrontendEvent } from 'vs/workbench/services/languageRuntime/common/positronFrontendComm'; /** * The onDidChangeRuntimeItems throttle threshold and throttle interval. The throttle threshold @@ -1515,6 +1516,13 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst } })); + // Add the onDidReceiveRuntimeClientEvent event handler. + this._runtimeDisposableStore.add(this._runtime.onDidReceiveRuntimeClientEvent((event) => { + if (event.name === FrontendEvent.ClearConsole) { + this.clearConsole(); + } + })); + this._runtimeDisposableStore.add(this._runtime.onDidEndSession((exit) => { // If trace is enabled, add a trace runtime item. if (this._trace) { diff --git a/src/vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService.ts b/src/vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService.ts new file mode 100644 index 00000000000..7fff75f3908 --- /dev/null +++ b/src/vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +// Create the decorator for the Positron data tool service (used in dependency injection). +export const IPositronDataToolService = createDecorator('positronDataToolService'); + +/** + * PositronDataToolLayout enumeration. + */ +export enum PositronDataToolLayout { + ColumnsLeft = 'ColumnsLeft', + ColumnsRight = 'ColumnsRight', + ColumnsHidden = 'ColumnsHidden', +} + +/** + * IPositronDataToolService interface. + */ +export interface IPositronDataToolService { + /** + * Needed for service branding in dependency injector. + */ + readonly _serviceBrand: undefined; + + /** + * Test open function. + * @param identifier The identifier. + */ + testOpen(identifier: string): Promise; + + /** + * + * @param identifier + */ + getInstance(identifier: string): IPositronDataToolInstance | undefined; +} + +/** + * IPositronDataToolInstance interface. + */ +export interface IPositronDataToolInstance { + /** + * Gets the identifier. + */ + readonly identifier: string; + + /** + * Gets or sets the layout. + */ + layout: PositronDataToolLayout; + + /** + * Gets or sets the columns width percent. + */ + columnsWidthPercent: number; + + /** + * Gets or sets the columns scroll offset. + */ + columnsScrollOffset: number; + + /** + * Gets or sets the rows scroll offset. + */ + rowsScrollOffset: number; + + /** + * The onDidChangeLayout event. + */ + readonly onDidChangeLayout: Event; + + /** + * The onDidChangeColumnsWidthPercent event. + */ + readonly onDidChangeColumnsWidthPercent: Event; + + /** + * The onDidChangeColumnsScrollOffset event. + */ + readonly onDidChangeColumnsScrollOffset: Event; + + /** + * The onDidChangeRowsScrollOffset event. + */ + readonly onDidChangeRowsScrollOffset: Event; +} diff --git a/src/vs/workbench/services/positronDataTool/browser/positronDataToolService.ts b/src/vs/workbench/services/positronDataTool/browser/positronDataToolService.ts new file mode 100644 index 00000000000..1b31345fcbe --- /dev/null +++ b/src/vs/workbench/services/positronDataTool/browser/positronDataToolService.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { PositronDataToolUri } from 'vs/workbench/services/positronDataTool/common/positronDataToolUri'; +import { IPositronDataToolInstance, IPositronDataToolService, PositronDataToolLayout } from 'vs/workbench/services/positronDataTool/browser/interfaces/positronDataToolService'; + +/** + * PositronDataToolService class. + */ +class PositronDataToolService extends Disposable implements IPositronDataToolService { + //#region Private Properties + + /** + * The Positron data tool instance map. + */ + private _positronDataToolInstanceMap = new Map(); + + //#endregion Private Properties + + //#region Constructor & Dispose + + /** + * Constructor. + * @param _editorService The editor service. + */ + constructor( + @IEditorService private readonly _editorService: IEditorService + ) { + // Call the disposable constrcutor. + super(); + } + + //#endregion Constructor & Dispose + + //#region IPositronDataToolService Implementation + + /** + * Needed for service branding in dependency injector. + */ + declare readonly _serviceBrand: undefined; + + /** + * Test open function. + */ + async testOpen(identifier: string): Promise { + // Add the instance, if necessary. + if (!this._positronDataToolInstanceMap.has(identifier)) { + const positronDataToolInstance = new PositronDataToolInstance(identifier); + this._positronDataToolInstanceMap.set(identifier, positronDataToolInstance); + } + + // Open the editor. + await this._editorService.openEditor({ + resource: PositronDataToolUri.generate(identifier) + }); + } + + /** + * Gets a Positron data tool instance. + * @param identifier The identifier of the Positron data tool instance. + */ + getInstance(identifier: string): IPositronDataToolInstance | undefined { + return this._positronDataToolInstanceMap.get(identifier); + } + + //#endregion IPositronDataToolService Implementation +} + +/** +* PositronDataToolInstance class. +*/ +class PositronDataToolInstance extends Disposable implements IPositronDataToolInstance { + //#region Private Properties + + /** + * Gets or sets the layout. + */ + private _layout = PositronDataToolLayout.ColumnsLeft; + + /** + * Gets or sets the columns width percent. + */ + private _columnsWidthPercent = 0.25; + + /** + * Gets or sets the columns scroll offset. + */ + private _columnsScrollOffset = 200; + + /** + * Gets or sets the rows scroll offset. + */ + private _rowsScrollOffset = 0; + + /** + * The onDidChangeLayout event emitter. + */ + private readonly _onDidChangeLayoutEmitter = this._register(new Emitter); + + /** + * The onDidChangeColumnsWidthPercent event emitter. + */ + private readonly _onDidChangeColumnsWidthPercentEmitter = this._register(new Emitter); + + /** + * The onDidChangeColumnsScrollOffset event emitter. + */ + private readonly _onDidChangeColumnsScrollOffsetEmitter = this._register(new Emitter); + + /** + * The onDidChangeRowsScrollOffset event emitter. + */ + private readonly _onDidChangeRowsScrollOffsetEmitter = this._register(new Emitter); + + //#endregion Private Properties + + //#region Constructor & Dispose + + /** + * Constructor. + * @param identifier The identifier. + */ + constructor(readonly identifier: string) { + // Call the base class's constructor. + super(); + } + + //#endregion Constructor & Dispose + + //#region IPositronDataToolInstance Implementation + + /** + * Gets the layout. + */ + get layout() { + return this._layout; + } + + /** + * Sets the layout. + */ + set layout(layout: PositronDataToolLayout) { + if (layout !== this._layout) { + this._layout = layout; + this._onDidChangeLayoutEmitter.fire(this._layout); + } + } + + /** + * Gets the columns width percent. + */ + get columnsWidthPercent() { + return this._columnsWidthPercent; + } + + /** + * Sets the columns width percent. + */ + set columnsWidthPercent(columnsWidthPercent: number) { + if (columnsWidthPercent !== this._columnsWidthPercent) { + this._columnsWidthPercent = columnsWidthPercent; + this._onDidChangeColumnsWidthPercentEmitter.fire(this._columnsWidthPercent); + } + } + + /** + * Gets the columns scroll offset. + */ + get columnsScrollOffset() { + return this._columnsScrollOffset; + } + + /** + * Sets the columns scroll offset. + */ + set columnsScrollOffset(columnsScrollOffset: number) { + console.log(`************************* setting column scroll offset to ${columnsScrollOffset}`); + if (columnsScrollOffset !== this._columnsScrollOffset) { + this._columnsScrollOffset = columnsScrollOffset; + this._onDidChangeColumnsScrollOffsetEmitter.fire(this._columnsScrollOffset); + } + } + + /** + * Gets the rows scroll offset. + */ + get rowsScrollOffset() { + return this._rowsScrollOffset; + } + + /** + * Sets the rows scroll offset. + */ + set rowsScrollOffset(rowsScrollOffset: number) { + if (rowsScrollOffset !== this._rowsScrollOffset) { + this._rowsScrollOffset = rowsScrollOffset; + this._onDidChangeRowsScrollOffsetEmitter.fire(this._rowsScrollOffset); + } + } + + /** + * onDidChangeLayout event. + */ + readonly onDidChangeLayout = this._onDidChangeLayoutEmitter.event; + + /** + * onDidChangeColumnsWidthPercent event. + */ + readonly onDidChangeColumnsWidthPercent = this._onDidChangeColumnsWidthPercentEmitter.event; + + /** + * onDidChangeColumnsScrollOffset event. + */ + readonly onDidChangeColumnsScrollOffset = this._onDidChangeColumnsScrollOffsetEmitter.event; + + /** + * onDidChangeRowsScrollOffset event. + */ + readonly onDidChangeRowsScrollOffset = this._onDidChangeRowsScrollOffsetEmitter.event; + + //#endregion IPositronDataToolInstance Implementation +} + +// Register the Positron data tool service. +registerSingleton(IPositronDataToolService, PositronDataToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/positronDataTool/common/positronDataToolUri.ts b/src/vs/workbench/services/positronDataTool/common/positronDataToolUri.ts new file mode 100644 index 00000000000..6e8161def9e --- /dev/null +++ b/src/vs/workbench/services/positronDataTool/common/positronDataToolUri.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; + +/** + * PositronDataToolUri class. + */ +export class PositronDataToolUri { + /** + * The Positron data tool URI scheme. + */ + public static Scheme = Schemas.positronDataTool; + + /** + * Generates a Positron data tool URI. + * @param identifier The identifier. + * @returns The Positron data tool URI. + */ + public static generate(identifier: string): URI { + return URI.from({ + scheme: PositronDataToolUri.Scheme, + path: `positron-data-tool-${identifier}` + }); + } + + /** + * Parses a Positron data tool URI. + * @param resource The resource. + * @returns The identifier, if successful; otherwise, undefined. + */ + public static parse(resource: URI): string | undefined { + // Check the scheme. + if (resource.scheme !== PositronDataToolUri.Scheme) { + return undefined; + } + + // Parse the resource. + const match = resource.path.match(/^positron-data-tool-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$/); + const identifier = match?.[1]; + if (typeof identifier !== 'string') { + return undefined; + } + + // Return the identifier. + return identifier; + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 747822acbed..e07e9204ba6 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -425,8 +425,9 @@ import 'vs/workbench/contrib/positronIPyWidgets/browser/positronIPyWidgets.contr // Workbench services import 'vs/workbench/services/languageRuntime/common/languageRuntime'; -import 'vs/workbench/contrib/positronHelp/browser/positronHelpService'; import 'vs/workbench/services/positronConsole/browser/positronConsoleService'; +import 'vs/workbench/contrib/positronHelp/browser/positronHelpService'; import 'vs/workbench/services/positronVariables/common/positronVariablesService'; +import 'vs/workbench/services/positronDataTool/browser/positronDataToolService'; // --- End Positron --- diff --git a/test/smoke/src/areas/positron/variables/variablespane.test.ts b/test/smoke/src/areas/positron/variables/variablespane.test.ts new file mode 100644 index 00000000000..5194d8480bb --- /dev/null +++ b/test/smoke/src/areas/positron/variables/variablespane.test.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { Application, Logger } from '../../../../../automation'; +import { installAllHandlers } from '../../../utils'; + +export function setup(logger: Logger) { + describe('Variables Pane', () => { + + // Shared before/after handling + installAllHandlers(logger); + + it('verifies Variables pane exists', async function () { + const app = this.app as Application; + + await app.code.waitForElement('.positron-variables-container'); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index cd964d010ca..a329c139370 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -16,18 +16,20 @@ import fetch from 'node-fetch'; import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion } from '../../automation'; import { retry, timeout } from './utils'; -import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; -import { setup as setupPreferencesTests } from './areas/preferences/preferences.test'; -import { setup as setupSearchTests } from './areas/search/search.test'; -import { setup as setupNotebookTests } from './areas/notebook/notebook.test'; -import { setup as setupLanguagesTests } from './areas/languages/languages.test'; -import { setup as setupStatusbarTests } from './areas/statusbar/statusbar.test'; -import { setup as setupExtensionTests } from './areas/extensions/extensions.test'; -import { setup as setupMultirootTests } from './areas/multiroot/multiroot.test'; -import { setup as setupLocalizationTests } from './areas/workbench/localization.test'; -import { setup as setupLaunchTests } from './areas/workbench/launch.test'; -import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; -import { setup as setupTaskTests } from './areas/task/task.test'; +// Disabling all but Positron Tests for now +// import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; +// import { setup as setupPreferencesTests } from './areas/preferences/preferences.test'; +// import { setup as setupSearchTests } from './areas/search/search.test'; +// import { setup as setupNotebookTests } from './areas/notebook/notebook.test'; +// import { setup as setupLanguagesTests } from './areas/languages/languages.test'; +// import { setup as setupStatusbarTests } from './areas/statusbar/statusbar.test'; +// import { setup as setupExtensionTests } from './areas/extensions/extensions.test'; +// import { setup as setupMultirootTests } from './areas/multiroot/multiroot.test'; +// import { setup as setupLocalizationTests } from './areas/workbench/localization.test'; +// import { setup as setupLaunchTests } from './areas/workbench/launch.test'; +// import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; +// import { setup as setupTaskTests } from './areas/task/task.test'; +import { setup as setupVariablesTest } from './areas/positron/variables/variablespane.test'; const rootPath = path.join(__dirname, '..', '..', '..'); @@ -397,16 +399,18 @@ after(async function () { }); describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { - if (!opts.web) { setupDataLossTests(() => opts['stable-build'] /* Do not change, deferred for a reason! */, logger); } - setupPreferencesTests(logger); - setupSearchTests(logger); - setupNotebookTests(logger); - setupLanguagesTests(logger); - if (opts.web) { setupTerminalTests(logger); } // Not stable on desktop/remote https://github.com/microsoft/vscode/issues/146811 - setupTaskTests(logger); - setupStatusbarTests(logger); - if (quality !== Quality.Dev && quality !== Quality.OSS) { setupExtensionTests(logger); } - setupMultirootTests(logger); - if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } - if (!opts.web && !opts.remote) { setupLaunchTests(logger); } + // Disabling all but Positron Tests for now + // if (!opts.web) { setupDataLossTests(() => opts['stable-build'] /* Do not change, deferred for a reason! */, logger); } + // setupPreferencesTests(logger); + // setupSearchTests(logger); + // setupNotebookTests(logger); + // setupLanguagesTests(logger); + // if (opts.web) { setupTerminalTests(logger); } // Not stable on desktop/remote https://github.com/microsoft/vscode/issues/146811 + // setupTaskTests(logger); + // setupStatusbarTests(logger); + // if (quality !== Quality.Dev && quality !== Quality.OSS) { setupExtensionTests(logger); } + // setupMultirootTests(logger); + // if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } + // if (!opts.web && !opts.remote) { setupLaunchTests(logger); } + setupVariablesTest(logger); }); diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index 81c5752111d..f61514f72f1 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -33,7 +33,7 @@ const minimist = require('minimist'); * dev: boolean; * reporter: string; * 'reporter-options': string; - * 'wait-server': string; + * 'waitServer': string; * timeout: string; * 'crash-reporter-directory': string; * tfs: string; @@ -43,8 +43,8 @@ const minimist = require('minimist'); * }} */ const args = minimist(process.argv.slice(2), { - string: ['grep', 'run', 'runGlob', 'dev', 'reporter', 'reporter-options', 'wait-server', 'timeout', 'crash-reporter-directory', 'tfs'], - boolean: ['build', 'coverage', 'help'], + string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs'], + boolean: ['build', 'coverage', 'help', 'dev'], alias: { 'grep': ['g', 'f'], 'runGlob': ['glob', 'runGrep'], @@ -69,7 +69,7 @@ Options: --dev, --dev-tools, --devTools open dev tools, keep window open, reuse app data --reporter the mocha reporter (default: "spec") --reporter-options the mocha reporter options (default: "") ---wait-server port to connect to and wait before running tests +--waitServer port to connect to and wait before running tests --timeout timeout for tests --crash-reporter-directory crash reporter directory --tfs TFS server URL @@ -232,8 +232,8 @@ app.on('ready', () => { win.webContents.openDevTools(); } - if (args['wait-server']) { - waitForServer(Number(args['wait-server'])).then(sendRun); + if (args.waitServer) { + waitForServer(Number(args.waitServer)).then(sendRun); } else { sendRun(); }