Skip to content

Support ~, ./ and named workspace folders in cwd #4687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 9, 2023
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ updates:
- "esbuild"
- "eslint"
- "@typescript-eslint/*"
ignore:
- dependency-name: "untildify"
versions: ["5.x"]
- package-ecosystem: github-actions
directory: "/"
schedule:
Expand Down
2 changes: 1 addition & 1 deletion .mocharc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
".jsx"
],
"require": "source-map-support/register",
"timeout": 60000,
"timeout": 600000,
"slow": 2000,
"spec": "out/test/**/*.test.js"
}
2 changes: 1 addition & 1 deletion extension-dev.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"mochaExplorer.autoload": false, // The test instance pops up every time discovery or run is done, this could be annoying on startup.
"mochaExplorer.debuggerPort": 59229, // Matches the launch config, we dont want to use the default port as we are launching a duplicate instance of vscode and it might conflict.
"mochaExplorer.ipcRole": "server",
"mochaExplorer.ipcTimeout": 10000,
"mochaExplorer.ipcTimeout": 30000, // 30 seconds
"testExplorer.useNativeTesting": true,
"mochaExplorer.env": {
"VSCODE_VERSION": "insiders",
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@vscode/extension-telemetry": "0.8.2",
"node-fetch": "2.6.12",
"semver": "7.5.4",
"untildify": "4.0.0",
"uuid": "9.0.0",
"vscode-languageclient": "8.1.0",
"vscode-languageserver-protocol": "3.17.3"
Expand Down Expand Up @@ -734,7 +735,7 @@
"powershell.cwd": {
"type": "string",
"default": "",
"markdownDescription": "An explicit start path where the Extension Terminal will be launched. Both the PowerShell process's and the shell's location will be set to this directory. **Path must be fully resolved: variables are not supported!**"
"markdownDescription": "A path where the Extension Terminal will be launched. Both the PowerShell process's and the shell's location will be set to this directory. Does not support variables, but does support the use of '~' and paths relative to a single workspace. **For multi-root workspaces, use the name of the folder you wish to have as the cwd.**"
},
"powershell.scriptAnalysis.enable": {
"type": "boolean",
Expand Down
5 changes: 2 additions & 3 deletions src/features/PesterTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as path from "path";
import vscode = require("vscode");
import { ILogger } from "../logging";
import { SessionManager } from "../session";
import { getSettings, chosenWorkspace, validateCwdSetting } from "../settings";
import { getSettings, getChosenWorkspace } from "../settings";
import utils = require("../utils");

enum LaunchType {
Expand Down Expand Up @@ -132,8 +132,7 @@ export class PesterTestsFeature implements vscode.Disposable {

// Ensure the necessary script exists (for testing). The debugger will
// start regardless, but we also pass its success along.
await validateCwdSetting(this.logger);
return await utils.checkIfFileExists(this.invokePesterStubScriptPath)
&& vscode.debug.startDebugging(chosenWorkspace, launchConfig);
&& vscode.debug.startDebugging(await getChosenWorkspace(this.logger), launchConfig);
}
}
6 changes: 2 additions & 4 deletions src/features/RunCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import vscode = require("vscode");
import { SessionManager } from "../session";
import { ILogger } from "../logging";
import { getSettings, chosenWorkspace, validateCwdSetting } from "../settings";
import { getSettings, getChosenWorkspace } from "../settings";

enum LaunchType {
Debug,
Expand Down Expand Up @@ -40,9 +40,7 @@ export class RunCodeFeature implements vscode.Disposable {
// Create or show the interactive console
// TODO: #367: Check if "newSession" mode is configured
this.sessionManager.showDebugTerminal(true);

await validateCwdSetting(this.logger);
await vscode.debug.startDebugging(chosenWorkspace, launchConfig);
await vscode.debug.startDebugging(await getChosenWorkspace(this.logger), launchConfig);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export class Logger implements ILogger {
public async writeAndShowInformation(message: string, ...additionalMessages: string[]): Promise<void> {
this.write(message, ...additionalMessages);

const selection = await vscode.window.showInformationMessage(message, "Show Logs");
if (selection !== undefined) {
const selection = await vscode.window.showInformationMessage(message, "Show Logs", "Okay");
if (selection === "Show Logs") {
this.showLogPanel();
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ShowHelpFeature } from "./features/ShowHelp";
import { SpecifyScriptArgsFeature } from "./features/DebugSession";
import { Logger } from "./logging";
import { SessionManager } from "./session";
import { LogLevel, getSettings, validateCwdSetting } from "./settings";
import { LogLevel, getSettings } from "./settings";
import { PowerShellLanguageId } from "./utils";
import { LanguageClientConsumer } from "./languageClientConsumer";

Expand Down Expand Up @@ -57,8 +57,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<IPower

telemetryReporter = new TelemetryReporter(TELEMETRY_KEY);

// Load and validate settings (will prompt for 'cwd' if necessary).
await validateCwdSetting(logger);
const settings = getSettings();
logger.writeVerbose(`Loaded settings:\n${JSON.stringify(settings, undefined, 2)}`);

Expand Down
5 changes: 4 additions & 1 deletion src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import vscode = require("vscode");
import { integer } from "vscode-languageserver-protocol";
import { ILogger } from "./logging";
import { changeSetting, getSettings, PowerShellAdditionalExePathSettings } from "./settings";
import untildify from "untildify";

// This uses require so we can rewire it in unit tests!
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-var-requires
Expand Down Expand Up @@ -232,11 +233,13 @@ export class PowerShellExeFinder {
private *enumerateAdditionalPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
for (const versionName in this.additionalPowerShellExes) {
if (Object.prototype.hasOwnProperty.call(this.additionalPowerShellExes, versionName)) {
const exePath = utils.stripQuotePair(this.additionalPowerShellExes[versionName]);
let exePath = utils.stripQuotePair(this.additionalPowerShellExes[versionName]);
if (!exePath) {
continue;
}

exePath = untildify(exePath);

// Always search for what the user gave us first
yield new PossiblePowerShellExe(exePath, versionName);

Expand Down
6 changes: 3 additions & 3 deletions src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cp = require("child_process");
import path = require("path");
import vscode = require("vscode");
import { ILogger } from "./logging";
import Settings = require("./settings");
import { Settings, validateCwdSetting } from "./settings";
import utils = require("./utils");
import { IEditorServicesSessionDetails } from "./session";
import { promisify } from "util";
Expand All @@ -29,7 +29,7 @@ export class PowerShellProcess {
private logger: ILogger,
private startPsesArgs: string,
private sessionFilePath: vscode.Uri,
private sessionSettings: Settings.Settings) {
private sessionSettings: Settings) {

this.onExited = this.onExitedEmitter.event;
}
Expand Down Expand Up @@ -103,7 +103,7 @@ export class PowerShellProcess {
name: this.title,
shellPath: this.exePath,
shellArgs: powerShellArgs,
cwd: this.sessionSettings.cwd,
cwd: await validateCwdSetting(this.logger),
iconPath: new vscode.ThemeIcon("terminal-powershell"),
isTransient: true,
hideFromUser: this.sessionSettings.integratedConsole.startInBackground,
Expand Down
9 changes: 4 additions & 5 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,7 @@ export class SessionManager implements Middleware {
this.logger.write("Restarting session...");
await this.stop();

// Re-load and validate the settings.
await validateCwdSetting(this.logger);
// Re-load the settings.
this.sessionSettings = getSettings();

await this.start(exeNameOverride);
Expand Down Expand Up @@ -470,12 +469,12 @@ export class SessionManager implements Middleware {
this.logger.updateLogLevel(settings.developer.editorServicesLogLevel);

// Detect any setting changes that would affect the session.
if (!this.suppressRestartPrompt &&
if (!this.suppressRestartPrompt && this.sessionStatus === SessionStatus.Running &&
(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.developer.editorServicesWaitForDebugger !== this.sessionSettings.developer.editorServicesWaitForDebugger
|| settings.developer.editorServicesWaitForDebugger !== this.sessionSettings.developer.editorServicesWaitForDebugger
|| settings.integratedConsole.useLegacyReadLine !== this.sessionSettings.integratedConsole.useLegacyReadLine
|| settings.integratedConsole.startInBackground !== this.sessionSettings.integratedConsole.startInBackground
|| settings.integratedConsole.startLocation !== this.sessionSettings.integratedConsole.startLocation)) {
Expand Down Expand Up @@ -644,7 +643,7 @@ export class SessionManager implements Middleware {
// NOTE: Some settings are only applicable on startup, so we send them during initialization.
initializationOptions: {
enableProfileLoading: this.sessionSettings.enableProfileLoading,
initialWorkingDirectory: this.sessionSettings.cwd,
initialWorkingDirectory: await validateCwdSetting(this.logger),
shellIntegrationEnabled: vscode.workspace.getConfiguration("terminal.integrated.shellIntegration").get<boolean>("enabled"),
},
errorHandler: {
Expand Down
105 changes: 81 additions & 24 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import vscode = require("vscode");
import utils = require("./utils");
import os = require("os");
import { ILogger } from "./logging";
import untildify from "untildify";
import path = require("path");

// 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
Expand Down Expand Up @@ -36,7 +38,7 @@ export class Settings extends PartialSettings {
sideBar = new SideBarSettings();
pester = new PesterSettings();
buttons = new ButtonSettings();
cwd = "";
cwd = ""; // NOTE: use validateCwdSetting() instead of this directly!
enableReferencesCodeLens = true;
analyzeOpenDocumentsOnly = false;
// TODO: Add (deprecated) useX86Host (for testing)
Expand Down Expand Up @@ -213,44 +215,99 @@ export async function changeSetting(
}

// We don't want to query the user more than once, so this is idempotent.
let hasPrompted = false;
export let chosenWorkspace: vscode.WorkspaceFolder | undefined = undefined;

export async function validateCwdSetting(logger: ILogger): Promise<string> {
let cwd: string | undefined = utils.stripQuotePair(
vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get<string>("cwd"));

// Only use the cwd setting if it exists.
if (cwd !== undefined && await utils.checkIfDirectoryExists(cwd)) {
return cwd;
let hasChosen = false;
let chosenWorkspace: vscode.WorkspaceFolder | undefined = undefined;
export async function getChosenWorkspace(logger: ILogger | undefined): Promise<vscode.WorkspaceFolder | undefined> {
if (hasChosen) {
return chosenWorkspace;
}

// If there is no workspace, or there is but it has no folders, fallback.
if (vscode.workspace.workspaceFolders === undefined
|| vscode.workspace.workspaceFolders.length === 0) {
cwd = undefined;
chosenWorkspace = undefined;
// If there is exactly one workspace folder, use that.
} else if (vscode.workspace.workspaceFolders.length === 1) {
cwd = vscode.workspace.workspaceFolders[0].uri.fsPath;
chosenWorkspace = vscode.workspace.workspaceFolders[0];
// If there is more than one workspace folder, prompt the user once.
} else if (vscode.workspace.workspaceFolders.length > 1 && !hasPrompted) {
hasPrompted = true;
} else if (vscode.workspace.workspaceFolders.length > 1) {
const options: vscode.WorkspaceFolderPickOptions = {
placeHolder: "Select a workspace folder to use for the PowerShell Extension.",
};

chosenWorkspace = await vscode.window.showWorkspaceFolderPick(options);
cwd = chosenWorkspace?.uri.fsPath;
// 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 changeSetting("cwd", cwd, undefined, logger);

logger?.writeVerbose(`User selected workspace: '${chosenWorkspace?.name}'`);
if (chosenWorkspace === undefined) {
chosenWorkspace = vscode.workspace.workspaceFolders[0];
} else {
const response = await vscode.window.showInformationMessage(
`Would you like to save this choice by setting this workspace's 'powershell.cwd' value to '${chosenWorkspace.name}'?`,
"Yes", "No");

if (response === "Yes") {
await changeSetting("cwd", chosenWorkspace.name, vscode.ConfigurationTarget.Workspace, logger);
}
}
}

// If there were no workspace folders, or somehow they don't exist, use
// the home directory.
if (cwd === undefined || !await utils.checkIfDirectoryExists(cwd)) {
// NOTE: We don't rely on checking if `chosenWorkspace` is undefined because
// that may be the case if the user dismissed the prompt, and we don't want
// to show it again this session.
hasChosen = true;
return chosenWorkspace;
}

export async function validateCwdSetting(logger: ILogger | undefined): Promise<string> {
let cwd = utils.stripQuotePair(
vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get<string>("cwd"))
?? "";

// Replace ~ with home directory.
cwd = untildify(cwd);

// Use the cwd setting if it's absolute and exists. We don't use or resolve
// relative paths here because it'll be relative to the Code process's cwd,
// which is not what the user is expecting.
if (path.isAbsolute(cwd) && await utils.checkIfDirectoryExists(cwd)) {
return cwd;
}

// If the cwd matches the name of a workspace folder, use it. Essentially
// "choose" a workspace folder based off the cwd path, and so set the state
// appropriately for `getChosenWorkspace`.
if (vscode.workspace.workspaceFolders) {
for (const workspaceFolder of vscode.workspace.workspaceFolders) {
// TODO: With some more work, we could support paths relative to a
// workspace folder name too.
if (cwd === workspaceFolder.name) {
hasChosen = true;
chosenWorkspace = workspaceFolder;
cwd = "";
}
}
}

// Otherwise get a cwd from the workspace, if possible.
const workspace = await getChosenWorkspace(logger);
if (workspace === undefined) {
logger?.writeVerbose("Workspace was undefined, using homedir!");
return os.homedir();
}
return cwd;

const workspacePath = workspace.uri.fsPath;

// Use the chosen workspace's root to resolve the cwd.
const relativePath = path.join(workspacePath, cwd);
if (await utils.checkIfDirectoryExists(relativePath)) {
return relativePath;
}

// Just use the workspace path.
if (await utils.checkIfDirectoryExists(workspacePath)) {
return workspacePath;
}

// If all else fails, use the home directory.
return os.homedir();
}
2 changes: 1 addition & 1 deletion test/TestEnvironment.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"settings": {
"git.openRepositoryInParentFolders": "never",
"csharp.suppressDotnetRestoreNotification": true,
"extensions.ignoreRecommendations": true
"extensions.ignoreRecommendations": true,
}
}
Loading