mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-12 21:28:51 +00:00
vscode: amended config to use binary from globalStoragePath, added ui for downloading
This commit is contained in:
parent
3e0e4e90ae
commit
5d88c1db38
10 changed files with 229 additions and 41 deletions
13
editors/code/package-lock.json
generated
13
editors/code/package-lock.json
generated
|
@ -753,6 +753,19 @@
|
||||||
"os-tmpdir": "~1.0.1"
|
"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": {
|
"tslib": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"jsonc-parser": "^2.1.0",
|
"jsonc-parser": "^2.1.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"throttle-debounce": "^2.1.0",
|
"throttle-debounce": "^2.1.0",
|
||||||
|
"ts-not-nil": "^1.0.1",
|
||||||
"vscode-languageclient": "^6.1.0"
|
"vscode-languageclient": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -173,10 +174,11 @@
|
||||||
},
|
},
|
||||||
"rust-analyzer.raLspServerPath": {
|
"rust-analyzer.raLspServerPath": {
|
||||||
"type": [
|
"type": [
|
||||||
|
"null",
|
||||||
"string"
|
"string"
|
||||||
],
|
],
|
||||||
"default": "ra_lsp_server",
|
"default": null,
|
||||||
"description": "Path to ra_lsp_server executable"
|
"description": "Path to ra_lsp_server executable (points to bundled binary by default)"
|
||||||
},
|
},
|
||||||
"rust-analyzer.excludeGlobs": {
|
"rust-analyzer.excludeGlobs": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
import { homedir } from 'os';
|
|
||||||
import * as lc from 'vscode-languageclient';
|
import * as lc from 'vscode-languageclient';
|
||||||
import { spawnSync } from 'child_process';
|
|
||||||
|
|
||||||
import { window, workspace } from 'vscode';
|
import { window, workspace } from 'vscode';
|
||||||
import { Config } from './config';
|
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
|
// '.' Is the fallback if no folder is open
|
||||||
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
|
// 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.
|
// It might be a good idea to test if the uri points to a file.
|
||||||
const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
|
const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
|
||||||
|
|
||||||
const raLspServerPath = expandPathResolving(config.raLspServerPath);
|
const raLspServerPath = await ensureLanguageServerBinary(config.raLspServerSource);
|
||||||
if (spawnSync(raLspServerPath, ["--version"]).status !== 0) {
|
if (!raLspServerPath) return null;
|
||||||
window.showErrorMessage(
|
|
||||||
`Unable to execute '${raLspServerPath} --version'\n\n` +
|
|
||||||
`Perhaps it is not in $PATH?\n\n` +
|
|
||||||
`PATH=${process.env.PATH}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const run: lc.Executable = {
|
const run: lc.Executable = {
|
||||||
command: raLspServerPath,
|
command: raLspServerPath,
|
||||||
options: { cwd: workspaceFolderPath },
|
options: { cwd: workspaceFolderPath },
|
||||||
|
@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
|
||||||
res.registerProposedFeatures();
|
res.registerProposedFeatures();
|
||||||
return res;
|
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 * as vscode from 'vscode';
|
||||||
|
import { BinarySource, BinarySourceType } from "./installation/interfaces";
|
||||||
|
|
||||||
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
|
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
|
||||||
|
|
||||||
|
@ -16,10 +18,24 @@ export interface CargoFeatures {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Config {
|
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;
|
highlightingOn = true;
|
||||||
rainbowHighlightingOn = false;
|
rainbowHighlightingOn = false;
|
||||||
enableEnhancedTyping = true;
|
enableEnhancedTyping = true;
|
||||||
raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
|
|
||||||
lruCapacity: null | number = null;
|
lruCapacity: null | number = null;
|
||||||
displayInlayHints = true;
|
displayInlayHints = true;
|
||||||
maxInlayHintLength: null | number = null;
|
maxInlayHintLength: null | number = null;
|
||||||
|
@ -45,11 +61,20 @@ export class Config {
|
||||||
private prevCargoWatchOptions: null | CargoWatchOptions = null;
|
private prevCargoWatchOptions: null | CargoWatchOptions = null;
|
||||||
|
|
||||||
constructor(ctx: vscode.ExtensionContext) {
|
constructor(ctx: vscode.ExtensionContext) {
|
||||||
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions);
|
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
|
||||||
this.refresh();
|
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');
|
const config = vscode.workspace.getConfiguration('rust-analyzer');
|
||||||
|
|
||||||
let requireReloadMessage = null;
|
let requireReloadMessage = null;
|
||||||
|
@ -82,9 +107,26 @@ export class Config {
|
||||||
this.prevEnhancedTyping = this.enableEnhancedTyping;
|
this.prevEnhancedTyping = this.enableEnhancedTyping;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.has('raLspServerPath')) {
|
{
|
||||||
this.raLspServerPath =
|
const raLspServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
|
||||||
RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
|
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')) {
|
if (config.has('cargo-watch.enable')) {
|
||||||
|
|
|
@ -29,7 +29,11 @@ export class Ctx {
|
||||||
await old.stop();
|
await old.stop();
|
||||||
}
|
}
|
||||||
this.client = null;
|
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());
|
this.pushCleanup(client.start());
|
||||||
await client.onReady();
|
await client.onReady();
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ export async function downloadFile(
|
||||||
destFilePath: fs.PathLike,
|
destFilePath: fs.PathLike,
|
||||||
onProgress: (readBytes: number, totalBytes: number) => void
|
onProgress: (readBytes: number, totalBytes: number) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
onProgress = throttle(100, /* noTrailing: */ true, onProgress);
|
onProgress = throttle(1000, /* noTrailing: */ true, onProgress);
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
import { GithubRepo, ArtifactMetadata } from "./interfaces";
|
||||||
|
|
||||||
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
||||||
|
|
||||||
export interface FetchLatestArtifactMetadataOpts {
|
export interface FetchLatestArtifactMetadataOpts {
|
||||||
repoName: string;
|
repo: GithubRepo;
|
||||||
repoOwner: string;
|
|
||||||
artifactFileName: string;
|
artifactFileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtifactMetadata {
|
|
||||||
releaseName: string;
|
|
||||||
releaseDate: Date;
|
|
||||||
downloadUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchLatestArtifactMetadata(
|
export async function fetchLatestArtifactMetadata(
|
||||||
opts: FetchLatestArtifactMetadataOpts
|
opts: FetchLatestArtifactMetadataOpts
|
||||||
): Promise<ArtifactMetadata | null> {
|
): Promise<null | ArtifactMetadata> {
|
||||||
|
|
||||||
const repoOwner = encodeURIComponent(opts.repoOwner);
|
const repoOwner = encodeURIComponent(opts.repo.owner);
|
||||||
const repoName = encodeURIComponent(opts.repoName);
|
const repoName = encodeURIComponent(opts.repo.name);
|
||||||
|
|
||||||
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
|
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
|
||||||
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
||||||
|
@ -35,14 +29,12 @@ export async function fetchLatestArtifactMetadata(
|
||||||
|
|
||||||
return !artifact ? null : {
|
return !artifact ? null : {
|
||||||
releaseName: response.name,
|
releaseName: response.name,
|
||||||
releaseDate: new Date(response.published_at),
|
|
||||||
downloadUrl: artifact.browser_download_url
|
downloadUrl: artifact.browser_download_url
|
||||||
};
|
};
|
||||||
|
|
||||||
// Noise denotes tremendous amount of data that we are not using here
|
// Noise denotes tremendous amount of data that we are not using here
|
||||||
interface GithubRelease {
|
interface GithubRelease {
|
||||||
name: string;
|
name: string;
|
||||||
published_at: Date;
|
|
||||||
assets: Array<{
|
assets: Array<{
|
||||||
browser_download_url: string;
|
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": [
|
"lib": [
|
||||||
"es2019"
|
"es2019"
|
||||||
],
|
],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
Loading…
Reference in a new issue