Skip to content

Commit

Permalink
feat(vscode): automatically reload extension features when updating w…
Browse files Browse the repository at this point in the history
…ing (#4094)

Fixes #4006
Fixes #4022

This is best-effort approach, where we watch the specific winglang binary for changes (followed symlinks). It's difficult to check how this works on other systems, but it works on my machine (haha). After this ships I was hoping to try it on other machines. 
It also works when changing the configuration in the extension settings.

Misc:
- Fixed using the wing.bin configuration to set a custom wing binary
- Unified logging in the extension
- Added more logging, mostly for error cases
- Removed the ability to use `npx` for the wing binary. I double people are using it and it's not really a great way to use wing anyways

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
MarkMcCulloh authored Sep 11, 2023
1 parent d0b321c commit 466955f
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 142 deletions.
3 changes: 1 addition & 2 deletions apps/vscode-wing/.projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,8 @@ const contributes: VSCodeExtensionContributions = {
properties: {
"wing.bin": {
type: "string",
default: "wing",
description:
"Path to the Wing binary. Will be `wing` from PATH by default.\nSet to `npx` to automatically retrieve the version that matches this extension",
"Path to the Wing binary. Will be `wing` from PATH by default.",
},
},
},
Expand Down
7 changes: 3 additions & 4 deletions apps/vscode-wing/package.json

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

174 changes: 94 additions & 80 deletions apps/vscode-wing/src/bin-helper.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,139 @@
import { exec, execSync } from "child_process";
import { exec } from "child_process";
import { realpath } from "fs/promises";
import { promisify } from "node:util";
import { basename, join } from "path";
import { env } from "process";
import {
StatusBarItem,
ExtensionContext,
ProgressLocation,
window,
workspace,
} from "vscode";
import { StatusBarItem, ProgressLocation, window, workspace } from "vscode";
import which from "which";
import { CFG_WING, CFG_WING_BIN } from "./constants";
import { Loggers } from "./logging";

let STATUS_BAR_ITEM: StatusBarItem;

/**
* Install wing globally using npm if desired by the user
*
* @returns "wing" if wing is successfully installed, "npx" otherwise
* @returns true if wing is successfully installed, false otherwise
*/
async function guidedWingInstallation(version: string) {
async function guidedWingInstallation() {
return window.withProgress(
{
location: ProgressLocation.Notification,
cancellable: false,
},
async (progress) => {
progress.report({ message: `Installing Wing v${version}...` });
return new Promise((resolve, reject) => {
exec(`npm install -g winglang@${version}`, (error, stdout) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
})
progress.report({ message: `Installing Wing...` });
return executeCommand("npm install -g winglang@latest")
.then(() => {
void window.showInformationMessage(
`Wing v${version} has been installed!`
);
return "wing";
void window.showInformationMessage(`Wing has been installed!`);
return true;
})
.catch((e) => {
void window.showErrorMessage(
`Failed to install Wing v${version}: ${e}`
);
return "npx";
void window.showErrorMessage(`Failed to install Wing: ${e}`);
return false;
});
}
);
}

async function updateStatusBar(wingBin: string, args?: string[]) {
let clean_args = args ? [...args] : [];
clean_args.push("-V");

// get current wing version
const version = await new Promise<string>((resolve, reject) => {
exec(`${wingBin} ${clean_args.join(" ")}`, (error, stdout) => {
if (error) {
reject(error);
} else {
resolve(stdout.trim());
}
});
}).catch(() => "unknown");

if (version === "unknown") {
return;
export async function updateStatusBar(wingBin: string) {
let version = "not found";
try {
version = await executeCommand(`"${wingBin}" -V --no-update-check`);
} catch (e) {
Loggers.default.appendLine(
`Failed to get wing version from ${wingBin}: ${e}`
);
}

// update status bar
const status = `Wing v${version}`;
const status = `Wing (${version})`;
if (!STATUS_BAR_ITEM) {
STATUS_BAR_ITEM = window.createStatusBarItem();
}

STATUS_BAR_ITEM.text = status;
STATUS_BAR_ITEM.tooltip = wingBin;
STATUS_BAR_ITEM.show();
}

/**
* Get the absolute location of the wing executable
*
* @param command The command to search for, defaults to "wing"
* @returns The absolute path to the wing executable, or null if not found/installed
*/
function wingBinaryLocation(command?: string) {
return which.sync(command ?? "wing", { nothrow: true });
return version !== "not found";
}

export async function getWingBinAndArgs(context: ExtensionContext) {
const extVersion = context.extension.packageJSON.version;
const configuredWingBin = workspace
.getConfiguration(CFG_WING)
.get<string>(CFG_WING_BIN, "wing");
let wingBin = env.WING_BIN ?? configuredWingBin;

if (wingBin !== "npx") {
const result = wingBinaryLocation(wingBin);
if (!result) {
const npmInstallOption = `Install globally with npm`;
const choice = await window.showWarningMessage(
`"${wingBin}" is not in PATH, please choose one of the following options to use the Wing language server`,
npmInstallOption
);
export async function getWingBin(): Promise<string | null> {
let configuredWingBin =
env.WING_BIN ??
workspace.getConfiguration(CFG_WING).get<string>(CFG_WING_BIN, "wing");

configuredWingBin = configuredWingBin.trim();

if (!configuredWingBin) {
configuredWingBin = "wing";
}

const configuredPath = await resolvePath(configuredWingBin);
if (configuredPath) {
// this is already a path, so we can just return it
return configuredPath;
}

try {
const whichPath = await which(configuredWingBin);
Loggers.default.appendLine(
`"which ${configuredWingBin}" => "${whichPath}"`
);
return resolvePath(whichPath);
} catch (e) {
const choice = await window.showWarningMessage(
`Unable to find wing from "${configuredWingBin}" (not in PATH). Install globally with \`npm install -g winglang@latest\`? (${e})`,
"Yes!"
);

if (choice === npmInstallOption) {
wingBin = await guidedWingInstallation(extVersion);
if (choice === "Yes!") {
if (await guidedWingInstallation()) {
return getWingBin();
} else {
// User decided to ignore the warning
return;
return null;
}
} else {
// User decided to ignore the warning
return null;
}
}
}

export async function resolvePath(p: string) {
try {
const real = await realpath(p);

if (basename(real) === "volta-shim") {
// Handle volta shims by resolving the true path
// https://docs.volta.sh/guide/#how-does-it-work
const voltaPath = join(real, "..", "volta");
const vResult = await executeCommand(`"${voltaPath}" which wing`).catch(
async () => {
void window.showWarningMessage(
`Failed to resolve volta from shim: ${real}`
);
return null;
}
);

const args =
wingBin === "npx"
? ["-y", "-q", `winglang@${extVersion}`, "--no-update-check"]
: ["--no-update-check"];
if (!vResult) {
return null;
} else {
return resolvePath(vResult);
}
}

await updateStatusBar(wingBin, args);
return real;
} catch (e) {
// the path doesn't exist or is invalid
return null;
}
}

return [wingBin, ...args];
export async function executeCommand(command: string): Promise<string> {
return promisify(exec)(command, {
encoding: "utf-8",
}).then((out) => out.stdout.trim());
}
27 changes: 18 additions & 9 deletions apps/vscode-wing/src/console/console-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
window,
WebviewPanel,
commands,
OutputChannel,
TreeView,
ViewColumn,
Uri,
Expand All @@ -21,6 +20,7 @@ import {
} from "./explorer-providers/TestsExplorerProvider";
import { type Client } from "./services/client";
import { VIEW_TYPE_CONSOLE } from "../constants";
import { Loggers } from "../logging";

export interface ConsoleInstance {
id: string;
Expand All @@ -37,11 +37,11 @@ export interface ConsoleManager {
setActiveInstance: (instanceId: string) => Promise<void>;
activeInstances: () => boolean;
getActiveInstanceId: () => string | undefined;
stopAll: () => Promise<void>;
}

export const createConsoleManager = (
context: ExtensionContext,
logger: OutputChannel
context: ExtensionContext
): ConsoleManager => {
const instances: Record<string, ConsoleInstance> = {};
const resourcesExplorer = new ResourcesExplorerProvider();
Expand Down Expand Up @@ -115,7 +115,7 @@ export const createConsoleManager = (
const logs = await instance.client.getLogs({ time: logsTimestamp });

logs.forEach((log) => {
logger.appendLine(`[${log.level}] ${log.message}`);
Loggers.console.appendLine(`[${log.level}] ${log.message}`);
});
logsTimestamp = Date.now();
};
Expand All @@ -136,7 +136,9 @@ export const createConsoleManager = (
};

const addInstance = async (instance: ConsoleInstance) => {
logger.appendLine(`Wing Console is running at http://${instance.url}`);
Loggers.console.appendLine(
`Wing Console is running at http://${instance.url}`
);

instance.client.onInvalidateQuery({
onData: async (key) => {
Expand All @@ -155,7 +157,7 @@ export const createConsoleManager = (
}, 300);
},
onError: (err) => {
logger.appendLine(err);
Loggers.console.appendLine(err);
},
});

Expand Down Expand Up @@ -190,7 +192,7 @@ export const createConsoleManager = (
});
},
onError: (err) => {
logger.appendLine(err);
Loggers.console.appendLine(err);
},
});

Expand Down Expand Up @@ -235,7 +237,7 @@ export const createConsoleManager = (
await closeInstance(id);
});
});
logger.show();
Loggers.console.show();
}

webviewPanel.title = `${instance.wingfile} - [console]`;
Expand Down Expand Up @@ -290,7 +292,9 @@ export const createConsoleManager = (
if (!instance) {
return;
}
logger.appendLine(`Closing Console instance: '${instance.wingfile}'`);
Loggers.console.appendLine(
`Closing Console instance: '${instance.wingfile}'`
);

instance.client.close();
instance.onDidClose();
Expand Down Expand Up @@ -318,5 +322,10 @@ export const createConsoleManager = (
getActiveInstanceId: () => {
return activeInstanceId;
},
stopAll: async () => {
Object.keys(instances).forEach(async (id) => {
await closeInstance(id);
});
},
};
};
Loading

0 comments on commit 466955f

Please sign in to comment.