From 49fe8b3dc85e96e84d0b82d095c4bd50de6ec014 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 17 Sep 2021 15:21:59 -0700 Subject: [PATCH] Fixes #93, add ability to report issues --- CHANGELOG.md | 8 +++ package-lock.json | 86 ++++++++++++++++++++++++--- package.json | 30 ++++++++-- resources/report_issue_template.md | 57 ++++++++++++++++++ src/ConfigurationService.ts | 4 ++ src/LanguageServer.ts | 93 ++++++++---------------------- src/LanguageServerErrorHandler.ts | 42 ++++++++++++++ src/LoggingService.ts | 78 +++++++++++++++++++++---- src/commands.ts | 74 +++++++++++++++++++++++- src/constants.ts | 8 +++ src/extension.ts | 10 +++- src/utils.ts | 21 +++++++ 12 files changed, 416 insertions(+), 95 deletions(-) create mode 100644 resources/report_issue_template.md create mode 100644 src/LanguageServerErrorHandler.ts create mode 100644 src/constants.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 55da10c..29bbec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.3.0] - 2021-09-17 + +### Added + +- New "Report Issue" command (#93) +- New "Show Output" command +- Extend OutputChannel to be able to buffer output internally for error reporting (up to 1000 lines) + ## [2.2.3] - 2021-09-17 ### Fixed diff --git a/package-lock.json b/package-lock.json index dff7142..9ba3a34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "version": "2.2.3", "license": "MIT", "dependencies": { + "@types/fs-extra": "^9.0.12", + "fs-extra": "^10.0.0", "mz": "^2.7.0", "promisify-child-process": "^4.1.1", "semver": "^7.3.5", @@ -164,6 +166,14 @@ "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, + "node_modules/@types/fs-extra": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.12.tgz", + "integrity": "sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -173,8 +183,7 @@ "node_modules/@types/node": { "version": "14.17.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.17.tgz", - "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==", - "dev": true + "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==" }, "node_modules/@types/semver": { "version": "7.3.8", @@ -1008,6 +1017,19 @@ "node": ">=8" } }, + "node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1075,8 +1097,7 @@ "node_modules/graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/has": { "version": "1.0.3", @@ -1326,6 +1347,17 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2366,6 +2398,14 @@ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2869,6 +2909,14 @@ "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, + "@types/fs-extra": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.12.tgz", + "integrity": "sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw==", + "requires": { + "@types/node": "*" + } + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -2878,8 +2926,7 @@ "@types/node": { "version": "14.17.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.17.tgz", - "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==", - "dev": true + "integrity": "sha512-niAjcewgEYvSPCZm3OaM9y6YQrL2SEPH9PymtE6fuZAvFiP6ereCcvApGl2jKTq7copTIguX3PBvfP08LN4LvQ==" }, "@types/semver": { "version": "7.3.8", @@ -3549,6 +3596,16 @@ "path-exists": "^4.0.0" } }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3601,8 +3658,7 @@ "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "has": { "version": "1.0.3", @@ -3781,6 +3837,15 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4552,6 +4617,11 @@ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 2d1e910..8a26277 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ } ], "license": "MIT", - "version": "2.2.3", + "bugs": { + "url": "https://github.com/psalm/psalm-vscode-plugin/issues" + }, + "version": "2.3.0", "publisher": "getpsalm", "categories": [ "Linters", @@ -50,12 +53,13 @@ }, "capabilities": { "untrustedWorkspaces": { - "supported":false, + "supported": false, "description": "Since this runs Psalm, and Psalm can be configured to execute code on your computer, you should avoid opening untrusted projects while using this plugin" } - }, - "extensionKind": ["workspace"], + "extensionKind": [ + "workspace" + ], "contributes": { "configuration": { "type": "object", @@ -183,6 +187,16 @@ "command": "psalm.analyzeWorkSpace", "title": "Analyze Workspace", "category": "Psalm" + }, + { + "command": "psalm.reportIssue", + "title": "Report Issue", + "category": "Psalm" + }, + { + "command": "psalm.showOutput", + "title": "Show Output", + "category": "Psalm" } ], "menus": { @@ -192,6 +206,12 @@ }, { "command": "psalm.analyzeWorkSpace" + }, + { + "command": "psalm.reportIssue" + }, + { + "command": "psalm.showOutput" } ] } @@ -211,6 +231,8 @@ "webpack-cli": "^4.8.0" }, "dependencies": { + "@types/fs-extra": "^9.0.12", + "fs-extra": "^10.0.0", "mz": "^2.7.0", "promisify-child-process": "^4.1.1", "semver": "^7.3.5", diff --git a/resources/report_issue_template.md b/resources/report_issue_template.md new file mode 100644 index 0000000..67f4885 --- /dev/null +++ b/resources/report_issue_template.md @@ -0,0 +1,57 @@ + + +# Behaviour + +## Expected + +XXX + +## Actual + +XXX + +## Steps to reproduce: + +[**NOTE**: Self-contained, minimal reproducing code samples are **extremely** helpful and will expedite addressing your issue] + +1. + + + +# Diagnostic data + +- PHP version: {0} +- Psalm version: {1} + +
+ +"Psalm Language Server" channel in the OUTPUT panel (Last 1000 lines) + +

