-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(vscode): automatically reload extension features when updating w…
…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
1 parent
d0b321c
commit 466955f
Showing
10 changed files
with
281 additions
and
142 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.