mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-12 05:08:52 +00:00
Rewrite auto-update
Everything now happens in main.ts, in the bootstrap family of functions. The current flow is: * check everything only on extension installation. * if the user is on nightly channel, try to download the nightly extension and reload. * when we install nightly extension, we persist its release id, so that we can check if the current release is different. * if server binary was not downloaded by the current version of the extension, redownload it (we persist the version of ext that downloaded the server).
This commit is contained in:
parent
f0a1b64d7e
commit
fb6e655de8
13 changed files with 270 additions and 696 deletions
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
|
@ -16,6 +16,7 @@
|
|||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
// "--user-data-dir=${workspaceFolder}/target/code",
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/code"
|
||||
],
|
||||
|
|
|
@ -228,7 +228,7 @@
|
|||
"default": "stable",
|
||||
"markdownEnumDescriptions": [
|
||||
"`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general",
|
||||
"`\"nightly\"` updates are shipped daily, they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**"
|
||||
"`\"nightly\"` updates are shipped daily (extension updates automatically by downloading artifacts directly from GitHub), they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**"
|
||||
],
|
||||
"markdownDescription": "Choose `\"nightly\"` updates to get the latest features and bug fixes every day. While `\"stable\"` releases occur weekly and don't contain cutting-edge features from VSCode proposed APIs"
|
||||
},
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { ensureServerBinary } from '../installation/server';
|
||||
import * as vscode from "vscode";
|
||||
import { spawnSync } from "child_process";
|
||||
import { Ctx, Cmd } from '../ctx';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
export function serverVersion(ctx: Ctx): Cmd {
|
||||
return async () => {
|
||||
const binaryPath = await ensureServerBinary(ctx.config, ctx.state);
|
||||
|
||||
if (binaryPath == null) {
|
||||
throw new Error(
|
||||
"Rust Analyzer Language Server is not available. " +
|
||||
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
|
||||
);
|
||||
}
|
||||
|
||||
const version = spawnSync(binaryPath, ["--version"], { encoding: "utf8" }).stdout;
|
||||
const version = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }).stdout;
|
||||
vscode.window.showInformationMessage('rust-analyzer version : ' + version);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import * as os from "os";
|
||||
import * as vscode from 'vscode';
|
||||
import { ArtifactSource } from "./installation/interfaces";
|
||||
import { log, vscodeReloadWindow } from "./util";
|
||||
|
||||
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
|
||||
import { log } from "./util";
|
||||
|
||||
export interface InlayHintOptions {
|
||||
typeHints: boolean;
|
||||
|
@ -25,10 +21,7 @@ export interface CargoFeatures {
|
|||
loadOutDirsFromCheck: boolean;
|
||||
}
|
||||
|
||||
export const enum UpdatesChannel {
|
||||
Stable = "stable",
|
||||
Nightly = "nightly"
|
||||
}
|
||||
export type UpdatesChannel = "stable" | "nightly";
|
||||
|
||||
export const NIGHTLY_TAG = "nightly";
|
||||
export class Config {
|
||||
|
@ -41,6 +34,7 @@ export class Config {
|
|||
"cargo-watch",
|
||||
"highlighting.semanticTokens",
|
||||
"inlayHints",
|
||||
"updates.channel",
|
||||
]
|
||||
.map(opt => `${this.rootSection}.${opt}`);
|
||||
|
||||
|
@ -94,100 +88,17 @@ export class Config {
|
|||
);
|
||||
|
||||
if (userResponse === "Reload now") {
|
||||
await vscodeReloadWindow();
|
||||
await vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||
}
|
||||
}
|
||||
|
||||
private static replaceTildeWithHomeDir(path: string) {
|
||||
if (path.startsWith("~/")) {
|
||||
return os.homedir() + path.slice("~".length);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the binary artifact for `rust-analyzer` that is published for
|
||||
* `platform` on GitHub releases. (It is also stored under the same name when
|
||||
* downloaded by the extension).
|
||||
*/
|
||||
get prebuiltServerFileName(): null | string {
|
||||
// See possible `arch` values here:
|
||||
// https://nodejs.org/api/process.html#process_process_arch
|
||||
|
||||
switch (process.platform) {
|
||||
|
||||
case "linux": {
|
||||
switch (process.arch) {
|
||||
case "arm":
|
||||
case "arm64": return null;
|
||||
|
||||
default: return "rust-analyzer-linux";
|
||||
}
|
||||
}
|
||||
|
||||
case "darwin": return "rust-analyzer-mac";
|
||||
case "win32": return "rust-analyzer-windows.exe";
|
||||
|
||||
// Users on these platforms yet need to manually build from sources
|
||||
case "aix":
|
||||
case "android":
|
||||
case "freebsd":
|
||||
case "openbsd":
|
||||
case "sunos":
|
||||
case "cygwin":
|
||||
case "netbsd": return null;
|
||||
// The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
|
||||
}
|
||||
}
|
||||
|
||||
get installedExtensionUpdateChannel(): UpdatesChannel {
|
||||
return this.extensionReleaseTag === NIGHTLY_TAG
|
||||
? UpdatesChannel.Nightly
|
||||
: UpdatesChannel.Stable;
|
||||
}
|
||||
|
||||
get serverSource(): null | ArtifactSource {
|
||||
const serverPath = RA_LSP_DEBUG ?? this.serverPath;
|
||||
|
||||
if (serverPath) {
|
||||
return {
|
||||
type: ArtifactSource.Type.ExplicitPath,
|
||||
path: Config.replaceTildeWithHomeDir(serverPath)
|
||||
};
|
||||
}
|
||||
|
||||
const prebuiltBinaryName = this.prebuiltServerFileName;
|
||||
|
||||
if (!prebuiltBinaryName) return null;
|
||||
|
||||
return this.createGithubReleaseSource(
|
||||
prebuiltBinaryName,
|
||||
this.extensionReleaseTag
|
||||
);
|
||||
}
|
||||
|
||||
private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease {
|
||||
return {
|
||||
type: ArtifactSource.Type.GithubRelease,
|
||||
file,
|
||||
tag,
|
||||
dir: this.ctx.globalStoragePath,
|
||||
repo: {
|
||||
name: "rust-analyzer",
|
||||
owner: "rust-analyzer",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get nightlyVsixSource(): ArtifactSource.GithubRelease {
|
||||
return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
|
||||
}
|
||||
get globalStoragePath(): string { return this.ctx.globalStoragePath; }
|
||||
|
||||
// We don't do runtime config validation here for simplicity. More on stackoverflow:
|
||||
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
|
||||
|
||||
private get serverPath() { return this.cfg.get("serverPath") as null | string; }
|
||||
get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; }
|
||||
get serverPath() { return this.cfg.get("serverPath") as null | string; }
|
||||
get channel() { return this.cfg.get<"stable" | "nightly">("updates.channel")!; }
|
||||
get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
|
||||
get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
|
||||
get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
|
||||
|
|
|
@ -4,21 +4,20 @@ import * as lc from 'vscode-languageclient';
|
|||
import { Config } from './config';
|
||||
import { createClient } from './client';
|
||||
import { isRustEditor, RustEditor } from './util';
|
||||
import { PersistentState } from './persistent_state';
|
||||
|
||||
export class Ctx {
|
||||
private constructor(
|
||||
readonly config: Config,
|
||||
readonly state: PersistentState,
|
||||
private readonly extCtx: vscode.ExtensionContext,
|
||||
readonly client: lc.LanguageClient
|
||||
readonly client: lc.LanguageClient,
|
||||
readonly serverPath: string,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
|
||||
static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
|
||||
const client = await createClient(config, serverPath);
|
||||
const res = new Ctx(config, state, extCtx, client);
|
||||
const res = new Ctx(config, extCtx, client, serverPath);
|
||||
res.pushCleanup(client.start());
|
||||
await client.onReady();
|
||||
return res;
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util";
|
||||
import { Config, UpdatesChannel } from "../config";
|
||||
import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
|
||||
import { downloadArtifactWithProgressUi } from "./downloads";
|
||||
import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
|
||||
import { PersistentState } from "../persistent_state";
|
||||
|
||||
const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
|
||||
|
||||
/**
|
||||
* Installs `stable` or latest `nightly` version or does nothing if the current
|
||||
* extension version is what's needed according to `desiredUpdateChannel`.
|
||||
*/
|
||||
export async function ensureProperExtensionVersion(config: Config, state: PersistentState): Promise<never | void> {
|
||||
// User has built lsp server from sources, she should manage updates manually
|
||||
if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
|
||||
|
||||
const currentUpdChannel = config.installedExtensionUpdateChannel;
|
||||
const desiredUpdChannel = config.updatesChannel;
|
||||
|
||||
if (currentUpdChannel === UpdatesChannel.Stable) {
|
||||
// Release date is present only when we are on nightly
|
||||
await state.installedNightlyExtensionReleaseDate.set(null);
|
||||
}
|
||||
|
||||
if (desiredUpdChannel === UpdatesChannel.Stable) {
|
||||
// VSCode should handle updates for stable channel
|
||||
if (currentUpdChannel === UpdatesChannel.Stable) return;
|
||||
|
||||
if (!await askToDownloadProperExtensionVersion(config)) return;
|
||||
|
||||
await vscodeReinstallExtension(config.extensionId);
|
||||
await vscodeReloadWindow(); // never returns
|
||||
}
|
||||
|
||||
if (currentUpdChannel === UpdatesChannel.Stable) {
|
||||
if (!await askToDownloadProperExtensionVersion(config)) return;
|
||||
|
||||
return await tryDownloadNightlyExtension(config, state);
|
||||
}
|
||||
|
||||
const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get();
|
||||
|
||||
if (currentExtReleaseDate === null) {
|
||||
void vscode.window.showErrorMessage(
|
||||
"Nightly release date must've been set during the installation. " +
|
||||
"Did you download and install the nightly .vsix package manually?"
|
||||
);
|
||||
throw new Error("Nightly release date was not set in globalStorage");
|
||||
}
|
||||
|
||||
const dateNow = new Date;
|
||||
const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow);
|
||||
log.debug(
|
||||
"Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate,
|
||||
"hours ago, namely:", currentExtReleaseDate, "and now is", dateNow
|
||||
);
|
||||
|
||||
if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) {
|
||||
return;
|
||||
}
|
||||
if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tryDownloadNightlyExtension(config, state, releaseInfo => {
|
||||
assert(
|
||||
currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(),
|
||||
"Other active VSCode instance has reinstalled the extension"
|
||||
);
|
||||
|
||||
if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) {
|
||||
vscode.window.showInformationMessage(
|
||||
"Whoops, it appears that your nightly version is up-to-date. " +
|
||||
"There might be some problems with the upcomming nightly release " +
|
||||
"or you traveled too far into the future. Sorry for that 😅! "
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function askToDownloadProperExtensionVersion(config: Config, reason = "") {
|
||||
if (!config.askBeforeDownload) return true;
|
||||
|
||||
const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly";
|
||||
|
||||
// In case of reentering this function and showing the same info message
|
||||
// (e.g. after we had shown this message, the user changed the config)
|
||||
// vscode will dismiss the already shown one (i.e. return undefined).
|
||||
// This behaviour is what we want, but likely it is not documented
|
||||
|
||||
const userResponse = await vscode.window.showInformationMessage(
|
||||
reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` +
|
||||
`version and reload the window now?`,
|
||||
"Download now", "Cancel"
|
||||
);
|
||||
return userResponse === "Download now";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdowns the process in case of success (i.e. reloads the window) or throws an error.
|
||||
*
|
||||
* ACHTUNG!: this function has a crazy amount of state transitions, handling errors during
|
||||
* each of them would result in a ton of code (especially accounting for cross-process
|
||||
* shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort.
|
||||
*/
|
||||
const tryDownloadNightlyExtension = notReentrant(async (
|
||||
config: Config,
|
||||
state: PersistentState,
|
||||
shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
|
||||
): Promise<never | void> => {
|
||||
const vsixSource = config.nightlyVsixSource;
|
||||
try {
|
||||
const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag);
|
||||
|
||||
if (!shouldDownload(releaseInfo)) return;
|
||||
|
||||
await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension");
|
||||
|
||||
const vsixPath = path.join(vsixSource.dir, vsixSource.file);
|
||||
|
||||
await vscodeInstallExtensionFromVsix(vsixPath);
|
||||
await state.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
|
||||
await fs.unlink(vsixPath);
|
||||
|
||||
await vscodeReloadWindow(); // never returns
|
||||
} catch (err) {
|
||||
log.downloadError(err, "nightly extension", vsixSource.repo.name);
|
||||
}
|
||||
});
|
||||
|
||||
function diffInHours(a: Date, b: Date): number {
|
||||
// Discard the time and time-zone information (to abstract from daylight saving time bugs)
|
||||
// https://stackoverflow.com/a/15289883/9259330
|
||||
|
||||
const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
||||
const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
||||
|
||||
return (utcA - utcB) / (1000 * 60 * 60);
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import fetch from "node-fetch";
|
||||
import { GithubRepo, ArtifactReleaseInfo } from "./interfaces";
|
||||
import { log } from "../util";
|
||||
|
||||
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
||||
|
||||
/**
|
||||
* Fetches the release with `releaseTag` from GitHub `repo` and
|
||||
* returns metadata about `artifactFileName` shipped with
|
||||
* this release.
|
||||
*
|
||||
* @throws Error upon network failure or if no such repository, release, or artifact exists.
|
||||
*/
|
||||
export async function fetchArtifactReleaseInfo(
|
||||
repo: GithubRepo,
|
||||
artifactFileName: string,
|
||||
releaseTag: string
|
||||
): Promise<ArtifactReleaseInfo> {
|
||||
|
||||
const repoOwner = encodeURIComponent(repo.owner);
|
||||
const repoName = encodeURIComponent(repo.name);
|
||||
|
||||
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}`;
|
||||
|
||||
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
||||
|
||||
log.debug("Issuing request for released artifacts metadata to", requestUrl);
|
||||
|
||||
const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
|
||||
|
||||
if (!response.ok) {
|
||||
log.error("Error fetching artifact release info", {
|
||||
requestUrl,
|
||||
releaseTag,
|
||||
artifactFileName,
|
||||
response: {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
`Got response ${response.status} when trying to fetch ` +
|
||||
`"${artifactFileName}" artifact release info for ${releaseTag} release`
|
||||
);
|
||||
}
|
||||
|
||||
// We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
|
||||
const release: GithubRelease = await response.json();
|
||||
|
||||
const artifact = release.assets.find(artifact => artifact.name === artifactFileName);
|
||||
|
||||
if (!artifact) {
|
||||
throw new Error(
|
||||
`Artifact ${artifactFileName} was not found in ${release.name} release!`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
releaseName: release.name,
|
||||
releaseDate: new Date(release.published_at),
|
||||
downloadUrl: artifact.browser_download_url
|
||||
};
|
||||
|
||||
// We omit declaration of tremendous amount of fields that we are not using here
|
||||
interface GithubRelease {
|
||||
name: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
published_at: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
browser_download_url: string;
|
||||
}>;
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
export interface GithubRepo {
|
||||
name: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata about particular artifact retrieved from GitHub releases.
|
||||
*/
|
||||
export interface ArtifactReleaseInfo {
|
||||
releaseDate: Date;
|
||||
releaseName: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the source of a an artifact which is either specified by the user
|
||||
* explicitly, or bundled by this extension from GitHub releases.
|
||||
*/
|
||||
export type ArtifactSource = ArtifactSource.ExplicitPath | ArtifactSource.GithubRelease;
|
||||
|
||||
export namespace ArtifactSource {
|
||||
/**
|
||||
* Type tag for `ArtifactSource` discriminated union.
|
||||
*/
|
||||
export const enum Type { ExplicitPath, GithubRelease }
|
||||
|
||||
export interface ExplicitPath {
|
||||
type: Type.ExplicitPath;
|
||||
|
||||
/**
|
||||
* Filesystem path to the binary specified by the user explicitly.
|
||||
*/
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface GithubRelease {
|
||||
type: Type.GithubRelease;
|
||||
|
||||
/**
|
||||
* Repository where the binary is stored.
|
||||
*/
|
||||
repo: GithubRepo;
|
||||
|
||||
|
||||
// FIXME: add installationPath: string;
|
||||
|
||||
/**
|
||||
* Directory on the filesystem where the bundled binary is stored.
|
||||
*/
|
||||
dir: string;
|
||||
|
||||
/**
|
||||
* Name of the binary file. It is stored under the same name on GitHub releases
|
||||
* and in local `.dir`.
|
||||
*/
|
||||
file: string;
|
||||
|
||||
/**
|
||||
* Tag of github release that denotes a version required by this extension.
|
||||
*/
|
||||
tag: string;
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
import { ArtifactSource } from "./interfaces";
|
||||
import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
|
||||
import { downloadArtifactWithProgressUi } from "./downloads";
|
||||
import { log, assert, notReentrant } from "../util";
|
||||
import { Config, NIGHTLY_TAG } from "../config";
|
||||
import { PersistentState } from "../persistent_state";
|
||||
|
||||
export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
|
||||
const source = config.serverSource;
|
||||
|
||||
if (!source) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Unfortunately we don't ship binaries for your platform yet. " +
|
||||
"You need to manually clone rust-analyzer repository and " +
|
||||
"run `cargo xtask install --server` to build the language server from sources. " +
|
||||
"If you feel that your platform should be supported, please create an issue " +
|
||||
"about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
|
||||
"will consider it."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (source.type) {
|
||||
case ArtifactSource.Type.ExplicitPath: {
|
||||
if (isBinaryAvailable(source.path)) {
|
||||
return source.path;
|
||||
}
|
||||
|
||||
vscode.window.showErrorMessage(
|
||||
`Unable to run ${source.path} binary. ` +
|
||||
`To use the pre-built language server, set "rust-analyzer.serverPath" ` +
|
||||
"value to `null` or remove it from the settings to use it by default."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
case ArtifactSource.Type.GithubRelease: {
|
||||
if (!shouldDownloadServer(state, source)) {
|
||||
return path.join(source.dir, source.file);
|
||||
}
|
||||
|
||||
if (config.askBeforeDownload) {
|
||||
const userResponse = await vscode.window.showInformationMessage(
|
||||
`Language server version ${source.tag} for rust-analyzer is not installed. ` +
|
||||
"Do you want to download it now?",
|
||||
"Download now", "Cancel"
|
||||
);
|
||||
if (userResponse !== "Download now") return null;
|
||||
}
|
||||
|
||||
return await downloadServer(state, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDownloadServer(
|
||||
state: PersistentState,
|
||||
source: ArtifactSource.GithubRelease,
|
||||
): boolean {
|
||||
if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
|
||||
|
||||
const installed = {
|
||||
tag: state.serverReleaseTag.get(),
|
||||
date: state.serverReleaseDate.get()
|
||||
};
|
||||
const required = {
|
||||
tag: source.tag,
|
||||
date: state.installedNightlyExtensionReleaseDate.get()
|
||||
};
|
||||
|
||||
log.debug("Installed server:", installed, "required:", required);
|
||||
|
||||
if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) {
|
||||
return required.tag !== installed.tag;
|
||||
}
|
||||
|
||||
assert(required.date !== null, "Extension release date should have been saved during its installation");
|
||||
assert(installed.date !== null, "Server release date should have been saved during its installation");
|
||||
|
||||
return installed.date.getTime() !== required.date.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing no reentrancy for this is best-effort.
|
||||
*/
|
||||
const downloadServer = notReentrant(async (
|
||||
state: PersistentState,
|
||||
source: ArtifactSource.GithubRelease,
|
||||
): Promise<null | string> => {
|
||||
try {
|
||||
const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
|
||||
|
||||
await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
|
||||
await Promise.all([
|
||||
state.serverReleaseTag.set(releaseInfo.releaseName),
|
||||
state.serverReleaseDate.set(releaseInfo.releaseDate)
|
||||
]);
|
||||
} catch (err) {
|
||||
log.downloadError(err, "language server", source.repo.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
const binaryPath = path.join(source.dir, source.file);
|
||||
|
||||
assert(isBinaryAvailable(binaryPath),
|
||||
`Downloaded language server binary is not functional.` +
|
||||
`Downloaded from GitHub repo ${source.repo.owner}/${source.repo.name} ` +
|
||||
`to ${binaryPath}`
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
"Rust analyzer language server was successfully installed 🦀"
|
||||
);
|
||||
|
||||
return binaryPath;
|
||||
});
|
||||
|
||||
function isBinaryAvailable(binaryPath: string): boolean {
|
||||
const res = spawnSync(binaryPath, ["--version"]);
|
||||
|
||||
// ACHTUNG! `res` type declaration is inherently wrong, see
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
|
||||
|
||||
log.debug("Checked binary availablity via --version", res);
|
||||
log.debug(binaryPath, "--version output:", res.output?.map(String));
|
||||
|
||||
return res.status === 0;
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
import * as vscode from 'vscode';
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
import * as commands from './commands';
|
||||
import { activateInlayHints } from './inlay_hints';
|
||||
import { activateStatusDisplay } from './status_display';
|
||||
import { Ctx } from './ctx';
|
||||
import { activateHighlighting } from './highlighting';
|
||||
import { ensureServerBinary } from './installation/server';
|
||||
import { Config } from './config';
|
||||
import { log } from './util';
|
||||
import { ensureProperExtensionVersion } from './installation/extension';
|
||||
import { Config, NIGHTLY_TAG } from './config';
|
||||
import { log, assert } from './util';
|
||||
import { PersistentState } from './persistent_state';
|
||||
import { fetchRelease, download } from './net';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
let ctx: Ctx | undefined;
|
||||
|
||||
|
@ -35,27 +38,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
context.subscriptions.push(defaultOnEnter);
|
||||
|
||||
const config = new Config(context);
|
||||
const state = new PersistentState(context);
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error));
|
||||
|
||||
// Don't await the user response here, otherwise we will block the lsp server bootstrap
|
||||
void ensureProperExtensionVersion(config, state).catch(log.error);
|
||||
|
||||
const serverPath = await ensureServerBinary(config, state);
|
||||
|
||||
if (serverPath == null) {
|
||||
throw new Error(
|
||||
"Rust Analyzer Language Server is not available. " +
|
||||
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
|
||||
);
|
||||
}
|
||||
const state = new PersistentState(context.globalState);
|
||||
const serverPath = await bootstrap(config, state);
|
||||
|
||||
// Note: we try to start the server before we activate type hints so that it
|
||||
// registers its `onDidChangeDocument` handler before us.
|
||||
//
|
||||
// This a horribly, horribly wrong way to deal with this problem.
|
||||
ctx = await Ctx.create(config, state, context, serverPath);
|
||||
ctx = await Ctx.create(config, context, serverPath);
|
||||
|
||||
// Commands which invokes manually via command palette, shortcut, etc.
|
||||
ctx.registerCommand('reload', (ctx) => {
|
||||
|
@ -109,3 +99,131 @@ export async function deactivate() {
|
|||
await ctx?.client?.stop();
|
||||
ctx = undefined;
|
||||
}
|
||||
|
||||
async function bootstrap(config: Config, state: PersistentState): Promise<string> {
|
||||
await fs.mkdir(config.globalStoragePath, { recursive: true });
|
||||
|
||||
await bootstrapExtension(config, state);
|
||||
const path = await bootstrapServer(config, state);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
|
||||
if (config.channel === "stable") {
|
||||
if (config.extensionReleaseTag === NIGHTLY_TAG) {
|
||||
vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension.
|
||||
To switch to stable, uninstall the extension and re-install it from the marketplace`);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const lastCheck = state.lastCheck;
|
||||
const now = Date.now();
|
||||
|
||||
const anHour = 60 * 60 * 1000;
|
||||
const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
|
||||
|
||||
if (!shouldDownloadNightly) return;
|
||||
|
||||
const release = await fetchRelease("nightly").catch((e) => {
|
||||
log.error(e);
|
||||
if (state.releaseId === undefined) { // Show error only for the initial download
|
||||
vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
if (release === undefined || release.id === state.releaseId) return;
|
||||
|
||||
const userResponse = await vscode.window.showInformationMessage(
|
||||
"New version of rust-analyzer (nightly) is available (requires reload).",
|
||||
"Update"
|
||||
);
|
||||
if (userResponse !== "Update") return;
|
||||
|
||||
const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
|
||||
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
|
||||
|
||||
const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
|
||||
await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension");
|
||||
|
||||
await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
|
||||
await fs.unlink(dest);
|
||||
|
||||
await state.updateReleaseId(release.id);
|
||||
await state.updateLastCheck(now);
|
||||
await vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||
}
|
||||
|
||||
async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
|
||||
const path = await getServer(config, state);
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
"Rust Analyzer Language Server is not available. " +
|
||||
"Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
|
||||
);
|
||||
}
|
||||
|
||||
const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
|
||||
log.debug("Checked binary availability via --version", res);
|
||||
log.debug(res, "--version output:", res.output);
|
||||
if (res.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to execute ${path} --version`
|
||||
);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
|
||||
const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
|
||||
if (explicitPath) {
|
||||
if (explicitPath.startsWith("~/")) {
|
||||
return os.homedir() + explicitPath.slice("~".length);
|
||||
}
|
||||
return explicitPath;
|
||||
};
|
||||
|
||||
let binaryName: string | undefined = undefined;
|
||||
if (process.arch === "x64" || process.arch === "x32") {
|
||||
if (process.platform === "linux") binaryName = "rust-analyzer-linux";
|
||||
if (process.platform === "darwin") binaryName = "rust-analyzer-mac";
|
||||
if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe";
|
||||
}
|
||||
if (binaryName === undefined) {
|
||||
vscode.window.showErrorMessage(
|
||||
"Unfortunately we don't ship binaries for your platform yet. " +
|
||||
"You need to manually clone rust-analyzer repository and " +
|
||||
"run `cargo xtask install --server` to build the language server from sources. " +
|
||||
"If you feel that your platform should be supported, please create an issue " +
|
||||
"about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
|
||||
"will consider it."
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dest = path.join(config.globalStoragePath, binaryName);
|
||||
const exists = await fs.stat(dest).then(() => true, () => false);
|
||||
if (!exists) {
|
||||
await state.updateServerVersion(undefined);
|
||||
}
|
||||
|
||||
if (state.serverVersion === config.packageJsonVersion) return dest;
|
||||
|
||||
if (config.askBeforeDownload) {
|
||||
const userResponse = await vscode.window.showInformationMessage(
|
||||
`Language server version ${config.packageJsonVersion} for rust-analyzer is not installed.`,
|
||||
"Download now"
|
||||
);
|
||||
if (userResponse !== "Download now") return dest;
|
||||
}
|
||||
|
||||
const release = await fetchRelease(config.extensionReleaseTag);
|
||||
const artifact = release.assets.find(artifact => artifact.name === binaryName);
|
||||
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
|
||||
|
||||
await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
|
||||
await state.updateServerVersion(config.packageJsonVersion);
|
||||
return dest;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,101 @@
|
|||
import fetch from "node-fetch";
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import * as stream from "stream";
|
||||
import * as util from "util";
|
||||
import { log, assert } from "../util";
|
||||
import { ArtifactReleaseInfo } from "./interfaces";
|
||||
import { log, assert } from "./util";
|
||||
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
||||
const OWNER = "rust-analyzer";
|
||||
const REPO = "rust-analyzer";
|
||||
|
||||
export async function fetchRelease(
|
||||
releaseTag: string
|
||||
): Promise<GithubRelease> {
|
||||
|
||||
const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`;
|
||||
|
||||
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
||||
|
||||
log.debug("Issuing request for released artifacts metadata to", requestUrl);
|
||||
|
||||
const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
|
||||
|
||||
if (!response.ok) {
|
||||
log.error("Error fetching artifact release info", {
|
||||
requestUrl,
|
||||
releaseTag,
|
||||
response: {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
body: await response.text(),
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
`Got response ${response.status} when trying to fetch ` +
|
||||
`release info for ${releaseTag} release`
|
||||
);
|
||||
}
|
||||
|
||||
// We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
|
||||
const release: GithubRelease = await response.json();
|
||||
return release;
|
||||
}
|
||||
|
||||
// We omit declaration of tremendous amount of fields that we are not using here
|
||||
export interface GithubRelease {
|
||||
name: string;
|
||||
id: number;
|
||||
// eslint-disable-next-line camelcase
|
||||
published_at: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
browser_download_url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
export async function download(
|
||||
downloadUrl: string,
|
||||
destinationPath: string,
|
||||
progressTitle: string,
|
||||
{ mode }: { mode?: number } = {},
|
||||
) {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false,
|
||||
title: progressTitle
|
||||
},
|
||||
async (progress, _cancellationToken) => {
|
||||
let lastPercentage = 0;
|
||||
await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => {
|
||||
const newPercentage = (readBytes / totalBytes) * 100;
|
||||
progress.report({
|
||||
message: newPercentage.toFixed(0) + "%",
|
||||
increment: newPercentage - lastPercentage
|
||||
});
|
||||
|
||||
lastPercentage = newPercentage;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
|
||||
* `onProgress` callback is called on recieveing each chunk of bytes
|
||||
* to track the progress of downloading, it gets the already read and total
|
||||
* amount of bytes to read as its parameters.
|
||||
*/
|
||||
export async function downloadFile(
|
||||
async function downloadFile(
|
||||
url: string,
|
||||
destFilePath: fs.PathLike,
|
||||
destFilePermissions: number,
|
||||
mode: number | undefined,
|
||||
onProgress: (readBytes: number, totalBytes: number) => void
|
||||
): Promise<void> {
|
||||
const res = await fetch(url);
|
||||
|
@ -41,7 +118,7 @@ export async function downloadFile(
|
|||
onProgress(readBytes, totalBytes);
|
||||
});
|
||||
|
||||
const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions });
|
||||
const destFileStream = fs.createWriteStream(destFilePath, { mode });
|
||||
|
||||
await pipeline(res.body, destFileStream);
|
||||
return new Promise<void>(resolve => {
|
||||
|
@ -52,46 +129,3 @@ export async function downloadFile(
|
|||
// Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads artifact from given `downloadUrl`.
|
||||
* Creates `installationDir` if it is not yet created and puts the artifact under
|
||||
* `artifactFileName`.
|
||||
* Displays info about the download progress in an info message printing the name
|
||||
* of the artifact as `displayName`.
|
||||
*/
|
||||
export async function downloadArtifactWithProgressUi(
|
||||
{ downloadUrl, releaseName }: ArtifactReleaseInfo,
|
||||
artifactFileName: string,
|
||||
installationDir: string,
|
||||
displayName: string,
|
||||
) {
|
||||
await fs.promises.mkdir(installationDir).catch(err => assert(
|
||||
err?.code === "EEXIST",
|
||||
`Couldn't create directory "${installationDir}" to download ` +
|
||||
`${artifactFileName} artifact: ${err?.message}`
|
||||
));
|
||||
|
||||
const installationPath = path.join(installationDir, artifactFileName);
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false, // FIXME: add support for canceling download?
|
||||
title: `Downloading rust-analyzer ${displayName} (${releaseName})`
|
||||
},
|
||||
async (progress, _cancellationToken) => {
|
||||
let lastPrecentage = 0;
|
||||
const filePermissions = 0o755; // (rwx, r_x, r_x)
|
||||
await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => {
|
||||
const newPercentage = (readBytes / totalBytes) * 100;
|
||||
progress.report({
|
||||
message: newPercentage.toFixed(0) + "%",
|
||||
increment: newPercentage - lastPrecentage
|
||||
});
|
||||
|
||||
lastPrecentage = newPercentage;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,49 +1,41 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { log } from "./util";
|
||||
import { log } from './util';
|
||||
|
||||
export class PersistentState {
|
||||
constructor(private readonly ctx: vscode.ExtensionContext) {
|
||||
constructor(private readonly globalState: vscode.Memento) {
|
||||
const { lastCheck, releaseId, serverVersion } = this;
|
||||
log.debug("PersistentState: ", { lastCheck, releaseId, serverVersion });
|
||||
}
|
||||
|
||||
readonly installedNightlyExtensionReleaseDate = new DateStorage(
|
||||
"installed-nightly-extension-release-date",
|
||||
this.ctx.globalState
|
||||
);
|
||||
readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
|
||||
readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
|
||||
}
|
||||
|
||||
|
||||
export class Storage<T> {
|
||||
constructor(
|
||||
private readonly key: string,
|
||||
private readonly storage: vscode.Memento,
|
||||
private readonly defaultVal: T
|
||||
) { }
|
||||
|
||||
get(): T {
|
||||
const val = this.storage.get(this.key, this.defaultVal);
|
||||
log.debug(this.key, "==", val);
|
||||
return val;
|
||||
/**
|
||||
* Used to check for *nightly* updates once an hour.
|
||||
*/
|
||||
get lastCheck(): number | undefined {
|
||||
return this.globalState.get("lastCheck");
|
||||
}
|
||||
async set(val: T) {
|
||||
log.debug(this.key, "=", val);
|
||||
await this.storage.update(this.key, val);
|
||||
}
|
||||
}
|
||||
export class DateStorage {
|
||||
inner: Storage<null | string>;
|
||||
|
||||
constructor(key: string, storage: vscode.Memento) {
|
||||
this.inner = new Storage(key, storage, null);
|
||||
}
|
||||
|
||||
get(): null | Date {
|
||||
const dateStr = this.inner.get();
|
||||
return dateStr ? new Date(dateStr) : null;
|
||||
}
|
||||
|
||||
async set(date: null | Date) {
|
||||
await this.inner.set(date ? date.toString() : null);
|
||||
async updateLastCheck(value: number) {
|
||||
await this.globalState.update("lastCheck", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release id of the *nightly* extension.
|
||||
* Used to check if we should update.
|
||||
*/
|
||||
get releaseId(): number | undefined {
|
||||
return this.globalState.get("releaseId");
|
||||
}
|
||||
async updateReleaseId(value: number) {
|
||||
await this.globalState.update("releaseId", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of the extension that installed the server.
|
||||
* Used to check if we need to update the server.
|
||||
*/
|
||||
get serverVersion(): string | undefined {
|
||||
return this.globalState.get("serverVersion");
|
||||
}
|
||||
async updateServerVersion(value: string | undefined) {
|
||||
await this.globalState.update("serverVersion", value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as lc from "vscode-languageclient";
|
||||
import * as vscode from "vscode";
|
||||
import { promises as dns } from "dns";
|
||||
import { strict as nativeAssert } from "assert";
|
||||
|
||||
export function assert(condition: boolean, explanation: string): asserts condition {
|
||||
|
@ -31,22 +30,6 @@ export const log = new class {
|
|||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...optionalParams);
|
||||
}
|
||||
|
||||
downloadError(err: Error, artifactName: string, repoName: string) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to download the rust-analyzer ${artifactName} from ${repoName} ` +
|
||||
`GitHub repository: ${err.message}`
|
||||
);
|
||||
log.error(err);
|
||||
dns.resolve('example.com').then(
|
||||
addrs => log.debug("DNS resolution for example.com was successful", addrs),
|
||||
err => log.error(
|
||||
"DNS resolution for example.com failed, " +
|
||||
"there might be an issue with Internet availability",
|
||||
err
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function sendRequestWithRetry<TParam, TRet>(
|
||||
|
@ -86,17 +69,6 @@ function sleep(ms: number) {
|
|||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function notReentrant<TThis, TParams extends any[], TRet>(
|
||||
fn: (this: TThis, ...params: TParams) => Promise<TRet>
|
||||
): typeof fn {
|
||||
let entered = false;
|
||||
return function(...params) {
|
||||
assert(!entered, `Reentrancy invariant for ${fn.name} is violated`);
|
||||
entered = true;
|
||||
return fn.apply(this, params).finally(() => entered = false);
|
||||
};
|
||||
}
|
||||
|
||||
export type RustDocument = vscode.TextDocument & { languageId: "rust" };
|
||||
export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
|
||||
|
||||
|
@ -110,29 +82,3 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
|
|||
export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
|
||||
return isRustDocument(editor.document);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extensionId The canonical extension identifier in the form of: `publisher.name`
|
||||
*/
|
||||
export async function vscodeReinstallExtension(extensionId: string) {
|
||||
// Unfortunately there is no straightforward way as of now, these commands
|
||||
// were found in vscode source code.
|
||||
|
||||
log.debug("Uninstalling extension", extensionId);
|
||||
await vscode.commands.executeCommand("workbench.extensions.uninstallExtension", extensionId);
|
||||
log.debug("Installing extension", extensionId);
|
||||
await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId);
|
||||
}
|
||||
|
||||
export async function vscodeReloadWindow(): Promise<never> {
|
||||
await vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||
|
||||
assert(false, "unreachable");
|
||||
}
|
||||
|
||||
export async function vscodeInstallExtensionFromVsix(vsixPath: string) {
|
||||
await vscode.commands.executeCommand(
|
||||
"workbench.extensions.installExtension",
|
||||
vscode.Uri.file(vsixPath)
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue