From 007b5f318b2dfe4f1e388603b6eb91551162dd05 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Tue, 8 Jun 2021 07:10:28 +1000 Subject: [PATCH] feat: add support for tasks and test code lens (#436) --- README.md | 9 +- client/src/commands.ts | 97 ++++++++++----- client/src/content_provider.ts | 2 +- client/src/debug_config_provider.ts | 4 +- client/src/extension.ts | 110 +++------------- client/src/tasks.ts | 114 +++++++++++++++++ client/src/{interfaces.d.ts => types.d.ts} | 2 + client/src/util.ts | 99 +++++++++++++++ client/src/welcome.ts | 3 +- docs/tasks.md | 49 ++++++++ docs/testing.md | 25 ++++ docs/workspaceFolders.md | 2 +- package.json | 138 +++++++++++++++++++++ screenshots/deno_test_code_lens.png | Bin 0 -> 12041 bytes typescript-deno-plugin/src/index.ts | 2 +- 15 files changed, 525 insertions(+), 131 deletions(-) create mode 100644 client/src/tasks.ts rename client/src/{interfaces.d.ts => types.d.ts} (98%) create mode 100644 client/src/util.ts create mode 100644 docs/tasks.md create mode 100644 docs/testing.md create mode 100644 screenshots/deno_test_code_lens.png diff --git a/README.md b/README.md index ba1246d2..e0174ad9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This extension adds support for using [Deno](https://deno.land/) with Visual Studio Code, powered by the Deno language server. -> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.7 or +> ⚠️ **Important:** You need to have a version of Deno CLI installed (v1.10.3 or > later). The extension requires the executable and by default will use the > environment path. You can explicitly set the path to the executable in Visual > Studio Code Settings for `deno.path`. @@ -36,6 +36,8 @@ Studio Code, powered by the Deno language server. used with the Deno CLI. - [Auto completion for imports](./docs/ImportCompletions.md). - [Workspace folder configuration](./docs/workspaceFolders.md). +- [Testing Code Lens](./docs/testing.md). +- [Provides Tasks for the Deno CLI](./docs/tasks.md). ## Usage @@ -107,6 +109,11 @@ extension has the following configuration options: - `deno.codeLens.referencesAllFunctions`: Enables or disables the display of code lens information for all functions in the code. Requires `deno.codeLens.references` to be enabled as well. _boolean, default `false`_ +- `deno.codeLens.test`: Enables or disables the display of test code lens on + Deno tests. _boolean, default `true`_. +- `deno.codeLens.testArgs`: Provides additional arguments that should be set + when invoking the Deno CLI test from a code lens. _array of strings, default + `[ "--allow-all" ]`_. - `deno.config`: The file path to a `tsconfig.json` file. This is the equivalent to using `--config` on the command line. The path can be either be relative to the workspace, or an absolute path. _string, default `null`, examples: diff --git a/client/src/commands.ts b/client/src/commands.ts index 0ee4e256..918e9c8b 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -10,21 +10,16 @@ import { LANGUAGE_CLIENT_NAME, } from "./constants"; import { pickInitWorkspace } from "./initialize_project"; -import type { DenoExtensionContext } from "./interfaces"; import { cache as cacheReq, reloadImportRegistries as reloadImportRegistriesReq, } from "./lsp_extensions"; +import * as tasks from "./tasks"; +import type { DenoExtensionContext } from "./types"; import { WelcomePanel } from "./welcome"; +import { assert } from "./util"; -import { - commands, - ExtensionContext, - ProgressLocation, - Uri, - window, - workspace, -} from "vscode"; +import * as vscode from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import type { DocumentUri, @@ -35,23 +30,23 @@ import type { // deno-lint-ignore no-explicit-any export type Callback = (...args: any[]) => unknown; export type Factory = ( - context: ExtensionContext, + context: vscode.ExtensionContext, extensionContext: DenoExtensionContext, ) => Callback; /** For the current document active in the editor tell the Deno LSP to cache * the file and all of its dependencies in the local cache. */ export function cache( - _context: ExtensionContext, + _context: vscode.ExtensionContext, extensionContext: DenoExtensionContext, ): Callback { return (uris: DocumentUri[] = []) => { - const activeEditor = window.activeTextEditor; + const activeEditor = vscode.window.activeTextEditor; if (!activeEditor) { return; } - return window.withProgress({ - location: ProgressLocation.Window, + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, title: "caching", }, () => { return extensionContext.client.sendRequest( @@ -68,27 +63,27 @@ export function cache( } export function initializeWorkspace( - _context: ExtensionContext, + _context: vscode.ExtensionContext, _extensionContext: DenoExtensionContext, ): Callback { return async () => { try { const settings = await pickInitWorkspace(); - const config = workspace.getConfiguration(EXTENSION_NS); + const config = vscode.workspace.getConfiguration(EXTENSION_NS); await config.update("enable", true); await config.update("lint", settings.lint); await config.update("unstable", settings.unstable); - await window.showInformationMessage( + await vscode.window.showInformationMessage( "Deno is now setup in this workspace.", ); } catch { - window.showErrorMessage("Deno project initialization failed."); + vscode.window.showErrorMessage("Deno project initialization failed."); } }; } export function reloadImportRegistries( - _context: ExtensionContext, + _context: vscode.ExtensionContext, { client }: DenoExtensionContext, ): Callback { return () => client.sendRequest(reloadImportRegistriesReq); @@ -96,7 +91,7 @@ export function reloadImportRegistries( /** Start (or restart) the Deno Language Server */ export function startLanguageServer( - context: ExtensionContext, + context: vscode.ExtensionContext, extensionContext: DenoExtensionContext, ): Callback { return async () => { @@ -104,7 +99,7 @@ export function startLanguageServer( if (extensionContext.client) { await extensionContext.client.stop(); statusBarItem.hide(); - commands.executeCommand("setContext", ENABLEMENT_FLAG, false); + vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, false); } const client = extensionContext.client = new LanguageClient( LANGUAGE_CLIENT_ID, @@ -114,7 +109,7 @@ export function startLanguageServer( ); context.subscriptions.push(client.start()); await client.onReady(); - commands.executeCommand("setContext", ENABLEMENT_FLAG, true); + vscode.commands.executeCommand("setContext", ENABLEMENT_FLAG, true); const serverVersion = extensionContext.serverVersion = (client.initializeResult?.serverInfo?.version ?? "") .split( @@ -128,13 +123,13 @@ export function startLanguageServer( } export function showReferences( - _content: ExtensionContext, + _content: vscode.ExtensionContext, extensionContext: DenoExtensionContext, ): Callback { return (uri: string, position: Position, locations: Location[]) => { - commands.executeCommand( + vscode.commands.executeCommand( "editor.action.showReferences", - Uri.parse(uri), + vscode.Uri.parse(uri), extensionContext.client.protocol2CodeConverter.asPosition(position), locations.map(extensionContext.client.protocol2CodeConverter.asLocation), ); @@ -144,17 +139,61 @@ export function showReferences( /** Open and display the "virtual document" which provides the status of the * Deno Language Server. */ export function status( - _context: ExtensionContext, + _context: vscode.ExtensionContext, _extensionContext: DenoExtensionContext, ): Callback { return () => { - const uri = Uri.parse("deno:/status.md"); - return commands.executeCommand("markdown.showPreviewToSide", uri); + const uri = vscode.Uri.parse("deno:/status.md"); + return vscode.commands.executeCommand("markdown.showPreviewToSide", uri); + }; +} + +export function test( + _context: vscode.ExtensionContext, + _extensionContext: DenoExtensionContext, +): Callback { + return async (uriStr: string, name: string) => { + const uri = vscode.Uri.parse(uriStr, true); + const path = uri.fsPath; + const config = vscode.workspace.getConfiguration(EXTENSION_NS, uri); + const testArgs: string[] = [ + ...(config.get("codeLens.testArgs") ?? []), + ]; + if (config.get("unstable")) { + testArgs.push("--unstable"); + } + const args = ["test", ...testArgs, "--filter", name, path]; + + const definition: tasks.DenoTaskDefinition = { + type: tasks.TASK_TYPE, + command: "test", + args, + cwd: ".", + }; + + assert(vscode.workspace.workspaceFolders); + const target = vscode.workspace.workspaceFolders[0]; + const task = await tasks.buildDenoTask( + target, + definition, + `test "${name}"`, + args, + ["$deno-test"], + ); + + task.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: vscode.TaskPanelKind.Dedicated, + clear: true, + }; + task.group = vscode.TaskGroup.Test; + + return vscode.tasks.executeTask(task); }; } export function welcome( - context: ExtensionContext, + context: vscode.ExtensionContext, _extensionContext: DenoExtensionContext, ): Callback { return () => { diff --git a/client/src/content_provider.ts b/client/src/content_provider.ts index 17d4f528..e1297397 100644 --- a/client/src/content_provider.ts +++ b/client/src/content_provider.ts @@ -1,7 +1,7 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import type { DenoExtensionContext } from "./interfaces"; import { virtualTextDocument } from "./lsp_extensions"; +import type { DenoExtensionContext } from "./types"; import type { CancellationToken, diff --git a/client/src/debug_config_provider.ts b/client/src/debug_config_provider.ts index 642ffff9..2b42122f 100644 --- a/client/src/debug_config_provider.ts +++ b/client/src/debug_config_provider.ts @@ -1,5 +1,7 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +import type { Settings } from "./types"; import * as vscode from "vscode"; -import type { Settings } from "./interfaces"; export class DenoDebugConfigurationProvider implements vscode.DebugConfigurationProvider { diff --git a/client/src/extension.ts b/client/src/extension.ts index e1483a34..597b57c2 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -7,17 +7,18 @@ import { EXTENSION_TS_PLUGIN, TS_LANGUAGE_FEATURES_EXTENSION, } from "./constants"; -import type { - DenoExtensionContext, - Settings, - TsLanguageFeaturesApiV0, -} from "./interfaces"; import { DenoTextDocumentContentProvider, SCHEME } from "./content_provider"; import { DenoDebugConfigurationProvider } from "./debug_config_provider"; import { registryState } from "./lsp_extensions"; import { createRegistryStateHandler } from "./notification_handlers"; -import * as fs from "fs"; -import * as os from "os"; +import { activateTaskProvider } from "./tasks"; +import type { + DenoExtensionContext, + Settings, + TsLanguageFeaturesApiV0, +} from "./types"; +import { assert, getDenoCommand } from "./util"; + import * as path from "path"; import * as semver from "semver"; import * as vscode from "vscode"; @@ -38,13 +39,6 @@ interface TsLanguageFeatures { getAPI(version: 0): TsLanguageFeaturesApiV0 | undefined; } -/** Assert that the condition is "truthy", otherwise throw. */ -function assert(cond: unknown, msg = "Assertion failed."): asserts cond { - if (!cond) { - throw new Error(msg); - } -} - async function getTsApi(): Promise { const extension: vscode.Extension | undefined = vscode .extensions.getExtension(TS_LANGUAGE_FEATURES_EXTENSION); @@ -72,6 +66,7 @@ const workspaceSettingsKeys: Array = [ * a file or folder. */ const resourceSettingsKeys: Array = [ "enable", + "codeLens", ]; /** Convert a workspace configuration to `Settings` for a workspace. */ @@ -180,7 +175,7 @@ const extensionContext = {} as DenoExtensionContext; export async function activate( context: vscode.ExtensionContext, ): Promise { - const command = await getCommand(); + const command = await getDenoCommand(); const run: Executable = { command, args: ["lsp"], @@ -256,6 +251,9 @@ export async function activate( ), ); + // Activate the task provider. + context.subscriptions.push(activateTaskProvider()); + // Register any commands. const registerCommand = createRegisterCommand(context); registerCommand("cache", commands.cache); @@ -264,6 +262,7 @@ export async function activate( registerCommand("reloadImportRegistries", commands.reloadImportRegistries); registerCommand("showReferences", commands.showReferences); registerCommand("status", commands.status); + registerCommand("test", commands.test); registerCommand("welcome", commands.welcome); extensionContext.tsApi = await getTsApi(); @@ -341,84 +340,3 @@ function createRegisterCommand( ); }; } - -async function getCommand() { - let command = vscode.workspace.getConfiguration("deno").get("path"); - const workspaceFolders = vscode.workspace.workspaceFolders; - const defaultCommand = await getDefaultDenoCommand(); - if (!command || !workspaceFolders) { - command = command ?? defaultCommand; - } else if (!path.isAbsolute(command)) { - // if sent a relative path, iterate over workspace folders to try and resolve. - const list = []; - for (const workspace of workspaceFolders) { - const dir = path.resolve(workspace.uri.path, command); - try { - const stat = await fs.promises.stat(dir); - if (stat.isFile()) { - list.push(dir); - } - } catch { - // we simply don't push onto the array if we encounter an error - } - } - command = list.shift() ?? defaultCommand; - } - return command; -} - -function getDefaultDenoCommand() { - switch (os.platform()) { - case "win32": - return getDenoWindowsPath(); - default: - return Promise.resolve("deno"); - } - - async function getDenoWindowsPath() { - // Adapted from https://github.com/npm/node-which/blob/master/which.js - // Within vscode it will do `require("child_process").spawn("deno")`, - // which will prioritize "deno.exe" on the path instead of a possible - // higher precedence non-exe executable. This is a problem because, for - // example, version managers may have a `deno.bat` shim on the path. To - // ensure the resolution of the `deno` command matches what occurs on the - // command line, attempt to manually resolve the file path (issue #361). - const denoCmd = "deno"; - // deno-lint-ignore no-undef - const pathExtValue = process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM"; - // deno-lint-ignore no-undef - const pathValue = process.env.PATH ?? ""; - const pathExtItems = splitEnvValue(pathExtValue); - const pathFolderPaths = splitEnvValue(pathValue); - - for (const pathFolderPath of pathFolderPaths) { - for (const pathExtItem of pathExtItems) { - const cmdFilePath = path.join(pathFolderPath, denoCmd + pathExtItem); - if (await fileExists(cmdFilePath)) { - return cmdFilePath; - } - } - } - - // nothing found; return back command - return denoCmd; - - function splitEnvValue(value: string) { - return value - .split(";") - .map((item) => item.trim()) - .filter((item) => item.length > 0); - } - } - - function fileExists(executableFilePath: string): Promise { - return new Promise((resolve) => { - fs.stat(executableFilePath, (err, stat) => { - resolve(err == null && stat.isFile()); - }); - }).catch(() => { - // ignore all errors - return false; - }); - } -} diff --git a/client/src/tasks.ts b/client/src/tasks.ts new file mode 100644 index 00000000..51d609f1 --- /dev/null +++ b/client/src/tasks.ts @@ -0,0 +1,114 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +import { getDenoCommand } from "./util"; + +import * as vscode from "vscode"; + +export const TASK_TYPE = "deno"; +export const TASK_SOURCE = "deno"; + +export interface DenoTaskDefinition extends vscode.TaskDefinition { + command: string; + args?: string[]; + cwd?: string; + env?: Record; +} + +export async function buildDenoTask( + target: vscode.WorkspaceFolder, + definition: DenoTaskDefinition, + name: string, + args: string[], + problemMatchers: string[], +): Promise { + const exec = new vscode.ProcessExecution( + await getDenoCommand(), + args, + definition, + ); + + return new vscode.Task( + definition, + target, + name, + TASK_SOURCE, + exec, + problemMatchers, + ); +} + +function isWorkspaceFolder(value: unknown): value is vscode.WorkspaceFolder { + return typeof value === "object" && value != null && + (value as vscode.WorkspaceFolder).name !== undefined; +} + +class DenoTaskProvider implements vscode.TaskProvider { + async provideTasks(): Promise { + const defs = [ + { + command: "bundle", + group: vscode.TaskGroup.Build, + problemMatchers: ["$deno"], + }, + { + command: "cache", + group: vscode.TaskGroup.Build, + problemMatchers: ["$deno"], + }, + { + command: "compile", + group: vscode.TaskGroup.Build, + problemMatchers: ["$deno"], + }, + { + command: "lint", + group: vscode.TaskGroup.Test, + problemMatchers: ["$deno-lint"], + }, + { command: "run", group: undefined, problemMatchers: ["$deno"] }, + { + command: "test", + group: vscode.TaskGroup.Test, + problemMatchers: ["$deno-test"], + }, + ]; + + const tasks: vscode.Task[] = []; + for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) { + for (const { command, group, problemMatchers } of defs) { + const task = await buildDenoTask( + workspaceFolder, + { type: TASK_TYPE, command }, + command, + [command], + problemMatchers, + ); + task.group = group; + tasks.push(task); + } + } + return tasks; + } + + async resolveTask(task: vscode.Task): Promise { + const definition = task.definition as DenoTaskDefinition; + + if (definition.type === TASK_TYPE && definition.command) { + const args = [definition.command].concat(definition.args ?? []); + if (isWorkspaceFolder(task.scope)) { + return await buildDenoTask( + task.scope, + definition, + task.name, + args, + task.problemMatchers, + ); + } + } + } +} + +export function activateTaskProvider(): vscode.Disposable { + const provider = new DenoTaskProvider(); + return vscode.tasks.registerTaskProvider(TASK_TYPE, provider); +} diff --git a/client/src/interfaces.d.ts b/client/src/types.d.ts similarity index 98% rename from client/src/interfaces.d.ts rename to client/src/types.d.ts index 74006939..307a8bb6 100644 --- a/client/src/interfaces.d.ts +++ b/client/src/types.d.ts @@ -17,6 +17,8 @@ export interface Settings { implementations: boolean; references: boolean; referencesAllFunctions: boolean; + test: boolean; + testArgs: string[]; } | null; /** A path to a `tsconfig.json` that should be applied. */ config: string | null; diff --git a/client/src/util.ts b/client/src/util.ts new file mode 100644 index 00000000..c8724516 --- /dev/null +++ b/client/src/util.ts @@ -0,0 +1,99 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; + +/** Assert that the condition is "truthy", otherwise throw. */ +export function assert(cond: unknown, msg = "Assertion failed."): asserts cond { + if (!cond) { + throw new Error(msg); + } +} + +let memoizedCommand: string | undefined; + +export async function getDenoCommand(): Promise { + if (memoizedCommand !== undefined) { + return memoizedCommand; + } + let command = vscode.workspace.getConfiguration("deno").get("path"); + const workspaceFolders = vscode.workspace.workspaceFolders; + const defaultCommand = await getDefaultDenoCommand(); + if (!command || !workspaceFolders) { + command = command ?? defaultCommand; + } else if (!path.isAbsolute(command)) { + // if sent a relative path, iterate over workspace folders to try and resolve. + const list = []; + for (const workspace of workspaceFolders) { + const dir = path.resolve(workspace.uri.path, command); + try { + const stat = await fs.promises.stat(dir); + if (stat.isFile()) { + list.push(dir); + } + } catch { + // we simply don't push onto the array if we encounter an error + } + } + command = list.shift() ?? defaultCommand; + } + return memoizedCommand = command; +} + +function getDefaultDenoCommand() { + switch (os.platform()) { + case "win32": + return getDenoWindowsPath(); + default: + return Promise.resolve("deno"); + } + + async function getDenoWindowsPath() { + // Adapted from https://github.com/npm/node-which/blob/master/which.js + // Within vscode it will do `require("child_process").spawn("deno")`, + // which will prioritize "deno.exe" on the path instead of a possible + // higher precedence non-exe executable. This is a problem because, for + // example, version managers may have a `deno.bat` shim on the path. To + // ensure the resolution of the `deno` command matches what occurs on the + // command line, attempt to manually resolve the file path (issue #361). + const denoCmd = "deno"; + // deno-lint-ignore no-undef + const pathExtValue = process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM"; + // deno-lint-ignore no-undef + const pathValue = process.env.PATH ?? ""; + const pathExtItems = splitEnvValue(pathExtValue); + const pathFolderPaths = splitEnvValue(pathValue); + + for (const pathFolderPath of pathFolderPaths) { + for (const pathExtItem of pathExtItems) { + const cmdFilePath = path.join(pathFolderPath, denoCmd + pathExtItem); + if (await fileExists(cmdFilePath)) { + return cmdFilePath; + } + } + } + + // nothing found; return back command + return denoCmd; + + function splitEnvValue(value: string) { + return value + .split(";") + .map((item) => item.trim()) + .filter((item) => item.length > 0); + } + } + + function fileExists(executableFilePath: string): Promise { + return new Promise((resolve) => { + fs.stat(executableFilePath, (err, stat) => { + resolve(err == null && stat.isFile()); + }); + }).catch(() => { + // ignore all errors + return false; + }); + } +} diff --git a/client/src/welcome.ts b/client/src/welcome.ts index 3e7cbbe9..629b926b 100644 --- a/client/src/welcome.ts +++ b/client/src/welcome.ts @@ -1,8 +1,9 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import * as vscode from "vscode"; import { EXTENSION_ID } from "./constants"; +import * as vscode from "vscode"; + function getNonce() { let text = ""; const possible = diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 00000000..a4742e90 --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,49 @@ +# Tasks + +The extension provides several tasks for the Deno CLI which can be integrated +into a project. + +## Deno CLI Tasks + +The template for a Deno CLI task has the following interface, which can be +configured in a `tasks.json`: + +```ts +interface DenoTaskDefinition { + type: "deno"; + // This is the `deno` command to run (e.g. `run`, `test`, `cache`, etc.) + command: string; + // Additional arguments pass on the command line + args?: string[]; + // The current working directory to execute the command + cwd?: string; + // Any environment variables that should be set when executing + env?: Record; +} +``` + +There are several task templates provided to make it easy for you to configure +up your `tasks.json` which are available via the `Tasks: Configure Task` in the +command pallet. + +An example of a `deno run` command would look something like this in the +`tasks.json`: + +```json +{ + "version": "2.0.0", + "tasks": [ + { + "type": "deno", + "command": "run", + "args": [ + "mod.ts" + ], + "problemMatcher": [ + "$deno" + ], + "label": "deno: run" + } + ] +} +``` diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..f2409d07 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,25 @@ +# Testing + +## Code Lenses + +The extension identifies test cases using the integrated Deno test API and +provides a user interface via the vscode code lens API to allow you to run the +test individually from inside the IDE. + +When the extension detects a test in the code, it will display a `▶ Run Test` +code lens above the test: + +![a screenshot of a Deno.test showing the run test code lens](../screenshots/deno_test_code_lens.png "example of the test code lens") + +When this is activated, the extension will spawn the Deno CLI, instructing it to +run just that test. + +The code lenses are enabled by default. To disable them, set +`deno.codeLens.test` to `false` in your settings. + +Additional arguments, outside of just the module to test and the test filter, +are supplied when executing the Deno CLI. These are configured via +`deno.codeLens.testArgs`. They default to `[ "--allow-all" ]`. In addition, when +executing the test, the extension will reflect the `deno.unstable` setting in +the command line, meaning that if it is `true` then the `--unstable` flag will +be sent as an argument to the test command. diff --git a/docs/workspaceFolders.md b/docs/workspaceFolders.md index dfebccc9..6f5fa913 100644 --- a/docs/workspaceFolders.md +++ b/docs/workspaceFolders.md @@ -10,7 +10,7 @@ access to the per folder settings. If you look at the `.vscode/settings.json` in a folder, you will see a visual indication of what settings apply to folder, versus those that come from the workspace configuration: -![alt text](../screenshots/workspace_folder_config.png ".vscode/settings.json example") +![screenshot of the .vscode/setting.json configured as a workspace folder](../screenshots/workspace_folder_config.png ".vscode/settings.json example") ## Workspace Folder Settings diff --git a/package.json b/package.json index 9b542b1c..8612d186 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,23 @@ false ] }, + "deno.codeLens.test": { + "type": "boolean", + "default": true, + "markdownDescription": "Enables or disables the display of code lenses that allow running of individual tests in the code.", + "scope": "resource" + }, + "deno.codeLens.testArgs": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "--allow-all" + ], + "markdownDescription": "Additional arguments to use with the run test code lens. Defaults to `[ \"--allow-all\" ]`.", + "scope": "resource" + }, "deno.config": { "type": "string", "default": null, @@ -290,6 +307,127 @@ "url": "./schemas/deno-import-intellisense.schema.json" } ], + "problemPatterns": [ + { + "name": "deno", + "patterns": [ + { + "regexp": "^(warning|error): (?:(\\S+) \\[(?:ERROR|WARN)\\]: )?(.*)$", + "severity": 1, + "code": 2, + "message": 3 + }, + { + "regexp": "^\\s{4}at\\s.*((?:file|http|https|data|blob):[^:]+):(\\d+):(\\d+)$", + "file": 1, + "line": 2, + "column": 3 + } + ] + }, + { + "name": "deno-test", + "patterns": [ + { + "regexp": "^(\\S+:\\s.*)$", + "message": 1 + }, + { + "regexp": "^\\s{4}at\\s.*((?:file|http|https|data|blob):[^:]+):(\\d+):(\\d+)$", + "file": 1, + "line": 2, + "column": 3 + } + ] + }, + { + "name": "deno-lint", + "patterns": [ + { + "regexp": "^\\(([^)]*)\\)\\s(.*)$", + "code": 1, + "message": 2 + }, + { + "regexp": "^\\s{4}at\\s([^:]+):(\\d+):(\\d+)$", + "file": 1, + "line": 2, + "column": 3 + } + ] + } + ], + "problemMatchers": [ + { + "name": "deno", + "owner": "deno-cli", + "source": "deno-cli", + "fileLocation": [ + "absolute" + ], + "pattern": "$deno" + }, + { + "name": "deno-test", + "owner": "deno-test", + "source": "deno-test", + "fileLocation": [ + "absolute" + ], + "severity": "error", + "pattern": "$deno-test" + }, + { + "name": "deno-lint", + "owner": "deno-lint", + "source": "deno-lint", + "fileLocation": [ + "absolute" + ], + "severity": "warning", + "pattern": "$deno-lint" + } + ], + "taskDefinitions": [ + { + "type": "deno", + "required": [ + "command" + ], + "properties": { + "label": { + "type": "string" + }, + "command": { + "type": "string", + "description": "The Deno command to run.", + "examples": [ + "bundle", + "cache", + "compile", + "lint", + "test" + ] + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Arguments to pass to the command." + }, + "env": { + "type": "object", + "patternProperties": { + ".+": { + "type": "string" + } + }, + "description": "Environment variables to set when executing the command." + } + } + } + ], "typescriptServerPlugins": [ { "name": "typescript-deno-plugin", diff --git a/screenshots/deno_test_code_lens.png b/screenshots/deno_test_code_lens.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa76b80e452deedab36f509f47f99ee712bc8a0 GIT binary patch literal 12041 zcmZ{~1z40p*FOx1bR#L<(hX8l(kKecqG*lo1l)T{FevsbLRvR|Nxi1O!Y%_!kiYluZg_ z6$7ZP@1d`%B4Xv@#A#veVrj$aJS=E@oE)9qMSR5R9(#zu%J6C~ zI-18W9uDGk`l=c4_6T`E^lvd zPH#R=7dKljo|i9Qa&hx=@$z!OdT_Y=I(u07a5%fu{~hE%H5ADl`)lM#Uu0`+K6`yspI~H+8X$&Zkl-78dJ8Hrj=urN6TdeEFaJ? zSi?-UKX@R2X2@rHcec#8pbfZXn2TplE`1s^zBS08R4xVXn3K^ zjFR9&Z{K7ASQCt9oJkTamxX3R2=Dq{@`zydqO$4UWhA?sDCR(8tiFDx_olR7aGGO! zesR&*aicgBVqjWj2b`KpRQWq62w7!hG9$}fI%*`H$TpSu^Y$5?WsTGD)$8-R4Ps)7 z(0_ReZ5zrkZxJ(;9dE<9C17MHOM zHqqf3zW%mM;_VpYa!h(f@LW^~A`MRWDX(SE5(qm~zN21|q!Mtl(YgcQEb#U(Bhrii zDbQAW?e|^NK#?h`8=KOrYHGOx(!}%Pkc7EG(t9P}Spt9lMbU5GHGR>)drT^ftl|ak z?F!oywCg`sOpr|oe9wjF@JiF&ooeO#h-htn^RotFW5lTLD02&ouO#}sgMHMUU3c%MtWQi+rODRl$FH@2m(n&bS^mWrSpINYCpfu+;XC9gKFKESFkI! z`GhRSq6iYeHiT3XS*1dK@_HiQp=&*6JVA3MY(10vmdv;y+->KKv(8kRg-z{sWkIVf z?t@*05$Ge%=)J2p=OL{|S6g(&{zjYtT{J(9qn4tSo0AUDnG?XN;wmp3E+O1B=oyZ= zDC&gn_Ynpp>eP#K8qAXcjmOk|!MB&w@5DDZiS?h22MsM!ATixdzc3aVE4P=(x7 z8m=BUvY^Ey+iH&5L}=k0xt)wr5>q(;Dn<N&&OG#3kqTH_QEB1I0T@jRZ&Yk~3bl=*O9uWIM&gEGa;5XxR#v#u~+>^eFSPB;K|bh9BlD^kzRxMR7HIEcji`qb8tHpTwoc(20H=P7AqzGGl7}b1!UcN#q+^ z#8du1$N3s*e;I_4s&)ir+4FDq51i`GJRWt!b}o$mY;3D*YIox! zc(KF#$6^zoKv|>pNnpp}{mryqF1ZA|^c!{F$GKMsR*5%l3*4mfp`kVSIF6G95&@M8 z>}{Z9Bj2~pv`VptwH|ZH^7=!nI%yqZx7w=hh)=pzf}CXXFWpi|SH|1Udiy z__2MZGV@Q8&F`Zup}p>e_Qm?ZhQgrH?2?Vz!AvNz1FIqvTrZv3BF!8omQgGZUV0H{(ulZ4sGOzRm{cC9iuL%sJ8{+;j{LQnL0FncU%

vvY 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 D