vscode: amended config to use binary from globalStoragePath, added ui for downloading

This commit is contained in:
Veetaha 2020-02-08 04:22:44 +02:00
parent 3e0e4e90ae
commit 5d88c1db38
10 changed files with 229 additions and 41 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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')) {

View File

@ -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();

View File

@ -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);

View File

@ -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;

View 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;
}

View 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;
}
}

View File

@ -6,6 +6,8 @@
"lib": [
"es2019"
],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"rootDir": "src",
"strict": true,