diff --git a/package.json b/package.json index 3177d41d0c..0654eeb635 100644 --- a/package.json +++ b/package.json @@ -573,6 +573,7 @@ }, "powershell.powerShellAdditionalExePaths": { "type": "object", + "default": {}, "description": "Specifies a list of versionName / exePath pairs where exePath points to a non-standard install location for PowerShell and versionName can be used to reference this path with the powershell.powerShellDefaultVersion setting.", "additionalProperties": { "type": "string" @@ -580,6 +581,7 @@ }, "powershell.powerShellDefaultVersion": { "type": "string", + "default": "", "description": "Specifies the PowerShell version name, as displayed by the 'PowerShell: Show Session Menu' command, used when the extension loads e.g \"Windows PowerShell (x86)\" or \"PowerShell Core 7 (x64)\". You can specify additional PowerShell executables by using the \"powershell.powerShellAdditionalExePaths\" setting." }, "powershell.powerShellExePath": { @@ -639,7 +641,8 @@ "powershell.bugReporting.project": { "type": "string", "default": "https://github.com/PowerShell/vscode-powershell", - "description": "Specifies the URL of the GitHub project in which to generate bug reports." + "description": "Specifies the URL of the GitHub project in which to generate bug reports.", + "deprecationMessage": "This setting was never meant to be changed!" }, "powershell.helpCompletion": { "type": "string", @@ -653,7 +656,7 @@ }, "powershell.cwd": { "type": "string", - "default": null, + "default": "", "description": "An explicit start path where the PowerShell Extension Terminal will be launched. Both the PowerShell process's and the shell's location will be set to this directory. A fully resolved path must be provided!" }, "powershell.scriptAnalysis.enable": { @@ -811,12 +814,13 @@ }, "powershell.integratedConsole.forceClearScrollbackBuffer": { "type": "boolean", + "default": false, "description": "Use the vscode API to clear the terminal since that's the only reliable way to clear the scrollback buffer. Turn this on if you're used to 'Clear-Host' clearing scroll history as well as clear-terminal-via-lsp." }, "powershell.integratedConsole.suppressStartupBanner": { "type": "boolean", "default": false, - "description": "Do not show the Powershell Extension Terminal banner on launch" + "description": "Do not show the PowerShell Extension Terminal banner on launch." }, "powershell.debugging.createTemporaryIntegratedConsole": { "type": "boolean", @@ -825,6 +829,7 @@ }, "powershell.developer.bundledModulesPath": { "type": "string", + "default": "../../PowerShellEditorServices/module", "description": "Specifies an alternate path to the folder containing modules that are bundled with the PowerShell extension (i.e. PowerShell Editor Services, PSScriptAnalyzer, Plaster)" }, "powershell.developer.editorServicesLogLevel": { diff --git a/src/features/Console.ts b/src/features/Console.ts index 279a981127..6569fe1b9e 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -6,7 +6,7 @@ import { NotificationType, RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { ICheckboxQuickPickItem, showCheckboxQuickPick } from "../controls/checkboxQuickPick"; import { Logger } from "../logging"; -import Settings = require("../settings"); +import { getSettings } from "../settings"; import { LanguageClientConsumer } from "../languageClientConsumer"; export const EvaluateRequestType = new RequestType("evaluate"); @@ -182,7 +182,7 @@ export class ConsoleFeature extends LanguageClientConsumer { // 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); + vscode.window.activeTerminal.show(!getSettings().integratedConsole.focusConsoleOnExecute); await vscode.commands.executeCommand("workbench.action.terminal.scrollToBottom"); return; diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index 39d90f097a..db79bce515 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -11,7 +11,7 @@ import { LanguageClient } from "vscode-languageclient/node"; import { getPlatformDetails, OperatingSystem } from "../platform"; import { PowerShellProcess } from "../process"; import { IEditorServicesSessionDetails, SessionManager, SessionStatus } from "../session"; -import Settings = require("../settings"); +import { getSettings } from "../settings"; import { Logger } from "../logging"; import { LanguageClientConsumer } from "../languageClientConsumer"; import path = require("path"); @@ -169,7 +169,7 @@ export class DebugSessionFeature extends LanguageClientConsumer // setting. Otherwise, the launch config value overrides the setting. // // Also start the temporary process and console for this configuration. - const settings = Settings.load(); + const settings = getSettings(); config.createTemporaryIntegratedConsole = config.createTemporaryIntegratedConsole ?? settings.debugging.createTemporaryIntegratedConsole; diff --git a/src/features/ExtensionCommands.ts b/src/features/ExtensionCommands.ts index c62dde9bc1..9d43ce2cf1 100644 --- a/src/features/ExtensionCommands.ts +++ b/src/features/ExtensionCommands.ts @@ -10,7 +10,7 @@ import { } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { Logger } from "../logging"; -import Settings = require("../settings"); +import { getSettings } from "../settings"; import { LanguageClientConsumer } from "../languageClientConsumer"; export interface IExtensionCommand { @@ -260,7 +260,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { () => { // We check to see if they have TrueClear on. If not, no-op because the // overriden Clear-Host already calls [System.Console]::Clear() - if (Settings.load().integratedConsole.forceClearScrollbackBuffer) { + if (getSettings().integratedConsole.forceClearScrollbackBuffer) { void vscode.commands.executeCommand("workbench.action.terminal.clear"); } }) diff --git a/src/features/GenerateBugReport.ts b/src/features/GenerateBugReport.ts index d7db34c500..5d0049428c 100644 --- a/src/features/GenerateBugReport.ts +++ b/src/features/GenerateBugReport.ts @@ -5,12 +5,10 @@ import os = require("os"); import vscode = require("vscode"); import child_process = require("child_process"); import { SessionManager } from "../session"; -import Settings = require("../settings"); const queryStringPrefix = "?"; -const settings = Settings.load(); -const project = settings.bugReporting.project; +const project = "https://github.com/PowerShell/vscode-powershell"; const issuesUrl = `${project}/issues/new`; const extensions = diff --git a/src/features/GetCommands.ts b/src/features/GetCommands.ts index 7756df75b9..c33acd85a8 100644 --- a/src/features/GetCommands.ts +++ b/src/features/GetCommands.ts @@ -6,6 +6,7 @@ import { RequestType0 } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { Logger } from "../logging"; import { LanguageClientConsumer } from "../languageClientConsumer"; +import { getSettings } from "../settings"; interface ICommand { name: string; @@ -68,8 +69,8 @@ export class GetCommandsFeature extends LanguageClientConsumer { return; } await this.languageClient.sendRequest(GetCommandRequestType).then((result) => { - const SidebarConfig = vscode.workspace.getConfiguration("powershell.sideBar"); - const excludeFilter = (SidebarConfig.CommandExplorerExcludeFilter).map((filter: string) => filter.toLowerCase()); + const exclusions = getSettings().sideBar.CommandExplorerExcludeFilter; + const excludeFilter = exclusions.map((filter: string) => filter.toLowerCase()); result = result.filter((command) => (excludeFilter.indexOf(command.moduleName.toLowerCase()) === -1)); this.commandsExplorerProvider.powerShellCommands = result.map(toCommand); this.commandsExplorerProvider.refresh(); diff --git a/src/features/HelpCompletion.ts b/src/features/HelpCompletion.ts index ec64795743..e2931dc461 100644 --- a/src/features/HelpCompletion.ts +++ b/src/features/HelpCompletion.ts @@ -7,7 +7,7 @@ import { } from "vscode"; import { RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; -import Settings = require("../settings"); +import { Settings, CommentType, getSettings } from "../settings"; import { LanguageClientConsumer } from "../languageClientConsumer"; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -27,13 +27,13 @@ enum SearchState { Searching, Locked, Found } export class HelpCompletionFeature extends LanguageClientConsumer { private helpCompletionProvider: HelpCompletionProvider | undefined; private disposable: Disposable | undefined; - private settings: Settings.ISettings; + private settings: Settings; constructor() { super(); - this.settings = Settings.load(); + this.settings = getSettings(); - if (this.settings.helpCompletion !== Settings.CommentType.Disabled) { + if (this.settings.helpCompletion !== CommentType.Disabled) { this.helpCompletionProvider = new HelpCompletionProvider(); this.disposable = workspace.onDidChangeTextDocument(async (e) => { await this.onEvent(e); }); } @@ -125,11 +125,11 @@ class HelpCompletionProvider { private lastChangeRange: Range | undefined; private lastDocument: TextDocument | undefined; private langClient: LanguageClient | undefined; - private settings: Settings.ISettings; + private settings: Settings; constructor() { this.triggerFinderHelpComment = new TriggerFinder("##"); - this.settings = Settings.load(); + this.settings = getSettings(); } public get triggerFound(): boolean { @@ -161,7 +161,7 @@ class HelpCompletionProvider { const result = await this.langClient.sendRequest(CommentHelpRequestType, { documentUri: doc.uri.toString(), triggerPosition: triggerStartPos, - blockComment: this.settings.helpCompletion === Settings.CommentType.BlockComment, + blockComment: this.settings.helpCompletion === CommentType.BlockComment, }); if (result.content.length === 0) { diff --git a/src/features/ISECompatibility.ts b/src/features/ISECompatibility.ts index 2dc052cc6e..0a4993ca69 100644 --- a/src/features/ISECompatibility.ts +++ b/src/features/ISECompatibility.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as vscode from "vscode"; -import * as Settings from "../settings"; +import { getSettings } from "../settings"; interface ISetting { path: string; @@ -63,7 +63,7 @@ export class ISECompatibilityFeature implements vscode.Disposable { // Show the PowerShell view container which has the Command Explorer view await vscode.commands.executeCommand("workbench.view.extension.PowerShell"); - if (!Settings.load().sideBar.CommandExplorerVisibility) { + if (!getSettings().sideBar.CommandExplorerVisibility) { // Hide the explorer if the setting says so. await vscode.commands.executeCommand("workbench.action.toggleSidebarVisibility"); } diff --git a/src/features/PesterTests.ts b/src/features/PesterTests.ts index dc703254c0..4e4c55bda8 100644 --- a/src/features/PesterTests.ts +++ b/src/features/PesterTests.ts @@ -4,7 +4,7 @@ import * as path from "path"; import vscode = require("vscode"); import { SessionManager } from "../session"; -import Settings = require("../settings"); +import { getSettings } from "../settings"; import utils = require("../utils"); enum LaunchType { @@ -83,7 +83,7 @@ export class PesterTestsFeature implements vscode.Disposable { lineNum?: number, outputPath?: string): vscode.DebugConfiguration { - const settings = Settings.load(); + const settings = getSettings(); // Since we pass the script path to PSES in single quotes to avoid issues with PowerShell // special chars like & $ @ () [], we do have to double up the interior single quotes. diff --git a/src/features/RunCode.ts b/src/features/RunCode.ts index 269ceebe9c..2bd8424cbd 100644 --- a/src/features/RunCode.ts +++ b/src/features/RunCode.ts @@ -3,7 +3,7 @@ import vscode = require("vscode"); import { SessionManager } from "../session"; -import Settings = require("../settings"); +import { getSettings } from "../settings"; enum LaunchType { Debug, @@ -46,7 +46,7 @@ export class RunCodeFeature implements vscode.Disposable { } function createLaunchConfig(launchType: LaunchType, commandToRun: string, args: string[]) { - const settings = Settings.load(); + const settings = getSettings(); const launchConfig = { request: "launch", diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts index aec794892f..2a83ccd547 100644 --- a/src/features/UpdatePowerShell.ts +++ b/src/features/UpdatePowerShell.ts @@ -14,7 +14,7 @@ import { MessageItem, ProgressLocation, window } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; import { Logger } from "../logging"; import { SessionManager } from "../session"; -import * as Settings from "../settings"; +import { changeSetting } from "../settings"; import { isMacOS, isWindows } from "../utils"; import { EvaluateRequestType } from "./Console"; @@ -195,7 +195,7 @@ export async function InvokePowerShellUpdateCheck( // Never choice. case 2: - await Settings.change("promptToUpdatePowerShell", false, true, logger); + await changeSetting("promptToUpdatePowerShell", false, true, logger); break; default: break; diff --git a/src/main.ts b/src/main.ts index e34ff062d7..800b51a69f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,7 +26,7 @@ import { ShowHelpFeature } from "./features/ShowHelp"; import { SpecifyScriptArgsFeature } from "./features/DebugSession"; import { Logger } from "./logging"; import { SessionManager } from "./session"; -import Settings = require("./settings"); +import { LogLevel, getSettings, validateCwdSetting } from "./settings"; import { PowerShellLanguageId } from "./utils"; import { LanguageClientConsumer } from "./languageClientConsumer"; @@ -51,7 +51,7 @@ const documentSelector: DocumentSelector = [ export async function activate(context: vscode.ExtensionContext): Promise { const logLevel = vscode.workspace.getConfiguration(`${PowerShellLanguageId}.developer`) - .get("editorServicesLogLevel", "Normal"); + .get("editorServicesLogLevel", LogLevel.Normal); logger = new Logger(logLevel, context.globalStorageUri); telemetryReporter = new TelemetryReporter(PackageJSON.name, PackageJSON.version, AI_KEY); @@ -65,8 +65,8 @@ export async function activate(context: vscode.ExtensionContext): Promise 0 ? this.sessionSettings.developer.featureFlags.map((f) => `'${f}'`).join(", ") : ""; diff --git a/src/session.ts b/src/session.ts index e771ef9343..4f706826db 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,7 +9,7 @@ import TelemetryReporter, { TelemetryEventProperties, TelemetryEventMeasurements import { Message } from "vscode-jsonrpc"; import { Logger } from "./logging"; import { PowerShellProcess } from "./process"; -import Settings = require("./settings"); +import { Settings, changeSetting, getSettings, getEffectiveConfigurationTarget, validateCwdSetting } from "./settings"; import utils = require("./utils"); import { @@ -102,7 +102,7 @@ export class SessionManager implements Middleware { constructor( private extensionContext: vscode.ExtensionContext, - private sessionSettings: Settings.ISettings, + private sessionSettings: Settings, private logger: Logger, private documentSelector: DocumentSelector, hostName: string, @@ -211,8 +211,8 @@ export class SessionManager implements Middleware { await this.stop(); // Re-load and validate the settings. - await Settings.validateCwdSetting(this.logger); - this.sessionSettings = Settings.load(); + await validateCwdSetting(this.logger); + this.sessionSettings = getSettings(); await this.start(exeNameOverride); } @@ -234,7 +234,7 @@ export class SessionManager implements Middleware { return vscode.Uri.joinPath(this.sessionsFolder, `PSES-VSCode-${process.env.VSCODE_PID}-${uniqueId}.json`); } - public async createDebugSessionProcess(settings: Settings.ISettings): Promise { + public async createDebugSessionProcess(settings: Settings): Promise { // NOTE: We only support one temporary Extension Terminal at a time. To // support more, we need to track each separately, and tie the session // for the event handler to the right process (and dispose of the event @@ -343,18 +343,18 @@ export class SessionManager implements Middleware { const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId); const deprecatedSetting = "codeFormatting.whitespaceAroundPipe"; const newSetting = "codeFormatting.addWhitespaceAroundPipe"; - const configurationTargetOfNewSetting = Settings.getEffectiveConfigurationTarget(newSetting); - const configurationTargetOfOldSetting = Settings.getEffectiveConfigurationTarget(deprecatedSetting); + const configurationTargetOfNewSetting = getEffectiveConfigurationTarget(newSetting); + const configurationTargetOfOldSetting = getEffectiveConfigurationTarget(deprecatedSetting); if (configurationTargetOfOldSetting !== undefined && configurationTargetOfNewSetting === undefined) { const value = configuration.get(deprecatedSetting, configurationTargetOfOldSetting); - await Settings.change(newSetting, value, configurationTargetOfOldSetting, this.logger); - await Settings.change(deprecatedSetting, undefined, configurationTargetOfOldSetting, this.logger); + await changeSetting(newSetting, value, configurationTargetOfOldSetting, this.logger); + await changeSetting(deprecatedSetting, undefined, configurationTargetOfOldSetting, this.logger); } } // TODO: Remove this migration code. private async promptPowerShellExeSettingsCleanup() { - if (!this.sessionSettings.powerShellExePath) { // undefined or null + if (this.sessionSettings.powerShellExePath === "") { return; } @@ -372,25 +372,25 @@ export class SessionManager implements Middleware { this.suppressRestartPrompt = true; try { - await Settings.change("powerShellExePath", undefined, true, this.logger); + await changeSetting("powerShellExePath", undefined, true, this.logger); } finally { this.suppressRestartPrompt = false; } // Show the session menu at the end if they don't have a PowerShellDefaultVersion. - if (this.sessionSettings.powerShellDefaultVersion === undefined) { + if (this.sessionSettings.powerShellDefaultVersion === "") { await vscode.commands.executeCommand(this.ShowSessionMenuCommandName); } } private async onConfigurationUpdated() { - const settings = Settings.load(); + const settings = getSettings(); this.logger.updateLogLevel(settings.developer.editorServicesLogLevel); // Detect any setting changes that would affect the session if (!this.suppressRestartPrompt && - (settings.cwd?.toLowerCase() !== this.sessionSettings.cwd?.toLowerCase() - || settings.powerShellDefaultVersion?.toLowerCase() !== this.sessionSettings.powerShellDefaultVersion?.toLowerCase() + (settings.cwd.toLowerCase() !== this.sessionSettings.cwd.toLowerCase() + || 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 @@ -489,7 +489,7 @@ export class SessionManager implements Middleware { let foundPowerShell: IPowerShellExeDetails | undefined; try { let defaultPowerShell: IPowerShellExeDetails | undefined; - if (this.sessionSettings.powerShellDefaultVersion !== undefined) { + if (this.sessionSettings.powerShellDefaultVersion !== "") { for await (const details of powershellExeFinder.enumeratePowerShellInstallations()) { // Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723 const wantedName = this.sessionSettings.powerShellDefaultVersion; @@ -525,13 +525,14 @@ export class SessionManager implements Middleware { } private async getBundledModulesPath(): Promise { - let bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath); + // Because the extension is always at `/out/main.js` + let bundledModulesPath = path.resolve(__dirname, "../modules"); if (this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { const devBundledModulesPath = path.resolve(__dirname, this.sessionSettings.developer.bundledModulesPath); // Make sure the module's bin path exists - if (await utils.checkIfDirectoryExists(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { + if (await utils.checkIfDirectoryExists(devBundledModulesPath)) { bundledModulesPath = devBundledModulesPath; } else { void this.logger.writeAndShowWarning( @@ -577,9 +578,7 @@ Type 'help' to get help. editorServicesArgs += "-WaitForDebugger "; } - if (this.sessionSettings.developer.editorServicesLogLevel) { - editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; - } + editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; return editorServicesArgs; } @@ -626,6 +625,7 @@ Type 'help' to get help. const clientOptions: LanguageClientOptions = { documentSelector: this.documentSelector, synchronize: { + // TODO: This is deprecated and they should be pulled by the server. // backend uses "files" and "search" to ignore references. configurationSection: [utils.PowerShellLanguageId, "files", "search"], // TODO: fileEvents: vscode.workspace.createFileSystemWatcher('**/.eslintrc') @@ -658,6 +658,7 @@ Type 'help' to get help. this.languageClient = new LanguageClient("PowerShell Editor Services", connectFunc, clientOptions); // This enables handling Semantic Highlighting messages in PowerShell Editor Services + // TODO: We should only turn this on in preview. this.languageClient.registerProposedFeatures(); this.languageClient.onTelemetry((event) => { @@ -807,7 +808,7 @@ Type 'help' to get help. private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails) { this.suppressRestartPrompt = true; try { - await Settings.change("powerShellDefaultVersion", exePath.displayName, true, this.logger); + await changeSetting("powerShellDefaultVersion", exePath.displayName, true, this.logger); } finally { this.suppressRestartPrompt = false; } diff --git a/src/settings.ts b/src/settings.ts index 6a64992ce3..9380baa4ce 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -6,18 +6,62 @@ import utils = require("./utils"); import os = require("os"); import { Logger } from "./logging"; -enum CodeFormattingPreset { - Custom, - Allman, - OTBS, - Stroustrup, -} - -enum PipelineIndentationStyle { - IncreaseIndentationForFirstPipeline, - IncreaseIndentationAfterEveryPipeline, - NoIndentation, - None, +// TODO: Quite a few of these settings are unused in the client and instead +// exist just for the server. Those settings do not need to be represented in +// this class, as the LSP layers take care of communicating them. Frankly, this +// class is over-engineered and seems to have originally been created to avoid +// using vscode.workspace.getConfiguration() directly. It wasn't a bad idea to +// keep things organized so consistent...but it ended up failing in execution. +// Perhaps we just get rid of this entirely? + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class PartialSettings { } + +export class Settings extends PartialSettings { + powerShellAdditionalExePaths: PowerShellAdditionalExePathSettings = {}; + powerShellDefaultVersion = ""; + // This setting is no longer used but is here to assist in cleaning up the users settings. + powerShellExePath = ""; + promptToUpdatePowerShell = true; + startAsLoginShell = new StartAsLoginShellSettings(); + startAutomatically = true; + enableProfileLoading = true; + helpCompletion = CommentType.BlockComment; + scriptAnalysis = new ScriptAnalysisSettings(); + debugging = new DebuggingSettings(); + developer = new DeveloperSettings(); + codeFormatting = new CodeFormattingSettings(); + integratedConsole = new IntegratedConsoleSettings(); + sideBar = new SideBarSettings(); + pester = new PesterSettings(); + buttons = new ButtonSettings(); + cwd = ""; + enableReferencesCodeLens = true; + analyzeOpenDocumentsOnly = false; + // TODO: Add (deprecated) useX86Host (for testing) +} + +export enum CodeFormattingPreset { + Custom = "Custom", + Allman = "Allman", + OTBS = "OTBS", + Stroustrup = "Stroustrup", +} + +export enum PipelineIndentationStyle { + IncreaseIndentationForFirstPipeline = "IncreaseIndentationForFirstPipeline", + IncreaseIndentationAfterEveryPipeline = "IncreaseIndentationAfterEveryPipeline", + NoIndentation = "NoIndentation", + None = "None", +} + +export enum LogLevel { + Diagnostic = "Diagnostic", + Verbose = "Verbose", + Normal = "Normal", + Warning = "Warning", + Error = "Error", + None = "None", } export enum CommentType { @@ -26,256 +70,104 @@ export enum CommentType { LineComment = "LineComment", } -export type IPowerShellAdditionalExePathSettings = Record; - -export interface IBugReportingSettings { - project: string; -} +export type PowerShellAdditionalExePathSettings = Record; -export interface ICodeFoldingSettings { - enable?: boolean; - showLastLine?: boolean; +class CodeFormattingSettings extends PartialSettings { + autoCorrectAliases = false; + avoidSemicolonsAsLineTerminators = 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; + useConstantStrings = false; + useCorrectCasing = false; } -export interface ICodeFormattingSettings { - autoCorrectAliases: boolean; - avoidSemicolonsAsLineTerminators: 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; - useConstantStrings: boolean; - useCorrectCasing: boolean; +class ScriptAnalysisSettings extends PartialSettings { + enable = true; + settingsPath = "PSScriptAnalyzerSettings.psd1"; } -export interface IScriptAnalysisSettings { - enable?: boolean; - settingsPath: string; +class DebuggingSettings extends PartialSettings { + createTemporaryIntegratedConsole = false; } -export interface IDebuggingSettings { - createTemporaryIntegratedConsole?: boolean; +class DeveloperSettings extends PartialSettings { + featureFlags: string[] = []; + // From `/out/main.js` we go to the directory before and + // then into the other repo. + bundledModulesPath = "../../PowerShellEditorServices/module"; + editorServicesLogLevel = LogLevel.Normal; + editorServicesWaitForDebugger = false; + waitForSessionFileTimeoutSeconds = 240; } -export interface IDeveloperSettings { - featureFlags?: string[]; - bundledModulesPath: string; - editorServicesLogLevel: string; - editorServicesWaitForDebugger?: boolean; - waitForSessionFileTimeoutSeconds: number; +// 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" +class StartAsLoginShellSettings extends PartialSettings { + osx = true; + linux = false; } -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; - bundledModulesPath: string; - startAsLoginShell: IStartAsLoginShellSettings; - startAutomatically?: 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; - enableReferencesCodeLens?: boolean; - analyzeOpenDocumentsOnly?: boolean; +class IntegratedConsoleSettings extends PartialSettings { + showOnStartup = true; + startInBackground = false; + focusConsoleOnExecute = true; + useLegacyReadLine = false; + forceClearScrollbackBuffer = false; + suppressStartupBanner = false; } -export interface IStartAsLoginShellSettings { - osx: boolean; - linux: boolean; +class SideBarSettings extends PartialSettings { + CommandExplorerVisibility = true; + CommandExplorerExcludeFilter: string[] = []; } -export interface IIntegratedConsoleSettings { - showOnStartup?: boolean; - startInBackground?: boolean; - focusConsoleOnExecute: boolean; - useLegacyReadLine?: boolean; - forceClearScrollbackBuffer?: boolean; - suppressStartupBanner?: boolean; +class PesterSettings extends PartialSettings { + useLegacyCodeLens = true; + outputVerbosity = "FromPreference"; + debugOutputVerbosity = "Diagnostic"; } -export interface ISideBarSettings { - CommandExplorerVisibility: boolean; +class ButtonSettings extends PartialSettings { + showRunButtons = true; + showPanelMovementButtons = false; } -export interface IPesterSettings { - useLegacyCodeLens: boolean; - outputVerbosity: string; - debugOutputVerbosity: string; -} +// This is a recursive function which unpacks a WorkspaceConfiguration into our settings. +function getSetting(key: string | undefined, value: TSetting, configuration: vscode.WorkspaceConfiguration): TSetting { + // Base case where we're looking at a primitive type (or our special record). + if (key !== undefined && !(value instanceof PartialSettings)) { + return configuration.get(key, value); + } -export interface IButtonSettings { - showRunButtons?: boolean; - showPanelMovementButtons?: boolean; -} + // Otherwise we're looking at one of our interfaces and need to extract. + for (const property in value) { + const subKey = key !== undefined ? `${key}.${property}` : property; + value[property] = getSetting(subKey, value[property], configuration); + } -export interface INotebooksSettings { - saveMarkdownCellsAs?: CommentType; + return value; } -// TODO: This could probably be async, and call `validateCwdSetting()` directly. -export function load(): ISettings { +export function getSettings(): Settings { 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: [], - // From `/out/main.js` we go to the directory before and - // then into the other repo. - bundledModulesPath: "../../PowerShellEditorServices/module", - editorServicesLogLevel: "Normal", - editorServicesWaitForDebugger: false, - waitForSessionFileTimeoutSeconds: 240, - }; - - const defaultCodeFoldingSettings: ICodeFoldingSettings = { - enable: true, - showLastLine: false, - }; - - const defaultCodeFormattingSettings: ICodeFormattingSettings = { - autoCorrectAliases: false, - avoidSemicolonsAsLineTerminators: 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, - useConstantStrings: false, - useCorrectCasing: false, - }; - - const defaultStartAsLoginShellSettings: IStartAsLoginShellSettings = { - osx: true, - linux: false, - }; - - const defaultIntegratedConsoleSettings: IIntegratedConsoleSettings = { - showOnStartup: true, - startInBackground: false, - 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, - }; - - // TODO: I believe all the defaults can be removed, as the `package.json` should supply them (and be the source of truth). - return { - startAutomatically: - configuration.get("startAutomatically", true), - powerShellAdditionalExePaths: - configuration.get("powerShellAdditionalExePaths"), - powerShellDefaultVersion: - configuration.get("powerShellDefaultVersion"), - powerShellExePath: - configuration.get("powerShellExePath"), - promptToUpdatePowerShell: - configuration.get("promptToUpdatePowerShell", true), - bundledModulesPath: - "../modules", // Because the extension is always at `/out/main.js` - enableProfileLoading: - configuration.get("enableProfileLoading", false), - helpCompletion: - configuration.get("helpCompletion", CommentType.BlockComment), - scriptAnalysis: - configuration.get("scriptAnalysis", defaultScriptAnalysisSettings), - debugging: - configuration.get("debugging", defaultDebuggingSettings), - developer: - getWorkspaceSettingsWithDefaults(configuration, "developer", defaultDeveloperSettings), - codeFolding: - configuration.get("codeFolding", defaultCodeFoldingSettings), - codeFormatting: - configuration.get("codeFormatting", defaultCodeFormattingSettings), - integratedConsole: - configuration.get("integratedConsole", defaultIntegratedConsoleSettings), - bugReporting: - configuration.get("bugReporting", defaultBugReportingSettings), - sideBar: - configuration.get("sideBar", defaultSideBarSettings), - pester: - configuration.get("pester", defaultPesterSettings), - buttons: - configuration.get("buttons", defaultButtonSettings), - notebooks: - configuration.get("notebooks", defaultNotebooksSettings), - startAsLoginShell: - // 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("startAsLoginShell", defaultStartAsLoginShellSettings), - cwd: // NOTE: This must be validated at startup via `validateCwdSetting()`. There's probably a better way to do this. - configuration.get("cwd"), - enableReferencesCodeLens: - configuration.get("enableReferencesCodeLens", true), - analyzeOpenDocumentsOnly: - configuration.get("analyzeOpenDocumentsOnly", true), - }; + return getSetting(undefined, new Settings(), configuration); } // Get the ConfigurationTarget (read: scope) of where the *effective* setting value comes from @@ -296,7 +188,7 @@ export function getEffectiveConfigurationTarget(settingName: string): vscode.Con return undefined; } -export async function change( +export async function changeSetting( settingName: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any newValue: any, @@ -313,19 +205,6 @@ export async function change( } } -function getWorkspaceSettingsWithDefaults( - workspaceConfiguration: vscode.WorkspaceConfiguration, - settingName: string, - defaultSettings: TSettings): TSettings { - - const importedSettings: TSettings = workspaceConfiguration.get(settingName, defaultSettings); - - for (const setting in importedSettings) { - defaultSettings[setting] = importedSettings[setting]; - } - return defaultSettings; -} - // We don't want to query the user more than once, so this is idempotent. let hasPrompted = false; @@ -354,7 +233,7 @@ export async function validateCwdSetting(logger: Logger): Promise { // Save the picked 'cwd' to the workspace settings. // We have to check again because the user may not have picked. if (cwd !== undefined && await utils.checkIfDirectoryExists(cwd)) { - await change("cwd", cwd, undefined, logger); + await changeSetting("cwd", cwd, undefined, logger); } } diff --git a/test/.vscode/settings.json b/test/.vscode/settings.json index 9678952187..b731d24659 100644 --- a/test/.vscode/settings.json +++ b/test/.vscode/settings.json @@ -1,4 +1,7 @@ { "terminal.integrated.shellIntegration.enabled": false, "powershell.enableProfileLoading": false, + "powershell.powerShellAdditionalExePaths": { + "Some PowerShell": "somePath" + }, } diff --git a/test/core/settings.test.ts b/test/core/settings.test.ts index 8e9f771309..00b1c5d5ee 100644 --- a/test/core/settings.test.ts +++ b/test/core/settings.test.ts @@ -3,25 +3,34 @@ import * as assert from "assert"; import * as vscode from "vscode"; -import Settings = require("../../src/settings"); +import * as settings from "../../src/settings"; describe("Settings module", function () { it("Loads without error", function () { - assert.doesNotThrow(Settings.load); + assert.doesNotThrow(settings.getSettings); }); + it("Loads the correct defaults", function () { + const testSettings = new settings.Settings(); + testSettings.enableProfileLoading = false; + testSettings.powerShellAdditionalExePaths = { "Some PowerShell": "somePath" }; + const actualSettings = settings.getSettings(); + assert.deepStrictEqual(actualSettings, testSettings); + }); + + it("Updates correctly", async function () { - await Settings.change("helpCompletion", "LineComment", false, undefined); - assert.strictEqual(Settings.load().helpCompletion, "LineComment"); + await settings.changeSetting("helpCompletion", settings.CommentType.LineComment, false, undefined); + assert.strictEqual(settings.getSettings().helpCompletion, settings.CommentType.LineComment); }); it("Gets the effective configuration target", async function () { - await Settings.change("helpCompletion", "LineComment", false, undefined); - let target = Settings.getEffectiveConfigurationTarget("helpCompletion"); + await settings.changeSetting("helpCompletion", settings.CommentType.LineComment, false, undefined); + let target = settings.getEffectiveConfigurationTarget("helpCompletion"); assert.strictEqual(target, vscode.ConfigurationTarget.Workspace); - await Settings.change("helpCompletion", undefined, false, undefined); - target = Settings.getEffectiveConfigurationTarget("helpCompletion"); + await settings.changeSetting("helpCompletion", undefined, false, undefined); + target = settings.getEffectiveConfigurationTarget("helpCompletion"); assert.strictEqual(target, undefined); }); });