Add extension folder and connect extension with language server (#32)
* add extension folder and connect with language server * delete the old .vscode folder and move to new .vscode folder
This commit is contained in:
Родитель
7a9c69b279
Коммит
f02b6c3165
|
@ -42,7 +42,7 @@ Generated\ Files/
|
|||
# Visual Studio Code setting directory
|
||||
.vscode/
|
||||
!powershell-module/.vscode/
|
||||
!vscode-extension/.vscode/
|
||||
!vscode-extension/extension/.vscode/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/class-name-casing": "warn",
|
||||
"@typescript-eslint/semi": "warn",
|
||||
"curly": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "compile",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"panel": "dedicated",
|
||||
"reveal": "never"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/test/**
|
||||
src/**
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
|
@ -0,0 +1,9 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to the "vscode-extension-azurerm-migration-test" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
|
@ -0,0 +1,65 @@
|
|||
# vscode-extension-azurerm-migration-test README
|
||||
|
||||
This is the README for your extension "vscode-extension-azurerm-migration-test". After writing up a brief description, we recommend including the following sections.
|
||||
|
||||
## Features
|
||||
|
||||
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
|
||||
|
||||
For example if there is an image subfolder under your extension project workspace:
|
||||
|
||||
\!\[feature X\]\(images/feature-x.png\)
|
||||
|
||||
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
|
||||
|
||||
## Requirements
|
||||
|
||||
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
|
||||
|
||||
## Extension Settings
|
||||
|
||||
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
|
||||
|
||||
For example:
|
||||
|
||||
This extension contributes the following settings:
|
||||
|
||||
* `myExtension.enable`: enable/disable this extension
|
||||
* `myExtension.thing`: set to `blah` to do something
|
||||
|
||||
## Known Issues
|
||||
|
||||
Calling out known issues can help limit users opening duplicate issues against your extension.
|
||||
|
||||
## Release Notes
|
||||
|
||||
Users appreciate release notes as you update your extension.
|
||||
|
||||
### 1.0.0
|
||||
|
||||
Initial release of ...
|
||||
|
||||
### 1.0.1
|
||||
|
||||
Fixed issue #.
|
||||
|
||||
### 1.1.0
|
||||
|
||||
Added features X, Y, and Z.
|
||||
|
||||
-----------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Working with Markdown
|
||||
|
||||
**Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||
|
||||
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
|
||||
* Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
|
||||
* Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets
|
||||
|
||||
### For more information
|
||||
|
||||
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
|
||||
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
|
||||
|
||||
**Enjoy!**
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "azure-powershell-migration",
|
||||
"displayName": "Azure PowerShell Migration",
|
||||
"description": "TBD",
|
||||
"version": "0.0.1",
|
||||
"publisher": "azure-powershell",
|
||||
"engines": {
|
||||
"vscode": "^1.47.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onCommand:extension.helloWorld",
|
||||
"onCommand:azure-powershell-migration.selectVersion"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.helloWorld",
|
||||
"title": "Hello World"
|
||||
},
|
||||
{
|
||||
"title": "Migrate Azure PowerShell Script",
|
||||
"command": "azure-powershell-migration.selectVersion"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "tsc -p ./",
|
||||
"lint": "eslint src --ext ts",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"test": "node ./out/test/runTest.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.0",
|
||||
"semver": "^7.3.2",
|
||||
"uuid": "^8.3.0",
|
||||
"vscode-extension-telemetry": "~0.1.6",
|
||||
"vscode-languageclient": "~6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.47.0",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^13.11.0",
|
||||
"eslint": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"glob": "^7.1.6",
|
||||
"mocha": "^7.1.2",
|
||||
"typescript": "^3.8.3",
|
||||
"vscode-test": "^1.3.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
Disposable,
|
||||
StatusBarAlignment,
|
||||
StatusBarItem,
|
||||
ThemeColor,
|
||||
window,
|
||||
Command} from "vscode";
|
||||
|
||||
export function showAnimatedStatusBarMessage(text: string, hideWhenDone: Thenable<any>): Disposable {
|
||||
const animatedStatusBarItem: AnimatedStatusBarItem = new AnimatedStatusBarItem(text);
|
||||
animatedStatusBarItem.show(hideWhenDone);
|
||||
return animatedStatusBarItem;
|
||||
}
|
||||
|
||||
class AnimatedStatusBarItem implements StatusBarItem {
|
||||
private readonly animationRate: number;
|
||||
private statusBarItem: StatusBarItem;
|
||||
private maxCount: number;
|
||||
private counter: number;
|
||||
private baseText: string;
|
||||
private timerInterval: number;
|
||||
private elapsedTime: number;
|
||||
private intervalId: NodeJS.Timer;
|
||||
private suffixStates: string[];
|
||||
|
||||
public get alignment(): StatusBarAlignment {
|
||||
return this.statusBarItem.alignment;
|
||||
}
|
||||
|
||||
public get priority(): number {
|
||||
return this.statusBarItem.priority;
|
||||
}
|
||||
|
||||
public get text(): string {
|
||||
return this.statusBarItem.text;
|
||||
}
|
||||
|
||||
public set text(value: string) {
|
||||
this.statusBarItem.text = value;
|
||||
}
|
||||
|
||||
public get tooltip(): string {
|
||||
return this.statusBarItem.tooltip;
|
||||
}
|
||||
|
||||
public set tooltip(value: string) {
|
||||
this.statusBarItem.tooltip = value;
|
||||
}
|
||||
|
||||
public get color(): string | ThemeColor {
|
||||
return this.statusBarItem.color;
|
||||
}
|
||||
|
||||
public set color(value: string | ThemeColor) {
|
||||
this.statusBarItem.color = value;
|
||||
}
|
||||
|
||||
public get command(): string | Command {
|
||||
return this.statusBarItem.command;
|
||||
}
|
||||
|
||||
public set command(value: string | Command) {
|
||||
this.statusBarItem.command = value;
|
||||
}
|
||||
|
||||
constructor(baseText: string, alignment?: StatusBarAlignment, priority?: number) {
|
||||
this.animationRate = 1;
|
||||
this.statusBarItem = window.createStatusBarItem(alignment, priority);
|
||||
this.baseText = baseText;
|
||||
this.counter = 0;
|
||||
this.suffixStates = [" ", ". ", ".. ", "..."];
|
||||
this.maxCount = this.suffixStates.length;
|
||||
this.timerInterval = ((1 / this.maxCount) * 1000) / this.animationRate;
|
||||
this.elapsedTime = 0;
|
||||
}
|
||||
|
||||
public show(hideWhenDone?: Thenable<any>): void {
|
||||
this.statusBarItem.show();
|
||||
this.start();
|
||||
if (hideWhenDone !== undefined) {
|
||||
hideWhenDone.then(() => this.hide());
|
||||
}
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.stop();
|
||||
this.statusBarItem.hide();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.statusBarItem.dispose();
|
||||
}
|
||||
|
||||
private updateCounter(): void {
|
||||
this.counter = (this.counter + 1) % this.maxCount;
|
||||
this.elapsedTime = this.elapsedTime + this.timerInterval;
|
||||
}
|
||||
|
||||
private updateText(): void {
|
||||
this.text = this.baseText + this.suffixStates[this.counter];
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
this.updateCounter();
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this.counter = 0;
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
private start(): void {
|
||||
this.reset();
|
||||
this.intervalId = setInterval(() => this.update(), this.timerInterval);
|
||||
}
|
||||
|
||||
private stop(): void {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import vscode = require("vscode");
|
||||
|
||||
const confirmItemLabel: string = "$(checklist) Confirm";
|
||||
const checkedPrefix: string = "[ $(check) ]";
|
||||
const uncheckedPrefix: string = "[ ]";
|
||||
const defaultPlaceHolder: string = "Select 'Confirm' to confirm or press 'Esc' key to cancel";
|
||||
|
||||
export interface ICheckboxQuickPickItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export interface ICheckboxQuickPickOptions {
|
||||
confirmPlaceHolder: string;
|
||||
}
|
||||
|
||||
const defaultOptions: ICheckboxQuickPickOptions = { confirmPlaceHolder: defaultPlaceHolder};
|
||||
|
||||
export function showCheckboxQuickPick(
|
||||
items: ICheckboxQuickPickItem[],
|
||||
options: ICheckboxQuickPickOptions = defaultOptions): Thenable<ICheckboxQuickPickItem[]> {
|
||||
|
||||
return showInner(items, options).then(
|
||||
(selectedItem) => {
|
||||
// We're mutating the original item list so just return it for now.
|
||||
// If 'selectedItem' is undefined it means the user cancelled the
|
||||
// inner showQuickPick UI so pass the undefined along.
|
||||
return selectedItem !== undefined ? items : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getQuickPickItems(items: ICheckboxQuickPickItem[]): vscode.QuickPickItem[] {
|
||||
|
||||
const quickPickItems: vscode.QuickPickItem[] = [];
|
||||
quickPickItems.push({ label: confirmItemLabel, description: "" });
|
||||
|
||||
items.forEach((item) =>
|
||||
quickPickItems.push({
|
||||
label: convertToCheckBox(item),
|
||||
description: item.description,
|
||||
}));
|
||||
|
||||
return quickPickItems;
|
||||
}
|
||||
|
||||
function showInner(
|
||||
items: ICheckboxQuickPickItem[],
|
||||
options: ICheckboxQuickPickOptions): Thenable<vscode.QuickPickItem> {
|
||||
|
||||
const quickPickThenable: Thenable<vscode.QuickPickItem> =
|
||||
vscode.window.showQuickPick(
|
||||
getQuickPickItems(items),
|
||||
{
|
||||
ignoreFocusOut: true,
|
||||
matchOnDescription: true,
|
||||
placeHolder: options.confirmPlaceHolder,
|
||||
});
|
||||
|
||||
return quickPickThenable.then(
|
||||
(selection) => {
|
||||
if (!selection) {
|
||||
return Promise.resolve<vscode.QuickPickItem>(undefined);
|
||||
}
|
||||
|
||||
if (selection.label === confirmItemLabel) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
const index: number = getItemIndex(items, selection.label);
|
||||
|
||||
if (index >= 0) {
|
||||
toggleSelection(items[index]);
|
||||
} else {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Couldn't find CheckboxQuickPickItem for label '${selection.label}'`);
|
||||
}
|
||||
|
||||
return showInner(items, options);
|
||||
});
|
||||
}
|
||||
|
||||
function getItemIndex(items: ICheckboxQuickPickItem[], itemLabel: string): number {
|
||||
const trimmedLabel = itemLabel.substr(itemLabel.indexOf("]") + 2);
|
||||
return items.findIndex((item) => item.label === trimmedLabel);
|
||||
}
|
||||
|
||||
function toggleSelection(item: ICheckboxQuickPickItem): void {
|
||||
item.isSelected = !item.isSelected;
|
||||
}
|
||||
|
||||
function convertToCheckBox(item: ICheckboxQuickPickItem): string {
|
||||
return `${item.isSelected ? checkedPrefix : uncheckedPrefix} ${item.label}`;
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
// import * as vscode from 'vscode';
|
||||
"use strict";
|
||||
|
||||
import path = require("path");
|
||||
import vscode = require("vscode");
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import { DocumentSelector } from "vscode-languageclient";
|
||||
import { CodeActionsFeature } from "./features/CodeActions";
|
||||
import { Logger, LogLevel } from "./logging";
|
||||
import { SessionManager } from "./session";
|
||||
import Settings = require("./settings");
|
||||
import { PowerShellLanguageId } from "./utils";
|
||||
import { LanguageClientConsumer } from "./languageClientConsumer";
|
||||
import { getSrcVersion } from "./selectVersion";
|
||||
|
||||
// The most reliable way to get the name and version of the current extension.
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
const PackageJSON: any = require("../package.json");
|
||||
|
||||
// the application insights key (also known as instrumentation key) used for telemetry.
|
||||
|
||||
let logger: Logger;
|
||||
let sessionManager: SessionManager;
|
||||
let languageClientConsumers: LanguageClientConsumer[] = [];
|
||||
let commandRegistrations: vscode.Disposable[] = [];
|
||||
let telemetryReporter: TelemetryReporter;
|
||||
|
||||
const documentSelector: DocumentSelector = [
|
||||
{ language: "powershell", scheme: "file" },
|
||||
{ language: "Powershell", scheme: "file" },
|
||||
{ language: "powershell", scheme: "untitled" },
|
||||
];
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): void {
|
||||
console.debug('===========================================');
|
||||
console.log('Congratulations, your extension "azure-powershell-migration" is now active!');
|
||||
|
||||
// The command has been defined in the package.json file
|
||||
// Now provide the implementation of the command with registerCommand
|
||||
// The commandId parameter must match the command field in package.json
|
||||
|
||||
// Create the logger
|
||||
logger = new Logger();
|
||||
|
||||
// Set the log level
|
||||
const extensionSettings = Settings.load();
|
||||
logger.MinimumLogLevel = LogLevel[extensionSettings.developer.editorServicesLogLevel];
|
||||
|
||||
sessionManager =
|
||||
new SessionManager(
|
||||
logger,
|
||||
documentSelector,
|
||||
PackageJSON.displayName,
|
||||
PackageJSON.version,
|
||||
telemetryReporter);
|
||||
|
||||
// Register commands that do not require Language client
|
||||
commandRegistrations = [
|
||||
// new ExamplesFeature(),
|
||||
// new GenerateBugReportFeature(sessionManager),
|
||||
// new ISECompatibilityFeature(),
|
||||
// new OpenInISEFeature(),
|
||||
// new PesterTestsFeature(sessionManager),
|
||||
// new RunCodeFeature(sessionManager),
|
||||
new CodeActionsFeature(logger),
|
||||
// new SpecifyScriptArgsFeature(context),
|
||||
]
|
||||
|
||||
// Features and command registrations that require language client
|
||||
languageClientConsumers = [
|
||||
// new GetDiagnosticFeature(logger, context)
|
||||
// new GetAstFeature(logger, context),
|
||||
// new ConsoleFeature(logger),
|
||||
// new ExpandAliasFeature(logger),
|
||||
// new GetCommandsFeature(logger),
|
||||
// new ShowHelpFeature(logger),
|
||||
// new FindModuleFeature(),
|
||||
// new ExtensionCommandsFeature(logger),
|
||||
// new NewFileOrProjectFeature(),
|
||||
// new RemoteFilesFeature(),
|
||||
// new DebugSessionFeature(context, sessionManager, logger),
|
||||
// new PickPSHostProcessFeature(),
|
||||
// new HelpCompletionFeature(logger),
|
||||
// new CustomViewsFeature(),
|
||||
// new PickRunspaceFeature(),
|
||||
// new ExternalApiFeature(sessionManager, logger)
|
||||
];
|
||||
|
||||
sessionManager.setLanguageClientConsumers(languageClientConsumers);
|
||||
|
||||
if (extensionSettings.startAutomatically) {
|
||||
sessionManager.start();
|
||||
}
|
||||
|
||||
let disposable = vscode.commands.registerCommand('azure-powershell-migration.selectVersion', async () => {
|
||||
var sourceVersion = await getSrcVersion();
|
||||
vscode.window.showInformationMessage(`Updating powershell scripts from '${sourceVersion}' to latest`);
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
// Clean up all extension features
|
||||
languageClientConsumers.forEach((languageClientConsumer) => {
|
||||
languageClientConsumer.dispose();
|
||||
});
|
||||
|
||||
commandRegistrations.forEach((commandRegistration) => {
|
||||
commandRegistration.dispose();
|
||||
});
|
||||
|
||||
// Dispose of the current session
|
||||
sessionManager.dispose();
|
||||
|
||||
// Dispose of the logger
|
||||
logger.dispose();
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import vscode = require("vscode");
|
||||
import Window = vscode.window;
|
||||
import { ILogger } from "../logging";
|
||||
|
||||
export class CodeActionsFeature implements vscode.Disposable {
|
||||
private applyEditsCommand: vscode.Disposable;
|
||||
private showDocumentationCommand: vscode.Disposable;
|
||||
|
||||
constructor(private log: ILogger) {
|
||||
this.applyEditsCommand = vscode.commands.registerCommand("PowerShell.ApplyCodeActionEdits", (edit: any) => {
|
||||
Window.activeTextEditor.edit((editBuilder) => {
|
||||
editBuilder.replace(
|
||||
new vscode.Range(
|
||||
edit.StartLineNumber - 1,
|
||||
edit.StartColumnNumber - 1,
|
||||
edit.EndLineNumber - 1,
|
||||
edit.EndColumnNumber - 1),
|
||||
edit.Text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.applyEditsCommand.dispose();
|
||||
this.showDocumentationCommand.dispose();
|
||||
}
|
||||
|
||||
public showRuleDocumentation(ruleId: string) {
|
||||
const pssaDocBaseURL = "https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation";
|
||||
|
||||
if (!ruleId) {
|
||||
this.log.writeWarning("Cannot show documentation for code action, no ruleName was supplied.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ruleId.startsWith("PS")) {
|
||||
ruleId = ruleId.substr(2);
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(pssaDocBaseURL + `/${ruleId}.md`));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import vscode = require("vscode");
|
||||
import { LanguageClient, NotificationType, RequestType } from "vscode-languageclient";
|
||||
import { ICheckboxQuickPickItem, showCheckboxQuickPick } from "../controls/checkboxQuickPick";
|
||||
import { Logger } from "../logging";
|
||||
import Settings = require("../settings");
|
||||
import { LanguageClientConsumer } from "../languageClientConsumer";
|
||||
|
||||
export const EvaluateRequestType = new RequestType<IEvaluateRequestArguments, void, void, void>("evaluate");
|
||||
export const OutputNotificationType = new NotificationType<IOutputNotificationBody, void>("output");
|
||||
export const ExecutionStatusChangedNotificationType =
|
||||
new NotificationType<IExecutionStatusDetails, void>("powerShell/executionStatusChanged");
|
||||
|
||||
export const ShowChoicePromptRequestType =
|
||||
new RequestType<IShowChoicePromptRequestArgs,
|
||||
IShowChoicePromptResponseBody, string, void>("powerShell/showChoicePrompt");
|
||||
|
||||
export const ShowInputPromptRequestType =
|
||||
new RequestType<IShowInputPromptRequestArgs,
|
||||
IShowInputPromptResponseBody, string, void>("powerShell/showInputPrompt");
|
||||
|
||||
export interface IEvaluateRequestArguments {
|
||||
expression: string;
|
||||
}
|
||||
|
||||
export interface IOutputNotificationBody {
|
||||
category: string;
|
||||
output: string;
|
||||
}
|
||||
|
||||
interface IExecutionStatusDetails {
|
||||
executionOptions: IExecutionOptions;
|
||||
executionStatus: ExecutionStatus;
|
||||
hadErrors: boolean;
|
||||
}
|
||||
|
||||
interface IChoiceDetails {
|
||||
label: string;
|
||||
helpMessage: string;
|
||||
}
|
||||
|
||||
interface IShowInputPromptRequestArgs {
|
||||
name: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface IShowChoicePromptRequestArgs {
|
||||
isMultiChoice: boolean;
|
||||
caption: string;
|
||||
message: string;
|
||||
choices: IChoiceDetails[];
|
||||
defaultChoices: number[];
|
||||
}
|
||||
|
||||
interface IShowChoicePromptResponseBody {
|
||||
responseText: string;
|
||||
promptCancelled: boolean;
|
||||
}
|
||||
|
||||
interface IShowInputPromptResponseBody {
|
||||
responseText: string;
|
||||
promptCancelled: boolean;
|
||||
}
|
||||
|
||||
enum ExecutionStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Failed,
|
||||
Aborted,
|
||||
Completed,
|
||||
}
|
||||
|
||||
interface IExecutionOptions {
|
||||
writeOutputToHost: boolean;
|
||||
writeErrorsToHost: boolean;
|
||||
addToHistory: boolean;
|
||||
interruptCommandPrompt: boolean;
|
||||
}
|
||||
|
||||
function showChoicePrompt(
|
||||
promptDetails: IShowChoicePromptRequestArgs,
|
||||
client: LanguageClient): Thenable<IShowChoicePromptResponseBody> {
|
||||
|
||||
let resultThenable: Thenable<IShowChoicePromptResponseBody>;
|
||||
|
||||
if (!promptDetails.isMultiChoice) {
|
||||
let quickPickItems =
|
||||
promptDetails.choices.map<vscode.QuickPickItem>((choice) => {
|
||||
return {
|
||||
label: choice.label,
|
||||
description: choice.helpMessage,
|
||||
};
|
||||
});
|
||||
|
||||
if (promptDetails.defaultChoices && promptDetails.defaultChoices.length > 0) {
|
||||
// Shift the default items to the front of the
|
||||
// array so that the user can select it easily
|
||||
const defaultChoice = promptDetails.defaultChoices[0];
|
||||
if (defaultChoice > -1 &&
|
||||
defaultChoice < promptDetails.choices.length) {
|
||||
|
||||
const defaultChoiceItem = quickPickItems[defaultChoice];
|
||||
quickPickItems.splice(defaultChoice, 1);
|
||||
|
||||
// Add the default choice to the head of the array
|
||||
quickPickItems = [defaultChoiceItem].concat(quickPickItems);
|
||||
}
|
||||
}
|
||||
|
||||
resultThenable =
|
||||
vscode.window
|
||||
.showQuickPick(
|
||||
quickPickItems,
|
||||
{ placeHolder: promptDetails.message })
|
||||
.then(onItemSelected);
|
||||
} else {
|
||||
const checkboxQuickPickItems =
|
||||
promptDetails.choices.map<ICheckboxQuickPickItem>((choice) => {
|
||||
return {
|
||||
label: choice.label,
|
||||
description: choice.helpMessage,
|
||||
isSelected: false,
|
||||
};
|
||||
});
|
||||
|
||||
// Select the defaults
|
||||
promptDetails.defaultChoices.forEach((choiceIndex) => {
|
||||
checkboxQuickPickItems[choiceIndex].isSelected = true;
|
||||
});
|
||||
|
||||
resultThenable =
|
||||
showCheckboxQuickPick(
|
||||
checkboxQuickPickItems,
|
||||
{ confirmPlaceHolder: promptDetails.message })
|
||||
.then(onItemsSelected);
|
||||
}
|
||||
|
||||
return resultThenable;
|
||||
}
|
||||
|
||||
function showInputPrompt(
|
||||
promptDetails: IShowInputPromptRequestArgs,
|
||||
client: LanguageClient): Thenable<IShowInputPromptResponseBody> {
|
||||
|
||||
const resultThenable =
|
||||
vscode.window.showInputBox({
|
||||
placeHolder: promptDetails.name + ": ",
|
||||
}).then(onInputEntered);
|
||||
|
||||
return resultThenable;
|
||||
}
|
||||
|
||||
function onItemsSelected(chosenItems: ICheckboxQuickPickItem[]): IShowChoicePromptResponseBody {
|
||||
if (chosenItems !== undefined) {
|
||||
return {
|
||||
promptCancelled: false,
|
||||
responseText: chosenItems.filter((item) => item.isSelected).map((item) => item.label).join(", "),
|
||||
};
|
||||
} else {
|
||||
// User cancelled the prompt, send the cancellation
|
||||
return {
|
||||
promptCancelled: true,
|
||||
responseText: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function onItemSelected(chosenItem: vscode.QuickPickItem): IShowChoicePromptResponseBody {
|
||||
if (chosenItem !== undefined) {
|
||||
return {
|
||||
promptCancelled: false,
|
||||
responseText: chosenItem.label,
|
||||
};
|
||||
} else {
|
||||
// User cancelled the prompt, send the cancellation
|
||||
return {
|
||||
promptCancelled: true,
|
||||
responseText: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function onInputEntered(responseText: string): IShowInputPromptResponseBody {
|
||||
if (responseText !== undefined) {
|
||||
return {
|
||||
promptCancelled: false,
|
||||
responseText,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
promptCancelled: true,
|
||||
responseText: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ConsoleFeature extends LanguageClientConsumer {
|
||||
private commands: vscode.Disposable[];
|
||||
private resolveStatusBarPromise: (value?: {} | PromiseLike<{}>) => void;
|
||||
|
||||
constructor(private log: Logger) {
|
||||
super();
|
||||
this.commands = [
|
||||
vscode.commands.registerCommand("PowerShell.RunSelection", async () => {
|
||||
|
||||
if (vscode.window.activeTerminal &&
|
||||
vscode.window.activeTerminal.name !== "PowerShell Integrated Console") {
|
||||
this.log.write("PSIC is not active terminal. Running in active terminal using 'runSelectedText'");
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.runSelectedText");
|
||||
|
||||
// We need to honor the focusConsoleOnExecute setting here too. However, the boolean that `show`
|
||||
// takes is called `preserveFocus` which when `true` the terminal will not take focus.
|
||||
// This is the inverse of focusConsoleOnExecute so we have to inverse the boolean.
|
||||
vscode.window.activeTerminal.show(!Settings.load().integratedConsole.focusConsoleOnExecute);
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.scrollToBottom");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
let selectionRange: vscode.Range;
|
||||
|
||||
if (!editor.selection.isEmpty) {
|
||||
selectionRange =
|
||||
new vscode.Range(
|
||||
editor.selection.start,
|
||||
editor.selection.end);
|
||||
} else {
|
||||
selectionRange = editor.document.lineAt(editor.selection.start.line).range;
|
||||
}
|
||||
|
||||
this.languageClient.sendRequest(EvaluateRequestType, {
|
||||
expression: editor.document.getText(selectionRange),
|
||||
});
|
||||
|
||||
// Show the integrated console if it isn't already visible and
|
||||
// scroll terminal to bottom so new output is visible
|
||||
await vscode.commands.executeCommand("PowerShell.ShowSessionConsole", true);
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.scrollToBottom");
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
// Make sure we cancel any status bar
|
||||
this.clearStatusBar();
|
||||
this.commands.forEach((command) => command.dispose());
|
||||
}
|
||||
|
||||
public setLanguageClient(languageClient: LanguageClient) {
|
||||
this.languageClient = languageClient;
|
||||
this.languageClient.onRequest(
|
||||
ShowChoicePromptRequestType,
|
||||
(promptDetails) => showChoicePrompt(promptDetails, this.languageClient));
|
||||
|
||||
this.languageClient.onRequest(
|
||||
ShowInputPromptRequestType,
|
||||
(promptDetails) => showInputPrompt(promptDetails, this.languageClient));
|
||||
|
||||
// Set up status bar alerts for when PowerShell is executing a script
|
||||
this.languageClient.onNotification(
|
||||
ExecutionStatusChangedNotificationType,
|
||||
(executionStatusDetails) => {
|
||||
switch (executionStatusDetails.executionStatus) {
|
||||
// If execution has changed to running, make a notification
|
||||
case ExecutionStatus.Running:
|
||||
this.showExecutionStatus("PowerShell");
|
||||
break;
|
||||
|
||||
// If the execution has stopped, destroy the previous notification
|
||||
case ExecutionStatus.Completed:
|
||||
case ExecutionStatus.Aborted:
|
||||
case ExecutionStatus.Failed:
|
||||
this.clearStatusBar();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private showExecutionStatus(message: string) {
|
||||
vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Window,
|
||||
}, (progress) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.clearStatusBar();
|
||||
|
||||
this.resolveStatusBarPromise = resolve;
|
||||
progress.report({ message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private clearStatusBar() {
|
||||
if (this.resolveStatusBarPromise) {
|
||||
this.resolveStatusBarPromise();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { spawn } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import fetch, { RequestInit } from "node-fetch";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as semver from "semver";
|
||||
import * as stream from "stream";
|
||||
import * as util from "util";
|
||||
import { MessageItem, ProgressLocation, window } from "vscode";
|
||||
import { LanguageClient } from "vscode-languageclient";
|
||||
import { SessionManager } from "../session";
|
||||
import * as Settings from "../settings";
|
||||
import { isMacOS, isWindows } from "../utils";
|
||||
import { EvaluateRequestType } from "./Console";
|
||||
|
||||
const streamPipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const PowerShellGitHubReleasesUrl =
|
||||
"https://api.github.com/repos/PowerShell/PowerShell/releases/latest";
|
||||
const PowerShellGitHubPrereleasesUrl =
|
||||
"https://api.github.com/repos/PowerShell/PowerShell/releases";
|
||||
|
||||
export class GitHubReleaseInformation {
|
||||
public static async FetchLatestRelease(preview: boolean): Promise<GitHubReleaseInformation> {
|
||||
const requestConfig: RequestInit = {};
|
||||
|
||||
// For CI. This prevents GitHub from rate limiting us.
|
||||
if (process.env.PS_TEST_GITHUB_API_USERNAME && process.env.PS_TEST_GITHUB_API_PAT) {
|
||||
const authHeaderValue = Buffer
|
||||
.from(`${process.env.PS_TEST_GITHUB_API_USERNAME}:${process.env.PS_TEST_GITHUB_API_PAT}`)
|
||||
.toString("base64");
|
||||
requestConfig.headers = {
|
||||
Authorization: `Basic ${authHeaderValue}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch the latest PowerShell releases from GitHub.
|
||||
const response = await fetch(
|
||||
preview ? PowerShellGitHubPrereleasesUrl : PowerShellGitHubReleasesUrl,
|
||||
requestConfig);
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json.message || json || "response was not ok.");
|
||||
}
|
||||
|
||||
// For preview, we grab all the releases and then grab the first prerelease.
|
||||
const releaseJson = preview
|
||||
? (await response.json()).find((release: any) => release.prerelease)
|
||||
: await response.json();
|
||||
|
||||
return new GitHubReleaseInformation(
|
||||
releaseJson.tag_name, releaseJson.assets);
|
||||
}
|
||||
|
||||
public version: semver.SemVer;
|
||||
public isPreview: boolean = false;
|
||||
public assets: any[];
|
||||
|
||||
public constructor(version: string | semver.SemVer, assets: any[] = []) {
|
||||
this.version = semver.parse(version);
|
||||
|
||||
if (semver.prerelease(this.version)) {
|
||||
this.isPreview = true;
|
||||
}
|
||||
|
||||
this.assets = assets;
|
||||
}
|
||||
}
|
||||
|
||||
interface IUpdateMessageItem extends MessageItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export async function InvokePowerShellUpdateCheck(
|
||||
sessionManager: SessionManager,
|
||||
languageServerClient: LanguageClient,
|
||||
localVersion: semver.SemVer,
|
||||
arch: string,
|
||||
release: GitHubReleaseInformation) {
|
||||
const options: IUpdateMessageItem[] = [
|
||||
{
|
||||
id: 0,
|
||||
title: "Yes",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: "Not now",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Do not show this notification again",
|
||||
},
|
||||
];
|
||||
|
||||
// If our local version is up-to-date, we can return early.
|
||||
if (semver.compare(localVersion, release.version) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commonText: string = `You have an old version of PowerShell (${
|
||||
localVersion.raw
|
||||
}). The current latest release is ${
|
||||
release.version.raw
|
||||
}.`;
|
||||
|
||||
if (process.platform === "linux") {
|
||||
await window.showInformationMessage(
|
||||
`${commonText} We recommend updating to the latest version.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await window.showInformationMessage(
|
||||
`${commonText} Would you like to update the version? ${
|
||||
isMacOS ? "(Homebrew is required on macOS)"
|
||||
: "(This will close ALL pwsh terminals running in this Visual Studio Code session)"
|
||||
}`, ...options);
|
||||
|
||||
// If the user cancels the notification.
|
||||
if (!result) { return; }
|
||||
|
||||
// Yes choice.
|
||||
switch (result.id) {
|
||||
// Yes choice.
|
||||
case 0:
|
||||
if (isWindows) {
|
||||
const msiMatcher = arch === "x86" ?
|
||||
"win-x86.msi" : "win-x64.msi";
|
||||
|
||||
const asset = release.assets.filter((a: any) => a.name.indexOf(msiMatcher) >= 0)[0];
|
||||
const msiDownloadPath = path.join(os.tmpdir(), asset.name);
|
||||
|
||||
const res = await fetch(asset.browser_download_url);
|
||||
if (!res.ok) {
|
||||
throw new Error("unable to fetch MSI");
|
||||
}
|
||||
|
||||
await window.withProgress({
|
||||
title: "Downloading PowerShell Installer...",
|
||||
location: ProgressLocation.Notification,
|
||||
cancellable: false,
|
||||
},
|
||||
async () => {
|
||||
// Streams the body of the request to a file.
|
||||
await streamPipeline(res.body, fs.createWriteStream(msiDownloadPath));
|
||||
});
|
||||
|
||||
// Stop the Integrated Console session because Windows likes to hold on to files.
|
||||
sessionManager.stop();
|
||||
|
||||
// Close all terminals with the name "pwsh" in the current VS Code session.
|
||||
// This will encourage folks to not close the instance of VS Code that spawned
|
||||
// the MSI process.
|
||||
for (const terminal of window.terminals) {
|
||||
if (terminal.name === "pwsh") {
|
||||
terminal.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the MSI via cmd.
|
||||
const msi = spawn("msiexec", ["/i", msiDownloadPath]);
|
||||
|
||||
msi.on("close", (code) => {
|
||||
// Now that the MSI is finished, start the Integrated Console session.
|
||||
sessionManager.start();
|
||||
fs.unlinkSync(msiDownloadPath);
|
||||
});
|
||||
|
||||
} else if (isMacOS) {
|
||||
const script = release.isPreview
|
||||
? "brew cask upgrade powershell-preview"
|
||||
: "brew cask upgrade powershell";
|
||||
|
||||
await languageServerClient.sendRequest(EvaluateRequestType, {
|
||||
expression: script,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Never choice.
|
||||
case 2:
|
||||
await Settings.change("promptToUpdatePowerShell", false, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { window } from "vscode";
|
||||
import { LanguageClient } from "vscode-languageclient";
|
||||
|
||||
export abstract class LanguageClientConsumer {
|
||||
|
||||
private _languageClient: LanguageClient;
|
||||
|
||||
public setLanguageClient(languageClient: LanguageClient) {
|
||||
this.languageClient = languageClient;
|
||||
}
|
||||
|
||||
abstract dispose(): void;
|
||||
|
||||
public get languageClient(): LanguageClient {
|
||||
if (!this._languageClient) {
|
||||
window.showInformationMessage(
|
||||
"PowerShell extension has not finished starting up yet. Please try again in a few moments.");
|
||||
}
|
||||
return this._languageClient;
|
||||
}
|
||||
|
||||
public set languageClient(value: LanguageClient) {
|
||||
this._languageClient = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import fs = require("fs");
|
||||
import os = require("os");
|
||||
import path = require("path");
|
||||
import vscode = require("vscode");
|
||||
import utils = require("./utils");
|
||||
|
||||
export enum LogLevel {
|
||||
Diagnostic,
|
||||
Verbose,
|
||||
Normal,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
/** Interface for logging operations. New features should use this interface for the "type" of logger.
|
||||
* This will allow for easy mocking of the logger during unit tests.
|
||||
*/
|
||||
export interface ILogger {
|
||||
write(message: string, ...additionalMessages: string[]);
|
||||
writeDiagnostic(message: string, ...additionalMessages: string[]);
|
||||
writeVerbose(message: string, ...additionalMessages: string[]);
|
||||
writeWarning(message: string, ...additionalMessages: string[]);
|
||||
writeAndShowWarning(message: string, ...additionalMessages: string[]);
|
||||
writeError(message: string, ...additionalMessages: string[]);
|
||||
}
|
||||
|
||||
export class Logger implements ILogger {
|
||||
|
||||
public logBasePath: string;
|
||||
public logSessionPath: string;
|
||||
public MinimumLogLevel: LogLevel = LogLevel.Normal;
|
||||
|
||||
private commands: vscode.Disposable[];
|
||||
private logChannel: vscode.OutputChannel;
|
||||
private logFilePath: string;
|
||||
|
||||
constructor() {
|
||||
this.logChannel = vscode.window.createOutputChannel("PowerShell Extension Logs");
|
||||
|
||||
this.logBasePath = path.resolve(__dirname, "../../logs");
|
||||
utils.ensurePathExists(this.logBasePath);
|
||||
|
||||
this.commands = [
|
||||
vscode.commands.registerCommand(
|
||||
"PowerShell.ShowLogs",
|
||||
() => { this.showLogPanel(); }),
|
||||
|
||||
vscode.commands.registerCommand(
|
||||
"PowerShell.OpenLogFolder",
|
||||
() => { this.openLogFolder(); }),
|
||||
];
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.commands.forEach((command) => { command.dispose(); });
|
||||
this.logChannel.dispose();
|
||||
}
|
||||
|
||||
public getLogFilePath(baseName: string): string {
|
||||
return path.resolve(this.logSessionPath, `${baseName}.log`);
|
||||
}
|
||||
|
||||
public writeAtLevel(logLevel: LogLevel, message: string, ...additionalMessages: string[]) {
|
||||
if (logLevel >= this.MinimumLogLevel) {
|
||||
this.writeLine(message, logLevel);
|
||||
|
||||
additionalMessages.forEach((line) => {
|
||||
this.writeLine(line, logLevel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public write(message: string, ...additionalMessages: string[]) {
|
||||
this.writeAtLevel(LogLevel.Normal, message, ...additionalMessages);
|
||||
}
|
||||
|
||||
public writeDiagnostic(message: string, ...additionalMessages: string[]) {
|
||||
this.writeAtLevel(LogLevel.Diagnostic, message, ...additionalMessages);
|
||||
}
|
||||
|
||||
public writeVerbose(message: string, ...additionalMessages: string[]) {
|
||||
this.writeAtLevel(LogLevel.Verbose, message, ...additionalMessages);
|
||||
}
|
||||
|
||||
public writeWarning(message: string, ...additionalMessages: string[]) {
|
||||
this.writeAtLevel(LogLevel.Warning, message, ...additionalMessages);
|
||||
}
|
||||
|
||||
public writeAndShowWarning(message: string, ...additionalMessages: string[]) {
|
||||
this.writeWarning(message, ...additionalMessages);
|
||||
|
||||
vscode.window.showWarningMessage(message, "Show Logs").then((selection) => {
|
||||
if (selection !== undefined) {
|
||||
this.showLogPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public writeError(message: string, ...additionalMessages: string[]) {
|
||||
this.writeAtLevel(LogLevel.Error, message, ...additionalMessages);
|
||||
}
|
||||
|
||||
public writeAndShowError(message: string, ...additionalMessages: string[]) {
|
||||
this.writeError(message, ...additionalMessages);
|
||||
|
||||
vscode.window.showErrorMessage(message, "Show Logs").then((selection) => {
|
||||
if (selection !== undefined) {
|
||||
this.showLogPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async writeAndShowErrorWithActions(
|
||||
message: string,
|
||||
actions: { prompt: string; action: () => Promise<void> }[]) {
|
||||
this.writeError(message);
|
||||
|
||||
const fullActions = [
|
||||
...actions,
|
||||
{ prompt: "Show Logs", action: async () => { this.showLogPanel(); } },
|
||||
];
|
||||
|
||||
const actionKeys: string[] = fullActions.map((action) => action.prompt);
|
||||
|
||||
const choice = await vscode.window.showErrorMessage(message, ...actionKeys);
|
||||
if (choice) {
|
||||
for (const action of fullActions) {
|
||||
if (choice === action.prompt) {
|
||||
await action.action();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public startNewLog(minimumLogLevel: string = "Normal") {
|
||||
this.MinimumLogLevel = this.logLevelNameToValue(minimumLogLevel.trim());
|
||||
|
||||
this.logSessionPath =
|
||||
path.resolve(
|
||||
this.logBasePath,
|
||||
`${Math.floor(Date.now() / 1000)}-${vscode.env.sessionId}`);
|
||||
|
||||
this.logFilePath = this.getLogFilePath("vscode-powershell");
|
||||
|
||||
utils.ensurePathExists(this.logSessionPath);
|
||||
}
|
||||
|
||||
private logLevelNameToValue(logLevelName: string): LogLevel {
|
||||
switch (logLevelName.toLowerCase()) {
|
||||
case "diagnostic": return LogLevel.Diagnostic;
|
||||
case "verbose": return LogLevel.Verbose;
|
||||
case "normal": return LogLevel.Normal;
|
||||
case "warning": return LogLevel.Warning;
|
||||
case "error": return LogLevel.Error;
|
||||
default: return LogLevel.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
private showLogPanel() {
|
||||
this.logChannel.show();
|
||||
}
|
||||
|
||||
private openLogFolder() {
|
||||
if (this.logSessionPath) {
|
||||
// Open the folder in VS Code since there isn't an easy way to
|
||||
// open the folder in the platform's file browser
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.openFolder",
|
||||
vscode.Uri.file(this.logSessionPath),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
private writeLine(message: string, level: LogLevel = LogLevel.Normal) {
|
||||
const now = new Date();
|
||||
const timestampedMessage =
|
||||
`${now.toLocaleDateString()} ${now.toLocaleTimeString()} [${LogLevel[level].toUpperCase()}] - ${message}`;
|
||||
|
||||
this.logChannel.appendLine(timestampedMessage);
|
||||
if (this.logFilePath) {
|
||||
fs.appendFile(
|
||||
this.logFilePath,
|
||||
timestampedMessage + os.EOL,
|
||||
(err) => {
|
||||
if (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(`Error writing to vscode-powershell log file: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,498 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as process from "process";
|
||||
import { IPowerShellAdditionalExePathSettings } from "./settings";
|
||||
|
||||
const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)";
|
||||
const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)";
|
||||
|
||||
const LinuxExePath = "/usr/bin/pwsh";
|
||||
const LinuxPreviewExePath = "/usr/bin/pwsh-preview";
|
||||
|
||||
const SnapExePath = "/snap/bin/pwsh";
|
||||
const SnapPreviewExePath = "/snap/bin/pwsh-preview";
|
||||
|
||||
const MacOSExePath = "/usr/local/bin/pwsh";
|
||||
const MacOSPreviewExePath = "/usr/local/bin/pwsh-preview";
|
||||
|
||||
export enum OperatingSystem {
|
||||
Unknown,
|
||||
Windows,
|
||||
MacOS,
|
||||
Linux,
|
||||
}
|
||||
|
||||
export interface IPlatformDetails {
|
||||
operatingSystem: OperatingSystem;
|
||||
isOS64Bit: boolean;
|
||||
isProcess64Bit: boolean;
|
||||
}
|
||||
|
||||
export interface IPowerShellExeDetails {
|
||||
readonly displayName: string;
|
||||
readonly exePath: string;
|
||||
}
|
||||
|
||||
export function getPlatformDetails(): IPlatformDetails {
|
||||
let operatingSystem = OperatingSystem.Unknown;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
operatingSystem = OperatingSystem.Windows;
|
||||
} else if (process.platform === "darwin") {
|
||||
operatingSystem = OperatingSystem.MacOS;
|
||||
} else if (process.platform === "linux") {
|
||||
operatingSystem = OperatingSystem.Linux;
|
||||
}
|
||||
|
||||
const isProcess64Bit = process.arch === "x64";
|
||||
|
||||
return {
|
||||
operatingSystem,
|
||||
isOS64Bit: isProcess64Bit || process.env.hasOwnProperty("PROCESSOR_ARCHITEW6432"),
|
||||
isProcess64Bit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to lazily find installed PowerShell executables on a machine.
|
||||
* When given a list of additional PowerShell executables,
|
||||
* this will also surface those at the end of the list.
|
||||
*/
|
||||
export class PowerShellExeFinder {
|
||||
// This is required, since parseInt("7-preview") will return 7.
|
||||
private static IntRegex: RegExp = /^\d+$/;
|
||||
|
||||
private static PwshMsixRegex: RegExp = /^Microsoft.PowerShell_.*/;
|
||||
|
||||
private static PwshPreviewMsixRegex: RegExp = /^Microsoft.PowerShellPreview_.*/;
|
||||
|
||||
// The platform details descriptor for the platform we're on
|
||||
private readonly platformDetails: IPlatformDetails;
|
||||
|
||||
// Additional configured PowerShells
|
||||
private readonly additionalPSExeSettings: Iterable<IPowerShellAdditionalExePathSettings>;
|
||||
|
||||
private winPS: IPossiblePowerShellExe;
|
||||
|
||||
private alternateBitnessWinPS: IPossiblePowerShellExe;
|
||||
|
||||
/**
|
||||
* Create a new PowerShellFinder object to discover PowerShell installations.
|
||||
* @param platformDetails Information about the machine we are running on.
|
||||
* @param additionalPowerShellExes Additional PowerShell installations as configured in the settings.
|
||||
*/
|
||||
constructor(
|
||||
platformDetails?: IPlatformDetails,
|
||||
additionalPowerShellExes?: Iterable<IPowerShellAdditionalExePathSettings>) {
|
||||
|
||||
this.platformDetails = platformDetails || getPlatformDetails();
|
||||
this.additionalPSExeSettings = additionalPowerShellExes || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first available PowerShell executable found in the search order.
|
||||
*/
|
||||
public getFirstAvailablePowerShellInstallation(): IPowerShellExeDetails {
|
||||
for (const pwsh of this.enumeratePowerShellInstallations()) {
|
||||
return pwsh;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all PowerShell executables found when searching for PowerShell installations.
|
||||
*/
|
||||
public getAllAvailablePowerShellInstallations(): IPowerShellExeDetails[] {
|
||||
return Array.from(this.enumeratePowerShellInstallations());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes PowerShell paths when Windows PowerShell is set to the non-native bitness.
|
||||
* @param configuredPowerShellPath the PowerShell path configured by the user.
|
||||
*/
|
||||
public fixWindowsPowerShellPath(configuredPowerShellPath: string): string {
|
||||
const altWinPS = this.findWinPS({ useAlternateBitness: true });
|
||||
|
||||
if (!altWinPS) {
|
||||
return configuredPowerShellPath;
|
||||
}
|
||||
|
||||
const lowerAltWinPSPath = altWinPS.exePath.toLocaleLowerCase();
|
||||
const lowerConfiguredPath = configuredPowerShellPath.toLocaleLowerCase();
|
||||
|
||||
if (lowerConfiguredPath === lowerAltWinPSPath) {
|
||||
return this.findWinPS().exePath;
|
||||
}
|
||||
|
||||
return configuredPowerShellPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through PowerShell installations on the machine according
|
||||
* to configuration passed in through the constructor.
|
||||
* PowerShell items returned by this object are verified
|
||||
* to exist on the filesystem.
|
||||
*/
|
||||
public *enumeratePowerShellInstallations(): Iterable<IPowerShellExeDetails> {
|
||||
// Get the default PowerShell installations first
|
||||
for (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) {
|
||||
if (defaultPwsh && defaultPwsh.exists()) {
|
||||
yield defaultPwsh;
|
||||
}
|
||||
}
|
||||
|
||||
// Also show any additionally configured PowerShells
|
||||
// These may be duplicates of the default installations, but given a different name.
|
||||
for (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) {
|
||||
if (additionalPwsh && additionalPwsh.exists()) {
|
||||
yield additionalPwsh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through all the possible well-known PowerShell installations on a machine.
|
||||
* Returned values may not exist, but come with an .exists property
|
||||
* which will check whether the executable exists.
|
||||
*/
|
||||
private *enumerateDefaultPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
|
||||
// Find PSCore stable first
|
||||
yield this.findPSCoreStable();
|
||||
|
||||
switch (this.platformDetails.operatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
// On Linux, find the snap
|
||||
yield this.findPSCoreStableSnap();
|
||||
break;
|
||||
|
||||
case OperatingSystem.Windows:
|
||||
// Windows may have a 32-bit pwsh.exe
|
||||
yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true });
|
||||
|
||||
// Also look for the MSIX/UWP installation
|
||||
yield this.findPSCoreMsix();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Look for the .NET global tool
|
||||
// Some older versions of PowerShell have a bug in this where startup will fail,
|
||||
// but this is fixed in newer versions
|
||||
yield this.findPSCoreDotnetGlobalTool();
|
||||
|
||||
// Look for PSCore preview
|
||||
yield this.findPSCorePreview();
|
||||
|
||||
switch (this.platformDetails.operatingSystem) {
|
||||
// On Linux, there might be a preview snap
|
||||
case OperatingSystem.Linux:
|
||||
yield this.findPSCorePreviewSnap();
|
||||
break;
|
||||
|
||||
case OperatingSystem.Windows:
|
||||
// Find a preview MSIX
|
||||
yield this.findPSCoreMsix({ findPreview: true });
|
||||
|
||||
// Look for pwsh-preview with the opposite bitness
|
||||
yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true, findPreview: true });
|
||||
|
||||
// Finally, get Windows PowerShell
|
||||
|
||||
// Get the natural Windows PowerShell for the process bitness
|
||||
yield this.findWinPS();
|
||||
|
||||
// Get the alternate bitness Windows PowerShell
|
||||
yield this.findWinPS({ useAlternateBitness: true });
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through the configured additonal PowerShell executable locations,
|
||||
* without checking for their existence.
|
||||
*/
|
||||
private *enumerateAdditionalPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
|
||||
for (const additionalPwshSetting of this.additionalPSExeSettings) {
|
||||
yield new PossiblePowerShellExe(additionalPwshSetting.exePath, additionalPwshSetting.versionName);
|
||||
}
|
||||
}
|
||||
|
||||
private findPSCoreStable(): IPossiblePowerShellExe {
|
||||
switch (this.platformDetails.operatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
return new PossiblePowerShellExe(LinuxExePath, "PowerShell");
|
||||
|
||||
case OperatingSystem.MacOS:
|
||||
return new PossiblePowerShellExe(MacOSExePath, "PowerShell");
|
||||
|
||||
case OperatingSystem.Windows:
|
||||
return this.findPSCoreWindowsInstallation();
|
||||
}
|
||||
}
|
||||
|
||||
private findPSCorePreview(): IPossiblePowerShellExe {
|
||||
switch (this.platformDetails.operatingSystem) {
|
||||
case OperatingSystem.Linux:
|
||||
return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview");
|
||||
|
||||
case OperatingSystem.MacOS:
|
||||
return new PossiblePowerShellExe(MacOSPreviewExePath, "PowerShell Preview");
|
||||
|
||||
case OperatingSystem.Windows:
|
||||
return this.findPSCoreWindowsInstallation({ findPreview: true });
|
||||
}
|
||||
}
|
||||
|
||||
private findPSCoreDotnetGlobalTool(): IPossiblePowerShellExe {
|
||||
const exeName: string = this.platformDetails.operatingSystem === OperatingSystem.Windows
|
||||
? "pwsh.exe"
|
||||
: "pwsh";
|
||||
|
||||
const dotnetGlobalToolExePath: string = path.join(os.homedir(), ".dotnet", "tools", exeName);
|
||||
|
||||
return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool");
|
||||
}
|
||||
|
||||
private findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): IPossiblePowerShellExe {
|
||||
// We can't proceed if there's no LOCALAPPDATA path
|
||||
if (!process.env.LOCALAPPDATA) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the base directory for MSIX application exe shortcuts
|
||||
const msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps");
|
||||
|
||||
if (!fs.existsSync(msixAppDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Define whether we're looking for the preview or the stable
|
||||
const { pwshMsixDirRegex, pwshMsixName } = findPreview
|
||||
? { pwshMsixDirRegex: PowerShellExeFinder.PwshPreviewMsixRegex, pwshMsixName: "PowerShell Preview (Store)" }
|
||||
: { pwshMsixDirRegex: PowerShellExeFinder.PwshMsixRegex, pwshMsixName: "PowerShell (Store)" };
|
||||
|
||||
// We should find only one such application, so return on the first one
|
||||
for (const subdir of fs.readdirSync(msixAppDir)) {
|
||||
if (pwshMsixDirRegex.test(subdir)) {
|
||||
const pwshMsixPath = path.join(msixAppDir, subdir, "pwsh.exe");
|
||||
return new PossiblePowerShellExe(pwshMsixPath, pwshMsixName);
|
||||
}
|
||||
}
|
||||
|
||||
// If we find nothing, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
private findPSCoreStableSnap(): IPossiblePowerShellExe {
|
||||
return new PossiblePowerShellExe(SnapExePath, "PowerShell Snap");
|
||||
}
|
||||
|
||||
private findPSCorePreviewSnap(): IPossiblePowerShellExe {
|
||||
return new PossiblePowerShellExe(SnapPreviewExePath, "PowerShell Preview Snap");
|
||||
}
|
||||
|
||||
private findPSCoreWindowsInstallation(
|
||||
{ useAlternateBitness = false, findPreview = false }:
|
||||
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): IPossiblePowerShellExe {
|
||||
|
||||
const programFilesPath: string = this.getProgramFilesPath({ useAlternateBitness });
|
||||
|
||||
if (!programFilesPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell");
|
||||
|
||||
// Ensure the base directory exists
|
||||
if (!(fs.existsSync(powerShellInstallBaseDir) && fs.lstatSync(powerShellInstallBaseDir).isDirectory())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let highestSeenVersion: number = -1;
|
||||
let pwshExePath: string = null;
|
||||
for (const item of fs.readdirSync(powerShellInstallBaseDir)) {
|
||||
|
||||
let currentVersion: number = -1;
|
||||
if (findPreview) {
|
||||
// We are looking for something like "7-preview"
|
||||
|
||||
// Preview dirs all have dashes in them
|
||||
const dashIndex = item.indexOf("-");
|
||||
if (dashIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that the part before the dash is an integer
|
||||
const intPart: string = item.substring(0, dashIndex);
|
||||
if (!PowerShellExeFinder.IntRegex.test(intPart)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that the part after the dash is "preview"
|
||||
if (item.substring(dashIndex + 1) !== "preview") {
|
||||
continue;
|
||||
}
|
||||
|
||||
currentVersion = parseInt(intPart, 10);
|
||||
} else {
|
||||
// Search for a directory like "6" or "7"
|
||||
if (!PowerShellExeFinder.IntRegex.test(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
currentVersion = parseInt(item, 10);
|
||||
}
|
||||
|
||||
// Ensure we haven't already seen a higher version
|
||||
if (currentVersion <= highestSeenVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now look for the file
|
||||
const exePath = path.join(powerShellInstallBaseDir, item, "pwsh.exe");
|
||||
if (!fs.existsSync(exePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pwshExePath = exePath;
|
||||
highestSeenVersion = currentVersion;
|
||||
}
|
||||
|
||||
if (!pwshExePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bitness: string = programFilesPath.includes("x86")
|
||||
? "(x86)"
|
||||
: "(x64)";
|
||||
|
||||
const preview: string = findPreview ? " Preview" : "";
|
||||
|
||||
return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview} ${bitness}`);
|
||||
}
|
||||
|
||||
private findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe {
|
||||
|
||||
// 32-bit OSes only have one WinPS on them
|
||||
if (!this.platformDetails.isOS64Bit && useAlternateBitness) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let winPS = useAlternateBitness ? this.alternateBitnessWinPS : this.winPS;
|
||||
if (winPS === undefined) {
|
||||
const systemFolderPath: string = this.getSystem32Path({ useAlternateBitness });
|
||||
|
||||
const winPSPath = path.join(systemFolderPath, "WindowsPowerShell", "v1.0", "powershell.exe");
|
||||
|
||||
let displayName: string;
|
||||
if (this.platformDetails.isProcess64Bit) {
|
||||
displayName = useAlternateBitness
|
||||
? WindowsPowerShell32BitLabel
|
||||
: WindowsPowerShell64BitLabel;
|
||||
} else if (this.platformDetails.isOS64Bit) {
|
||||
displayName = useAlternateBitness
|
||||
? WindowsPowerShell64BitLabel
|
||||
: WindowsPowerShell32BitLabel;
|
||||
} else {
|
||||
displayName = WindowsPowerShell32BitLabel;
|
||||
}
|
||||
|
||||
winPS = new PossiblePowerShellExe(winPSPath, displayName, { knownToExist: true });
|
||||
|
||||
if (useAlternateBitness) {
|
||||
this.alternateBitnessWinPS = winPS;
|
||||
} else {
|
||||
this.winPS = winPS;
|
||||
}
|
||||
}
|
||||
|
||||
return winPS;
|
||||
}
|
||||
|
||||
private getProgramFilesPath(
|
||||
{ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null {
|
||||
|
||||
if (!useAlternateBitness) {
|
||||
// Just use the native system bitness
|
||||
return process.env.ProgramFiles;
|
||||
}
|
||||
|
||||
// We might be a 64-bit process looking for 32-bit program files
|
||||
if (this.platformDetails.isProcess64Bit) {
|
||||
return process.env["ProgramFiles(x86)"];
|
||||
}
|
||||
|
||||
// We might be a 32-bit process looking for 64-bit program files
|
||||
if (this.platformDetails.isOS64Bit) {
|
||||
return process.env.ProgramW6432;
|
||||
}
|
||||
|
||||
// We're a 32-bit process on 32-bit Windows, there is no other Program Files dir
|
||||
return null;
|
||||
}
|
||||
|
||||
private getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null {
|
||||
const windir: string = process.env.windir;
|
||||
|
||||
if (!useAlternateBitness) {
|
||||
// Just use the native system bitness
|
||||
return path.join(windir, "System32");
|
||||
}
|
||||
|
||||
// We might be a 64-bit process looking for 32-bit system32
|
||||
if (this.platformDetails.isProcess64Bit) {
|
||||
return path.join(windir, "SysWOW64");
|
||||
}
|
||||
|
||||
// We might be a 32-bit process looking for 64-bit system32
|
||||
if (this.platformDetails.isOS64Bit) {
|
||||
return path.join(windir, "Sysnative");
|
||||
}
|
||||
|
||||
// We're on a 32-bit Windows, so no alternate bitness
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getWindowsSystemPowerShellPath(systemFolderName: string) {
|
||||
return path.join(
|
||||
process.env.windir,
|
||||
systemFolderName,
|
||||
"WindowsPowerShell",
|
||||
"v1.0",
|
||||
"powershell.exe");
|
||||
}
|
||||
|
||||
interface IPossiblePowerShellExe extends IPowerShellExeDetails {
|
||||
exists(): boolean;
|
||||
}
|
||||
|
||||
class PossiblePowerShellExe implements IPossiblePowerShellExe {
|
||||
public readonly exePath: string;
|
||||
public readonly displayName: string;
|
||||
|
||||
private knownToExist: boolean;
|
||||
|
||||
constructor(
|
||||
pathToExe: string,
|
||||
installationName: string,
|
||||
{ knownToExist = false }: { knownToExist?: boolean } = {}) {
|
||||
|
||||
this.exePath = pathToExe;
|
||||
this.displayName = installationName;
|
||||
this.knownToExist = knownToExist || undefined;
|
||||
}
|
||||
|
||||
public exists(): boolean {
|
||||
if (this.knownToExist === undefined) {
|
||||
this.knownToExist = fs.existsSync(this.exePath);
|
||||
}
|
||||
return this.knownToExist;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import cp = require("child_process");
|
||||
import fs = require("fs");
|
||||
import net = require("net");
|
||||
import os = require("os");
|
||||
import path = require("path");
|
||||
import vscode = require("vscode");
|
||||
import { Logger } from "./logging";
|
||||
import Settings = require("./settings");
|
||||
import utils = require("./utils");
|
||||
|
||||
export class PowerShellProcess {
|
||||
public static escapeSingleQuotes(pspath: string): string {
|
||||
return pspath.replace(new RegExp("'", "g"), "''");
|
||||
}
|
||||
|
||||
// This is used to warn the user that the extension is taking longer than expected to startup.
|
||||
// After the 15th try we've hit 30 seconds and should warn.
|
||||
private static warnUserThreshold = 15;
|
||||
|
||||
public onExited: vscode.Event<void>;
|
||||
private onExitedEmitter = new vscode.EventEmitter<void>();
|
||||
|
||||
private consoleTerminal: vscode.Terminal = undefined;
|
||||
private consoleCloseSubscription: vscode.Disposable;
|
||||
|
||||
constructor(
|
||||
public exePath: string,
|
||||
private bundledModulesPath: string,
|
||||
private title: string,
|
||||
private log: Logger,
|
||||
private startPsesArgs: string,
|
||||
private sessionFilePath: string,
|
||||
private sessionSettings: Settings.ISettings) {
|
||||
|
||||
this.onExited = this.onExitedEmitter.event;
|
||||
}
|
||||
|
||||
public async start(logFileName: string): Promise<utils.IEditorServicesSessionDetails> {
|
||||
const editorServicesLogPath = this.log.getLogFilePath(logFileName);
|
||||
|
||||
const psesModulePath =
|
||||
path.resolve(
|
||||
__dirname,
|
||||
this.bundledModulesPath,
|
||||
"PowerShellEditorServices/PowerShellEditorServices.psd1");
|
||||
|
||||
const featureFlags =
|
||||
this.sessionSettings.developer.featureFlags !== undefined
|
||||
? this.sessionSettings.developer.featureFlags.map((f) => `'${f}'`).join(", ")
|
||||
: "";
|
||||
|
||||
this.startPsesArgs +=
|
||||
`-LogPath '${PowerShellProcess.escapeSingleQuotes(editorServicesLogPath)}' ` +
|
||||
`-SessionDetailsPath '${PowerShellProcess.escapeSingleQuotes(this.sessionFilePath)}' ` +
|
||||
`-FeatureFlags @(${featureFlags}) `;
|
||||
|
||||
if (this.sessionSettings.integratedConsole.useLegacyReadLine) {
|
||||
this.startPsesArgs += "-UseLegacyReadLine";
|
||||
}
|
||||
|
||||
const powerShellArgs = [];
|
||||
|
||||
const useLoginShell: boolean =
|
||||
(utils.isMacOS && this.sessionSettings.startAsLoginShell.osx)
|
||||
|| (utils.isLinux && this.sessionSettings.startAsLoginShell.linux);
|
||||
|
||||
if (useLoginShell && this.isLoginShell(this.exePath)) {
|
||||
// This MUST be the first argument.
|
||||
powerShellArgs.push("-Login");
|
||||
}
|
||||
|
||||
powerShellArgs.push("-NoProfile");
|
||||
powerShellArgs.push("-NonInteractive");
|
||||
|
||||
// Only add ExecutionPolicy param on Windows
|
||||
if (utils.isWindows) {
|
||||
powerShellArgs.push("-ExecutionPolicy", "Bypass");
|
||||
}
|
||||
|
||||
const startEditorServices = "Import-Module '" +
|
||||
PowerShellProcess.escapeSingleQuotes(psesModulePath) +
|
||||
"'; Start-EditorServices " + this.startPsesArgs;
|
||||
|
||||
if (utils.isWindows) {
|
||||
powerShellArgs.push(
|
||||
"-Command",
|
||||
startEditorServices);
|
||||
} else {
|
||||
// Use -EncodedCommand for better quote support on non-Windows
|
||||
powerShellArgs.push(
|
||||
"-EncodedCommand",
|
||||
Buffer.from(startEditorServices, "utf16le").toString("base64"));
|
||||
}
|
||||
|
||||
this.log.write(
|
||||
"Language server starting --",
|
||||
" PowerShell executable: " + this.exePath,
|
||||
" PowerShell args: " + powerShellArgs.join(" "),
|
||||
" PowerShell Editor Services args: " + startEditorServices);
|
||||
|
||||
// Make sure no old session file exists
|
||||
utils.deleteSessionFile(this.sessionFilePath);
|
||||
|
||||
// Launch PowerShell in the integrated terminal
|
||||
this.consoleTerminal =
|
||||
vscode.window.createTerminal({
|
||||
name: this.title,
|
||||
shellPath: this.exePath,
|
||||
shellArgs: powerShellArgs,
|
||||
hideFromUser: !this.sessionSettings.integratedConsole.showOnStartup,
|
||||
cwd: this.sessionSettings.cwd
|
||||
});
|
||||
|
||||
const pwshName = path.basename(this.exePath);
|
||||
this.log.write(`${pwshName} started.`);
|
||||
|
||||
if (this.sessionSettings.integratedConsole.showOnStartup) {
|
||||
// We still need to run this to set the active terminal to the Integrated Console.
|
||||
this.consoleTerminal.show(true);
|
||||
}
|
||||
|
||||
// Start the language client
|
||||
this.log.write("Waiting for session file");
|
||||
const sessionDetails = await this.waitForSessionFile();
|
||||
|
||||
// Subscribe a log event for when the terminal closes
|
||||
this.log.write("Registering terminal close callback");
|
||||
this.consoleCloseSubscription = vscode.window.onDidCloseTerminal((terminal) => this.onTerminalClose(terminal));
|
||||
|
||||
// Log that the PowerShell terminal process has been started
|
||||
this.log.write("Registering terminal PID log callback");
|
||||
this.consoleTerminal.processId.then((pid) => this.logTerminalPid(pid, pwshName));
|
||||
|
||||
return sessionDetails;
|
||||
}
|
||||
|
||||
public showConsole(preserveFocus: boolean) {
|
||||
if (this.consoleTerminal) {
|
||||
this.consoleTerminal.show(preserveFocus);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
// Clean up the session file
|
||||
utils.deleteSessionFile(this.sessionFilePath);
|
||||
|
||||
if (this.consoleCloseSubscription) {
|
||||
this.consoleCloseSubscription.dispose();
|
||||
this.consoleCloseSubscription = undefined;
|
||||
}
|
||||
|
||||
if (this.consoleTerminal) {
|
||||
this.log.write("Terminating PowerShell process...");
|
||||
this.consoleTerminal.dispose();
|
||||
this.consoleTerminal = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private logTerminalPid(pid: number, exeName: string) {
|
||||
this.log.write(`${exeName} PID: ${pid}`);
|
||||
}
|
||||
|
||||
private isLoginShell(pwshPath: string): boolean {
|
||||
try {
|
||||
// We can't know what version of PowerShell we have without running it
|
||||
// So we try to start PowerShell with -Login
|
||||
// If it exits successfully, we return true
|
||||
// If it exits unsuccessfully, node throws, we catch, and return false
|
||||
cp.execFileSync(pwshPath, ["-Login", "-NoProfile", "-NoLogo", "-Command", "exit 0"]);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async waitForSessionFile(): Promise<utils.IEditorServicesSessionDetails> {
|
||||
// Determine how many tries by dividing by 2000 thus checking every 2 seconds.
|
||||
const numOfTries = this.sessionSettings.developer.waitForSessionFileTimeoutSeconds / 2;
|
||||
const warnAt = numOfTries - PowerShellProcess.warnUserThreshold;
|
||||
|
||||
// Check every 2 seconds
|
||||
for (let i = numOfTries; i > 0; i--) {
|
||||
if (utils.checkIfFileExists(this.sessionFilePath)) {
|
||||
this.log.write("Session file found");
|
||||
const sessionDetails = utils.readSessionFile(this.sessionFilePath);
|
||||
utils.deleteSessionFile(this.sessionFilePath);
|
||||
return sessionDetails;
|
||||
}
|
||||
|
||||
if (warnAt === i) {
|
||||
vscode.window.showWarningMessage(`Loading the PowerShell extension is taking longer than expected.
|
||||
If you're using privilege enforcement software, this can affect start up performance.`);
|
||||
}
|
||||
|
||||
// Wait a bit and try again
|
||||
await utils.sleep(2000);
|
||||
}
|
||||
|
||||
const err = "Timed out waiting for session file to appear.";
|
||||
this.log.write(err);
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
private onTerminalClose(terminal: vscode.Terminal) {
|
||||
if (terminal !== this.consoleTerminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.write("powershell.exe terminated or terminal UI was closed");
|
||||
this.onExitedEmitter.fire();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QuickPickItem, window, Disposable, QuickInputButton, QuickInput, ExtensionContext, QuickInputButtons, Uri } from 'vscode';
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Helper code that wraps the API for the multi-step case.
|
||||
// -------------------------------------------------------
|
||||
|
||||
type InputStep = (input: MultiStepInput) => Thenable<InputStep | void>;
|
||||
|
||||
interface QuickPickParameters<T extends QuickPickItem> {
|
||||
title: string;
|
||||
step: number;
|
||||
totalSteps: number;
|
||||
items: T[];
|
||||
activeItem?: T;
|
||||
placeholder: string;
|
||||
buttons?: QuickInputButton[];
|
||||
shouldResume: () => Thenable<boolean>;
|
||||
}
|
||||
|
||||
class InputFlowAction {
|
||||
static back = new InputFlowAction();
|
||||
static cancel = new InputFlowAction();
|
||||
static resume = new InputFlowAction();
|
||||
}
|
||||
|
||||
|
||||
interface InputBoxParameters {
|
||||
title: string;
|
||||
step: number;
|
||||
totalSteps: number;
|
||||
value: string;
|
||||
prompt: string;
|
||||
validate: (value: string) => Promise<string | undefined>;
|
||||
buttons?: QuickInputButton[];
|
||||
shouldResume: () => Thenable<boolean>;
|
||||
}
|
||||
|
||||
class MultiStepInput {
|
||||
|
||||
static async run<T>(start: InputStep) {
|
||||
const input = new MultiStepInput();
|
||||
return input.stepThrough(start);
|
||||
}
|
||||
|
||||
private current?: QuickInput;
|
||||
private steps: InputStep[] = [];
|
||||
|
||||
private async stepThrough<T>(start: InputStep) {
|
||||
let step: InputStep | void = start;
|
||||
while (step) {
|
||||
this.steps.push(step);
|
||||
if (this.current) {
|
||||
this.current.enabled = false;
|
||||
this.current.busy = true;
|
||||
}
|
||||
try {
|
||||
step = await step(this);
|
||||
} catch (err) {
|
||||
if (err === InputFlowAction.back) {
|
||||
this.steps.pop();
|
||||
step = this.steps.pop();
|
||||
} else if (err === InputFlowAction.resume) {
|
||||
step = this.steps.pop();
|
||||
} else if (err === InputFlowAction.cancel) {
|
||||
step = undefined;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.current) {
|
||||
this.current.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async showQuickPick<T extends QuickPickItem, P extends QuickPickParameters<T>>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P) {
|
||||
const disposables: Disposable[] = [];
|
||||
try {
|
||||
return await new Promise<T | (P extends { buttons: (infer I)[] } ? I : never)>((resolve, reject) => {
|
||||
const input = window.createQuickPick<T>();
|
||||
input.title = title;
|
||||
input.step = step;
|
||||
input.totalSteps = totalSteps;
|
||||
input.placeholder = placeholder;
|
||||
input.items = items;
|
||||
if (activeItem) {
|
||||
input.activeItems = [activeItem];
|
||||
}
|
||||
input.buttons = [
|
||||
...(this.steps.length > 1 ? [QuickInputButtons.Back] : []),
|
||||
...(buttons || [])
|
||||
];
|
||||
disposables.push(
|
||||
input.onDidTriggerButton(item => {
|
||||
if (item === QuickInputButtons.Back) {
|
||||
reject(InputFlowAction.back);
|
||||
} else {
|
||||
resolve(<any>item);
|
||||
}
|
||||
}),
|
||||
input.onDidChangeSelection(items => resolve(items[0])),
|
||||
input.onDidHide(() => {
|
||||
(async () => {
|
||||
reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel);
|
||||
})()
|
||||
.catch(reject);
|
||||
})
|
||||
);
|
||||
if (this.current) {
|
||||
this.current.dispose();
|
||||
}
|
||||
this.current = input;
|
||||
this.current.show();
|
||||
});
|
||||
} finally {
|
||||
disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
async showInputBox<P extends InputBoxParameters>({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P) {
|
||||
const disposables: Disposable[] = [];
|
||||
try {
|
||||
return await new Promise<string | (P extends { buttons: (infer I)[] } ? I : never)>((resolve, reject) => {
|
||||
const input = window.createInputBox();
|
||||
input.title = title;
|
||||
input.step = step;
|
||||
input.totalSteps = totalSteps;
|
||||
input.value = value || '';
|
||||
input.prompt = prompt;
|
||||
input.buttons = [
|
||||
...(this.steps.length > 1 ? [QuickInputButtons.Back] : []),
|
||||
...(buttons || [])
|
||||
];
|
||||
let validating = validate('');
|
||||
disposables.push(
|
||||
input.onDidTriggerButton(item => {
|
||||
if (item === QuickInputButtons.Back) {
|
||||
reject(InputFlowAction.back);
|
||||
} else {
|
||||
resolve(<any>item);
|
||||
}
|
||||
}),
|
||||
input.onDidAccept(async () => {
|
||||
const value = input.value;
|
||||
input.enabled = false;
|
||||
input.busy = true;
|
||||
if (!(await validate(value))) {
|
||||
resolve(value);
|
||||
}
|
||||
input.enabled = true;
|
||||
input.busy = false;
|
||||
}),
|
||||
input.onDidChangeValue(async text => {
|
||||
const current = validate(text);
|
||||
validating = current;
|
||||
const validationMessage = await current;
|
||||
if (current === validating) {
|
||||
input.validationMessage = validationMessage;
|
||||
}
|
||||
}),
|
||||
input.onDidHide(() => {
|
||||
(async () => {
|
||||
reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel);
|
||||
})()
|
||||
.catch(reject);
|
||||
})
|
||||
);
|
||||
if (this.current) {
|
||||
this.current.dispose();
|
||||
}
|
||||
this.current = input;
|
||||
this.current.show();
|
||||
});
|
||||
} finally {
|
||||
disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A multi-step input using window.createQuickPick() and window.createInputBox().
|
||||
*
|
||||
* This first part uses the helper class `MultiStepInput` that wraps the API for the multi-step case.
|
||||
*/
|
||||
export async function multiStepInput(context: ExtensionContext) {
|
||||
|
||||
|
||||
class MyButton implements QuickInputButton {
|
||||
constructor(public iconPath: { light: Uri; dark: Uri; }, public tooltip: string) { }
|
||||
}
|
||||
|
||||
const setSrcVersionButton = new MyButton({
|
||||
dark: Uri.file(context.asAbsolutePath('resources/dark/add.svg')),
|
||||
light: Uri.file(context.asAbsolutePath('resources/light/add.svg')),
|
||||
}, 'setSrcVersion');
|
||||
|
||||
const setTargetVersionButton = new MyButton({
|
||||
dark: Uri.file(context.asAbsolutePath('resources/dark/add.svg')),
|
||||
light: Uri.file(context.asAbsolutePath('resources/light/add.svg')),
|
||||
}, 'setTargetVersion');
|
||||
|
||||
const sourceVersionGroup: QuickPickItem[] = ['AzureRM','Az 1.0','Az 2.0','Az 3.0']
|
||||
.map(label => ({ label }));
|
||||
|
||||
|
||||
const targetVersionGroup: QuickPickItem[] = ['Az 1.0','Az 2.0','Az 3.0','Az 4.0']
|
||||
.map(label => ({ label }));
|
||||
|
||||
interface State {
|
||||
title: string;
|
||||
step: number;
|
||||
totalSteps: number;
|
||||
srcVersion: QuickPickItem | string;
|
||||
targetVersion: QuickPickItem | string;
|
||||
}
|
||||
|
||||
async function collectInputs() {
|
||||
const state = {} as Partial<State>;
|
||||
await MultiStepInput.run(input => setSourceVersionQuickPick(input, state));
|
||||
return state as State;
|
||||
}
|
||||
|
||||
const title = 'Set Migration Parameter';
|
||||
|
||||
async function setSourceVersionBox(input: MultiStepInput, state: Partial<State>) {
|
||||
state.srcVersion = await input.showInputBox({
|
||||
title,
|
||||
step: 1,
|
||||
totalSteps: 2,
|
||||
value: typeof state.srcVersion === 'string' ? state.srcVersion : '',
|
||||
prompt: 'Set Source Version',
|
||||
validate: validateNameIsUnique,
|
||||
shouldResume: shouldResume
|
||||
});
|
||||
return (input: MultiStepInput) => setTargetVersionBox(input, state);
|
||||
}
|
||||
|
||||
async function setSourceVersionQuickPick(input: MultiStepInput, state: Partial<State>) {
|
||||
const pick = await input.showQuickPick({
|
||||
title,
|
||||
step: 1,
|
||||
totalSteps: 2,
|
||||
placeholder: 'Set Source Version',
|
||||
items: sourceVersionGroup,
|
||||
activeItem: typeof state.srcVersion !== 'string' ? state.srcVersion : undefined,
|
||||
buttons: [setSrcVersionButton],
|
||||
shouldResume: shouldResume
|
||||
});
|
||||
if (pick instanceof MyButton) {
|
||||
return (input: MultiStepInput) => setSourceVersionBox(input, state);
|
||||
}
|
||||
state.srcVersion = pick.label;
|
||||
return (input: MultiStepInput) => setTargetVersionQuickPick(input, state);
|
||||
}
|
||||
|
||||
async function setTargetVersionBox(input: MultiStepInput, state: Partial<State>) {
|
||||
state.targetVersion = await input.showInputBox({
|
||||
title,
|
||||
step: 2,
|
||||
totalSteps: 2,
|
||||
value: typeof state.targetVersion === 'string' ? state.targetVersion : '',
|
||||
prompt: 'Set Target Version',
|
||||
validate: validateNameIsUnique,
|
||||
shouldResume: shouldResume
|
||||
});
|
||||
}
|
||||
|
||||
async function setTargetVersionQuickPick(input: MultiStepInput, state: Partial<State>) {
|
||||
const pick = await input.showQuickPick({
|
||||
title,
|
||||
step: 2,
|
||||
totalSteps: 2,
|
||||
placeholder: 'Set Target Version',
|
||||
items: targetVersionGroup,
|
||||
activeItem: typeof state.targetVersion !== 'string' ? state.targetVersion : undefined,
|
||||
buttons: [setTargetVersionButton],
|
||||
shouldResume: shouldResume
|
||||
});
|
||||
if (pick instanceof MyButton) {
|
||||
return (input: MultiStepInput) => setTargetVersionBox(input, state);
|
||||
}
|
||||
state.targetVersion = pick.label;
|
||||
}
|
||||
|
||||
|
||||
function shouldResume() {
|
||||
// Could show a notification with the option to resume.
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
// noop
|
||||
});
|
||||
}
|
||||
|
||||
async function validateNameIsUnique(name: string) {
|
||||
// ...validate...
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return name === 'vscode' ? 'Version not unique' : undefined;
|
||||
}
|
||||
|
||||
const state = await collectInputs();
|
||||
window.showInformationMessage(`Translating powershell scripts from '${state.srcVersion}' to '${state.targetVersion}'`);
|
||||
return [state.srcVersion,state.targetVersion];
|
||||
}
|
||||
|
||||
export async function getSrcVersion(){
|
||||
return window.showQuickPick(['AzureRM', 'Az1.0', 'Az2.0','Az3.0'],{
|
||||
placeHolder: 'Select source scripts version'
|
||||
});
|
||||
}
|
|
@ -0,0 +1,798 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import fs = require("fs");
|
||||
import net = require("net");
|
||||
import path = require("path");
|
||||
import * as semver from "semver";
|
||||
import vscode = require("vscode");
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
import { Message } from "vscode-jsonrpc";
|
||||
import { Logger } from "./logging";
|
||||
import { PowerShellProcess } from "./process";
|
||||
import Settings = require("./settings");
|
||||
import utils = require("./utils");
|
||||
|
||||
import {
|
||||
CloseAction, DocumentSelector, ErrorAction, LanguageClient, LanguageClientOptions,
|
||||
Middleware, NotificationType, RequestType0,
|
||||
ResolveCodeLensSignature, RevealOutputChannelOn, StreamInfo } from "vscode-languageclient";
|
||||
|
||||
import { GitHubReleaseInformation, InvokePowerShellUpdateCheck } from "./features/UpdatePowerShell";
|
||||
import {
|
||||
getPlatformDetails, IPlatformDetails, IPowerShellExeDetails,
|
||||
OperatingSystem, PowerShellExeFinder } from "./platform";
|
||||
import { LanguageClientConsumer } from "./languageClientConsumer";
|
||||
|
||||
export enum SessionStatus {
|
||||
NeverStarted,
|
||||
NotStarted,
|
||||
Initializing,
|
||||
Running,
|
||||
Stopping,
|
||||
Failed,
|
||||
}
|
||||
|
||||
export class SessionManager implements Middleware {
|
||||
public HostName: string;
|
||||
public HostVersion: string;
|
||||
public PowerShellExeDetails: IPowerShellExeDetails;
|
||||
private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu";
|
||||
private editorServicesArgs: string;
|
||||
private sessionStatus: SessionStatus = SessionStatus.NeverStarted;
|
||||
private suppressRestartPrompt: boolean;
|
||||
private focusConsoleOnExecute: boolean;
|
||||
private platformDetails: IPlatformDetails;
|
||||
private languageClientConsumers: LanguageClientConsumer[] = [];
|
||||
private statusBarItem: vscode.StatusBarItem;
|
||||
private languageServerProcess: PowerShellProcess;
|
||||
private debugSessionProcess: PowerShellProcess;
|
||||
private versionDetails: IPowerShellVersionDetails;
|
||||
private registeredCommands: vscode.Disposable[] = [];
|
||||
private languageServerClient: LanguageClient = undefined;
|
||||
private sessionSettings: Settings.ISettings = undefined;
|
||||
private sessionDetails: utils.IEditorServicesSessionDetails;
|
||||
private bundledModulesPath: string;
|
||||
private started: boolean = false;
|
||||
|
||||
// Initialized by the start() method, since this requires settings
|
||||
private powershellExeFinder: PowerShellExeFinder;
|
||||
|
||||
// When in development mode, VS Code's session ID is a fake
|
||||
// value of "someValue.machineId". Use that to detect dev
|
||||
// mode for now until Microsoft/vscode#10272 gets implemented.
|
||||
public readonly InDevelopmentMode =
|
||||
vscode.env.sessionId === "someValue.sessionId";
|
||||
|
||||
constructor(
|
||||
private log: Logger,
|
||||
private documentSelector: DocumentSelector,
|
||||
hostName: string,
|
||||
version: string,
|
||||
private telemetryReporter: TelemetryReporter) {
|
||||
|
||||
this.platformDetails = getPlatformDetails();
|
||||
|
||||
this.HostName = hostName;
|
||||
this.HostVersion = version;
|
||||
|
||||
const osBitness = this.platformDetails.isOS64Bit ? "64-bit" : "32-bit";
|
||||
const procBitness = this.platformDetails.isProcess64Bit ? "64-bit" : "32-bit";
|
||||
|
||||
this.log.write(
|
||||
`Visual Studio Code v${vscode.version} ${procBitness}`,
|
||||
`${this.HostName} Extension v${this.HostVersion}`,
|
||||
`Operating System: ${OperatingSystem[this.platformDetails.operatingSystem]} ${osBitness}`);
|
||||
|
||||
// Fix the host version so that PowerShell can consume it.
|
||||
// This is needed when the extension uses a prerelease
|
||||
// version string like 0.9.1-insiders-1234.
|
||||
this.HostVersion = this.HostVersion.split("-")[0];
|
||||
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// Stop the current session
|
||||
this.stop();
|
||||
|
||||
// Dispose of all commands
|
||||
this.registeredCommands.forEach((command) => { command.dispose(); });
|
||||
}
|
||||
|
||||
public setLanguageClientConsumers(languageClientConsumers: LanguageClientConsumer[]) {
|
||||
this.languageClientConsumers = languageClientConsumers;
|
||||
}
|
||||
|
||||
public start(exeNameOverride?: string) {
|
||||
this.sessionSettings = Settings.load();
|
||||
if (exeNameOverride) {
|
||||
this.sessionSettings.powerShellDefaultVersion = exeNameOverride;
|
||||
}
|
||||
|
||||
this.log.startNewLog(this.sessionSettings.developer.editorServicesLogLevel);
|
||||
|
||||
// Create the PowerShell executable finder now
|
||||
this.powershellExeFinder = new PowerShellExeFinder(
|
||||
this.platformDetails,
|
||||
this.sessionSettings.powerShellAdditionalExePaths);
|
||||
|
||||
this.focusConsoleOnExecute = this.sessionSettings.integratedConsole.focusConsoleOnExecute;
|
||||
|
||||
this.createStatusBarItem();
|
||||
|
||||
this.promptPowerShellExeSettingsCleanup();
|
||||
|
||||
this.migrateWhitespaceAroundPipeSetting();
|
||||
|
||||
try {
|
||||
let powerShellExeDetails;
|
||||
if (this.sessionSettings.powerShellDefaultVersion) {
|
||||
for (const details of this.powershellExeFinder.enumeratePowerShellInstallations()) {
|
||||
// Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723
|
||||
const wantedName = this.sessionSettings.powerShellDefaultVersion;
|
||||
if (wantedName.localeCompare(details.displayName, undefined, { sensitivity: "accent" }) === 0) {
|
||||
powerShellExeDetails = details;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.PowerShellExeDetails = powerShellExeDetails ||
|
||||
this.powershellExeFinder.getFirstAvailablePowerShellInstallation();
|
||||
|
||||
} catch (e) {
|
||||
this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`);
|
||||
}
|
||||
|
||||
this.suppressRestartPrompt = false;
|
||||
|
||||
if (!this.PowerShellExeDetails) {
|
||||
const message = "Unable to find PowerShell."
|
||||
+ " Do you have PowerShell installed?"
|
||||
+ " You can also configure custom PowerShell installations"
|
||||
+ " with the 'powershell.powerShellAdditionalExePaths' setting.";
|
||||
|
||||
this.log.writeAndShowErrorWithActions(message, [
|
||||
{
|
||||
prompt: "Get PowerShell",
|
||||
action: async () => {
|
||||
const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode");
|
||||
vscode.env.openExternal(getPSUri);
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath);
|
||||
|
||||
if (this.InDevelopmentMode) {
|
||||
const devBundledModulesPath =
|
||||
path.resolve(
|
||||
__dirname,
|
||||
this.sessionSettings.developer.bundledModulesPath);
|
||||
|
||||
// Make sure the module's bin path exists
|
||||
if (fs.existsSync(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) {
|
||||
this.bundledModulesPath = devBundledModulesPath;
|
||||
} else {
|
||||
this.log.write(
|
||||
"\nWARNING: In development mode but PowerShellEditorServices dev module path cannot be " +
|
||||
`found (or has not been built yet): ${devBundledModulesPath}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
this.editorServicesArgs =
|
||||
`-HostName 'Visual Studio Code Host' ` +
|
||||
`-HostProfileId 'Microsoft.VSCode' ` +
|
||||
`-HostVersion '${this.HostVersion}' ` +
|
||||
`-AdditionalModules @('PowerShellEditorServices.VSCode') ` +
|
||||
`-BundledModulesPath '${PowerShellProcess.escapeSingleQuotes(this.bundledModulesPath)}' ` +
|
||||
`-EnableConsoleRepl `;
|
||||
|
||||
if (this.sessionSettings.integratedConsole.suppressStartupBanner) {
|
||||
this.editorServicesArgs += "-StartupBanner '' ";
|
||||
} else {
|
||||
const startupBanner = `=====> ${this.HostName} Integrated Console v${this.HostVersion} <=====
|
||||
`;
|
||||
this.editorServicesArgs += `-StartupBanner '${startupBanner}' `;
|
||||
}
|
||||
|
||||
if (this.sessionSettings.developer.editorServicesWaitForDebugger) {
|
||||
this.editorServicesArgs += "-WaitForDebugger ";
|
||||
}
|
||||
if (this.sessionSettings.developer.editorServicesLogLevel) {
|
||||
this.editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `;
|
||||
}
|
||||
|
||||
this.startPowerShell();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
||||
// Shut down existing session if there is one
|
||||
this.log.write("Shutting down language client...");
|
||||
|
||||
if (this.sessionStatus === SessionStatus.Failed) {
|
||||
// Before moving further, clear out the client and process if
|
||||
// the process is already dead (i.e. it crashed)
|
||||
this.languageServerClient = undefined;
|
||||
this.languageServerProcess = undefined;
|
||||
}
|
||||
|
||||
this.sessionStatus = SessionStatus.Stopping;
|
||||
|
||||
// Close the language server client
|
||||
if (this.languageServerClient !== undefined) {
|
||||
this.languageServerClient.stop();
|
||||
this.languageServerClient = undefined;
|
||||
}
|
||||
|
||||
// Kill the PowerShell proceses we spawned
|
||||
if (this.debugSessionProcess) {
|
||||
this.debugSessionProcess.dispose();
|
||||
}
|
||||
if (this.languageServerProcess) {
|
||||
this.languageServerProcess.dispose();
|
||||
}
|
||||
|
||||
this.sessionStatus = SessionStatus.NotStarted;
|
||||
}
|
||||
|
||||
public restartSession(exeNameOverride?: string) {
|
||||
this.stop();
|
||||
this.start(exeNameOverride);
|
||||
}
|
||||
|
||||
public getSessionDetails(): utils.IEditorServicesSessionDetails {
|
||||
return this.sessionDetails;
|
||||
}
|
||||
|
||||
public getSessionStatus(): SessionStatus {
|
||||
return this.sessionStatus;
|
||||
}
|
||||
|
||||
public getPowerShellVersionDetails(): IPowerShellVersionDetails {
|
||||
return this.versionDetails;
|
||||
}
|
||||
|
||||
public createDebugSessionProcess(
|
||||
sessionPath: string,
|
||||
sessionSettings: Settings.ISettings): PowerShellProcess {
|
||||
|
||||
this.debugSessionProcess =
|
||||
new PowerShellProcess(
|
||||
this.PowerShellExeDetails.exePath,
|
||||
this.bundledModulesPath,
|
||||
"[TEMP] PowerShell Integrated Console",
|
||||
this.log,
|
||||
this.editorServicesArgs + "-DebugServiceOnly ",
|
||||
sessionPath,
|
||||
sessionSettings);
|
||||
|
||||
return this.debugSessionProcess;
|
||||
}
|
||||
|
||||
public async waitUntilStarted(): Promise<void> {
|
||||
while(!this.started) {
|
||||
await utils.sleep(300);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- LanguageClient middleware methods -----
|
||||
|
||||
public resolveCodeLens(
|
||||
codeLens: vscode.CodeLens,
|
||||
token: vscode.CancellationToken,
|
||||
next: ResolveCodeLensSignature): vscode.ProviderResult<vscode.CodeLens> {
|
||||
const resolvedCodeLens = next(codeLens, token);
|
||||
const resolveFunc =
|
||||
(codeLensToFix: vscode.CodeLens): vscode.CodeLens => {
|
||||
if (codeLensToFix.command?.command === "editor.action.showReferences") {
|
||||
const oldArgs = codeLensToFix.command.arguments;
|
||||
|
||||
// Our JSON objects don't get handled correctly by
|
||||
// VS Code's built in editor.action.showReferences
|
||||
// command so we need to convert them into the
|
||||
// appropriate types to send them as command
|
||||
// arguments.
|
||||
|
||||
codeLensToFix.command.arguments = [
|
||||
vscode.Uri.parse(oldArgs[0]),
|
||||
new vscode.Position(oldArgs[1].line, oldArgs[1].character),
|
||||
oldArgs[2].map((position) => {
|
||||
return new vscode.Location(
|
||||
vscode.Uri.parse(position.uri),
|
||||
new vscode.Range(
|
||||
position.range.start.line,
|
||||
position.range.start.character,
|
||||
position.range.end.line,
|
||||
position.range.end.character));
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return codeLensToFix;
|
||||
};
|
||||
|
||||
if ((resolvedCodeLens as Thenable<vscode.CodeLens>).then) {
|
||||
return (resolvedCodeLens as Thenable<vscode.CodeLens>).then(resolveFunc);
|
||||
} else if (resolvedCodeLens as vscode.CodeLens) {
|
||||
return resolveFunc(resolvedCodeLens as vscode.CodeLens);
|
||||
}
|
||||
|
||||
return resolvedCodeLens;
|
||||
}
|
||||
|
||||
// Move old setting codeFormatting.whitespaceAroundPipe to new setting codeFormatting.addWhitespaceAroundPipe
|
||||
private async migrateWhitespaceAroundPipeSetting() {
|
||||
const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId);
|
||||
const deprecatedSetting = 'codeFormatting.whitespaceAroundPipe'
|
||||
const newSetting = 'codeFormatting.addWhitespaceAroundPipe'
|
||||
const configurationTargetOfNewSetting = await Settings.getEffectiveConfigurationTarget(newSetting);
|
||||
if (configuration.has(deprecatedSetting) && configurationTargetOfNewSetting === null) {
|
||||
const configurationTarget = await Settings.getEffectiveConfigurationTarget(deprecatedSetting);
|
||||
const value = configuration.get(deprecatedSetting, configurationTarget)
|
||||
await Settings.change(newSetting, value, configurationTarget);
|
||||
await Settings.change(deprecatedSetting, undefined, configurationTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private async promptPowerShellExeSettingsCleanup() {
|
||||
if (this.sessionSettings.powerShellExePath) {
|
||||
let warningMessage = "The 'powerShell.powerShellExePath' setting is no longer used. ";
|
||||
warningMessage += this.sessionSettings.powerShellDefaultVersion
|
||||
? "We can automatically remove it for you."
|
||||
: "We can remove it from your settings and prompt you for which PowerShell you want to use.";
|
||||
|
||||
const choice = await vscode.window.showWarningMessage(warningMessage, "Let's do it!");
|
||||
|
||||
if (choice === undefined) {
|
||||
// They hit the 'x' to close the dialog.
|
||||
return;
|
||||
}
|
||||
|
||||
this.suppressRestartPrompt = true;
|
||||
try {
|
||||
await Settings.change("powerShellExePath", undefined, true);
|
||||
} finally {
|
||||
this.suppressRestartPrompt = false;
|
||||
}
|
||||
|
||||
// Show the session menu at the end if they don't have a PowerShellDefaultVersion.
|
||||
if (!this.sessionSettings.powerShellDefaultVersion) {
|
||||
await vscode.commands.executeCommand(this.ShowSessionMenuCommandName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigurationUpdated() {
|
||||
const settings = Settings.load();
|
||||
|
||||
this.focusConsoleOnExecute = settings.integratedConsole.focusConsoleOnExecute;
|
||||
|
||||
// Detect any setting changes that would affect the session
|
||||
if (!this.suppressRestartPrompt &&
|
||||
(settings.useX86Host !==
|
||||
this.sessionSettings.useX86Host ||
|
||||
settings.powerShellDefaultVersion.toLowerCase() !==
|
||||
this.sessionSettings.powerShellDefaultVersion.toLowerCase() ||
|
||||
settings.developer.editorServicesLogLevel.toLowerCase() !==
|
||||
this.sessionSettings.developer.editorServicesLogLevel.toLowerCase() ||
|
||||
settings.developer.bundledModulesPath.toLowerCase() !==
|
||||
this.sessionSettings.developer.bundledModulesPath.toLowerCase() ||
|
||||
settings.integratedConsole.useLegacyReadLine !==
|
||||
this.sessionSettings.integratedConsole.useLegacyReadLine)) {
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
"The PowerShell runtime configuration has changed, would you like to start a new session?",
|
||||
"Yes", "No")
|
||||
.then((response) => {
|
||||
if (response === "Yes") {
|
||||
this.restartSession();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setStatusBarVersionString(runspaceDetails: IRunspaceDetails) {
|
||||
|
||||
const psVersion = runspaceDetails.powerShellVersion;
|
||||
|
||||
let versionString =
|
||||
this.versionDetails.architecture === "x86"
|
||||
? `${psVersion.displayVersion} (${psVersion.architecture})`
|
||||
: psVersion.displayVersion;
|
||||
|
||||
if (runspaceDetails.runspaceType !== RunspaceType.Local) {
|
||||
versionString += ` [${runspaceDetails.connectionString}]`;
|
||||
}
|
||||
|
||||
this.setSessionStatus(
|
||||
versionString,
|
||||
SessionStatus.Running);
|
||||
}
|
||||
|
||||
private registerCommands(): void {
|
||||
this.registeredCommands = [
|
||||
vscode.commands.registerCommand("PowerShell.RestartSession", () => { this.restartSession(); }),
|
||||
vscode.commands.registerCommand(this.ShowSessionMenuCommandName, () => { this.showSessionMenu(); }),
|
||||
vscode.workspace.onDidChangeConfiguration(() => this.onConfigurationUpdated()),
|
||||
vscode.commands.registerCommand(
|
||||
"PowerShell.ShowSessionConsole", (isExecute?: boolean) => { this.showSessionConsole(isExecute); }),
|
||||
];
|
||||
}
|
||||
|
||||
private startPowerShell() {
|
||||
|
||||
this.setSessionStatus(
|
||||
"Starting PowerShell...",
|
||||
SessionStatus.Initializing);
|
||||
|
||||
const sessionFilePath =
|
||||
utils.getSessionFilePath(
|
||||
Math.floor(100000 + Math.random() * 900000));
|
||||
|
||||
this.languageServerProcess =
|
||||
new PowerShellProcess(
|
||||
this.PowerShellExeDetails.exePath,
|
||||
this.bundledModulesPath,
|
||||
"PowerShell Integrated Console",
|
||||
this.log,
|
||||
this.editorServicesArgs,
|
||||
sessionFilePath,
|
||||
this.sessionSettings);
|
||||
|
||||
this.languageServerProcess.onExited(
|
||||
() => {
|
||||
if (this.sessionStatus === SessionStatus.Running) {
|
||||
this.setSessionStatus("Session exited", SessionStatus.Failed);
|
||||
this.promptForRestart();
|
||||
}
|
||||
});
|
||||
|
||||
this.languageServerProcess
|
||||
.start("EditorServices")
|
||||
.then(
|
||||
(sessionDetails) => {
|
||||
this.sessionDetails = sessionDetails;
|
||||
|
||||
if (sessionDetails.status === "started") {
|
||||
this.log.write("Language server started.");
|
||||
|
||||
// Start the language service client
|
||||
this.startLanguageClient(sessionDetails);
|
||||
} else if (sessionDetails.status === "failed") {
|
||||
if (sessionDetails.reason === "unsupported") {
|
||||
this.setSessionFailure(
|
||||
"PowerShell language features are only supported on PowerShell version 5.1 and 6.1" +
|
||||
` and above. The current version is ${sessionDetails.powerShellVersion}.`);
|
||||
} else if (sessionDetails.reason === "languageMode") {
|
||||
this.setSessionFailure(
|
||||
"PowerShell language features are disabled due to an unsupported LanguageMode: " +
|
||||
`${sessionDetails.detail}`);
|
||||
} else {
|
||||
this.setSessionFailure(
|
||||
`PowerShell could not be started for an unknown reason '${sessionDetails.reason}'`);
|
||||
}
|
||||
} else {
|
||||
// TODO: Handle other response cases
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.log.write("Language server startup failed.");
|
||||
this.setSessionFailure("The language service could not be started: ", error);
|
||||
},
|
||||
)
|
||||
.catch((error) => {
|
||||
this.log.write("Language server startup failed.");
|
||||
this.setSessionFailure("The language server could not be started: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
private promptForRestart() {
|
||||
vscode.window.showErrorMessage(
|
||||
"The PowerShell session has terminated due to an error, would you like to restart it?",
|
||||
"Yes", "No")
|
||||
.then((answer) => { if (answer === "Yes") { this.restartSession(); }});
|
||||
}
|
||||
|
||||
private startLanguageClient(sessionDetails: utils.IEditorServicesSessionDetails) {
|
||||
// Log the session details object
|
||||
this.log.write(JSON.stringify(sessionDetails));
|
||||
|
||||
try {
|
||||
this.log.write(`Connecting to language service on pipe ${sessionDetails.languageServicePipeName}...`);
|
||||
|
||||
const connectFunc = () => {
|
||||
return new Promise<StreamInfo>(
|
||||
(resolve, reject) => {
|
||||
const socket = net.connect(sessionDetails.languageServicePipeName);
|
||||
socket.on(
|
||||
"connect",
|
||||
() => {
|
||||
this.log.write("Language service connected.");
|
||||
resolve({writer: socket, reader: socket});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: this.documentSelector,
|
||||
synchronize: {
|
||||
// backend uses "files" and "search" to ignore references.
|
||||
configurationSection: [ utils.PowerShellLanguageId, "files", "search" ],
|
||||
// fileEvents: vscode.workspace.createFileSystemWatcher('**/.eslintrc')
|
||||
},
|
||||
errorHandler: {
|
||||
// Override the default error handler to prevent it from
|
||||
// closing the LanguageClient incorrectly when the socket
|
||||
// hangs up (ECONNRESET errors).
|
||||
error: (error: any, message: Message, count: number): ErrorAction => {
|
||||
// TODO: Is there any error worth terminating on?
|
||||
return ErrorAction.Continue;
|
||||
},
|
||||
closed: () => {
|
||||
// We have our own restart experience
|
||||
return CloseAction.DoNotRestart;
|
||||
},
|
||||
},
|
||||
revealOutputChannelOn: RevealOutputChannelOn.Never,
|
||||
middleware: this,
|
||||
};
|
||||
|
||||
this.languageServerClient =
|
||||
new LanguageClient(
|
||||
"PowerShell Editor Services",
|
||||
connectFunc,
|
||||
clientOptions);
|
||||
|
||||
this.languageServerClient.onReady().then(
|
||||
() => {
|
||||
this.languageServerClient
|
||||
.sendRequest(PowerShellVersionRequestType)
|
||||
.then(
|
||||
async (versionDetails) => {
|
||||
this.versionDetails = versionDetails;
|
||||
this.started = true;
|
||||
|
||||
if (!this.InDevelopmentMode) {
|
||||
this.telemetryReporter.sendTelemetryEvent("powershellVersionCheck",
|
||||
{ powershellVersion: versionDetails.version });
|
||||
}
|
||||
|
||||
this.setSessionStatus(
|
||||
this.versionDetails.architecture === "x86"
|
||||
? `${this.versionDetails.displayVersion} (${this.versionDetails.architecture})`
|
||||
: this.versionDetails.displayVersion,
|
||||
SessionStatus.Running);
|
||||
|
||||
// If the user opted to not check for updates, then don't.
|
||||
if (!this.sessionSettings.promptToUpdatePowerShell) { return; }
|
||||
|
||||
try {
|
||||
const localVersion = semver.parse(this.versionDetails.version);
|
||||
if (semver.lt(localVersion, "6.0.0")) {
|
||||
// Skip prompting when using Windows PowerShell for now.
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the latest PowerShell releases from GitHub.
|
||||
const isPreRelease = !!semver.prerelease(localVersion);
|
||||
const release: GitHubReleaseInformation =
|
||||
await GitHubReleaseInformation.FetchLatestRelease(isPreRelease);
|
||||
|
||||
await InvokePowerShellUpdateCheck(
|
||||
this,
|
||||
this.languageServerClient,
|
||||
localVersion,
|
||||
this.versionDetails.architecture,
|
||||
release);
|
||||
} catch (e) {
|
||||
// best effort. This probably failed to fetch the data from GitHub.
|
||||
this.log.writeWarning(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Send the new LanguageClient to extension features
|
||||
// so that they can register their message handlers
|
||||
// before the connection is established.
|
||||
this.updateLanguageClientConsumers(this.languageServerClient);
|
||||
this.languageServerClient.onNotification(
|
||||
RunspaceChangedEventType,
|
||||
(runspaceDetails) => { this.setStatusBarVersionString(runspaceDetails); });
|
||||
},
|
||||
(reason) => {
|
||||
this.setSessionFailure("Could not start language service: ", reason);
|
||||
});
|
||||
|
||||
this.languageServerClient.start();
|
||||
} catch (e) {
|
||||
this.setSessionFailure("The language service could not be started: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLanguageClientConsumers(languageClient: LanguageClient) {
|
||||
this.languageClientConsumers.forEach((feature) => {
|
||||
feature.setLanguageClient(languageClient);
|
||||
});
|
||||
}
|
||||
|
||||
private createStatusBarItem() {
|
||||
if (this.statusBarItem === undefined) {
|
||||
// Create the status bar item and place it right next
|
||||
// to the language indicator
|
||||
this.statusBarItem =
|
||||
vscode.window.createStatusBarItem(
|
||||
vscode.StatusBarAlignment.Right,
|
||||
1);
|
||||
|
||||
this.statusBarItem.command = this.ShowSessionMenuCommandName;
|
||||
this.statusBarItem.tooltip = "Show PowerShell Session Menu";
|
||||
this.statusBarItem.show();
|
||||
vscode.window.onDidChangeActiveTextEditor((textEditor) => {
|
||||
if (textEditor === undefined
|
||||
|| textEditor.document.languageId !== "powershell") {
|
||||
this.statusBarItem.hide();
|
||||
} else {
|
||||
this.statusBarItem.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setSessionStatus(statusText: string, status: SessionStatus): void {
|
||||
// Set color and icon for 'Running' by default
|
||||
let statusIconText = "$(terminal) ";
|
||||
let statusColor = "#affc74";
|
||||
|
||||
if (status === SessionStatus.Initializing) {
|
||||
statusIconText = "$(sync) ";
|
||||
statusColor = "#f3fc74";
|
||||
} else if (status === SessionStatus.Failed) {
|
||||
statusIconText = "$(alert) ";
|
||||
statusColor = "#fcc174";
|
||||
}
|
||||
|
||||
this.sessionStatus = status;
|
||||
this.statusBarItem.color = statusColor;
|
||||
this.statusBarItem.text = statusIconText + statusText;
|
||||
}
|
||||
|
||||
private setSessionFailure(message: string, ...additionalMessages: string[]) {
|
||||
this.log.writeAndShowError(message, ...additionalMessages);
|
||||
|
||||
this.setSessionStatus(
|
||||
"Initialization Error",
|
||||
SessionStatus.Failed);
|
||||
}
|
||||
|
||||
private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails) {
|
||||
this.suppressRestartPrompt = true;
|
||||
await Settings.change("powerShellDefaultVersion", exePath.displayName, true);
|
||||
|
||||
// We pass in the display name so that we force the extension to use that version
|
||||
// rather than pull from the settings. The issue we prevent here is when a
|
||||
// workspace setting is defined which gets priority over user settings which
|
||||
// is what the change above sets.
|
||||
this.restartSession(exePath.displayName);
|
||||
}
|
||||
|
||||
private showSessionConsole(isExecute?: boolean) {
|
||||
if (this.languageServerProcess) {
|
||||
this.languageServerProcess.showConsole(isExecute && !this.focusConsoleOnExecute);
|
||||
}
|
||||
}
|
||||
|
||||
private showSessionMenu() {
|
||||
const availablePowerShellExes = this.powershellExeFinder.getAllAvailablePowerShellInstallations();
|
||||
|
||||
let sessionText: string;
|
||||
|
||||
switch (this.sessionStatus) {
|
||||
case SessionStatus.Running:
|
||||
case SessionStatus.Initializing:
|
||||
case SessionStatus.NotStarted:
|
||||
case SessionStatus.NeverStarted:
|
||||
case SessionStatus.Stopping:
|
||||
const currentPowerShellExe =
|
||||
availablePowerShellExes
|
||||
.find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails.displayName);
|
||||
|
||||
const powerShellSessionName =
|
||||
currentPowerShellExe ?
|
||||
currentPowerShellExe.displayName :
|
||||
`PowerShell ${this.versionDetails.displayVersion} ` +
|
||||
`(${this.versionDetails.architecture}) ${this.versionDetails.edition} Edition ` +
|
||||
`[${this.versionDetails.version}]`;
|
||||
|
||||
sessionText = `Current session: ${powerShellSessionName}`;
|
||||
break;
|
||||
|
||||
case SessionStatus.Failed:
|
||||
sessionText = "Session initialization failed, click here to show PowerShell extension logs";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new TypeError("Not a valid value for the enum 'SessionStatus'");
|
||||
}
|
||||
|
||||
const powerShellItems =
|
||||
availablePowerShellExes
|
||||
.filter((item) => item.displayName !== this.PowerShellExeDetails.displayName)
|
||||
.map((item) => {
|
||||
return new SessionMenuItem(
|
||||
`Switch to: ${item.displayName}`,
|
||||
() => { this.changePowerShellDefaultVersion(item); });
|
||||
});
|
||||
|
||||
const menuItems: SessionMenuItem[] = [
|
||||
new SessionMenuItem(
|
||||
sessionText,
|
||||
() => { vscode.commands.executeCommand("PowerShell.ShowLogs"); }),
|
||||
|
||||
// Add all of the different PowerShell options
|
||||
...powerShellItems,
|
||||
|
||||
new SessionMenuItem(
|
||||
"Restart Current Session",
|
||||
() => {
|
||||
// We pass in the display name so we guarentee that the session
|
||||
// will be the same PowerShell.
|
||||
this.restartSession(this.PowerShellExeDetails.displayName);
|
||||
}),
|
||||
|
||||
new SessionMenuItem(
|
||||
"Open Session Logs Folder",
|
||||
() => { vscode.commands.executeCommand("PowerShell.OpenLogFolder"); }),
|
||||
|
||||
new SessionMenuItem(
|
||||
"Modify 'powerShell.powerShellAdditionalExePaths' in Settings",
|
||||
() => { vscode.commands.executeCommand("workbench.action.openSettingsJson"); }),
|
||||
];
|
||||
|
||||
vscode
|
||||
.window
|
||||
.showQuickPick<SessionMenuItem>(menuItems)
|
||||
.then((selectedItem) => { selectedItem.callback(); });
|
||||
}
|
||||
}
|
||||
|
||||
class SessionMenuItem implements vscode.QuickPickItem {
|
||||
public description: string;
|
||||
|
||||
constructor(
|
||||
public readonly label: string,
|
||||
// tslint:disable-next-line:no-empty
|
||||
public readonly callback: () => void = () => {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export const PowerShellVersionRequestType =
|
||||
new RequestType0<IPowerShellVersionDetails, void, void>(
|
||||
"powerShell/getVersion");
|
||||
|
||||
export const RunspaceChangedEventType =
|
||||
new NotificationType<IRunspaceDetails, void>(
|
||||
"powerShell/runspaceChanged");
|
||||
|
||||
export enum RunspaceType {
|
||||
Local,
|
||||
Process,
|
||||
Remote,
|
||||
}
|
||||
|
||||
export interface IPowerShellVersionDetails {
|
||||
version: string;
|
||||
displayVersion: string;
|
||||
edition: string;
|
||||
architecture: string;
|
||||
}
|
||||
|
||||
export interface IRunspaceDetails {
|
||||
powerShellVersion: IPowerShellVersionDetails;
|
||||
runspaceType: RunspaceType;
|
||||
connectionString: string;
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import vscode = require("vscode");
|
||||
import utils = require("./utils");
|
||||
|
||||
enum CodeFormattingPreset {
|
||||
Custom,
|
||||
Allman,
|
||||
OTBS,
|
||||
Stroustrup,
|
||||
}
|
||||
|
||||
enum PipelineIndentationStyle {
|
||||
IncreaseIndentationForFirstPipeline,
|
||||
IncreaseIndentationAfterEveryPipeline,
|
||||
NoIndentation,
|
||||
None,
|
||||
}
|
||||
|
||||
export enum CommentType {
|
||||
Disabled = "Disabled",
|
||||
BlockComment = "BlockComment",
|
||||
LineComment = "LineComment",
|
||||
}
|
||||
|
||||
export interface IPowerShellAdditionalExePathSettings {
|
||||
versionName: string;
|
||||
exePath: string;
|
||||
}
|
||||
|
||||
export interface IBugReportingSettings {
|
||||
project: string;
|
||||
}
|
||||
|
||||
export interface ICodeFoldingSettings {
|
||||
enable?: boolean;
|
||||
showLastLine?: boolean;
|
||||
}
|
||||
|
||||
export interface ICodeFormattingSettings {
|
||||
autoCorrectAliases: boolean;
|
||||
preset: CodeFormattingPreset;
|
||||
openBraceOnSameLine: boolean;
|
||||
newLineAfterOpenBrace: boolean;
|
||||
newLineAfterCloseBrace: boolean;
|
||||
pipelineIndentationStyle: PipelineIndentationStyle;
|
||||
whitespaceBeforeOpenBrace: boolean;
|
||||
whitespaceBeforeOpenParen: boolean;
|
||||
whitespaceAroundOperator: boolean;
|
||||
whitespaceAfterSeparator: boolean;
|
||||
whitespaceBetweenParameters: boolean;
|
||||
whitespaceInsideBrace: boolean;
|
||||
addWhitespaceAroundPipe: boolean;
|
||||
trimWhitespaceAroundPipe: boolean;
|
||||
ignoreOneLineBlock: boolean;
|
||||
alignPropertyValuePairs: boolean;
|
||||
useCorrectCasing: boolean;
|
||||
}
|
||||
|
||||
export interface IScriptAnalysisSettings {
|
||||
enable?: boolean;
|
||||
settingsPath: string;
|
||||
}
|
||||
|
||||
export interface IDebuggingSettings {
|
||||
createTemporaryIntegratedConsole?: boolean;
|
||||
}
|
||||
|
||||
export interface IDeveloperSettings {
|
||||
featureFlags?: string[];
|
||||
bundledModulesPath?: string;
|
||||
editorServicesLogLevel?: string;
|
||||
editorServicesWaitForDebugger?: boolean;
|
||||
waitForSessionFileTimeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export interface ISettings {
|
||||
powerShellAdditionalExePaths?: IPowerShellAdditionalExePathSettings[];
|
||||
powerShellDefaultVersion?: string;
|
||||
// This setting is no longer used but is here to assist in cleaning up the users settings.
|
||||
powerShellExePath?: string;
|
||||
promptToUpdatePowerShell?: boolean;
|
||||
promptToUpdatePackageManagement?: boolean;
|
||||
bundledModulesPath?: string;
|
||||
startAsLoginShell?: IStartAsLoginShellSettings;
|
||||
startAutomatically?: boolean;
|
||||
useX86Host?: boolean;
|
||||
enableProfileLoading?: boolean;
|
||||
helpCompletion: string;
|
||||
scriptAnalysis?: IScriptAnalysisSettings;
|
||||
debugging?: IDebuggingSettings;
|
||||
developer?: IDeveloperSettings;
|
||||
codeFolding?: ICodeFoldingSettings;
|
||||
codeFormatting?: ICodeFormattingSettings;
|
||||
integratedConsole?: IIntegratedConsoleSettings;
|
||||
bugReporting?: IBugReportingSettings;
|
||||
sideBar?: ISideBarSettings;
|
||||
pester?: IPesterSettings;
|
||||
buttons?: IButtonSettings;
|
||||
cwd?: string;
|
||||
notebooks?: INotebooksSettings;
|
||||
}
|
||||
|
||||
export interface IStartAsLoginShellSettings {
|
||||
osx?: boolean;
|
||||
linux?: boolean;
|
||||
}
|
||||
|
||||
export interface IIntegratedConsoleSettings {
|
||||
showOnStartup?: boolean;
|
||||
focusConsoleOnExecute?: boolean;
|
||||
useLegacyReadLine?: boolean;
|
||||
forceClearScrollbackBuffer?: boolean;
|
||||
suppressStartupBanner?: boolean;
|
||||
}
|
||||
|
||||
export interface ISideBarSettings {
|
||||
CommandExplorerVisibility?: boolean;
|
||||
}
|
||||
|
||||
export interface IPesterSettings {
|
||||
useLegacyCodeLens?: boolean;
|
||||
outputVerbosity?: string;
|
||||
debugOutputVerbosity?: string;
|
||||
}
|
||||
|
||||
export interface IButtonSettings {
|
||||
showRunButtons?: boolean;
|
||||
showPanelMovementButtons?: boolean;
|
||||
}
|
||||
|
||||
export interface INotebooksSettings {
|
||||
saveMarkdownCellsAs?: CommentType;
|
||||
}
|
||||
|
||||
export function load(): ISettings {
|
||||
const configuration: vscode.WorkspaceConfiguration =
|
||||
vscode.workspace.getConfiguration(
|
||||
utils.PowerShellLanguageId);
|
||||
|
||||
const defaultBugReportingSettings: IBugReportingSettings = {
|
||||
project: "https://github.com/PowerShell/vscode-powershell",
|
||||
};
|
||||
|
||||
const defaultScriptAnalysisSettings: IScriptAnalysisSettings = {
|
||||
enable: true,
|
||||
settingsPath: "PSScriptAnalyzerSettings.psd1",
|
||||
};
|
||||
|
||||
const defaultDebuggingSettings: IDebuggingSettings = {
|
||||
createTemporaryIntegratedConsole: false,
|
||||
};
|
||||
|
||||
const defaultDeveloperSettings: IDeveloperSettings = {
|
||||
featureFlags: [],
|
||||
bundledModulesPath: "../../server/module",
|
||||
editorServicesLogLevel: "Normal",
|
||||
editorServicesWaitForDebugger: false,
|
||||
waitForSessionFileTimeoutSeconds: 240,
|
||||
};
|
||||
|
||||
const defaultCodeFoldingSettings: ICodeFoldingSettings = {
|
||||
enable: true,
|
||||
showLastLine: false,
|
||||
};
|
||||
|
||||
const defaultCodeFormattingSettings: ICodeFormattingSettings = {
|
||||
autoCorrectAliases: false,
|
||||
preset: CodeFormattingPreset.Custom,
|
||||
openBraceOnSameLine: true,
|
||||
newLineAfterOpenBrace: true,
|
||||
newLineAfterCloseBrace: true,
|
||||
pipelineIndentationStyle: PipelineIndentationStyle.NoIndentation,
|
||||
whitespaceBeforeOpenBrace: true,
|
||||
whitespaceBeforeOpenParen: true,
|
||||
whitespaceAroundOperator: true,
|
||||
whitespaceAfterSeparator: true,
|
||||
whitespaceBetweenParameters: false,
|
||||
whitespaceInsideBrace: true,
|
||||
addWhitespaceAroundPipe: true,
|
||||
trimWhitespaceAroundPipe: false,
|
||||
ignoreOneLineBlock: true,
|
||||
alignPropertyValuePairs: true,
|
||||
useCorrectCasing: false,
|
||||
};
|
||||
|
||||
const defaultStartAsLoginShellSettings: IStartAsLoginShellSettings = {
|
||||
osx: true,
|
||||
linux: false,
|
||||
};
|
||||
|
||||
const defaultIntegratedConsoleSettings: IIntegratedConsoleSettings = {
|
||||
showOnStartup: true,
|
||||
focusConsoleOnExecute: true,
|
||||
useLegacyReadLine: false,
|
||||
forceClearScrollbackBuffer: false,
|
||||
};
|
||||
|
||||
const defaultSideBarSettings: ISideBarSettings = {
|
||||
CommandExplorerVisibility: true,
|
||||
};
|
||||
|
||||
const defaultButtonSettings: IButtonSettings = {
|
||||
showRunButtons: true,
|
||||
showPanelMovementButtons: false
|
||||
};
|
||||
|
||||
const defaultPesterSettings: IPesterSettings = {
|
||||
useLegacyCodeLens: true,
|
||||
outputVerbosity: "FromPreference",
|
||||
debugOutputVerbosity: "Diagnostic",
|
||||
};
|
||||
|
||||
const defaultNotebooksSettings: INotebooksSettings = {
|
||||
saveMarkdownCellsAs: CommentType.BlockComment,
|
||||
};
|
||||
|
||||
return {
|
||||
startAutomatically:
|
||||
configuration.get<boolean>("startAutomatically", true),
|
||||
powerShellAdditionalExePaths:
|
||||
configuration.get<IPowerShellAdditionalExePathSettings[]>("powerShellAdditionalExePaths", undefined),
|
||||
powerShellDefaultVersion:
|
||||
configuration.get<string>("powerShellDefaultVersion", undefined),
|
||||
powerShellExePath:
|
||||
configuration.get<string>("powerShellExePath", undefined),
|
||||
promptToUpdatePowerShell:
|
||||
configuration.get<boolean>("promptToUpdatePowerShell", true),
|
||||
promptToUpdatePackageManagement:
|
||||
configuration.get<boolean>("promptToUpdatePackageManagement", true),
|
||||
bundledModulesPath:
|
||||
"../../PowerShellEditorServices/module",
|
||||
useX86Host:
|
||||
configuration.get<boolean>("useX86Host", false),
|
||||
enableProfileLoading:
|
||||
configuration.get<boolean>("enableProfileLoading", false),
|
||||
helpCompletion:
|
||||
configuration.get<string>("helpCompletion", CommentType.BlockComment),
|
||||
scriptAnalysis:
|
||||
configuration.get<IScriptAnalysisSettings>("scriptAnalysis", defaultScriptAnalysisSettings),
|
||||
debugging:
|
||||
configuration.get<IDebuggingSettings>("debugging", defaultDebuggingSettings),
|
||||
developer:
|
||||
getWorkspaceSettingsWithDefaults<IDeveloperSettings>(configuration, "developer", defaultDeveloperSettings),
|
||||
codeFolding:
|
||||
configuration.get<ICodeFoldingSettings>("codeFolding", defaultCodeFoldingSettings),
|
||||
codeFormatting:
|
||||
configuration.get<ICodeFormattingSettings>("codeFormatting", defaultCodeFormattingSettings),
|
||||
integratedConsole:
|
||||
configuration.get<IIntegratedConsoleSettings>("integratedConsole", defaultIntegratedConsoleSettings),
|
||||
bugReporting:
|
||||
configuration.get<IBugReportingSettings>("bugReporting", defaultBugReportingSettings),
|
||||
sideBar:
|
||||
configuration.get<ISideBarSettings>("sideBar", defaultSideBarSettings),
|
||||
pester:
|
||||
configuration.get<IPesterSettings>("pester", defaultPesterSettings),
|
||||
buttons:
|
||||
configuration.get<IButtonSettings>("buttons", defaultButtonSettings),
|
||||
notebooks:
|
||||
configuration.get<INotebooksSettings>("notebooks", defaultNotebooksSettings),
|
||||
startAsLoginShell:
|
||||
// tslint:disable-next-line
|
||||
// We follow the same convention as VS Code - https://github.com/microsoft/vscode/blob/ff00badd955d6cfcb8eab5f25f3edc86b762f49f/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts#L105-L107
|
||||
// "Unlike on Linux, ~/.profile is not sourced when logging into a macOS session. This
|
||||
// is the reason terminals on macOS typically run login shells by default which set up
|
||||
// the environment. See http://unix.stackexchange.com/a/119675/115410"
|
||||
configuration.get<IStartAsLoginShellSettings>("startAsLoginShell", defaultStartAsLoginShellSettings),
|
||||
cwd:
|
||||
configuration.get<string>("cwd", null),
|
||||
};
|
||||
}
|
||||
|
||||
// Get the ConfigurationTarget (read: scope) of where the *effective* setting value comes from
|
||||
export async function getEffectiveConfigurationTarget(settingName: string): Promise<vscode.ConfigurationTarget> {
|
||||
const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId);
|
||||
|
||||
const detail = configuration.inspect(settingName);
|
||||
let configurationTarget = null;
|
||||
if (typeof detail.workspaceFolderValue !== "undefined") {
|
||||
configurationTarget = vscode.ConfigurationTarget.WorkspaceFolder;
|
||||
}
|
||||
else if (typeof detail.workspaceValue !== "undefined") {
|
||||
configurationTarget = vscode.ConfigurationTarget.Workspace;
|
||||
}
|
||||
else if (typeof detail.globalValue !== "undefined") {
|
||||
configurationTarget = vscode.ConfigurationTarget.Global;
|
||||
}
|
||||
return configurationTarget;
|
||||
}
|
||||
|
||||
export async function change(
|
||||
settingName: string,
|
||||
newValue: any,
|
||||
configurationTarget?: vscode.ConfigurationTarget | boolean): Promise<void> {
|
||||
|
||||
const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId);
|
||||
|
||||
await configuration.update(settingName, newValue, configurationTarget);
|
||||
}
|
||||
|
||||
function getWorkspaceSettingsWithDefaults<TSettings>(
|
||||
workspaceConfiguration: vscode.WorkspaceConfiguration,
|
||||
settingName: string,
|
||||
defaultSettings: TSettings): TSettings {
|
||||
|
||||
const importedSettings: TSettings = workspaceConfiguration.get<TSettings>(settingName, defaultSettings);
|
||||
|
||||
for (const setting in importedSettings) {
|
||||
if (importedSettings[setting]) {
|
||||
defaultSettings[setting] = importedSettings[setting];
|
||||
}
|
||||
}
|
||||
return defaultSettings;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { runTests } from 'vscode-test';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// The folder containing the Extension Manifest package.json
|
||||
// Passed to `--extensionDevelopmentPath`
|
||||
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
|
||||
|
||||
// The path to test runner
|
||||
// Passed to --extensionTestsPath
|
||||
const extensionTestsPath = path.resolve(__dirname, './suite/index');
|
||||
|
||||
// Download VS Code, unzip it and run the integration test
|
||||
await runTests({ extensionDevelopmentPath, extensionTestsPath });
|
||||
} catch (err) {
|
||||
console.error('Failed to run tests');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,15 @@
|
|||
import * as assert from 'assert';
|
||||
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
// import * as myExtension from '../../extension';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
|
||||
test('Sample test', () => {
|
||||
assert.equal(-1, [1, 2, 3].indexOf(5));
|
||||
assert.equal(-1, [1, 2, 3].indexOf(0));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import * as path from 'path';
|
||||
import * as Mocha from 'mocha';
|
||||
import * as glob from 'glob';
|
||||
|
||||
export function run(): Promise<void> {
|
||||
// Create the mocha test
|
||||
const mocha = new Mocha({
|
||||
ui: 'tdd',
|
||||
color: true
|
||||
});
|
||||
|
||||
const testsRoot = path.resolve(__dirname, '..');
|
||||
|
||||
return new Promise((c, e) => {
|
||||
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
|
||||
// Add files to the test suite
|
||||
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
|
||||
|
||||
try {
|
||||
// Run the mocha test
|
||||
mocha.run(failures => {
|
||||
if (failures > 0) {
|
||||
e(new Error(`${failures} tests failed.`));
|
||||
} else {
|
||||
c();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import fs = require("fs");
|
||||
import os = require("os");
|
||||
import path = require("path");
|
||||
|
||||
export const PowerShellLanguageId = "powershell";
|
||||
|
||||
export function ensurePathExists(targetPath: string) {
|
||||
// Ensure that the path exists
|
||||
try {
|
||||
fs.mkdirSync(targetPath);
|
||||
} catch (e) {
|
||||
// If the exception isn't to indicate that the folder exists already, rethrow it.
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPipePath(pipeName: string) {
|
||||
if (os.platform() === "win32") {
|
||||
return "\\\\.\\pipe\\" + pipeName;
|
||||
} else {
|
||||
// Windows uses NamedPipes where non-Windows platforms use Unix Domain Sockets.
|
||||
// This requires connecting to the pipe file in different locations on Windows vs non-Windows.
|
||||
return path.join(os.tmpdir(), `CoreFxPipe_${pipeName}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IEditorServicesSessionDetails {
|
||||
status: string;
|
||||
reason: string;
|
||||
detail: string;
|
||||
powerShellVersion: string;
|
||||
channel: string;
|
||||
languageServicePort: number;
|
||||
debugServicePort: number;
|
||||
languageServicePipeName: string;
|
||||
debugServicePipeName: string;
|
||||
}
|
||||
|
||||
export type IReadSessionFileCallback = (details: IEditorServicesSessionDetails) => void;
|
||||
|
||||
const sessionsFolder = path.resolve(__dirname, "..", "..", "sessions/");
|
||||
const sessionFilePathPrefix = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID);
|
||||
|
||||
// Create the sessions path if it doesn't exist already
|
||||
ensurePathExists(sessionsFolder);
|
||||
|
||||
export function getSessionFilePath(uniqueId: number) {
|
||||
return `${sessionFilePathPrefix}-${uniqueId}`;
|
||||
}
|
||||
|
||||
export function getDebugSessionFilePath() {
|
||||
return `${sessionFilePathPrefix}-Debug`;
|
||||
}
|
||||
|
||||
export function writeSessionFile(sessionFilePath: string, sessionDetails: IEditorServicesSessionDetails) {
|
||||
ensurePathExists(sessionsFolder);
|
||||
|
||||
const writeStream = fs.createWriteStream(sessionFilePath);
|
||||
writeStream.write(JSON.stringify(sessionDetails));
|
||||
writeStream.close();
|
||||
}
|
||||
|
||||
|
||||
export function readSessionFile(sessionFilePath: string): IEditorServicesSessionDetails {
|
||||
const fileContents = fs.readFileSync(sessionFilePath, "utf-8");
|
||||
return JSON.parse(fileContents);
|
||||
}
|
||||
|
||||
export function deleteSessionFile(sessionFilePath: string) {
|
||||
try {
|
||||
fs.unlinkSync(sessionFilePath);
|
||||
} catch (e) {
|
||||
// TODO: Be more specific about what we're catching
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIfFileExists(filePath: string): boolean {
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.R_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTimestampString() {
|
||||
const time = new Date();
|
||||
return `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}]`;
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const isMacOS: boolean = process.platform === "darwin";
|
||||
export const isWindows: boolean = process.platform === "win32";
|
||||
export const isLinux: boolean = !isMacOS && !isWindows;
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"DOM"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src"
|
||||
// "strict": true /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".vscode-test"
|
||||
]
|
||||
}
|
Загрузка…
Ссылка в новой задаче