npm: move debug script lens into npm, clean up parsing logic

This keeps our npm logic in one place and avoids activating the heavier-
weight `js-debug` extension on every .json file.

Also, use the same command to debug both from the explorer and
from script lens.

Fixes https://github.com/microsoft/vscode-js-debug/issues/782
This commit is contained in:
Connor Peet 2021-02-04 10:41:27 -08:00 committed by GitHub
parent 05b3f6f7ad
commit 5b2dc0cad2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 204 deletions

View File

@ -22,10 +22,7 @@ export function runSelectedScript(context: vscode.ExtensionContext) {
}
let document = editor.document;
let contents = document.getText();
let selection = editor.selection;
let offset = document.offsetAt(selection.anchor);
let script = findScriptAtPosition(contents, offset);
let script = findScriptAtPosition(editor.document, contents, editor.selection.anchor);
if (script) {
runScript(context, script, document);
} else {

View File

@ -10,6 +10,7 @@ import { runSelectedScript, selectAndRunScriptFromFolder } from './commands';
import { NpmScriptsTreeDataProvider } from './npmView';
import { getPackageManager, invalidateTasksCache, NpmTaskProvider } from './tasks';
import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover';
import { NpmScriptLensProvider } from './npmScriptLens';
let treeDataProvider: NpmScriptsTreeDataProvider | undefined;
@ -62,6 +63,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}
return '';
}));
context.subscriptions.push(new NpmScriptLensProvider());
}
function canRunNpmInCurrentWorkspace() {

View File

@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import {
CodeLens,
CodeLensProvider,
Disposable,
EventEmitter,
languages,
TextDocument,
Uri,
workspace
} from 'vscode';
import * as nls from 'vscode-nls';
import { findPreferredPM } from './preferred-pm';
import { readScripts } from './readScripts';
const localize = nls.loadMessageBundle();
const enum Constants {
ConfigKey = 'debug.javascript.codelens.npmScripts',
}
const getFreshLensLocation = () => workspace.getConfiguration().get(Constants.ConfigKey);
/**
* Npm script lens provider implementation. Can show a "Debug" text above any
* npm script, or the npm scripts section.
*/
export class NpmScriptLensProvider implements CodeLensProvider, Disposable {
private lensLocation = getFreshLensLocation();
private changeEmitter = new EventEmitter<void>();
private subscriptions: Disposable[] = [];
/**
* @inheritdoc
*/
public onDidChangeCodeLenses = this.changeEmitter.event;
constructor() {
this.subscriptions.push(
workspace.onDidChangeConfiguration(evt => {
if (evt.affectsConfiguration(Constants.ConfigKey)) {
this.lensLocation = getFreshLensLocation();
this.changeEmitter.fire();
}
}),
languages.registerCodeLensProvider(
{
language: 'json',
pattern: '**/package.json',
},
this,
)
);
}
/**
* @inheritdoc
*/
public async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
if (this.lensLocation === 'never') {
return [];
}
const tokens = readScripts(document);
if (!tokens) {
return [];
}
const title = localize('codelens.debug', '{0} Debug', '$(debug-start)');
const cwd = path.dirname(document.uri.fsPath);
if (this.lensLocation === 'top') {
return [
new CodeLens(
tokens.location.range,
{
title,
command: 'extension.js-debug.npmScript',
arguments: [cwd],
},
),
];
}
if (this.lensLocation === 'all') {
const packageManager = await findPreferredPM(Uri.joinPath(document.uri, '..').fsPath);
return tokens.scripts.map(
({ name, nameRange }) =>
new CodeLens(
nameRange,
{
title,
command: 'extension.js-debug.createDebuggerTerminal',
arguments: [`${packageManager.name} run ${name}`, workspace.getWorkspaceFolder(document.uri), { cwd }],
},
),
);
}
return [];
}
/**
* @inheritdoc
*/
public dispose() {
this.subscriptions.forEach(s => s.dispose());
}
}

View File

@ -3,21 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JSONVisitor, visit } from 'jsonc-parser';
import * as path from 'path';
import {
commands, Event, EventEmitter, ExtensionContext,
Range,
Selection, Task,
TaskGroup, tasks, TextDocument, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemLabel, TreeItemCollapsibleState, Uri,
window, workspace, WorkspaceFolder
window, workspace, WorkspaceFolder, Position, Location
} from 'vscode';
import * as nls from 'vscode-nls';
import { readScripts } from './readScripts';
import {
createTask, getPackageManager, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition,
NpmTaskProvider,
startDebugging,
TaskLocation,
TaskWithLocation
} from './tasks';
@ -78,7 +77,7 @@ class NpmScript extends TreeItem {
task: Task;
package: PackageJSON;
constructor(_context: ExtensionContext, packageJson: PackageJSON, task: Task, public taskLocation?: TaskLocation) {
constructor(_context: ExtensionContext, packageJson: PackageJSON, task: Task, public taskLocation?: Location) {
super(task.name, TreeItemCollapsibleState.None);
const command: ExplorerCommands = workspace.getConfiguration('npm').get<ExplorerCommands>('scriptExplorerAction') || 'open';
@ -87,9 +86,9 @@ class NpmScript extends TreeItem {
title: 'Edit Script',
command: 'vscode.open',
arguments: [
taskLocation?.document,
taskLocation?.uri,
taskLocation ? <TextDocumentShowOptions>{
selection: new Range(taskLocation.line, taskLocation.line)
selection: new Range(taskLocation.range.start, taskLocation.range.start)
} : undefined
]
},
@ -153,37 +152,18 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
startDebugging(this.extensionContext, script.task.definition.script, path.dirname(script.package.resourceUri!.fsPath), script.getFolder());
}
private findScript(document: TextDocument, script?: NpmScript): number {
let scriptOffset = 0;
let inScripts = false;
private findScriptPosition(document: TextDocument, script?: NpmScript) {
const scripts = readScripts(document);
if (!scripts) {
return undefined;
}
let visitor: JSONVisitor = {
onError() {
return scriptOffset;
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onObjectProperty(property: string, offset: number, _length: number) {
if (property === 'scripts') {
inScripts = true;
if (!script) { // select the script section
scriptOffset = offset;
}
}
else if (inScripts && script) {
let label = getTaskName(property, script.task.definition.path);
if (script.task.name === label) {
scriptOffset = offset;
}
}
}
};
visit(document.getText(), visitor);
return scriptOffset;
if (!script) {
return scripts.location.range.start;
}
const found = scripts.scripts.find(s => getTaskName(s.name, script.task.definition.path) === script.task.name);
return found?.nameRange.start;
}
private async runInstall(selection: PackageJSON) {
@ -209,8 +189,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
return;
}
let document: TextDocument = await workspace.openTextDocument(uri);
let offset = this.findScript(document, selection instanceof NpmScript ? selection : undefined);
let position = document.positionAt(offset);
let position = this.findScriptPosition(document, selection instanceof NpmScript ? selection : undefined) || new Position(0, 0);
await window.showTextDocument(document, { preserveFocus: true, selection: new Selection(position, position) });
}

View File

@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JSONVisitor, visit } from 'jsonc-parser';
import { Location, Position, Range, TextDocument } from 'vscode';
export interface INpmScriptReference {
name: string;
value: string;
nameRange: Range;
valueRange: Range;
}
export interface INpmScriptInfo {
location: Location;
scripts: INpmScriptReference[];
}
export const readScripts = (document: TextDocument, buffer = document.getText()): INpmScriptInfo | undefined => {
let start: Position | undefined;
let end: Position | undefined;
let inScripts = false;
let buildingScript: { name: string; nameRange: Range } | void;
let level = 0;
const scripts: INpmScriptReference[] = [];
const visitor: JSONVisitor = {
onError() {
// no-op
},
onObjectBegin() {
level++;
},
onObjectEnd(offset) {
if (inScripts) {
end = document.positionAt(offset);
inScripts = false;
}
level--;
},
onLiteralValue(value: unknown, offset: number, length: number) {
if (buildingScript && typeof value === 'string') {
scripts.push({
...buildingScript,
value,
valueRange: new Range(document.positionAt(offset), document.positionAt(offset + length)),
});
buildingScript = undefined;
}
},
onObjectProperty(property: string, offset: number, length: number) {
if (level === 1 && property === 'scripts') {
inScripts = true;
start = document.positionAt(offset);
} else if (inScripts) {
buildingScript = {
name: property,
nameRange: new Range(document.positionAt(offset), document.positionAt(offset + length))
};
}
},
};
visit(buffer, visitor);
if (start === undefined) {
return undefined;
}
return { location: new Location(document.uri, new Range(start, end ?? start)), scripts };
};

View File

@ -3,20 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ExtensionContext, TextDocument, commands, ProviderResult, CancellationToken,
workspace, tasks, Range, HoverProvider, Hover, Position, MarkdownString, Uri
} from 'vscode';
import {
createTask, startDebugging, findAllScriptRanges, getPackageManager
} from './tasks';
import * as nls from 'vscode-nls';
import { dirname } from 'path';
import {
CancellationToken, commands, ExtensionContext,
Hover, HoverProvider, MarkdownString, Position, ProviderResult,
tasks, TextDocument,
Uri, workspace
} from 'vscode';
import * as nls from 'vscode-nls';
import { INpmScriptInfo, readScripts } from './readScripts';
import {
createTask,
getPackageManager, startDebugging
} from './tasks';
const localize = nls.loadMessageBundle();
let cachedDocument: Uri | undefined = undefined;
let cachedScriptsMap: Map<string, [number, number, string]> | undefined = undefined;
let cachedScripts: INpmScriptInfo | undefined = undefined;
export function invalidateHoverScriptsCache(document?: TextDocument) {
if (!document) {
@ -42,20 +46,16 @@ export class NpmScriptHoverProvider implements HoverProvider {
let hover: Hover | undefined = undefined;
if (!cachedDocument || cachedDocument.fsPath !== document.uri.fsPath) {
cachedScriptsMap = findAllScriptRanges(document.getText());
cachedScripts = readScripts(document);
cachedDocument = document.uri;
}
cachedScriptsMap!.forEach((value, key) => {
let start = document.positionAt(value[0]);
let end = document.positionAt(value[0] + value[1]);
let range = new Range(start, end);
if (range.contains(position)) {
cachedScripts?.scripts.forEach(({ name, nameRange }) => {
if (nameRange.contains(position)) {
let contents: MarkdownString = new MarkdownString();
contents.isTrusted = true;
contents.appendMarkdown(this.createRunScriptMarkdown(key, document.uri));
contents.appendMarkdown(this.createDebugScriptMarkdown(key, document.uri));
contents.appendMarkdown(this.createRunScriptMarkdown(name, document.uri));
contents.appendMarkdown(this.createDebugScriptMarkdown(name, document.uri));
hover = new Hover(contents);
}
});

View File

@ -5,15 +5,15 @@
import {
TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace,
DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position, ExtensionContext, env,
ShellQuotedString, ShellQuoting
TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position, ExtensionContext, env,
ShellQuotedString, ShellQuoting, commands, Location
} from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as minimatch from 'minimatch';
import * as nls from 'vscode-nls';
import { JSONVisitor, visit, ParseErrorCode } from 'jsonc-parser';
import { findPreferredPM } from './preferred-pm';
import { readScripts } from './readScripts';
const localize = nls.loadMessageBundle();
@ -40,7 +40,7 @@ export interface TaskLocation {
export interface TaskWithLocation {
task: Task,
location?: TaskLocation
location?: Location
}
export class NpmTaskProvider implements TaskProvider {
@ -280,24 +280,24 @@ async function provideNpmScriptsForFolder(context: ExtensionContext, packageJson
const prePostScripts = getPrePostScripts(scripts);
const packageManager = await getPackageManager(context, folder.uri, showWarning);
for (const each of scripts.keys()) {
const scriptValue = scripts.get(each)!;
const task = await createTask(packageManager, each, ['run', each], folder!, packageJsonUri, scriptValue.script);
const lowerCaseTaskName = each.toLowerCase();
for (const { name, value, nameRange } of scripts.scripts) {
const task = await createTask(packageManager, name, ['run', name], folder!, packageJsonUri, value);
const lowerCaseTaskName = name.toLowerCase();
if (isBuildTask(lowerCaseTaskName)) {
task.group = TaskGroup.Build;
} else if (isTestTask(lowerCaseTaskName)) {
task.group = TaskGroup.Test;
}
if (prePostScripts.has(each)) {
if (prePostScripts.has(name)) {
task.group = TaskGroup.Clean; // hack: use Clean group to tag pre/post scripts
}
// todo@connor4312: all scripts are now debuggable, what is a 'debug script'?
if (isDebugScript(scriptValue.script)) {
if (isDebugScript(value)) {
task.group = TaskGroup.Rebuild; // hack: use Rebuild group to tag debug scripts
}
result.push({ task, location: scriptValue.location });
result.push({ task, location: new Location(packageJsonUri, nameRange) });
}
// always add npm install (without a problem matcher)
@ -398,145 +398,33 @@ export async function runScript(context: ExtensionContext, script: string, docum
}
export async function startDebugging(context: ExtensionContext, scriptName: string, cwd: string, folder: WorkspaceFolder) {
const config: DebugConfiguration = {
type: 'pwa-node',
request: 'launch',
name: `Debug ${scriptName}`,
cwd,
runtimeExecutable: await getPackageManager(context, folder.uri),
runtimeArgs: [
'run',
scriptName,
],
};
if (folder) {
debug.startDebugging(folder, config);
}
commands.executeCommand(
'extension.js-debug.createDebuggerTerminal',
`${await getPackageManager(context, folder.uri)} run ${scriptName}`,
folder,
{ cwd },
);
}
export type StringMap = { [s: string]: string; };
async function findAllScripts(document: TextDocument, buffer: string): Promise<Map<string, { script: string, location: TaskLocation }>> {
let scripts: Map<string, { script: string, location: TaskLocation }> = new Map();
let script: string | undefined = undefined;
let inScripts = false;
let scriptOffset = 0;
export function findScriptAtPosition(document: TextDocument, buffer: string, position: Position): string | undefined {
const read = readScripts(document, buffer);
if (!read) {
return undefined;
}
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
console.log(_error);
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onLiteralValue(value: any, _offset: number, _length: number) {
if (script) {
if (typeof value === 'string') {
scripts.set(script, { script: value, location: { document: document.uri, line: document.positionAt(scriptOffset) } });
}
script = undefined;
}
},
onObjectProperty(property: string, offset: number, _length: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts && !script) {
script = property;
scriptOffset = offset;
} else { // nested object which is invalid, ignore the script
script = undefined;
}
for (const script of read.scripts) {
if (script.nameRange.start.isBeforeOrEqual(position) && script.valueRange.end.isAfterOrEqual(position)) {
return script.name;
}
};
visit(buffer, visitor);
return scripts;
}
return undefined;
}
export function findAllScriptRanges(buffer: string): Map<string, [number, number, string]> {
let scripts: Map<string, [number, number, string]> = new Map();
let script: string | undefined = undefined;
let offset: number;
let length: number;
let inScripts = false;
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onLiteralValue(value: any, _offset: number, _length: number) {
if (script) {
scripts.set(script, [offset, length, value]);
script = undefined;
}
},
onObjectProperty(property: string, off: number, len: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts) {
script = property;
offset = off;
length = len;
}
}
};
visit(buffer, visitor);
return scripts;
}
export function findScriptAtPosition(buffer: string, offset: number): string | undefined {
let script: string | undefined = undefined;
let foundScript: string | undefined = undefined;
let inScripts = false;
let scriptStart: number | undefined;
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
scriptStart = undefined;
}
},
onLiteralValue(value: any, nodeOffset: number, nodeLength: number) {
if (inScripts && scriptStart) {
if (typeof value === 'string' && offset >= scriptStart && offset < nodeOffset + nodeLength) {
// found the script
inScripts = false;
foundScript = script;
} else {
script = undefined;
}
}
},
onObjectProperty(property: string, nodeOffset: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts) {
scriptStart = nodeOffset;
script = property;
} else { // nested object which is invalid, ignore the script
script = undefined;
}
}
};
visit(buffer, visitor);
return foundScript;
}
export async function getScripts(packageJsonUri: Uri): Promise<Map<string, { script: string, location: TaskLocation }> | undefined> {
export async function getScripts(packageJsonUri: Uri) {
if (packageJsonUri.scheme !== 'file') {
return undefined;
}
@ -548,9 +436,7 @@ export async function getScripts(packageJsonUri: Uri): Promise<Map<string, { scr
try {
const document: TextDocument = await workspace.openTextDocument(packageJsonUri);
let contents = document.getText();
let json = findAllScripts(document, contents);//JSON.parse(contents);
return json;
return readScripts(document);
} catch (e) {
let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJsonUri.fsPath);
throw new Error(localizedParseError);

View File

@ -91,7 +91,7 @@
},
{
"name": "ms-vscode.js-debug",
"version": "1.53.0",
"version": "1.54.0",
"repo": "https://github.com/microsoft/vscode-js-debug",
"metadata": {
"id": "25629058-ddac-4e17-abba-74678e126c5d",