+ + + +``` +{2} +``` + +

+
+ +
+ +User Settings + +

+ +``` +{3} +``` + +

+
diff --git a/src/ConfigurationService.ts b/src/ConfigurationService.ts index ea27caa..636ccef 100644 --- a/src/ConfigurationService.ts +++ b/src/ConfigurationService.ts @@ -79,4 +79,8 @@ export class ConfigurationService { } return this.config[key]; } + + public getAll(): { [key: string]: any } { + return this.config; + } } diff --git a/src/LanguageServer.ts b/src/LanguageServer.ts index 99a732f..d901bf1 100644 --- a/src/LanguageServer.ts +++ b/src/LanguageServer.ts @@ -1,4 +1,8 @@ -import { LanguageClient, StreamInfo } from 'vscode-languageclient/node'; +import { + LanguageClient, + StreamInfo, + ErrorHandler, +} from 'vscode-languageclient/node'; import { StatusBar, LanguageServerStatus } from './StatusBar'; import { spawn, ChildProcess } from 'child_process'; import { workspace, Uri, Disposable, ExtensionContext } from 'vscode'; @@ -7,6 +11,7 @@ import { DocumentSelector } from 'vscode-languageserver-protocol'; import { join, isAbsolute } from 'path'; import { execFile } from 'promisify-child-process'; import { ConfigurationService } from './ConfigurationService'; +import LanguageServerErrorHandler from './LanguageServerErrorHandler'; import { statSync, constants } from 'fs'; import { access } from 'fs/promises'; import * as semver from 'semver'; @@ -28,71 +33,6 @@ export class LanguageServer { private serverProcess: ChildProcess | null = null; private context: ExtensionContext; - public static async getInstance( - context: ExtensionContext, - workspacePath: string, - statusBar: StatusBar, - configurationService: ConfigurationService, - loggingService: LoggingService - ): Promise { - await configurationService.init(); - - const configPaths = configurationService.get('configPaths'); - - if (!configPaths.length) { - loggingService.logError( - `No Config Paths defined. Define some and try again` - ); - return null; - } - - const psalmXML = await workspace.findFiles( - `{${configPaths.join(',')}}` - // `**/vendor/**/{${configPaths.join(',')}}` - ); - if (!psalmXML.length) { - // no psalm.xml found - loggingService.logError( - `No Config file found in: ${configPaths.join(',')}` - ); - return null; - } - const configXml = psalmXML[0].path; - - loggingService.logDebug(`Found config file: ${configXml}`); - - const configWatcher = workspace.createFileSystemWatcher(configXml); - - const languageServer = new LanguageServer( - context, - workspacePath, - configXml, - statusBar, - configurationService, - loggingService - ); - - const onConfigChange = () => { - loggingService.logInfo(`Config file changed: ${configXml}`); - languageServer.restart(); - }; - - const onConfigDelete = () => { - loggingService.logInfo(`Config file deleted: ${configXml}`); - languageServer.stop(); - }; - - // Restart the language server when the tracked config file changes - configWatcher.onDidChange(onConfigChange); - configWatcher.onDidCreate(onConfigChange); - configWatcher.onDidDelete(onConfigDelete); - - // Start Lanuage Server - await languageServer.start(); - - return languageServer; - } - constructor( context: ExtensionContext, workspacePath: string, @@ -113,8 +53,8 @@ export class LanguageServer { 'Psalm Language Server', this.serverOptions.bind(this), { - outputChannel: this.loggingService.getOutputChannel(), - traceOutputChannel: this.loggingService.getOutputChannel(), + outputChannel: this.loggingService, + traceOutputChannel: this.loggingService, // Register the server for php (and maybe HTML) documents documentSelector: this.configurationService.get< string[] | DocumentSelector @@ -135,6 +75,7 @@ export class LanguageServer { ], }, progressOnInitialization: true, + errorHandler: this.createDefaultErrorHandler(5), }, this.debug ); @@ -148,6 +89,13 @@ export class LanguageServer { this.languageClient.onTelemetry(this.onTelemetry.bind(this)); } + public createDefaultErrorHandler(maxRestartCount?: number): ErrorHandler { + if (maxRestartCount !== undefined && maxRestartCount < 0) { + throw new Error(`Invalid maxRestartCount: ${maxRestartCount}`); + } + return new LanguageServerErrorHandler('Thing', maxRestartCount ?? 4); + } + private onTelemetry(params: any) { if ( typeof params === 'object' && @@ -491,6 +439,15 @@ export class LanguageServer { } } + /** + * Get the PHP version + * @return Promise A promise that resolves to the php version (Or null) + */ + public async getPHPVersion(): Promise { + const out = await this.executePhp(['--version']); + return out; + } + /** * Get the Psalm Language Server version * @return Promise A promise that resolves to the language server version (Or null) diff --git a/src/LanguageServerErrorHandler.ts b/src/LanguageServerErrorHandler.ts new file mode 100644 index 0000000..bac98ef --- /dev/null +++ b/src/LanguageServerErrorHandler.ts @@ -0,0 +1,42 @@ +import { + Message, + ErrorHandler, + ErrorAction, + CloseAction, +} from 'vscode-languageclient/node'; +import { showReportIssueErrorMessage } from './utils'; + +export default class LanguageServerErrorHandler implements ErrorHandler { + private readonly restarts: number[]; + + constructor(private name: string, private maxRestartCount: number) { + this.restarts = []; + } + + public error(_error: Error, _message: Message, count: number): ErrorAction { + if (count && count <= 3) { + return ErrorAction.Continue; + } + return ErrorAction.Shutdown; + } + public closed(): CloseAction { + this.restarts.push(Date.now()); + if (this.restarts.length <= this.maxRestartCount) { + return CloseAction.Restart; + } else { + let diff = + this.restarts[this.restarts.length - 1] - this.restarts[0]; + if (diff <= 3 * 60 * 1000) { + void showReportIssueErrorMessage( + `The ${this.name} server crashed ${ + this.maxRestartCount + 1 + } times in the last 3 minutes. The server will not be restarted.` + ); + return CloseAction.DoNotRestart; + } else { + this.restarts.shift(); + return CloseAction.Restart; + } + } + } +} diff --git a/src/LoggingService.ts b/src/LoggingService.ts index dbf85cd..fe6936a 100644 --- a/src/LoggingService.ts +++ b/src/LoggingService.ts @@ -1,12 +1,70 @@ import { window, OutputChannel } from 'vscode'; export type LogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE'; - -export class LoggingService { +export class LoggingService implements OutputChannel { private outputChannel = window.createOutputChannel('Psalm Language Server'); private logLevel: LogLevel = 'DEBUG'; + private content: string[] = []; + + private contentLimit = 1000; + + readonly name: string = 'Psalm Language Server'; + + /** + * Append the given value to the channel. + * + * @param value A string, falsy values will not be printed. + */ + append(value: string): void { + this.content.push(value); + this.content = this.content.slice(-this.contentLimit); + this.outputChannel.append(value); + } + + /** + * Append the given value and a line feed character + * to the channel. + * + * @param value A string, falsy values will be printed. + */ + appendLine(value: string): void { + this.content.push(value); + this.content = this.content.slice(-this.contentLimit); + this.outputChannel.appendLine(value); + } + + /** + * Removes all output from the channel. + */ + clear(): void { + this.outputChannel.clear(); + } + + /** + * Reveal this channel in the UI. + * + * @param preserveFocus When `true` the channel will not take focus. + */ + show(): void { + this.outputChannel.show(...arguments); + } + + /** + * Hide this channel from the UI. + */ + hide(): void { + this.outputChannel.hide(); + } + + /** + * Dispose and free associated resources. + */ + dispose(): void { + this.outputChannel.dispose(); + } + public setOutputLevel(logLevel: LogLevel) { this.logLevel = logLevel; } @@ -15,10 +73,6 @@ export class LoggingService { return this.logLevel; } - public getOutputChannel(): OutputChannel { - return this.outputChannel; - } - public logTrace(message: string, data?: unknown): void { if ( this.logLevel === 'NONE' || @@ -97,25 +151,25 @@ export class LoggingService { if (typeof error === 'string') { // Errors as a string usually only happen with // plugins that don't return the expected error. - this.outputChannel.appendLine(error); + this.appendLine(error); } else if (error?.message || error?.stack) { if (error?.message) { this.logMessage(error.message, 'ERROR'); } if (error?.stack) { - this.outputChannel.appendLine(error.stack); + this.appendLine(error.stack); } } else if (error) { this.logObject(error); } } - public show() { - this.outputChannel.show(); + public getContent(): string[] { + return this.content; } private logObject(data: unknown): void { - this.outputChannel.appendLine(JSON.stringify(data, null, 2)); + this.appendLine(JSON.stringify(data, null, 2)); } /** @@ -125,6 +179,6 @@ export class LoggingService { */ private logMessage(message: string, logLevel: LogLevel): void { const title = new Date().toLocaleTimeString(); - this.outputChannel.appendLine(`[${logLevel} - ${title}] ${message}`); + this.appendLine(`[${logLevel} - ${title}] ${message}`); } } diff --git a/src/commands.ts b/src/commands.ts index 85034e3..1478fb7 100755 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,6 +2,12 @@ import * as vscode from 'vscode'; import { ExitNotification } from 'vscode-languageclient/node'; import * as semver from 'semver'; import { LanguageServer } from './LanguageServer'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from './constants'; +import { formatFromTemplate } from './utils'; +import { ConfigurationService } from './ConfigurationService'; +import { LoggingService } from './LoggingService'; +import { EOL } from 'os'; interface Command { id: string; execute(): void; @@ -42,10 +48,76 @@ function restartPsalmServer(client: LanguageServer): Command { }; } -export function registerCommands(client: LanguageServer): vscode.Disposable[] { +function reportIssue( + client: LanguageServer, + configurationService: ConfigurationService, + loggingService: LoggingService +): Command { + return { + id: 'psalm.reportIssue', + async execute() { + const templatePath = path.join( + EXTENSION_ROOT_DIR, + 'resources', + 'report_issue_template.md' + ); + + const userSettings = Object.entries(configurationService.getAll()) + .map(([key, value]) => `${key}: ${JSON.stringify(value)}`) + .join(EOL); + const psalmLogs = loggingService.getContent().join(EOL); + + let phpVersion = 'unknown'; + try { + phpVersion = (await client.getPHPVersion()) ?? 'unknown'; + } catch (err) { + phpVersion = err.message; + } + + let psalmVersion: string | null = 'unknown'; + try { + psalmVersion = + (await client.getPsalmLanguageServerVersion()) ?? 'unknown'; + } catch (err) { + psalmVersion = err.message; + } + + await vscode.commands.executeCommand( + 'workbench.action.openIssueReporter', + { + extensionId: 'getpsalm.psalm-vscode-plugin', + issueBody: await formatFromTemplate( + templatePath, + phpVersion, // 0 + psalmVersion, // 1 + psalmLogs, // 2 + userSettings // 3 + ), + } + ); + }, + }; +} + +function showOutput(loggingService: LoggingService): Command { + return { + id: 'psalm.showOutput', + async execute() { + loggingService.show(); + }, + }; +} + +export function registerCommands( + client: LanguageServer, + configurationService: ConfigurationService, + loggingService: LoggingService +): vscode.Disposable[] { const commands: Command[] = [ restartPsalmServer(client), analyzeWorkSpace(client), + reportIssue(client, configurationService, loggingService), + showOutput(loggingService), ]; const disposables = commands.map((command) => { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..779ddf0 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; + +export const EXTENSION_ROOT_DIR = path.join(__dirname, '..'); diff --git a/src/extension.ts b/src/extension.ts index f70bf1d..efa93df 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -103,11 +103,17 @@ export async function activate( configWatcher.onDidCreate(onConfigChange); configWatcher.onDidDelete(onConfigDelete); + context.subscriptions.push( + ...registerCommands( + languageServer, + configurationService, + loggingService + ) + ); + // Start Lanuage Server await languageServer.start(); - context.subscriptions.push(...registerCommands(languageServer)); - vscode.workspace.onDidChangeConfiguration(async (change) => { if ( !change.affectsConfiguration('psalm') || diff --git a/src/utils.ts b/src/utils.ts index 5bbbe7b..d5f1245 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { window, commands } from 'vscode'; +import * as fs from 'fs-extra'; export async function showOpenSettingsPrompt( errorMessage: string @@ -12,6 +13,18 @@ export async function showOpenSettingsPrompt( } } +export async function showReportIssueErrorMessage( + errorMessage: string +): Promise { + const selected = await window.showErrorMessage( + errorMessage, + 'Report Issue' + ); + if (selected === 'Report Issue') { + await commands.executeCommand('psalm.reportIssue'); + } +} + export async function showErrorMessage( errorMessage: string ): Promise { @@ -23,3 +36,11 @@ export async function showWarningMessage( ): Promise { return window.showWarningMessage(errorMessage); } + +export async function formatFromTemplate(templatePath: string, ...args: any[]) { + const template = await fs.readFile(templatePath, 'utf8'); + + return template.replace(/{(\d+)}/g, (match, number) => + args[number] === undefined ? match : args[number] + ); +}