mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-01 15:01:51 +00:00
vscode: amended config to use binary from globalStoragePath, added ui for downloading
This commit is contained in:
parent
3e0e4e90ae
commit
5d88c1db38
13
editors/code/package-lock.json
generated
13
editors/code/package-lock.json
generated
@ -753,6 +753,19 @@
|
||||
"os-tmpdir": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-not-nil": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-not-nil/-/ts-not-nil-1.0.1.tgz",
|
||||
"integrity": "sha512-19+u+3okJddVZlrIdTOdFBaMsHYDInIGDPiujxfRa0RS2Ch5055zVG4GAqa+CZ/Rd1a+7ORSm8O4+2kesPymtw==",
|
||||
"requires": {
|
||||
"ts-typedefs": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"ts-typedefs": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-typedefs/-/ts-typedefs-3.2.0.tgz",
|
||||
"integrity": "sha512-NglEH2YiY40YxNAvwBISqqXRTKlQq6x+qoCF+tkjPxwrPbrkmq7V3LXavmxrD63fENtMhFkcqgMJtOirtow9iA=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||
|
@ -27,6 +27,7 @@
|
||||
"jsonc-parser": "^2.1.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"ts-not-nil": "^1.0.1",
|
||||
"vscode-languageclient": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -173,10 +174,11 @@
|
||||
},
|
||||
"rust-analyzer.raLspServerPath": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"default": "ra_lsp_server",
|
||||
"description": "Path to ra_lsp_server executable"
|
||||
"default": null,
|
||||
"description": "Path to ra_lsp_server executable (points to bundled binary by default)"
|
||||
},
|
||||
"rust-analyzer.excludeGlobs": {
|
||||
"type": "array",
|
||||
|
@ -1,24 +1,18 @@
|
||||
import { homedir } from 'os';
|
||||
import * as lc from 'vscode-languageclient';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
import { window, workspace } from 'vscode';
|
||||
import { Config } from './config';
|
||||
import { ensureLanguageServerBinary } from './installation/language_server';
|
||||
|
||||
export function createClient(config: Config): lc.LanguageClient {
|
||||
export async function createClient(config: Config): Promise<null | lc.LanguageClient> {
|
||||
// '.' Is the fallback if no folder is open
|
||||
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
|
||||
// It might be a good idea to test if the uri points to a file.
|
||||
const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
|
||||
|
||||
const raLspServerPath = expandPathResolving(config.raLspServerPath);
|
||||
if (spawnSync(raLspServerPath, ["--version"]).status !== 0) {
|
||||
window.showErrorMessage(
|
||||
`Unable to execute '${raLspServerPath} --version'\n\n` +
|
||||
`Perhaps it is not in $PATH?\n\n` +
|
||||
`PATH=${process.env.PATH}\n`
|
||||
);
|
||||
}
|
||||
const raLspServerPath = await ensureLanguageServerBinary(config.raLspServerSource);
|
||||
if (!raLspServerPath) return null;
|
||||
|
||||
const run: lc.Executable = {
|
||||
command: raLspServerPath,
|
||||
options: { cwd: workspaceFolderPath },
|
||||
@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
|
||||
res.registerProposedFeatures();
|
||||
return res;
|
||||
}
|
||||
function expandPathResolving(path: string) {
|
||||
if (path.startsWith('~/')) {
|
||||
return path.replace('~', homedir());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import * as os from "os";
|
||||
import * as vscode from 'vscode';
|
||||
import { BinarySource, BinarySourceType } from "./installation/interfaces";
|
||||
|
||||
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
|
||||
|
||||
@ -16,10 +18,24 @@ export interface CargoFeatures {
|
||||
}
|
||||
|
||||
export class Config {
|
||||
readonly raLspServerGithubArtifactName = {
|
||||
linux: "ra_lsp_server-linux",
|
||||
darwin: "ra_lsp_server-mac",
|
||||
win32: "ra_lsp_server-windows.exe",
|
||||
aix: null,
|
||||
android: null,
|
||||
freebsd: null,
|
||||
openbsd: null,
|
||||
sunos: null,
|
||||
cygwin: null,
|
||||
netbsd: null,
|
||||
}[process.platform];
|
||||
|
||||
raLspServerSource!: null | BinarySource;
|
||||
|
||||
highlightingOn = true;
|
||||
rainbowHighlightingOn = false;
|
||||
enableEnhancedTyping = true;
|
||||
raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
|
||||
lruCapacity: null | number = null;
|
||||
displayInlayHints = true;
|
||||
maxInlayHintLength: null | number = null;
|
||||
@ -45,11 +61,20 @@ export class Config {
|
||||
private prevCargoWatchOptions: null | CargoWatchOptions = null;
|
||||
|
||||
constructor(ctx: vscode.ExtensionContext) {
|
||||
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions);
|
||||
this.refresh();
|
||||
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
|
||||
this.refresh(ctx);
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
private static expandPathResolving(path: string) {
|
||||
if (path.startsWith('~/')) {
|
||||
return path.replace('~', os.homedir());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
|
||||
// values only in one place (i.e. remove default values from non-readonly members declarations)
|
||||
private refresh(ctx: vscode.ExtensionContext) {
|
||||
const config = vscode.workspace.getConfiguration('rust-analyzer');
|
||||
|
||||
let requireReloadMessage = null;
|
||||
@ -82,9 +107,26 @@ export class Config {
|
||||
this.prevEnhancedTyping = this.enableEnhancedTyping;
|
||||
}
|
||||
|
||||
if (config.has('raLspServerPath')) {
|
||||
this.raLspServerPath =
|
||||
RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
|
||||
{
|
||||
const raLspServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
|
||||
if (raLspServerPath) {
|
||||
this.raLspServerSource = {
|
||||
type: BinarySourceType.ExplicitPath,
|
||||
path: Config.expandPathResolving(raLspServerPath)
|
||||
};
|
||||
} else if (this.raLspServerGithubArtifactName) {
|
||||
this.raLspServerSource = {
|
||||
type: BinarySourceType.GithubBinary,
|
||||
dir: ctx.globalStoragePath,
|
||||
file: this.raLspServerGithubArtifactName,
|
||||
repo: {
|
||||
name: "rust-analyzer",
|
||||
owner: "rust-analyzer",
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.raLspServerSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.has('cargo-watch.enable')) {
|
||||
|
@ -29,7 +29,11 @@ export class Ctx {
|
||||
await old.stop();
|
||||
}
|
||||
this.client = null;
|
||||
const client = createClient(this.config);
|
||||
const client = await createClient(this.config);
|
||||
if (!client) {
|
||||
throw new Error("Rust Analyzer Language Server is not available");
|
||||
}
|
||||
|
||||
this.pushCleanup(client.start());
|
||||
await client.onReady();
|
||||
|
||||
|
@ -7,7 +7,7 @@ export async function downloadFile(
|
||||
destFilePath: fs.PathLike,
|
||||
onProgress: (readBytes: number, totalBytes: number) => void
|
||||
): Promise<void> {
|
||||
onProgress = throttle(100, /* noTrailing: */ true, onProgress);
|
||||
onProgress = throttle(1000, /* noTrailing: */ true, onProgress);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
@ -1,25 +1,19 @@
|
||||
import fetch from "node-fetch";
|
||||
import { GithubRepo, ArtifactMetadata } from "./interfaces";
|
||||
|
||||
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
||||
|
||||
export interface FetchLatestArtifactMetadataOpts {
|
||||
repoName: string;
|
||||
repoOwner: string;
|
||||
repo: GithubRepo;
|
||||
artifactFileName: string;
|
||||
}
|
||||
|
||||
export interface ArtifactMetadata {
|
||||
releaseName: string;
|
||||
releaseDate: Date;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
export async function fetchLatestArtifactMetadata(
|
||||
opts: FetchLatestArtifactMetadataOpts
|
||||
): Promise<ArtifactMetadata | null> {
|
||||
): Promise<null | ArtifactMetadata> {
|
||||
|
||||
const repoOwner = encodeURIComponent(opts.repoOwner);
|
||||
const repoName = encodeURIComponent(opts.repoName);
|
||||
const repoOwner = encodeURIComponent(opts.repo.owner);
|
||||
const repoName = encodeURIComponent(opts.repo.name);
|
||||
|
||||
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
|
||||
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
||||
@ -35,14 +29,12 @@ export async function fetchLatestArtifactMetadata(
|
||||
|
||||
return !artifact ? null : {
|
||||
releaseName: response.name,
|
||||
releaseDate: new Date(response.published_at),
|
||||
downloadUrl: artifact.browser_download_url
|
||||
};
|
||||
|
||||
// Noise denotes tremendous amount of data that we are not using here
|
||||
interface GithubRelease {
|
||||
name: string;
|
||||
published_at: Date;
|
||||
assets: Array<{
|
||||
browser_download_url: string;
|
||||
|
26
editors/code/src/installation/interfaces.ts
Normal file
26
editors/code/src/installation/interfaces.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export interface GithubRepo {
|
||||
name: string;
|
||||
owner: string;
|
||||
}
|
||||
|
||||
export interface ArtifactMetadata {
|
||||
releaseName: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
|
||||
export enum BinarySourceType { ExplicitPath, GithubBinary }
|
||||
|
||||
export type BinarySource = EplicitPathSource | GithubBinarySource;
|
||||
|
||||
export interface EplicitPathSource {
|
||||
type: BinarySourceType.ExplicitPath;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface GithubBinarySource {
|
||||
type: BinarySourceType.GithubBinary;
|
||||
repo: GithubRepo;
|
||||
dir: string;
|
||||
file: string;
|
||||
}
|
119
editors/code/src/installation/language_server.ts
Normal file
119
editors/code/src/installation/language_server.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { unwrapNotNil } from "ts-not-nil";
|
||||
import { spawnSync } from "child_process";
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import { strict as assert } from "assert";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces";
|
||||
import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
|
||||
import { downloadFile } from "./download_file";
|
||||
|
||||
export async function downloadLatestLanguageServer(
|
||||
{file: artifactFileName, dir: installationDir, repo}: GithubBinarySource
|
||||
) {
|
||||
const binaryMetadata = await fetchLatestArtifactMetadata({ artifactFileName, repo });
|
||||
|
||||
const {
|
||||
releaseName,
|
||||
downloadUrl
|
||||
} = unwrapNotNil(binaryMetadata, `Latest GitHub release lacks "${artifactFileName}" file`);
|
||||
|
||||
await fs.mkdir(installationDir).catch(err => assert.strictEqual(
|
||||
err && err.code,
|
||||
"EEXIST",
|
||||
`Couldn't create directory "${installationDir}" to download `+
|
||||
`language server binary: ${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 language server ${releaseName}`
|
||||
},
|
||||
async (progress, _) => {
|
||||
let lastPrecentage = 0;
|
||||
await downloadFile(downloadUrl, installationPath, (readBytes, totalBytes) => {
|
||||
const newPercentage = (readBytes / totalBytes) * 100;
|
||||
progress.report({
|
||||
message: newPercentage.toFixed(0) + "%",
|
||||
increment: newPercentage - lastPrecentage
|
||||
});
|
||||
|
||||
lastPrecentage = newPercentage;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
await fs.chmod(installationPath, 111); // Set xxx permissions
|
||||
}
|
||||
export async function ensureLanguageServerBinary(
|
||||
langServerSource: null | BinarySource
|
||||
): Promise<null | string> {
|
||||
|
||||
if (!langServerSource) {
|
||||
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 (langServerSource.type) {
|
||||
case BinarySourceType.ExplicitPath: {
|
||||
if (isBinaryAvailable(langServerSource.path)) {
|
||||
return langServerSource.path;
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Unable to execute ${'`'}${langServerSource.path} --version${'`'}. ` +
|
||||
"To use the bundled language server, set `rust-analyzer.raLspServerPath` " +
|
||||
"value to `null` or remove it from the settings to use it by default."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
case BinarySourceType.GithubBinary: {
|
||||
const bundledBinaryPath = path.join(langServerSource.dir, langServerSource.file);
|
||||
|
||||
if (!isBinaryAvailable(bundledBinaryPath)) {
|
||||
const userResponse = await vscode.window.showInformationMessage(
|
||||
`Language server binary for rust-analyzer was not found. ` +
|
||||
`Do you want to download it now?`,
|
||||
"Download now", "Cancel"
|
||||
);
|
||||
if (userResponse !== "Download now") return null;
|
||||
|
||||
try {
|
||||
await downloadLatestLanguageServer(langServerSource);
|
||||
} catch (err) {
|
||||
await vscode.window.showErrorMessage(
|
||||
`Failed to download language server from ${langServerSource.repo.name} ` +
|
||||
`GitHub repository: ${err.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
assert(
|
||||
isBinaryAvailable(bundledBinaryPath),
|
||||
"Downloaded language server binary is not functional"
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
"Rust analyzer language server was successfully installed"
|
||||
);
|
||||
}
|
||||
return bundledBinaryPath;
|
||||
}
|
||||
}
|
||||
|
||||
function isBinaryAvailable(binaryPath: string) {
|
||||
return spawnSync(binaryPath, ["--version"]).status === 0;
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
|
Loading…
Reference in New Issue
Block a user