Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
68
lib/vscode/extensions/npm/src/commands.ts
Normal file
68
lib/vscode/extensions/npm/src/commands.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {
|
||||
detectNpmScriptsForFolder,
|
||||
findScriptAtPosition,
|
||||
runScript,
|
||||
FolderTaskItem
|
||||
} from './tasks';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export function runSelectedScript() {
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
let document = editor.document;
|
||||
let contents = document.getText();
|
||||
let selection = editor.selection;
|
||||
let offset = document.offsetAt(selection.anchor);
|
||||
|
||||
let script = findScriptAtPosition(contents, offset);
|
||||
if (script) {
|
||||
runScript(script, document);
|
||||
} else {
|
||||
let message = localize('noScriptFound', 'Could not find a valid npm script at the selection.');
|
||||
vscode.window.showErrorMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectAndRunScriptFromFolder(selectedFolder: vscode.Uri) {
|
||||
let taskList: FolderTaskItem[] = await detectNpmScriptsForFolder(selectedFolder);
|
||||
|
||||
if (taskList && taskList.length > 0) {
|
||||
const quickPick = vscode.window.createQuickPick<FolderTaskItem>();
|
||||
quickPick.title = 'Run NPM script in Folder';
|
||||
quickPick.placeholder = 'Select an npm script';
|
||||
quickPick.items = taskList;
|
||||
|
||||
const toDispose: vscode.Disposable[] = [];
|
||||
|
||||
let pickPromise = new Promise<FolderTaskItem | undefined>((c) => {
|
||||
toDispose.push(quickPick.onDidAccept(() => {
|
||||
toDispose.forEach(d => d.dispose());
|
||||
c(quickPick.selectedItems[0]);
|
||||
}));
|
||||
toDispose.push(quickPick.onDidHide(() => {
|
||||
toDispose.forEach(d => d.dispose());
|
||||
c(undefined);
|
||||
}));
|
||||
});
|
||||
quickPick.show();
|
||||
let result = await pickPromise;
|
||||
quickPick.dispose();
|
||||
if (result) {
|
||||
vscode.tasks.executeTask(result.task);
|
||||
}
|
||||
}
|
||||
else {
|
||||
vscode.window.showInformationMessage(`No npm scripts found in ${selectedFolder.fsPath}`, { modal: true });
|
||||
}
|
||||
}
|
204
lib/vscode/extensions/npm/src/features/bowerJSONContribution.ts
Normal file
204
lib/vscode/extensions/npm/src/features/bowerJSONContribution.ts
Normal file
@ -0,0 +1,204 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MarkdownString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace } from 'vscode';
|
||||
import { IJSONContribution, ISuggestionsCollector } from './jsonContributions';
|
||||
import { XHRRequest } from 'request-light';
|
||||
import { Location } from 'jsonc-parser';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const USER_AGENT = 'Visual Studio Code';
|
||||
|
||||
export class BowerJSONContribution implements IJSONContribution {
|
||||
|
||||
private topRanked = ['twitter', 'bootstrap', 'angular-1.1.6', 'angular-latest', 'angulerjs', 'd3', 'myjquery', 'jq', 'abcdef1234567890', 'jQuery', 'jquery-1.11.1', 'jquery',
|
||||
'sushi-vanilla-x-data', 'font-awsome', 'Font-Awesome', 'font-awesome', 'fontawesome', 'html5-boilerplate', 'impress.js', 'homebrew',
|
||||
'backbone', 'moment1', 'momentjs', 'moment', 'linux', 'animate.css', 'animate-css', 'reveal.js', 'jquery-file-upload', 'blueimp-file-upload', 'threejs', 'express', 'chosen',
|
||||
'normalize-css', 'normalize.css', 'semantic', 'semantic-ui', 'Semantic-UI', 'modernizr', 'underscore', 'underscore1',
|
||||
'material-design-icons', 'ionic', 'chartjs', 'Chart.js', 'nnnick-chartjs', 'select2-ng', 'select2-dist', 'phantom', 'skrollr', 'scrollr', 'less.js', 'leancss', 'parser-lib',
|
||||
'hui', 'bootstrap-languages', 'async', 'gulp', 'jquery-pjax', 'coffeescript', 'hammer.js', 'ace', 'leaflet', 'jquery-mobile', 'sweetalert', 'typeahead.js', 'soup', 'typehead.js',
|
||||
'sails', 'codeigniter2'];
|
||||
|
||||
private xhr: XHRRequest;
|
||||
|
||||
public constructor(xhr: XHRRequest) {
|
||||
this.xhr = xhr;
|
||||
}
|
||||
|
||||
public getDocumentSelector(): DocumentSelector {
|
||||
return [{ language: 'json', scheme: '*', pattern: '**/bower.json' }, { language: 'json', scheme: '*', pattern: '**/.bower.json' }];
|
||||
}
|
||||
|
||||
private isEnabled() {
|
||||
return !!workspace.getConfiguration('npm').get('fetchOnlinePackageInfo');
|
||||
}
|
||||
|
||||
public collectDefaultSuggestions(_resource: string, collector: ISuggestionsCollector): Thenable<any> {
|
||||
const defaultValue = {
|
||||
'name': '${1:name}',
|
||||
'description': '${2:description}',
|
||||
'authors': ['${3:author}'],
|
||||
'version': '${4:1.0.0}',
|
||||
'main': '${5:pathToMain}',
|
||||
'dependencies': {}
|
||||
};
|
||||
const proposal = new CompletionItem(localize('json.bower.default', 'Default bower.json'));
|
||||
proposal.kind = CompletionItemKind.Class;
|
||||
proposal.insertText = new SnippetString(JSON.stringify(defaultValue, null, '\t'));
|
||||
collector.add(proposal);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public collectPropertySuggestions(_resource: string, location: Location, currentWord: string, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector): Thenable<any> | null {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if ((location.matches(['dependencies']) || location.matches(['devDependencies']))) {
|
||||
if (currentWord.length > 0) {
|
||||
const queryUrl = 'https://registry.bower.io/packages/search/' + encodeURIComponent(currentWord);
|
||||
|
||||
return this.xhr({
|
||||
url: queryUrl,
|
||||
agent: USER_AGENT
|
||||
}).then((success) => {
|
||||
if (success.status === 200) {
|
||||
try {
|
||||
const obj = JSON.parse(success.responseText);
|
||||
if (Array.isArray(obj)) {
|
||||
const results = <{ name: string; description: string; }[]>obj;
|
||||
for (const result of results) {
|
||||
const name = result.name;
|
||||
const description = result.description || '';
|
||||
const insertText = new SnippetString().appendText(JSON.stringify(name));
|
||||
if (addValue) {
|
||||
insertText.appendText(': ').appendPlaceholder('latest');
|
||||
if (!isLast) {
|
||||
insertText.appendText(',');
|
||||
}
|
||||
}
|
||||
const proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = insertText;
|
||||
proposal.filterText = JSON.stringify(name);
|
||||
proposal.documentation = description;
|
||||
collector.add(proposal);
|
||||
}
|
||||
collector.setAsIncomplete();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
collector.error(localize('json.bower.error.repoaccess', 'Request to the bower repository failed: {0}', success.responseText));
|
||||
return 0;
|
||||
}
|
||||
return undefined;
|
||||
}, (error) => {
|
||||
collector.error(localize('json.bower.error.repoaccess', 'Request to the bower repository failed: {0}', error.responseText));
|
||||
return 0;
|
||||
});
|
||||
} else {
|
||||
this.topRanked.forEach((name) => {
|
||||
const insertText = new SnippetString().appendText(JSON.stringify(name));
|
||||
if (addValue) {
|
||||
insertText.appendText(': ').appendPlaceholder('latest');
|
||||
if (!isLast) {
|
||||
insertText.appendText(',');
|
||||
}
|
||||
}
|
||||
|
||||
const proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = insertText;
|
||||
proposal.filterText = JSON.stringify(name);
|
||||
proposal.documentation = '';
|
||||
collector.add(proposal);
|
||||
});
|
||||
collector.setAsIncomplete();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public collectValueSuggestions(_resource: string, location: Location, collector: ISuggestionsCollector): Promise<any> | null {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) {
|
||||
// not implemented. Could be do done calling the bower command. Waiting for web API: https://github.com/bower/registry/issues/26
|
||||
const proposal = new CompletionItem(localize('json.bower.latest.version', 'latest'));
|
||||
proposal.insertText = new SnippetString('"${1:latest}"');
|
||||
proposal.filterText = '""';
|
||||
proposal.kind = CompletionItemKind.Value;
|
||||
proposal.documentation = 'The latest version of the package';
|
||||
collector.add(proposal);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public resolveSuggestion(item: CompletionItem): Thenable<CompletionItem | null> | null {
|
||||
if (item.kind === CompletionItemKind.Property && item.documentation === '') {
|
||||
return this.getInfo(item.label).then(documentation => {
|
||||
if (documentation) {
|
||||
item.documentation = documentation;
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getInfo(pack: string): Thenable<string | undefined> {
|
||||
const queryUrl = 'https://registry.bower.io/packages/' + encodeURIComponent(pack);
|
||||
|
||||
return this.xhr({
|
||||
url: queryUrl,
|
||||
agent: USER_AGENT
|
||||
}).then((success) => {
|
||||
try {
|
||||
const obj = JSON.parse(success.responseText);
|
||||
if (obj && obj.url) {
|
||||
let url: string = obj.url;
|
||||
if (url.indexOf('git://') === 0) {
|
||||
url = url.substring(6);
|
||||
}
|
||||
if (url.length >= 4 && url.substr(url.length - 4) === '.git') {
|
||||
url = url.substring(0, url.length - 4);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return undefined;
|
||||
}, () => {
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public getInfoContribution(_resource: string, location: Location): Thenable<MarkdownString[] | null> | null {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']))) {
|
||||
const pack = location.path[location.path.length - 1];
|
||||
if (typeof pack === 'string') {
|
||||
return this.getInfo(pack).then(documentation => {
|
||||
if (documentation) {
|
||||
const str = new MarkdownString();
|
||||
str.appendText(documentation);
|
||||
return [str];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
172
lib/vscode/extensions/npm/src/features/jsonContributions.ts
Normal file
172
lib/vscode/extensions/npm/src/features/jsonContributions.ts
Normal file
@ -0,0 +1,172 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Location, getLocation, createScanner, SyntaxKind, ScanError, JSONScanner } from 'jsonc-parser';
|
||||
import { basename } from 'path';
|
||||
import { BowerJSONContribution } from './bowerJSONContribution';
|
||||
import { PackageJSONContribution } from './packageJSONContribution';
|
||||
import { XHRRequest } from 'request-light';
|
||||
|
||||
import {
|
||||
CompletionItem, CompletionItemProvider, CompletionList, TextDocument, Position, Hover, HoverProvider,
|
||||
CancellationToken, Range, MarkedString, DocumentSelector, languages, Disposable
|
||||
} from 'vscode';
|
||||
|
||||
export interface ISuggestionsCollector {
|
||||
add(suggestion: CompletionItem): void;
|
||||
error(message: string): void;
|
||||
log(message: string): void;
|
||||
setAsIncomplete(): void;
|
||||
}
|
||||
|
||||
export interface IJSONContribution {
|
||||
getDocumentSelector(): DocumentSelector;
|
||||
getInfoContribution(fileName: string, location: Location): Thenable<MarkedString[] | null> | null;
|
||||
collectPropertySuggestions(fileName: string, location: Location, currentWord: string, addValue: boolean, isLast: boolean, result: ISuggestionsCollector): Thenable<any> | null;
|
||||
collectValueSuggestions(fileName: string, location: Location, result: ISuggestionsCollector): Thenable<any> | null;
|
||||
collectDefaultSuggestions(fileName: string, result: ISuggestionsCollector): Thenable<any>;
|
||||
resolveSuggestion?(item: CompletionItem): Thenable<CompletionItem | null> | null;
|
||||
}
|
||||
|
||||
export function addJSONProviders(xhr: XHRRequest, canRunNPM: boolean): Disposable {
|
||||
const contributions = [new PackageJSONContribution(xhr, canRunNPM), new BowerJSONContribution(xhr)];
|
||||
const subscriptions: Disposable[] = [];
|
||||
contributions.forEach(contribution => {
|
||||
const selector = contribution.getDocumentSelector();
|
||||
subscriptions.push(languages.registerCompletionItemProvider(selector, new JSONCompletionItemProvider(contribution), '"', ':'));
|
||||
subscriptions.push(languages.registerHoverProvider(selector, new JSONHoverProvider(contribution)));
|
||||
});
|
||||
return Disposable.from(...subscriptions);
|
||||
}
|
||||
|
||||
export class JSONHoverProvider implements HoverProvider {
|
||||
|
||||
constructor(private jsonContribution: IJSONContribution) {
|
||||
}
|
||||
|
||||
public provideHover(document: TextDocument, position: Position, _token: CancellationToken): Thenable<Hover> | null {
|
||||
const fileName = basename(document.fileName);
|
||||
const offset = document.offsetAt(position);
|
||||
const location = getLocation(document.getText(), offset);
|
||||
if (!location.previousNode) {
|
||||
return null;
|
||||
}
|
||||
const node = location.previousNode;
|
||||
if (node && node.offset <= offset && offset <= node.offset + node.length) {
|
||||
const promise = this.jsonContribution.getInfoContribution(fileName, location);
|
||||
if (promise) {
|
||||
return promise.then(htmlContent => {
|
||||
const range = new Range(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
|
||||
const result: Hover = {
|
||||
contents: htmlContent || [],
|
||||
range: range
|
||||
};
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class JSONCompletionItemProvider implements CompletionItemProvider {
|
||||
|
||||
constructor(private jsonContribution: IJSONContribution) {
|
||||
}
|
||||
|
||||
public resolveCompletionItem(item: CompletionItem, _token: CancellationToken): Thenable<CompletionItem | null> {
|
||||
if (this.jsonContribution.resolveSuggestion) {
|
||||
const resolver = this.jsonContribution.resolveSuggestion(item);
|
||||
if (resolver) {
|
||||
return resolver;
|
||||
}
|
||||
}
|
||||
return Promise.resolve(item);
|
||||
}
|
||||
|
||||
public provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken): Thenable<CompletionList | null> | null {
|
||||
|
||||
const fileName = basename(document.fileName);
|
||||
|
||||
const currentWord = this.getCurrentWord(document, position);
|
||||
let overwriteRange: Range;
|
||||
|
||||
const items: CompletionItem[] = [];
|
||||
let isIncomplete = false;
|
||||
|
||||
const offset = document.offsetAt(position);
|
||||
const location = getLocation(document.getText(), offset);
|
||||
|
||||
const node = location.previousNode;
|
||||
if (node && node.offset <= offset && offset <= node.offset + node.length && (node.type === 'property' || node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) {
|
||||
overwriteRange = new Range(document.positionAt(node.offset), document.positionAt(node.offset + node.length));
|
||||
} else {
|
||||
overwriteRange = new Range(document.positionAt(offset - currentWord.length), position);
|
||||
}
|
||||
|
||||
const proposed: { [key: string]: boolean } = {};
|
||||
const collector: ISuggestionsCollector = {
|
||||
add: (suggestion: CompletionItem) => {
|
||||
if (!proposed[suggestion.label]) {
|
||||
proposed[suggestion.label] = true;
|
||||
suggestion.range = { replacing: overwriteRange, inserting: new Range(overwriteRange.start, overwriteRange.start) };
|
||||
items.push(suggestion);
|
||||
}
|
||||
},
|
||||
setAsIncomplete: () => isIncomplete = true,
|
||||
error: (message: string) => console.error(message),
|
||||
log: (message: string) => console.log(message)
|
||||
};
|
||||
|
||||
let collectPromise: Thenable<any> | null = null;
|
||||
|
||||
if (location.isAtPropertyKey) {
|
||||
const scanner = createScanner(document.getText(), true);
|
||||
const addValue = !location.previousNode || !this.hasColonAfter(scanner, location.previousNode.offset + location.previousNode.length);
|
||||
const isLast = this.isLast(scanner, document.offsetAt(position));
|
||||
collectPromise = this.jsonContribution.collectPropertySuggestions(fileName, location, currentWord, addValue, isLast, collector);
|
||||
} else {
|
||||
if (location.path.length === 0) {
|
||||
collectPromise = this.jsonContribution.collectDefaultSuggestions(fileName, collector);
|
||||
} else {
|
||||
collectPromise = this.jsonContribution.collectValueSuggestions(fileName, location, collector);
|
||||
}
|
||||
}
|
||||
if (collectPromise) {
|
||||
return collectPromise.then(() => {
|
||||
if (items.length > 0) {
|
||||
return new CompletionList(items, isIncomplete);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getCurrentWord(document: TextDocument, position: Position) {
|
||||
let i = position.character - 1;
|
||||
const text = document.lineAt(position.line).text;
|
||||
while (i >= 0 && ' \t\n\r\v":{[,'.indexOf(text.charAt(i)) === -1) {
|
||||
i--;
|
||||
}
|
||||
return text.substring(i + 1, position.character);
|
||||
}
|
||||
|
||||
private isLast(scanner: JSONScanner, offset: number): boolean {
|
||||
scanner.setPosition(offset);
|
||||
let nextToken = scanner.scan();
|
||||
if (nextToken === SyntaxKind.StringLiteral && scanner.getTokenError() === ScanError.UnexpectedEndOfString) {
|
||||
nextToken = scanner.scan();
|
||||
}
|
||||
return nextToken === SyntaxKind.CloseBraceToken || nextToken === SyntaxKind.EOF;
|
||||
}
|
||||
private hasColonAfter(scanner: JSONScanner, offset: number): boolean {
|
||||
scanner.setPosition(offset);
|
||||
return scanner.scan() === SyntaxKind.ColonToken;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const xhrDisabled = () => Promise.reject({ responseText: 'Use of online resources is disabled.' });
|
@ -0,0 +1,383 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MarkedString, CompletionItemKind, CompletionItem, DocumentSelector, SnippetString, workspace, MarkdownString } from 'vscode';
|
||||
import { IJSONContribution, ISuggestionsCollector } from './jsonContributions';
|
||||
import { XHRRequest } from 'request-light';
|
||||
import { Location } from 'jsonc-parser';
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const LIMIT = 40;
|
||||
const SCOPED_LIMIT = 250;
|
||||
|
||||
const USER_AGENT = 'Visual Studio Code';
|
||||
|
||||
export class PackageJSONContribution implements IJSONContribution {
|
||||
|
||||
private mostDependedOn = ['lodash', 'async', 'underscore', 'request', 'commander', 'express', 'debug', 'chalk', 'colors', 'q', 'coffee-script',
|
||||
'mkdirp', 'optimist', 'through2', 'yeoman-generator', 'moment', 'bluebird', 'glob', 'gulp-util', 'minimist', 'cheerio', 'pug', 'redis', 'node-uuid',
|
||||
'socket', 'io', 'uglify-js', 'winston', 'through', 'fs-extra', 'handlebars', 'body-parser', 'rimraf', 'mime', 'semver', 'mongodb', 'jquery',
|
||||
'grunt', 'connect', 'yosay', 'underscore', 'string', 'xml2js', 'ejs', 'mongoose', 'marked', 'extend', 'mocha', 'superagent', 'js-yaml', 'xtend',
|
||||
'shelljs', 'gulp', 'yargs', 'browserify', 'minimatch', 'react', 'less', 'prompt', 'inquirer', 'ws', 'event-stream', 'inherits', 'mysql', 'esprima',
|
||||
'jsdom', 'stylus', 'when', 'readable-stream', 'aws-sdk', 'concat-stream', 'chai', 'Thenable', 'wrench'];
|
||||
|
||||
private knownScopes = ['@types', '@angular', '@babel', '@nuxtjs', '@vue', '@bazel'];
|
||||
|
||||
public getDocumentSelector(): DocumentSelector {
|
||||
return [{ language: 'json', scheme: '*', pattern: '**/package.json' }];
|
||||
}
|
||||
|
||||
public constructor(private xhr: XHRRequest, private canRunNPM: boolean) {
|
||||
}
|
||||
|
||||
public collectDefaultSuggestions(_fileName: string, result: ISuggestionsCollector): Thenable<any> {
|
||||
const defaultValue = {
|
||||
'name': '${1:name}',
|
||||
'description': '${2:description}',
|
||||
'authors': '${3:author}',
|
||||
'version': '${4:1.0.0}',
|
||||
'main': '${5:pathToMain}',
|
||||
'dependencies': {}
|
||||
};
|
||||
const proposal = new CompletionItem(localize('json.package.default', 'Default package.json'));
|
||||
proposal.kind = CompletionItemKind.Module;
|
||||
proposal.insertText = new SnippetString(JSON.stringify(defaultValue, null, '\t'));
|
||||
result.add(proposal);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
private isEnabled() {
|
||||
return this.canRunNPM || this.onlineEnabled();
|
||||
}
|
||||
|
||||
private onlineEnabled() {
|
||||
return !!workspace.getConfiguration('npm').get('fetchOnlinePackageInfo');
|
||||
}
|
||||
|
||||
public collectPropertySuggestions(
|
||||
_resource: string,
|
||||
location: Location,
|
||||
currentWord: string,
|
||||
addValue: boolean,
|
||||
isLast: boolean,
|
||||
collector: ISuggestionsCollector
|
||||
): Thenable<any> | null {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((location.matches(['dependencies']) || location.matches(['devDependencies']) || location.matches(['optionalDependencies']) || location.matches(['peerDependencies']))) {
|
||||
let queryUrl: string;
|
||||
if (currentWord.length > 0) {
|
||||
if (currentWord[0] === '@') {
|
||||
if (currentWord.indexOf('/') !== -1) {
|
||||
return this.collectScopedPackages(currentWord, addValue, isLast, collector);
|
||||
}
|
||||
for (let scope of this.knownScopes) {
|
||||
const proposal = new CompletionItem(scope);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = new SnippetString().appendText(`"${scope}/`).appendTabstop().appendText('"');
|
||||
proposal.filterText = JSON.stringify(scope);
|
||||
proposal.documentation = '';
|
||||
proposal.command = {
|
||||
title: '',
|
||||
command: 'editor.action.triggerSuggest'
|
||||
};
|
||||
collector.add(proposal);
|
||||
}
|
||||
collector.setAsIncomplete();
|
||||
}
|
||||
|
||||
queryUrl = `https://api.npms.io/v2/search/suggestions?size=${LIMIT}&q=${encodeURIComponent(currentWord)}`;
|
||||
return this.xhr({
|
||||
url: queryUrl,
|
||||
agent: USER_AGENT
|
||||
}).then((success) => {
|
||||
if (success.status === 200) {
|
||||
try {
|
||||
const obj = JSON.parse(success.responseText);
|
||||
if (obj && Array.isArray(obj)) {
|
||||
const results = <{ package: SearchPackageInfo; }[]>obj;
|
||||
for (const result of results) {
|
||||
this.processPackage(result.package, addValue, isLast, collector);
|
||||
}
|
||||
if (results.length === LIMIT) {
|
||||
collector.setAsIncomplete();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
collector.error(localize('json.npm.error.repoaccess', 'Request to the NPM repository failed: {0}', success.responseText));
|
||||
return 0;
|
||||
}
|
||||
return undefined;
|
||||
}, (error) => {
|
||||
collector.error(localize('json.npm.error.repoaccess', 'Request to the NPM repository failed: {0}', error.responseText));
|
||||
return 0;
|
||||
});
|
||||
} else {
|
||||
this.mostDependedOn.forEach((name) => {
|
||||
const insertText = new SnippetString().appendText(JSON.stringify(name));
|
||||
if (addValue) {
|
||||
insertText.appendText(': "').appendTabstop().appendText('"');
|
||||
if (!isLast) {
|
||||
insertText.appendText(',');
|
||||
}
|
||||
}
|
||||
const proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = insertText;
|
||||
proposal.filterText = JSON.stringify(name);
|
||||
proposal.documentation = '';
|
||||
collector.add(proposal);
|
||||
});
|
||||
this.collectScopedPackages(currentWord, addValue, isLast, collector);
|
||||
collector.setAsIncomplete();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private collectScopedPackages(currentWord: string, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector): Thenable<any> {
|
||||
let segments = currentWord.split('/');
|
||||
if (segments.length === 2 && segments[0].length > 1) {
|
||||
let scope = segments[0].substr(1);
|
||||
let name = segments[1];
|
||||
if (name.length < 4) {
|
||||
name = '';
|
||||
}
|
||||
let queryUrl = `https://api.npms.io/v2/search?q=scope:${scope}%20${name}&size=250`;
|
||||
return this.xhr({
|
||||
url: queryUrl,
|
||||
agent: USER_AGENT
|
||||
}).then((success) => {
|
||||
if (success.status === 200) {
|
||||
try {
|
||||
const obj = JSON.parse(success.responseText);
|
||||
if (obj && Array.isArray(obj.results)) {
|
||||
const objects = <{ package: SearchPackageInfo }[]>obj.results;
|
||||
for (let object of objects) {
|
||||
this.processPackage(object.package, addValue, isLast, collector);
|
||||
}
|
||||
if (objects.length === SCOPED_LIMIT) {
|
||||
collector.setAsIncomplete();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
collector.error(localize('json.npm.error.repoaccess', 'Request to the NPM repository failed: {0}', success.responseText));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public async collectValueSuggestions(_fileName: string, location: Location, result: ISuggestionsCollector): Promise<any> {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']) || location.matches(['optionalDependencies', '*']) || location.matches(['peerDependencies', '*']))) {
|
||||
const currentKey = location.path[location.path.length - 1];
|
||||
if (typeof currentKey === 'string') {
|
||||
const info = await this.fetchPackageInfo(currentKey);
|
||||
if (info && info.version) {
|
||||
|
||||
let name = JSON.stringify(info.version);
|
||||
let proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = name;
|
||||
proposal.documentation = localize('json.npm.latestversion', 'The currently latest version of the package');
|
||||
result.add(proposal);
|
||||
|
||||
name = JSON.stringify('^' + info.version);
|
||||
proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = name;
|
||||
proposal.documentation = localize('json.npm.majorversion', 'Matches the most recent major version (1.x.x)');
|
||||
result.add(proposal);
|
||||
|
||||
name = JSON.stringify('~' + info.version);
|
||||
proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = name;
|
||||
proposal.documentation = localize('json.npm.minorversion', 'Matches the most recent minor version (1.2.x)');
|
||||
result.add(proposal);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getDocumentation(description: string | undefined, version: string | undefined, homepage: string | undefined): MarkdownString {
|
||||
const str = new MarkdownString();
|
||||
if (description) {
|
||||
str.appendText(description);
|
||||
}
|
||||
if (version) {
|
||||
str.appendText('\n\n');
|
||||
str.appendText(localize('json.npm.version.hover', 'Latest version: {0}', version));
|
||||
}
|
||||
if (homepage) {
|
||||
str.appendText('\n\n');
|
||||
str.appendText(homepage);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public resolveSuggestion(item: CompletionItem): Thenable<CompletionItem | null> | null {
|
||||
if (item.kind === CompletionItemKind.Property && !item.documentation) {
|
||||
return this.fetchPackageInfo(item.label).then(info => {
|
||||
if (info) {
|
||||
item.documentation = this.getDocumentation(info.description, info.version, info.homepage);
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isValidNPMName(name: string): boolean {
|
||||
// following rules from https://github.com/npm/validate-npm-package-name
|
||||
if (!name || name.length > 214 || name.match(/^[_.]/)) {
|
||||
return false;
|
||||
}
|
||||
const match = name.match(/^(?:@([^/]+?)[/])?([^/]+?)$/);
|
||||
if (match) {
|
||||
const scope = match[1];
|
||||
if (scope && encodeURIComponent(scope) !== scope) {
|
||||
return false;
|
||||
}
|
||||
const name = match[2];
|
||||
return encodeURIComponent(name) === name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async fetchPackageInfo(pack: string): Promise<ViewPackageInfo | undefined> {
|
||||
if (!this.isValidNPMName(pack)) {
|
||||
return undefined; // avoid unnecessary lookups
|
||||
}
|
||||
let info: ViewPackageInfo | undefined;
|
||||
if (this.canRunNPM) {
|
||||
info = await this.npmView(pack);
|
||||
}
|
||||
if (!info && this.onlineEnabled()) {
|
||||
info = await this.npmjsView(pack);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
private npmView(pack: string): Promise<ViewPackageInfo | undefined> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
const args = ['view', '--json', pack, 'description', 'dist-tags.latest', 'homepage', 'version'];
|
||||
cp.execFile(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, (error, stdout) => {
|
||||
if (!error) {
|
||||
try {
|
||||
const content = JSON.parse(stdout);
|
||||
resolve({
|
||||
description: content['description'],
|
||||
version: content['dist-tags.latest'] || content['version'],
|
||||
homepage: content['homepage']
|
||||
});
|
||||
return;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async npmjsView(pack: string): Promise<ViewPackageInfo | undefined> {
|
||||
const queryUrl = 'https://api.npms.io/v2/package/' + encodeURIComponent(pack);
|
||||
try {
|
||||
const success = await this.xhr({
|
||||
url: queryUrl,
|
||||
agent: USER_AGENT
|
||||
});
|
||||
const obj = JSON.parse(success.responseText);
|
||||
const metadata = obj?.collected?.metadata;
|
||||
if (metadata) {
|
||||
return {
|
||||
description: metadata.description || '',
|
||||
version: metadata.version,
|
||||
homepage: metadata.links?.homepage || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
//ignore
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getInfoContribution(_fileName: string, location: Location): Thenable<MarkedString[] | null> | null {
|
||||
if (!this.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if ((location.matches(['dependencies', '*']) || location.matches(['devDependencies', '*']) || location.matches(['optionalDependencies', '*']) || location.matches(['peerDependencies', '*']))) {
|
||||
const pack = location.path[location.path.length - 1];
|
||||
if (typeof pack === 'string') {
|
||||
return this.fetchPackageInfo(pack).then(info => {
|
||||
if (info) {
|
||||
return [this.getDocumentation(info.description, info.version, info.homepage)];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private processPackage(pack: SearchPackageInfo, addValue: boolean, isLast: boolean, collector: ISuggestionsCollector) {
|
||||
if (pack && pack.name) {
|
||||
const name = pack.name;
|
||||
const insertText = new SnippetString().appendText(JSON.stringify(name));
|
||||
if (addValue) {
|
||||
insertText.appendText(': "');
|
||||
if (pack.version) {
|
||||
insertText.appendVariable('version', pack.version);
|
||||
} else {
|
||||
insertText.appendTabstop();
|
||||
}
|
||||
insertText.appendText('"');
|
||||
if (!isLast) {
|
||||
insertText.appendText(',');
|
||||
}
|
||||
}
|
||||
const proposal = new CompletionItem(name);
|
||||
proposal.kind = CompletionItemKind.Property;
|
||||
proposal.insertText = insertText;
|
||||
proposal.filterText = JSON.stringify(name);
|
||||
proposal.documentation = this.getDocumentation(pack.description, pack.version, pack?.links?.homepage);
|
||||
collector.add(proposal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchPackageInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
links?: { homepage?: string; };
|
||||
}
|
||||
|
||||
interface ViewPackageInfo {
|
||||
description: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
}
|
15
lib/vscode/extensions/npm/src/npmBrowserMain.ts
Normal file
15
lib/vscode/extensions/npm/src/npmBrowserMain.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as httpRequest from 'request-light';
|
||||
import * as vscode from 'vscode';
|
||||
import { addJSONProviders } from './features/jsonContributions';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||
context.subscriptions.push(addJSONProviders(httpRequest.xhr, false));
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
}
|
123
lib/vscode/extensions/npm/src/npmMain.ts
Normal file
123
lib/vscode/extensions/npm/src/npmMain.ts
Normal file
@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as httpRequest from 'request-light';
|
||||
import * as vscode from 'vscode';
|
||||
import { addJSONProviders } from './features/jsonContributions';
|
||||
import { runSelectedScript, selectAndRunScriptFromFolder } from './commands';
|
||||
import { NpmScriptsTreeDataProvider } from './npmView';
|
||||
import { getPackageManager, invalidateTasksCache, NpmTaskProvider } from './tasks';
|
||||
import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover';
|
||||
|
||||
let treeDataProvider: NpmScriptsTreeDataProvider | undefined;
|
||||
|
||||
function invalidateScriptCaches() {
|
||||
invalidateHoverScriptsCache();
|
||||
invalidateTasksCache();
|
||||
if (treeDataProvider) {
|
||||
treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||
configureHttpRequest();
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('http.proxy') || e.affectsConfiguration('http.proxyStrictSSL')) {
|
||||
configureHttpRequest();
|
||||
}
|
||||
}));
|
||||
|
||||
const canRunNPM = canRunNpmInCurrentWorkspace();
|
||||
context.subscriptions.push(addJSONProviders(httpRequest.xhr, canRunNPM));
|
||||
|
||||
treeDataProvider = registerExplorer(context);
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => {
|
||||
if (e.affectsConfiguration('npm.exclude') || e.affectsConfiguration('npm.autoDetect')) {
|
||||
invalidateTasksCache();
|
||||
if (treeDataProvider) {
|
||||
treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
if (e.affectsConfiguration('npm.scriptExplorerAction')) {
|
||||
if (treeDataProvider) {
|
||||
treeDataProvider.refresh();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
registerTaskProvider(context);
|
||||
registerHoverProvider(context);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('npm.runSelectedScript', runSelectedScript));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('npm.runScriptFromFolder', selectAndRunScriptFromFolder));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('npm.refresh', () => {
|
||||
invalidateScriptCaches();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('npm.packageManager', (args) => {
|
||||
if (args instanceof vscode.Uri) {
|
||||
return getPackageManager(args);
|
||||
}
|
||||
return '';
|
||||
}));
|
||||
}
|
||||
|
||||
function canRunNpmInCurrentWorkspace() {
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
return vscode.workspace.workspaceFolders.some(f => f.uri.scheme === 'file');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposable | undefined {
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
let watcher = vscode.workspace.createFileSystemWatcher('**/package.json');
|
||||
watcher.onDidChange((_e) => invalidateScriptCaches());
|
||||
watcher.onDidDelete((_e) => invalidateScriptCaches());
|
||||
watcher.onDidCreate((_e) => invalidateScriptCaches());
|
||||
context.subscriptions.push(watcher);
|
||||
|
||||
let workspaceWatcher = vscode.workspace.onDidChangeWorkspaceFolders((_e) => invalidateScriptCaches());
|
||||
context.subscriptions.push(workspaceWatcher);
|
||||
|
||||
let provider: vscode.TaskProvider = new NpmTaskProvider();
|
||||
let disposable = vscode.tasks.registerTaskProvider('npm', provider);
|
||||
context.subscriptions.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function registerExplorer(context: vscode.ExtensionContext): NpmScriptsTreeDataProvider | undefined {
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
let treeDataProvider = new NpmScriptsTreeDataProvider(context);
|
||||
const view = vscode.window.createTreeView('npm', { treeDataProvider: treeDataProvider, showCollapseAll: true });
|
||||
context.subscriptions.push(view);
|
||||
return treeDataProvider;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function registerHoverProvider(context: vscode.ExtensionContext): NpmScriptHoverProvider | undefined {
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
let npmSelector: vscode.DocumentSelector = {
|
||||
language: 'json',
|
||||
scheme: 'file',
|
||||
pattern: '**/package.json'
|
||||
};
|
||||
let provider = new NpmScriptHoverProvider(context);
|
||||
context.subscriptions.push(vscode.languages.registerHoverProvider(npmSelector, provider));
|
||||
return provider;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function configureHttpRequest() {
|
||||
const httpSettings = vscode.workspace.getConfiguration('http');
|
||||
httpRequest.configure(httpSettings.get<string>('proxy', ''), httpSettings.get<boolean>('proxyStrictSSL', true));
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
}
|
300
lib/vscode/extensions/npm/src/npmView.ts
Normal file
300
lib/vscode/extensions/npm/src/npmView.ts
Normal file
@ -0,0 +1,300 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as path from 'path';
|
||||
import {
|
||||
commands, Event, EventEmitter, ExtensionContext,
|
||||
Selection, Task,
|
||||
TaskGroup, tasks, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri,
|
||||
window, workspace, WorkspaceFolder
|
||||
} from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import {
|
||||
createTask, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition,
|
||||
startDebugging
|
||||
} from './tasks';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
class Folder extends TreeItem {
|
||||
packages: PackageJSON[] = [];
|
||||
workspaceFolder: WorkspaceFolder;
|
||||
|
||||
constructor(folder: WorkspaceFolder) {
|
||||
super(folder.name, TreeItemCollapsibleState.Expanded);
|
||||
this.contextValue = 'folder';
|
||||
this.resourceUri = folder.uri;
|
||||
this.workspaceFolder = folder;
|
||||
this.iconPath = ThemeIcon.Folder;
|
||||
}
|
||||
|
||||
addPackage(packageJson: PackageJSON) {
|
||||
this.packages.push(packageJson);
|
||||
}
|
||||
}
|
||||
|
||||
const packageName = 'package.json';
|
||||
|
||||
class PackageJSON extends TreeItem {
|
||||
path: string;
|
||||
folder: Folder;
|
||||
scripts: NpmScript[] = [];
|
||||
|
||||
static getLabel(_folderName: string, relativePath: string): string {
|
||||
if (relativePath.length > 0) {
|
||||
return path.join(relativePath, packageName);
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
constructor(folder: Folder, relativePath: string) {
|
||||
super(PackageJSON.getLabel(folder.label!, relativePath), TreeItemCollapsibleState.Expanded);
|
||||
this.folder = folder;
|
||||
this.path = relativePath;
|
||||
this.contextValue = 'packageJSON';
|
||||
if (relativePath) {
|
||||
this.resourceUri = Uri.file(path.join(folder!.resourceUri!.fsPath, relativePath, packageName));
|
||||
} else {
|
||||
this.resourceUri = Uri.file(path.join(folder!.resourceUri!.fsPath, packageName));
|
||||
}
|
||||
this.iconPath = ThemeIcon.File;
|
||||
}
|
||||
|
||||
addScript(script: NpmScript) {
|
||||
this.scripts.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
type ExplorerCommands = 'open' | 'run';
|
||||
|
||||
class NpmScript extends TreeItem {
|
||||
task: Task;
|
||||
package: PackageJSON;
|
||||
|
||||
constructor(_context: ExtensionContext, packageJson: PackageJSON, task: Task) {
|
||||
super(task.name, TreeItemCollapsibleState.None);
|
||||
const command: ExplorerCommands = workspace.getConfiguration('npm').get<ExplorerCommands>('scriptExplorerAction') || 'open';
|
||||
|
||||
const commandList = {
|
||||
'open': {
|
||||
title: 'Edit Script',
|
||||
command: 'npm.openScript',
|
||||
arguments: [this]
|
||||
},
|
||||
'run': {
|
||||
title: 'Run Script',
|
||||
command: 'npm.runScript',
|
||||
arguments: [this]
|
||||
}
|
||||
};
|
||||
this.contextValue = 'script';
|
||||
this.package = packageJson;
|
||||
this.task = task;
|
||||
this.command = commandList[command];
|
||||
|
||||
if (task.group && task.group === TaskGroup.Clean) {
|
||||
this.iconPath = new ThemeIcon('wrench-subaction');
|
||||
} else {
|
||||
this.iconPath = new ThemeIcon('wrench');
|
||||
}
|
||||
if (task.detail) {
|
||||
this.tooltip = task.detail;
|
||||
}
|
||||
}
|
||||
|
||||
getFolder(): WorkspaceFolder {
|
||||
return this.package.folder.workspaceFolder;
|
||||
}
|
||||
}
|
||||
|
||||
class NoScripts extends TreeItem {
|
||||
constructor(message: string) {
|
||||
super(message, TreeItemCollapsibleState.None);
|
||||
this.contextValue = 'noscripts';
|
||||
}
|
||||
}
|
||||
|
||||
export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
|
||||
private taskTree: Folder[] | PackageJSON[] | NoScripts[] | null = null;
|
||||
private extensionContext: ExtensionContext;
|
||||
private _onDidChangeTreeData: EventEmitter<TreeItem | null> = new EventEmitter<TreeItem | null>();
|
||||
readonly onDidChangeTreeData: Event<TreeItem | null> = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
const subscriptions = context.subscriptions;
|
||||
this.extensionContext = context;
|
||||
subscriptions.push(commands.registerCommand('npm.runScript', this.runScript, this));
|
||||
subscriptions.push(commands.registerCommand('npm.debugScript', this.debugScript, this));
|
||||
subscriptions.push(commands.registerCommand('npm.openScript', this.openScript, this));
|
||||
subscriptions.push(commands.registerCommand('npm.runInstall', this.runInstall, this));
|
||||
}
|
||||
|
||||
private async runScript(script: NpmScript) {
|
||||
tasks.executeTask(script.task);
|
||||
}
|
||||
|
||||
private async debugScript(script: NpmScript) {
|
||||
startDebugging(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;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
private async runInstall(selection: PackageJSON) {
|
||||
let uri: Uri | undefined = undefined;
|
||||
if (selection instanceof PackageJSON) {
|
||||
uri = selection.resourceUri;
|
||||
}
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
let task = createTask('install', 'install', selection.folder.workspaceFolder, uri, undefined, []);
|
||||
tasks.executeTask(task);
|
||||
}
|
||||
|
||||
private async openScript(selection: PackageJSON | NpmScript) {
|
||||
let uri: Uri | undefined = undefined;
|
||||
if (selection instanceof PackageJSON) {
|
||||
uri = selection.resourceUri!;
|
||||
} else if (selection instanceof NpmScript) {
|
||||
uri = selection.package.resourceUri;
|
||||
}
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
let document: TextDocument = await workspace.openTextDocument(uri);
|
||||
let offset = this.findScript(document, selection instanceof NpmScript ? selection : undefined);
|
||||
let position = document.positionAt(offset);
|
||||
await window.showTextDocument(document, { preserveFocus: true, selection: new Selection(position, position) });
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this.taskTree = null;
|
||||
this._onDidChangeTreeData.fire(null);
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeItem): TreeItem {
|
||||
return element;
|
||||
}
|
||||
|
||||
getParent(element: TreeItem): TreeItem | null {
|
||||
if (element instanceof Folder) {
|
||||
return null;
|
||||
}
|
||||
if (element instanceof PackageJSON) {
|
||||
return element.folder;
|
||||
}
|
||||
if (element instanceof NpmScript) {
|
||||
return element.package;
|
||||
}
|
||||
if (element instanceof NoScripts) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getChildren(element?: TreeItem): Promise<TreeItem[]> {
|
||||
if (!this.taskTree) {
|
||||
let taskItems = await tasks.fetchTasks({ type: 'npm' });
|
||||
if (taskItems) {
|
||||
this.taskTree = this.buildTaskTree(taskItems);
|
||||
if (this.taskTree.length === 0) {
|
||||
let message = localize('noScripts', 'No scripts found.');
|
||||
if (!isAutoDetectionEnabled()) {
|
||||
message = localize('autoDetectIsOff', 'The setting "npm.autoDetect" is "off".');
|
||||
}
|
||||
this.taskTree = [new NoScripts(message)];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (element instanceof Folder) {
|
||||
return element.packages;
|
||||
}
|
||||
if (element instanceof PackageJSON) {
|
||||
return element.scripts;
|
||||
}
|
||||
if (element instanceof NpmScript) {
|
||||
return [];
|
||||
}
|
||||
if (element instanceof NoScripts) {
|
||||
return [];
|
||||
}
|
||||
if (!element) {
|
||||
if (this.taskTree) {
|
||||
return this.taskTree;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private isInstallTask(task: Task): boolean {
|
||||
let fullName = getTaskName('install', task.definition.path);
|
||||
return fullName === task.name;
|
||||
}
|
||||
|
||||
private buildTaskTree(tasks: Task[]): Folder[] | PackageJSON[] | NoScripts[] {
|
||||
let folders: Map<String, Folder> = new Map();
|
||||
let packages: Map<String, PackageJSON> = new Map();
|
||||
|
||||
let folder = null;
|
||||
let packageJson = null;
|
||||
|
||||
tasks.forEach(each => {
|
||||
if (isWorkspaceFolder(each.scope) && !this.isInstallTask(each)) {
|
||||
folder = folders.get(each.scope.name);
|
||||
if (!folder) {
|
||||
folder = new Folder(each.scope);
|
||||
folders.set(each.scope.name, folder);
|
||||
}
|
||||
let definition: NpmTaskDefinition = <NpmTaskDefinition>each.definition;
|
||||
let relativePath = definition.path ? definition.path : '';
|
||||
let fullPath = path.join(each.scope.name, relativePath);
|
||||
packageJson = packages.get(fullPath);
|
||||
if (!packageJson) {
|
||||
packageJson = new PackageJSON(folder, relativePath);
|
||||
folder.addPackage(packageJson);
|
||||
packages.set(fullPath, packageJson);
|
||||
}
|
||||
let script = new NpmScript(this.extensionContext, packageJson, each);
|
||||
packageJson.addScript(script);
|
||||
}
|
||||
});
|
||||
if (folders.size === 1) {
|
||||
return [...packages.values()];
|
||||
}
|
||||
return [...folders.values()];
|
||||
}
|
||||
}
|
119
lib/vscode/extensions/npm/src/scriptHover.ts
Normal file
119
lib/vscode/extensions/npm/src/scriptHover.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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
|
||||
} from './tasks';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
let cachedDocument: Uri | undefined = undefined;
|
||||
let cachedScriptsMap: Map<string, [number, number, string]> | undefined = undefined;
|
||||
|
||||
export function invalidateHoverScriptsCache(document?: TextDocument) {
|
||||
if (!document) {
|
||||
cachedDocument = undefined;
|
||||
return;
|
||||
}
|
||||
if (document.uri === cachedDocument) {
|
||||
cachedDocument = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class NpmScriptHoverProvider implements HoverProvider {
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
context.subscriptions.push(commands.registerCommand('npm.runScriptFromHover', this.runScriptFromHover, this));
|
||||
context.subscriptions.push(commands.registerCommand('npm.debugScriptFromHover', this.debugScriptFromHover, this));
|
||||
context.subscriptions.push(workspace.onDidChangeTextDocument((e) => {
|
||||
invalidateHoverScriptsCache(e.document);
|
||||
}));
|
||||
}
|
||||
|
||||
public provideHover(document: TextDocument, position: Position, _token: CancellationToken): ProviderResult<Hover> {
|
||||
let hover: Hover | undefined = undefined;
|
||||
|
||||
if (!cachedDocument || cachedDocument.fsPath !== document.uri.fsPath) {
|
||||
cachedScriptsMap = findAllScriptRanges(document.getText());
|
||||
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)) {
|
||||
let contents: MarkdownString = new MarkdownString();
|
||||
contents.isTrusted = true;
|
||||
contents.appendMarkdown(this.createRunScriptMarkdown(key, document.uri));
|
||||
contents.appendMarkdown(this.createDebugScriptMarkdown(key, document.uri));
|
||||
hover = new Hover(contents);
|
||||
}
|
||||
});
|
||||
return hover;
|
||||
}
|
||||
|
||||
private createRunScriptMarkdown(script: string, documentUri: Uri): string {
|
||||
let args = {
|
||||
documentUri: documentUri,
|
||||
script: script,
|
||||
};
|
||||
return this.createMarkdownLink(
|
||||
localize('runScript', 'Run Script'),
|
||||
'npm.runScriptFromHover',
|
||||
args,
|
||||
localize('runScript.tooltip', 'Run the script as a task')
|
||||
);
|
||||
}
|
||||
|
||||
private createDebugScriptMarkdown(script: string, documentUri: Uri): string {
|
||||
const args = {
|
||||
documentUri: documentUri,
|
||||
script: script,
|
||||
};
|
||||
return this.createMarkdownLink(
|
||||
localize('debugScript', 'Debug Script'),
|
||||
'npm.debugScriptFromHover',
|
||||
args,
|
||||
localize('debugScript.tooltip', 'Runs the script under the debugger'),
|
||||
'|'
|
||||
);
|
||||
}
|
||||
|
||||
private createMarkdownLink(label: string, cmd: string, args: any, tooltip: string, separator?: string): string {
|
||||
let encodedArgs = encodeURIComponent(JSON.stringify(args));
|
||||
let prefix = '';
|
||||
if (separator) {
|
||||
prefix = ` ${separator} `;
|
||||
}
|
||||
return `${prefix}[${label}](command:${cmd}?${encodedArgs} "${tooltip}")`;
|
||||
}
|
||||
|
||||
public runScriptFromHover(args: any) {
|
||||
let script = args.script;
|
||||
let documentUri = args.documentUri;
|
||||
let folder = workspace.getWorkspaceFolder(documentUri);
|
||||
if (folder) {
|
||||
let task = createTask(script, `run ${script}`, folder, documentUri);
|
||||
tasks.executeTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
public debugScriptFromHover(args: { script: string; documentUri: Uri }) {
|
||||
let script = args.script;
|
||||
let documentUri = args.documentUri;
|
||||
let folder = workspace.getWorkspaceFolder(documentUri);
|
||||
if (folder) {
|
||||
startDebugging(script, dirname(documentUri.fsPath), folder);
|
||||
}
|
||||
}
|
||||
}
|
515
lib/vscode/extensions/npm/src/tasks.ts
Normal file
515
lib/vscode/extensions/npm/src/tasks.ts
Normal file
@ -0,0 +1,515 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace,
|
||||
DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem
|
||||
} 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';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface NpmTaskDefinition extends TaskDefinition {
|
||||
script: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface FolderTaskItem extends QuickPickItem {
|
||||
label: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
type AutoDetect = 'on' | 'off';
|
||||
|
||||
let cachedTasks: Task[] | undefined = undefined;
|
||||
|
||||
const INSTALL_SCRIPT = 'install';
|
||||
|
||||
export class NpmTaskProvider implements TaskProvider {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public provideTasks() {
|
||||
return provideNpmScripts();
|
||||
}
|
||||
|
||||
public resolveTask(_task: Task): Task | undefined {
|
||||
const npmTask = (<any>_task.definition).script;
|
||||
if (npmTask) {
|
||||
const kind: NpmTaskDefinition = (<any>_task.definition);
|
||||
let packageJsonUri: Uri;
|
||||
if (_task.scope === undefined || _task.scope === TaskScope.Global || _task.scope === TaskScope.Workspace) {
|
||||
// scope is required to be a WorkspaceFolder for resolveTask
|
||||
return undefined;
|
||||
}
|
||||
if (kind.path) {
|
||||
packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/' + kind.path + 'package.json' });
|
||||
} else {
|
||||
packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' });
|
||||
}
|
||||
return createTask(kind, `${kind.script === INSTALL_SCRIPT ? '' : 'run '}${kind.script}`, _task.scope, packageJsonUri);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateTasksCache() {
|
||||
cachedTasks = undefined;
|
||||
}
|
||||
|
||||
const buildNames: string[] = ['build', 'compile', 'watch'];
|
||||
function isBuildTask(name: string): boolean {
|
||||
for (let buildName of buildNames) {
|
||||
if (name.indexOf(buildName) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const testNames: string[] = ['test'];
|
||||
function isTestTask(name: string): boolean {
|
||||
for (let testName of testNames) {
|
||||
if (name === testName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getPrePostScripts(scripts: any): Set<string> {
|
||||
const prePostScripts: Set<string> = new Set([
|
||||
'preuninstall', 'postuninstall', 'prepack', 'postpack', 'preinstall', 'postinstall',
|
||||
'prepack', 'postpack', 'prepublish', 'postpublish', 'preversion', 'postversion',
|
||||
'prestop', 'poststop', 'prerestart', 'postrestart', 'preshrinkwrap', 'postshrinkwrap',
|
||||
'pretest', 'postest', 'prepublishOnly'
|
||||
]);
|
||||
let keys = Object.keys(scripts);
|
||||
for (const script of keys) {
|
||||
const prepost = ['pre' + script, 'post' + script];
|
||||
prepost.forEach(each => {
|
||||
if (scripts[each] !== undefined) {
|
||||
prePostScripts.add(each);
|
||||
}
|
||||
});
|
||||
}
|
||||
return prePostScripts;
|
||||
}
|
||||
|
||||
export function isWorkspaceFolder(value: any): value is WorkspaceFolder {
|
||||
return value && typeof value !== 'number';
|
||||
}
|
||||
|
||||
export function getPackageManager(folder: Uri): string {
|
||||
return workspace.getConfiguration('npm', folder).get<string>('packageManager', 'npm');
|
||||
}
|
||||
|
||||
export async function hasNpmScripts(): Promise<boolean> {
|
||||
let folders = workspace.workspaceFolders;
|
||||
if (!folders) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
for (const folder of folders) {
|
||||
if (isAutoDetectionEnabled(folder)) {
|
||||
let relativePattern = new RelativePattern(folder, '**/package.json');
|
||||
let paths = await workspace.findFiles(relativePattern, '**/node_modules/**');
|
||||
if (paths.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function detectNpmScripts(): Promise<Task[]> {
|
||||
|
||||
let emptyTasks: Task[] = [];
|
||||
let allTasks: Task[] = [];
|
||||
let visitedPackageJsonFiles: Set<string> = new Set();
|
||||
|
||||
let folders = workspace.workspaceFolders;
|
||||
if (!folders) {
|
||||
return emptyTasks;
|
||||
}
|
||||
try {
|
||||
for (const folder of folders) {
|
||||
if (isAutoDetectionEnabled(folder)) {
|
||||
let relativePattern = new RelativePattern(folder, '**/package.json');
|
||||
let paths = await workspace.findFiles(relativePattern, '**/{node_modules,.vscode-test}/**');
|
||||
for (const path of paths) {
|
||||
if (!isExcluded(folder, path) && !visitedPackageJsonFiles.has(path.fsPath)) {
|
||||
let tasks = await provideNpmScriptsForFolder(path);
|
||||
visitedPackageJsonFiles.add(path.fsPath);
|
||||
allTasks.push(...tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return allTasks;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function detectNpmScriptsForFolder(folder: Uri): Promise<FolderTaskItem[]> {
|
||||
|
||||
let folderTasks: FolderTaskItem[] = [];
|
||||
|
||||
try {
|
||||
let relativePattern = new RelativePattern(folder.fsPath, '**/package.json');
|
||||
let paths = await workspace.findFiles(relativePattern, '**/node_modules/**');
|
||||
|
||||
let visitedPackageJsonFiles: Set<string> = new Set();
|
||||
for (const path of paths) {
|
||||
if (!visitedPackageJsonFiles.has(path.fsPath)) {
|
||||
let tasks = await provideNpmScriptsForFolder(path);
|
||||
visitedPackageJsonFiles.add(path.fsPath);
|
||||
folderTasks.push(...tasks.map(t => ({ label: t.name, task: t })));
|
||||
}
|
||||
}
|
||||
return folderTasks;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function provideNpmScripts(): Promise<Task[]> {
|
||||
if (!cachedTasks) {
|
||||
cachedTasks = await detectNpmScripts();
|
||||
}
|
||||
return cachedTasks;
|
||||
}
|
||||
|
||||
export function isAutoDetectionEnabled(folder?: WorkspaceFolder): boolean {
|
||||
return workspace.getConfiguration('npm', folder?.uri).get<AutoDetect>('autoDetect') === 'on';
|
||||
}
|
||||
|
||||
function isExcluded(folder: WorkspaceFolder, packageJsonUri: Uri) {
|
||||
function testForExclusionPattern(path: string, pattern: string): boolean {
|
||||
return minimatch(path, pattern, { dot: true });
|
||||
}
|
||||
|
||||
let exclude = workspace.getConfiguration('npm', folder.uri).get<string | string[]>('exclude');
|
||||
let packageJsonFolder = path.dirname(packageJsonUri.fsPath);
|
||||
|
||||
if (exclude) {
|
||||
if (Array.isArray(exclude)) {
|
||||
for (let pattern of exclude) {
|
||||
if (testForExclusionPattern(packageJsonFolder, pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (testForExclusionPattern(packageJsonFolder, exclude)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDebugScript(script: string): boolean {
|
||||
let match = script.match(/--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/);
|
||||
return match !== null;
|
||||
}
|
||||
|
||||
async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise<Task[]> {
|
||||
let emptyTasks: Task[] = [];
|
||||
|
||||
let folder = workspace.getWorkspaceFolder(packageJsonUri);
|
||||
if (!folder) {
|
||||
return emptyTasks;
|
||||
}
|
||||
let scripts = await getScripts(packageJsonUri);
|
||||
if (!scripts) {
|
||||
return emptyTasks;
|
||||
}
|
||||
|
||||
const result: Task[] = [];
|
||||
|
||||
const prePostScripts = getPrePostScripts(scripts);
|
||||
Object.keys(scripts).forEach(each => {
|
||||
const task = createTask(each, `run ${each}`, folder!, packageJsonUri, scripts![each]);
|
||||
const lowerCaseTaskName = each.toLowerCase();
|
||||
if (isBuildTask(lowerCaseTaskName)) {
|
||||
task.group = TaskGroup.Build;
|
||||
} else if (isTestTask(lowerCaseTaskName)) {
|
||||
task.group = TaskGroup.Test;
|
||||
}
|
||||
if (prePostScripts.has(each)) {
|
||||
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(scripts![each])) {
|
||||
task.group = TaskGroup.Rebuild; // hack: use Rebuild group to tag debug scripts
|
||||
}
|
||||
result.push(task);
|
||||
});
|
||||
// always add npm install (without a problem matcher)
|
||||
result.push(createTask(INSTALL_SCRIPT, INSTALL_SCRIPT, folder, packageJsonUri, 'install dependencies from package', []));
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getTaskName(script: string, relativePath: string | undefined) {
|
||||
if (relativePath && relativePath.length) {
|
||||
return `${script} - ${relativePath.substring(0, relativePath.length - 1)}`;
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
export function createTask(script: NpmTaskDefinition | string, cmd: string, folder: WorkspaceFolder, packageJsonUri: Uri, detail?: string, matcher?: any): Task {
|
||||
let kind: NpmTaskDefinition;
|
||||
if (typeof script === 'string') {
|
||||
kind = { type: 'npm', script: script };
|
||||
} else {
|
||||
kind = script;
|
||||
}
|
||||
|
||||
function getCommandLine(folder: WorkspaceFolder, cmd: string): string {
|
||||
let packageManager = getPackageManager(folder.uri);
|
||||
if (workspace.getConfiguration('npm', folder.uri).get<boolean>('runSilent')) {
|
||||
return `${packageManager} --silent ${cmd}`;
|
||||
}
|
||||
return `${packageManager} ${cmd}`;
|
||||
}
|
||||
|
||||
function getRelativePath(folder: WorkspaceFolder, packageJsonUri: Uri): string {
|
||||
let rootUri = folder.uri;
|
||||
let absolutePath = packageJsonUri.path.substring(0, packageJsonUri.path.length - 'package.json'.length);
|
||||
return absolutePath.substring(rootUri.path.length + 1);
|
||||
}
|
||||
|
||||
let relativePackageJson = getRelativePath(folder, packageJsonUri);
|
||||
if (relativePackageJson.length) {
|
||||
kind.path = getRelativePath(folder, packageJsonUri);
|
||||
}
|
||||
let taskName = getTaskName(kind.script, relativePackageJson);
|
||||
let cwd = path.dirname(packageJsonUri.fsPath);
|
||||
const task = new Task(kind, folder, taskName, 'npm', new ShellExecution(getCommandLine(folder, cmd), { cwd: cwd }), matcher);
|
||||
task.detail = detail;
|
||||
return task;
|
||||
}
|
||||
|
||||
|
||||
export function getPackageJsonUriFromTask(task: Task): Uri | null {
|
||||
if (isWorkspaceFolder(task.scope)) {
|
||||
if (task.definition.path) {
|
||||
return Uri.file(path.join(task.scope.uri.fsPath, task.definition.path, 'package.json'));
|
||||
} else {
|
||||
return Uri.file(path.join(task.scope.uri.fsPath, 'package.json'));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function hasPackageJson(): Promise<boolean> {
|
||||
let folders = workspace.workspaceFolders;
|
||||
if (!folders) {
|
||||
return false;
|
||||
}
|
||||
for (const folder of folders) {
|
||||
if (folder.uri.scheme === 'file') {
|
||||
let packageJson = path.join(folder.uri.fsPath, 'package.json');
|
||||
if (await exists(packageJson)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function exists(file: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, _reject) => {
|
||||
fs.exists(file, (value) => {
|
||||
resolve(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readFile(file: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(data.toString());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function runScript(script: string, document: TextDocument) {
|
||||
let uri = document.uri;
|
||||
let folder = workspace.getWorkspaceFolder(uri);
|
||||
if (folder) {
|
||||
let task = createTask(script, `run ${script}`, folder, uri);
|
||||
tasks.executeTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
export function startDebugging(scriptName: string, cwd: string, folder: WorkspaceFolder) {
|
||||
const config: DebugConfiguration = {
|
||||
type: 'pwa-node',
|
||||
request: 'launch',
|
||||
name: `Debug ${scriptName}`,
|
||||
cwd,
|
||||
runtimeExecutable: getPackageManager(folder.uri),
|
||||
runtimeArgs: [
|
||||
'run',
|
||||
scriptName,
|
||||
],
|
||||
};
|
||||
|
||||
if (folder) {
|
||||
debug.startDebugging(folder, config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type StringMap = { [s: string]: string; };
|
||||
|
||||
async function findAllScripts(buffer: string): Promise<StringMap> {
|
||||
let scripts: StringMap = {};
|
||||
let script: string | undefined = undefined;
|
||||
let inScripts = false;
|
||||
|
||||
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[script] = value;
|
||||
}
|
||||
script = undefined;
|
||||
}
|
||||
},
|
||||
onObjectProperty(property: string, _offset: number, _length: number) {
|
||||
if (property === 'scripts') {
|
||||
inScripts = true;
|
||||
}
|
||||
else if (inScripts && !script) {
|
||||
script = property;
|
||||
} else { // nested object which is invalid, ignore the script
|
||||
script = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(buffer, visitor);
|
||||
return scripts;
|
||||
}
|
||||
|
||||
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<StringMap | undefined> {
|
||||
|
||||
if (packageJsonUri.scheme !== 'file') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let packageJson = packageJsonUri.fsPath;
|
||||
if (!await exists(packageJson)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
let contents = await readFile(packageJson);
|
||||
let json = findAllScripts(contents);//JSON.parse(contents);
|
||||
return json;
|
||||
} catch (e) {
|
||||
let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJsonUri.fsPath);
|
||||
throw new Error(localizedParseError);
|
||||
}
|
||||
}
|
8
lib/vscode/extensions/npm/src/typings/refs.d.ts
vendored
Normal file
8
lib/vscode/extensions/npm/src/typings/refs.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
Reference in New Issue
Block a user