From 64c92e1b69a686bc871ab5dc8faac2b72b540745 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Mon, 17 Aug 2020 20:14:28 +0200 Subject: [PATCH 1/8] Adopt rust-analyzer's Rust TM grammar --- CHANGELOG.md | 2 ++ package.json | 7 +++++++ rust-analyzer/editors/code/package.json | 5 ----- .../code/rust.tmGrammar.json => rust.tmGrammar.json | 0 4 files changed, 9 insertions(+), 5 deletions(-) rename rust-analyzer/editors/code/rust.tmGrammar.json => rust.tmGrammar.json (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e347e1e8..4f4722f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### Unreleased +* Update built-in Rust grammar + ### 0.7.8 - 2020-05-13 * Rebrand extension as RLS-agnostic diff --git a/package.json b/package.json index ff858f4a..f5eaa65f 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,13 @@ "configuration": "./language-configuration.json" } ], + "grammars": [ + { + "language": "rust", + "scopeName": "source.rust", + "path": "rust.tmGrammar.json" + } + ], "snippets": [ { "language": "rust", diff --git a/rust-analyzer/editors/code/package.json b/rust-analyzer/editors/code/package.json index aac4ba94..7f2b7400 100644 --- a/rust-analyzer/editors/code/package.json +++ b/rust-analyzer/editors/code/package.json @@ -641,11 +641,6 @@ } ], "grammars": [ - { - "language": "rust", - "scopeName": "source.rust", - "path": "rust.tmGrammar.json" - }, { "language": "ra_syntax_tree", "scopeName": "source.ra_syntax_tree", diff --git a/rust-analyzer/editors/code/rust.tmGrammar.json b/rust.tmGrammar.json similarity index 100% rename from rust-analyzer/editors/code/rust.tmGrammar.json rename to rust.tmGrammar.json From 54d74e80abdb09fd87db6770a3b4e78be352dbf2 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Mon, 17 Aug 2020 20:57:33 +0200 Subject: [PATCH 2/8] Move RA syntax tree grammar support --- package.json | 11 +++++++++++ ...mGrammar.json => ra_syntax_tree.tmGrammar.json | 0 rust-analyzer/editors/code/package.json | 15 --------------- 3 files changed, 11 insertions(+), 15 deletions(-) rename rust-analyzer/editors/code/ra_syntax_tree.tmGrammar.json => ra_syntax_tree.tmGrammar.json (100%) diff --git a/package.json b/package.json index f5eaa65f..65b3dad7 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,12 @@ ".rs" ], "configuration": "./language-configuration.json" + }, + { + "id": "ra_syntax_tree", + "extensions": [ + ".rast" + ] } ], "grammars": [ @@ -87,6 +93,11 @@ "language": "rust", "scopeName": "source.rust", "path": "rust.tmGrammar.json" + }, + { + "language": "ra_syntax_tree", + "scopeName": "source.ra_syntax_tree", + "path": "ra_syntax_tree.tmGrammar.json" } ], "snippets": [ diff --git a/rust-analyzer/editors/code/ra_syntax_tree.tmGrammar.json b/ra_syntax_tree.tmGrammar.json similarity index 100% rename from rust-analyzer/editors/code/ra_syntax_tree.tmGrammar.json rename to ra_syntax_tree.tmGrammar.json diff --git a/rust-analyzer/editors/code/package.json b/rust-analyzer/editors/code/package.json index 7f2b7400..b84eeecf 100644 --- a/rust-analyzer/editors/code/package.json +++ b/rust-analyzer/editors/code/package.json @@ -632,21 +632,6 @@ ] } ], - "languages": [ - { - "id": "ra_syntax_tree", - "extensions": [ - ".rast" - ] - } - ], - "grammars": [ - { - "language": "ra_syntax_tree", - "scopeName": "source.ra_syntax_tree", - "path": "ra_syntax_tree.tmGrammar.json" - } - ], "problemMatchers": [ { "name": "rustc", From 4a1de1a12b7e17180dd3ee48f713f7710986f9dc Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 18 Aug 2020 16:31:42 +0200 Subject: [PATCH 3/8] Adapt net.ts --- rust-analyzer/editors/code/src/net.ts | 143 -------------------------- src/net.ts | 44 +++++--- src/rustAnalyzer.ts | 10 +- 3 files changed, 35 insertions(+), 162 deletions(-) delete mode 100644 rust-analyzer/editors/code/src/net.ts diff --git a/rust-analyzer/editors/code/src/net.ts b/rust-analyzer/editors/code/src/net.ts deleted file mode 100644 index 681eaa9c..00000000 --- a/rust-analyzer/editors/code/src/net.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Replace with `import fetch from "node-fetch"` once this is fixed in rollup: -// https://github.com/rollup/plugins/issues/491 -const fetch = require("node-fetch") as typeof import("node-fetch")["default"]; - -import * as vscode from "vscode"; -import * as stream from "stream"; -import * as crypto from "crypto"; -import * as fs from "fs"; -import * as zlib from "zlib"; -import * as util from "util"; -import * as path from "path"; -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 { - - 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; - }>; -} - -interface DownloadOpts { - progressTitle: string; - url: string; - dest: string; - mode?: number; - gunzip?: boolean; -} - -export async function download(opts: DownloadOpts) { - // Put artifact into a temporary file (in the same dir for simplicity) - // to prevent partially downloaded files when user kills vscode - const dest = path.parse(opts.dest); - const randomHex = crypto.randomBytes(5).toString("hex"); - const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); - - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, - title: opts.progressTitle - }, - async (progress, _cancellationToken) => { - let lastPercentage = 0; - await downloadFile(opts.url, tempFile, opts.mode, !!opts.gunzip, (readBytes, totalBytes) => { - const newPercentage = (readBytes / totalBytes) * 100; - progress.report({ - message: newPercentage.toFixed(0) + "%", - increment: newPercentage - lastPercentage - }); - - lastPercentage = newPercentage; - }); - } - ); - - await fs.promises.rename(tempFile, opts.dest); -} - -async function downloadFile( - url: string, - destFilePath: fs.PathLike, - mode: number | undefined, - gunzip: boolean, - onProgress: (readBytes: number, totalBytes: number) => void -): Promise { - const res = await fetch(url); - - if (!res.ok) { - log.error("Error", res.status, "while downloading file from", url); - log.error({ body: await res.text(), headers: res.headers }); - - throw new Error(`Got response ${res.status} when trying to download a file.`); - } - - const totalBytes = Number(res.headers.get('content-length')); - assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); - - log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); - - let readBytes = 0; - res.body.on("data", (chunk: Buffer) => { - readBytes += chunk.length; - onProgress(readBytes, totalBytes); - }); - - const destFileStream = fs.createWriteStream(destFilePath, { mode }); - const srcStream = gunzip ? res.body.pipe(zlib.createGunzip()) : res.body; - - await pipeline(srcStream, destFileStream); - - await new Promise(resolve => { - destFileStream.on("close", resolve); - destFileStream.destroy(); - // This workaround is awaiting to be removed when vscode moves to newer nodejs version: - // https://github.com/rust-analyzer/rust-analyzer/issues/3167 - }); -} diff --git a/src/net.ts b/src/net.ts index 26cd05fb..939683c6 100644 --- a/src/net.ts +++ b/src/net.ts @@ -1,9 +1,12 @@ import * as assert from 'assert'; +import * as crypto from 'crypto'; import * as fs from 'fs'; import fetch from 'node-fetch'; +import * as path from 'path'; import * as stream from 'stream'; import * as util from 'util'; import * as vscode from 'vscode'; +import * as zlib from 'zlib'; const pipeline = util.promisify(stream.pipeline); @@ -62,24 +65,34 @@ export interface GithubRelease { }>; } -export async function download( - downloadUrl: string, - destinationPath: string, - progressTitle: string, - { mode }: { mode?: number } = {}, -) { +interface DownloadOpts { + progressTitle: string; + url: string; + dest: string; + mode?: number; + gunzip?: boolean; +} + +export async function download(opts: DownloadOpts) { + // Put artifact into a temporary file (in the same dir for simplicity) + // to prevent partially downloaded files when user kills vscode + const dest = path.parse(opts.dest); + const randomHex = crypto.randomBytes(5).toString('hex'); + const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); + await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, cancellable: false, - title: progressTitle, + title: opts.progressTitle, }, async (progress, _cancellationToken) => { let lastPercentage = 0; await downloadFile( - downloadUrl, - destinationPath, - mode, + opts.url, + tempFile, + opts.mode, + Boolean(opts.gunzip), (readBytes, totalBytes) => { const newPercentage = (readBytes / totalBytes) * 100; progress.report({ @@ -92,6 +105,8 @@ export async function download( ); }, ); + + return fs.promises.rename(tempFile, opts.dest); } /** @@ -104,6 +119,7 @@ async function downloadFile( url: string, destFilePath: fs.PathLike, mode: number | undefined, + gunzip: boolean, onProgress: (readBytes: number, totalBytes: number) => void, ): Promise { const res = await fetch(url); @@ -136,13 +152,13 @@ async function downloadFile( }); const destFileStream = fs.createWriteStream(destFilePath, { mode }); + const srcStream = gunzip ? res.body.pipe(zlib.createGunzip()) : res.body; - await pipeline(res.body, destFileStream); + await pipeline(srcStream, destFileStream); return new Promise(resolve => { destFileStream.on('close', resolve); destFileStream.destroy(); - - // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 - // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 + // This workaround is awaiting to be removed when vscode moves to newer nodejs version: + // https://github.com/rust-analyzer/rust-analyzer/issues/3167 }); } diff --git a/src/rustAnalyzer.ts b/src/rustAnalyzer.ts index 385986a3..fdf801e3 100644 --- a/src/rustAnalyzer.ts +++ b/src/rustAnalyzer.ts @@ -179,12 +179,12 @@ export async function getServer({ throw new Error(`Bad release: ${JSON.stringify(release)}`); } - await download( - artifact.browser_download_url, + await download({ + url: artifact.browser_download_url, dest, - 'Downloading rust-analyzer server', - { mode: 0o755 }, - ); + progressTitle: 'Downloading rust-analyzer server', + mode: 0o755, + }); await writeMetadata({ releaseTag: pkg.releaseTag }).catch(() => { vs.window.showWarningMessage(`Couldn't save rust-analyzer metadata`); From 9245654151c6cafbc9ae1b735c1d629c77968768 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 18 Aug 2020 16:51:09 +0200 Subject: [PATCH 4/8] Move existing rustAnalyzer.ts under dedicated submodule --- src/extension.ts | 2 +- src/rust-analyzer/index.ts | 6 ++++++ src/{ => rust-analyzer}/rustAnalyzer.ts | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 src/rust-analyzer/index.ts rename src/{ => rust-analyzer}/rustAnalyzer.ts (98%) diff --git a/src/extension.ts b/src/extension.ts index 51b45081..4a6c2594 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,7 @@ import * as lc from 'vscode-languageclient'; import { RLSConfiguration } from './configuration'; import * as rls from './rls'; -import * as rustAnalyzer from './rustAnalyzer'; +import * as rustAnalyzer from './rust-analyzer'; import { rustupUpdate } from './rustup'; import { startSpinner, stopSpinner } from './spinner'; import { activateTaskProvider, Execution, runRlsCommand } from './tasks'; diff --git a/src/rust-analyzer/index.ts b/src/rust-analyzer/index.ts new file mode 100644 index 00000000..c45c8e84 --- /dev/null +++ b/src/rust-analyzer/index.ts @@ -0,0 +1,6 @@ +export { + createLanguageClient, + getServer, + setupClient, + setupProgress, +} from './rustAnalyzer'; diff --git a/src/rustAnalyzer.ts b/src/rust-analyzer/rustAnalyzer.ts similarity index 98% rename from src/rustAnalyzer.ts rename to src/rust-analyzer/rustAnalyzer.ts index fdf801e3..686bd1ed 100644 --- a/src/rustAnalyzer.ts +++ b/src/rust-analyzer/rustAnalyzer.ts @@ -6,10 +6,10 @@ import { promisify } from 'util'; import * as vs from 'vscode'; import * as lc from 'vscode-languageclient'; -import { WorkspaceProgress } from './extension'; -import { download, fetchRelease } from './net'; -import * as rustup from './rustup'; -import { Observable } from './utils/observable'; +import { WorkspaceProgress } from '../extension'; +import { download, fetchRelease } from '../net'; +import * as rustup from '../rustup'; +import { Observable } from '../utils/observable'; const stat = promisify(fs.stat); const mkdir = promisify(fs.mkdir); From 94ed1027e54b5eb04a5ae0ed13f29f3fd00d770c Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 18 Aug 2020 17:10:20 +0200 Subject: [PATCH 5/8] Prepare home-grown persistent metadata for merge with RA one --- src/rust-analyzer/persistent_state.ts | 69 +++++++++++++++++++++++++++ src/rust-analyzer/rustAnalyzer.ts | 58 +--------------------- 2 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 src/rust-analyzer/persistent_state.ts diff --git a/src/rust-analyzer/persistent_state.ts b/src/rust-analyzer/persistent_state.ts new file mode 100644 index 00000000..0cffb35e --- /dev/null +++ b/src/rust-analyzer/persistent_state.ts @@ -0,0 +1,69 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; + +const stat = promisify(fs.stat); +const mkdir = promisify(fs.mkdir); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +/** Returns a path where persistent data for rust-analyzer should be installed. */ +function metadataDir(): string | undefined { + if (process.platform === 'linux' || process.platform === 'darwin') { + // Prefer, in this order: + // 1. $XDG_CONFIG_HOME/rust-analyzer + // 2. $HOME/.config/rust-analyzer + const { HOME, XDG_CONFIG_HOME } = process.env; + const baseDir = XDG_CONFIG_HOME || (HOME && path.join(HOME, '.config')); + + return baseDir && path.resolve(path.join(baseDir, 'rust-analyzer')); + } else if (process.platform === 'win32') { + // %LocalAppData%\rust-analyzer\ + const { LocalAppData } = process.env; + return ( + LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) + ); + } + + return undefined; +} + +export interface Metadata { + releaseTag: string; +} + +export async function readMetadata(): Promise< + Metadata | Record +> { + const stateDir = metadataDir(); + if (!stateDir) { + return { kind: 'error', code: 'NotSupported' }; + } + + const filePath = path.join(stateDir, 'metadata.json'); + if (!(await stat(filePath).catch(() => false))) { + return { kind: 'error', code: 'FileMissing' }; + } + + const contents = await readFile(filePath, 'utf8'); + const obj = JSON.parse(contents) as unknown; + return typeof obj === 'object' ? (obj as Record) : {}; +} + +export async function writeMetadata(config: Metadata) { + const stateDir = metadataDir(); + if (!stateDir) { + return false; + } + + if (!(await ensureDir(stateDir))) { + return false; + } + + const filePath = path.join(stateDir, 'metadata.json'); + return writeFile(filePath, JSON.stringify(config)).then(() => true); +} + +function ensureDir(path: string) { + return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); +} diff --git a/src/rust-analyzer/rustAnalyzer.ts b/src/rust-analyzer/rustAnalyzer.ts index 686bd1ed..1f0d3528 100644 --- a/src/rust-analyzer/rustAnalyzer.ts +++ b/src/rust-analyzer/rustAnalyzer.ts @@ -10,11 +10,10 @@ import { WorkspaceProgress } from '../extension'; import { download, fetchRelease } from '../net'; import * as rustup from '../rustup'; import { Observable } from '../utils/observable'; +import { Metadata, readMetadata, writeMetadata } from './persistent_state'; const stat = promisify(fs.stat); const mkdir = promisify(fs.mkdir); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); const REQUIRED_COMPONENTS = ['rust-src']; @@ -45,27 +44,6 @@ function installDir(): string | undefined { return undefined; } -/** Returns a path where persistent data for rust-analyzer should be installed. */ -function metadataDir(): string | undefined { - if (process.platform === 'linux' || process.platform === 'darwin') { - // Prefer, in this order: - // 1. $XDG_CONFIG_HOME/rust-analyzer - // 2. $HOME/.config/rust-analyzer - const { HOME, XDG_CONFIG_HOME } = process.env; - const baseDir = XDG_CONFIG_HOME || (HOME && path.join(HOME, '.config')); - - return baseDir && path.resolve(path.join(baseDir, 'rust-analyzer')); - } else if (process.platform === 'win32') { - // %LocalAppData%\rust-analyzer\ - const { LocalAppData } = process.env; - return ( - LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) - ); - } - - return undefined; -} - function ensureDir(path: string) { return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); } @@ -77,40 +55,6 @@ interface RustAnalyzerConfig { }; } -interface Metadata { - releaseTag: string; -} - -async function readMetadata(): Promise> { - const stateDir = metadataDir(); - if (!stateDir) { - return { kind: 'error', code: 'NotSupported' }; - } - - const filePath = path.join(stateDir, 'metadata.json'); - if (!(await stat(filePath).catch(() => false))) { - return { kind: 'error', code: 'FileMissing' }; - } - - const contents = await readFile(filePath, 'utf8'); - const obj = JSON.parse(contents) as unknown; - return typeof obj === 'object' ? (obj as Record) : {}; -} - -async function writeMetadata(config: Metadata) { - const stateDir = metadataDir(); - if (!stateDir) { - return false; - } - - if (!(await ensureDir(stateDir))) { - return false; - } - - const filePath = path.join(stateDir, 'metadata.json'); - return writeFile(filePath, JSON.stringify(config)).then(() => true); -} - export async function getServer({ askBeforeDownload, package: pkg, From 3cfc22ad1b6c7c3bfeacea8979a1fa7c6f7586c9 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 18 Aug 2020 21:49:56 +0200 Subject: [PATCH 6/8] Weave in global state when constructing lang server --- src/extension.ts | 49 +++++++++++++++++--------- src/rust-analyzer/persistent_state.ts | 50 +++++++++++++++++++++++---- src/rust-analyzer/rustAnalyzer.ts | 38 +++++++++++++------- 3 files changed, 102 insertions(+), 35 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4a6c2594..1b1b70ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,6 +11,7 @@ import { workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent, + Memento, } from 'vscode'; import * as lc from 'vscode-languageclient'; @@ -32,17 +33,21 @@ export interface Api { } export async function activate(context: ExtensionContext): Promise { + // Weave in global state when handling changed active text editor + const handleChangedActiveTextEd = (ed: TextEditor | undefined) => + onDidChangeActiveTextEditor(ed, context.globalState); + context.subscriptions.push( ...[ configureLanguage(), ...registerCommands(), workspace.onDidChangeWorkspaceFolders(whenChangingWorkspaceFolders), - window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), + window.onDidChangeActiveTextEditor(handleChangedActiveTextEd), ], ); // Manually trigger the first event to start up server instance if necessary, // since VSCode doesn't do that on startup by itself. - onDidChangeActiveTextEditor(window.activeTextEditor); + handleChangedActiveTextEd(window.activeTextEditor); // Migrate the users of multi-project setup for RLS to disable the setting // entirely (it's always on now) @@ -81,13 +86,16 @@ export async function deactivate() { /** Tracks dynamically updated progress for the active client workspace for UI purposes. */ let progressObserver: Disposable | undefined; -function onDidChangeActiveTextEditor(editor: TextEditor | undefined) { +function onDidChangeActiveTextEditor( + editor: TextEditor | undefined, + globalState: Memento, +) { if (!editor || !editor.document) { return; } const { languageId, uri } = editor.document; - const workspace = clientWorkspaceForUri(uri, { + const workspace = clientWorkspaceForUri(uri, globalState, { initializeIfMissing: languageId === 'rust' || languageId === 'toml', }); if (!workspace) { @@ -135,6 +143,7 @@ const workspaces: Map = new Map(); */ function clientWorkspaceForUri( uri: Uri, + globalState: Memento, options?: { initializeIfMissing: boolean }, ): ClientWorkspace | undefined { const rootFolder = workspace.getWorkspaceFolder(uri); @@ -149,7 +158,7 @@ function clientWorkspaceForUri( const existing = workspaces.get(folder.uri.toString()); if (!existing && options && options.initializeIfMissing) { - const workspace = new ClientWorkspace(folder); + const workspace = new ClientWorkspace(folder, globalState); workspaces.set(folder.uri.toString(), workspace); workspace.autoStart(); } @@ -173,15 +182,17 @@ export class ClientWorkspace { private lc: lc.LanguageClient | null = null; private disposables: Disposable[]; private _progress: Observable; + private globalState: Memento; get progress() { return this._progress; } - constructor(folder: WorkspaceFolder) { + constructor(folder: WorkspaceFolder, globalState: Memento) { this.config = RLSConfiguration.loadFromWorkspace(folder.uri.fsPath); this.folder = folder; this.disposables = []; this._progress = new Observable({ state: 'standby' }); + this.globalState = globalState; } /** @@ -198,18 +209,22 @@ export class ClientWorkspace { const { createLanguageClient, setupClient, setupProgress } = this.config.engine === 'rls' ? rls : rustAnalyzer; - const client = await createLanguageClient(this.folder, { - updateOnStartup: this.config.updateOnStartup, - revealOutputChannelOn: this.config.revealOutputChannelOn, - logToFile: this.config.logToFile, - rustup: { - channel: this.config.channel, - path: this.config.rustupPath, - disabled: this.config.rustupDisabled, + const client = await createLanguageClient( + this.folder, + { + updateOnStartup: this.config.updateOnStartup, + revealOutputChannelOn: this.config.revealOutputChannelOn, + logToFile: this.config.logToFile, + rustup: { + channel: this.config.channel, + path: this.config.rustupPath, + disabled: this.config.rustupDisabled, + }, + rls: { path: this.config.rlsPath }, + rustAnalyzer: this.config.rustAnalyzer, }, - rls: { path: this.config.rlsPath }, - rustAnalyzer: this.config.rustAnalyzer, - }); + this.globalState, + ); client.onDidChangeState(({ newState }) => { if (newState === lc.State.Starting) { diff --git a/src/rust-analyzer/persistent_state.ts b/src/rust-analyzer/persistent_state.ts index 0cffb35e..1dfbae72 100644 --- a/src/rust-analyzer/persistent_state.ts +++ b/src/rust-analyzer/persistent_state.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; +import * as vscode from 'vscode'; const stat = promisify(fs.stat); const mkdir = promisify(fs.mkdir); @@ -32,22 +33,20 @@ export interface Metadata { releaseTag: string; } -export async function readMetadata(): Promise< - Metadata | Record -> { +export async function readMetadata(): Promise> { const stateDir = metadataDir(); if (!stateDir) { - return { kind: 'error', code: 'NotSupported' }; + throw new Error('Not supported'); } const filePath = path.join(stateDir, 'metadata.json'); if (!(await stat(filePath).catch(() => false))) { - return { kind: 'error', code: 'FileMissing' }; + throw new Error('File missing'); } const contents = await readFile(filePath, 'utf8'); const obj = JSON.parse(contents) as unknown; - return typeof obj === 'object' ? (obj as Record) : {}; + return typeof obj === 'object' ? obj || {} : {}; } export async function writeMetadata(config: Metadata) { @@ -67,3 +66,42 @@ export async function writeMetadata(config: Metadata) { function ensureDir(path: string) { return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); } + +export class PersistentState { + constructor(private readonly globalState: vscode.Memento) { + const { lastCheck, releaseId, serverVersion } = this; + console.info('PersistentState:', { lastCheck, releaseId, serverVersion }); + } + + /** + * Used to check for *nightly* updates once an hour. + */ + get lastCheck(): number | undefined { + return this.globalState.get('lastCheck'); + } + 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); + } +} diff --git a/src/rust-analyzer/rustAnalyzer.ts b/src/rust-analyzer/rustAnalyzer.ts index 1f0d3528..289d6a87 100644 --- a/src/rust-analyzer/rustAnalyzer.ts +++ b/src/rust-analyzer/rustAnalyzer.ts @@ -10,7 +10,12 @@ import { WorkspaceProgress } from '../extension'; import { download, fetchRelease } from '../net'; import * as rustup from '../rustup'; import { Observable } from '../utils/observable'; -import { Metadata, readMetadata, writeMetadata } from './persistent_state'; +import { + Metadata, + readMetadata, + writeMetadata, + PersistentState, +} from './persistent_state'; const stat = promisify(fs.stat); const mkdir = promisify(fs.mkdir); @@ -55,10 +60,12 @@ interface RustAnalyzerConfig { }; } -export async function getServer({ - askBeforeDownload, - package: pkg, -}: RustAnalyzerConfig): Promise { +export async function getServer( + config: RustAnalyzerConfig, + state: PersistentState, +): Promise { + const { askBeforeDownload, package: pkg } = config; + let binaryName: string | undefined; if (process.arch === 'x64' || process.arch === 'ia32') { if (process.platform === 'linux') { @@ -159,6 +166,7 @@ export async function createLanguageClient( rustup: { disabled: boolean; path: string; channel: string }; rustAnalyzer: { path?: string; releaseTag: string }; }, + state: vs.Memento, ): Promise { if (!config.rustup.disabled) { await rustup.ensureToolchain(config.rustup); @@ -166,10 +174,13 @@ export async function createLanguageClient( } if (!config.rustAnalyzer.path) { - await getServer({ - askBeforeDownload: true, - package: { releaseTag: config.rustAnalyzer.releaseTag }, - }); + await getServer( + { + askBeforeDownload: true, + package: { releaseTag: config.rustAnalyzer.releaseTag }, + }, + new PersistentState(state), + ); } if (INSTANCE) { @@ -179,9 +190,12 @@ export async function createLanguageClient( const serverOptions: lc.ServerOptions = async () => { const binPath = config.rustAnalyzer.path || - (await getServer({ - package: { releaseTag: config.rustAnalyzer.releaseTag }, - })); + (await getServer( + { + package: { releaseTag: config.rustAnalyzer.releaseTag }, + }, + new PersistentState(state), + )); if (!binPath) { throw new Error("Couldn't fetch Rust Analyzer binary"); From 5a4dde068fe81d36143b4caa79c5f8aa801b2429 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 18 Aug 2020 23:05:40 +0200 Subject: [PATCH 7/8] Wholly use `vscode.Memento` API to store installed RA release tag --- src/rust-analyzer/persistent_state.ts | 79 ++------------------------- src/rust-analyzer/rustAnalyzer.ts | 32 +++++------ 2 files changed, 19 insertions(+), 92 deletions(-) diff --git a/src/rust-analyzer/persistent_state.ts b/src/rust-analyzer/persistent_state.ts index 1dfbae72..2022c6c3 100644 --- a/src/rust-analyzer/persistent_state.ts +++ b/src/rust-analyzer/persistent_state.ts @@ -1,75 +1,8 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { promisify } from 'util'; import * as vscode from 'vscode'; -const stat = promisify(fs.stat); -const mkdir = promisify(fs.mkdir); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); - -/** Returns a path where persistent data for rust-analyzer should be installed. */ -function metadataDir(): string | undefined { - if (process.platform === 'linux' || process.platform === 'darwin') { - // Prefer, in this order: - // 1. $XDG_CONFIG_HOME/rust-analyzer - // 2. $HOME/.config/rust-analyzer - const { HOME, XDG_CONFIG_HOME } = process.env; - const baseDir = XDG_CONFIG_HOME || (HOME && path.join(HOME, '.config')); - - return baseDir && path.resolve(path.join(baseDir, 'rust-analyzer')); - } else if (process.platform === 'win32') { - // %LocalAppData%\rust-analyzer\ - const { LocalAppData } = process.env; - return ( - LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) - ); - } - - return undefined; -} - -export interface Metadata { - releaseTag: string; -} - -export async function readMetadata(): Promise> { - const stateDir = metadataDir(); - if (!stateDir) { - throw new Error('Not supported'); - } - - const filePath = path.join(stateDir, 'metadata.json'); - if (!(await stat(filePath).catch(() => false))) { - throw new Error('File missing'); - } - - const contents = await readFile(filePath, 'utf8'); - const obj = JSON.parse(contents) as unknown; - return typeof obj === 'object' ? obj || {} : {}; -} - -export async function writeMetadata(config: Metadata) { - const stateDir = metadataDir(); - if (!stateDir) { - return false; - } - - if (!(await ensureDir(stateDir))) { - return false; - } - - const filePath = path.join(stateDir, 'metadata.json'); - return writeFile(filePath, JSON.stringify(config)).then(() => true); -} - -function ensureDir(path: string) { - return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); -} - export class PersistentState { constructor(private readonly globalState: vscode.Memento) { - const { lastCheck, releaseId, serverVersion } = this; + const { lastCheck, releaseId, releaseTag: serverVersion } = this; console.info('PersistentState:', { lastCheck, releaseId, serverVersion }); } @@ -95,13 +28,13 @@ export class PersistentState { } /** - * Version of the extension that installed the server. + * Release tag of the installed server. * Used to check if we need to update the server. */ - get serverVersion(): string | undefined { - return this.globalState.get('serverVersion'); + get releaseTag(): string | undefined { + return this.globalState.get('releaseTag'); } - async updateServerVersion(value: string | undefined) { - await this.globalState.update('serverVersion', value); + async updateReleaseTag(value: string | undefined) { + await this.globalState.update('releaseTag', value); } } diff --git a/src/rust-analyzer/rustAnalyzer.ts b/src/rust-analyzer/rustAnalyzer.ts index 289d6a87..6c4b3734 100644 --- a/src/rust-analyzer/rustAnalyzer.ts +++ b/src/rust-analyzer/rustAnalyzer.ts @@ -10,12 +10,7 @@ import { WorkspaceProgress } from '../extension'; import { download, fetchRelease } from '../net'; import * as rustup from '../rustup'; import { Observable } from '../utils/observable'; -import { - Metadata, - readMetadata, - writeMetadata, - PersistentState, -} from './persistent_state'; +import { PersistentState } from './persistent_state'; const stat = promisify(fs.stat); const mkdir = promisify(fs.mkdir); @@ -64,8 +59,6 @@ export async function getServer( config: RustAnalyzerConfig, state: PersistentState, ): Promise { - const { askBeforeDownload, package: pkg } = config; - let binaryName: string | undefined; if (process.arch === 'x64' || process.arch === 'ia32') { if (process.platform === 'linux') { @@ -96,22 +89,25 @@ export async function getServer( } await ensureDir(dir); - const metadata: Partial = await readMetadata().catch(() => ({})); - const dest = path.join(dir, binaryName); const exists = await stat(dest).catch(() => false); - if (exists && metadata.releaseTag === pkg.releaseTag) { + + if (!exists) { + await state.updateReleaseTag(undefined); + } else if (state.releaseTag === config.package.releaseTag) { return dest; } - if (askBeforeDownload) { + if (config.askBeforeDownload) { const userResponse = await vs.window.showInformationMessage( `${ - metadata.releaseTag && metadata.releaseTag !== pkg.releaseTag - ? `You seem to have installed release \`${metadata.releaseTag}\` but requested a different one.` + state.releaseTag && state.releaseTag !== config.package.releaseTag + ? `You seem to have installed release \`${state.releaseTag}\` but requested a different one.` : '' } - Release \`${pkg.releaseTag}\` of rust-analyzer is not installed.\n + Release \`${ + config.package.releaseTag + }\` of rust-analyzer is not installed.\n Install to ${dir}?`, 'Download', ); @@ -123,7 +119,7 @@ export async function getServer( const release = await fetchRelease( 'rust-analyzer', 'rust-analyzer', - pkg.releaseTag, + config.package.releaseTag, ); const artifact = release.assets.find(asset => asset.name === binaryName); if (!artifact) { @@ -137,9 +133,7 @@ export async function getServer( mode: 0o755, }); - await writeMetadata({ releaseTag: pkg.releaseTag }).catch(() => { - vs.window.showWarningMessage(`Couldn't save rust-analyzer metadata`); - }); + await state.updateReleaseTag(config.package.releaseTag); return dest; } From 0bec08314a4dc82e0991b59188cb0f4a444a1755 Mon Sep 17 00:00:00 2001 From: Igor Matuszewski Date: Tue, 25 Aug 2020 03:19:26 +0200 Subject: [PATCH 8/8] Implement nightly updates for rust-analyzer Also make the binary detection more robust, i.e. wrt. "rls.restart" commands. --- src/rust-analyzer/persistent_state.ts | 31 ++++----- src/rust-analyzer/rustAnalyzer.ts | 96 ++++++++++++++++----------- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/rust-analyzer/persistent_state.ts b/src/rust-analyzer/persistent_state.ts index 2022c6c3..37c3ff06 100644 --- a/src/rust-analyzer/persistent_state.ts +++ b/src/rust-analyzer/persistent_state.ts @@ -1,9 +1,17 @@ import * as vscode from 'vscode'; +export interface Release { + /** + * ID of a release. Used to disambiguate between different releases under *moving* tags. + */ + id: number; + tag: string; +} + export class PersistentState { constructor(private readonly globalState: vscode.Memento) { - const { lastCheck, releaseId, releaseTag: serverVersion } = this; - console.info('PersistentState:', { lastCheck, releaseId, serverVersion }); + const { lastCheck, installedRelease } = this; + console.info('PersistentState:', { lastCheck, installedRelease }); } /** @@ -16,25 +24,14 @@ export class PersistentState { 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); - } - /** * Release tag of the installed server. * Used to check if we need to update the server. */ - get releaseTag(): string | undefined { - return this.globalState.get('releaseTag'); + get installedRelease(): Release | undefined { + return this.globalState.get('installedRelease'); } - async updateReleaseTag(value: string | undefined) { - await this.globalState.update('releaseTag', value); + async updateInstalledRelease(value: Release | undefined) { + return this.globalState.update('installedRelease', value); } } diff --git a/src/rust-analyzer/rustAnalyzer.ts b/src/rust-analyzer/rustAnalyzer.ts index 6c4b3734..287907c7 100644 --- a/src/rust-analyzer/rustAnalyzer.ts +++ b/src/rust-analyzer/rustAnalyzer.ts @@ -44,10 +44,6 @@ function installDir(): string | undefined { return undefined; } -function ensureDir(path: string) { - return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); -} - interface RustAnalyzerConfig { askBeforeDownload?: boolean; package: { @@ -80,38 +76,39 @@ export async function getServer( 'about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we ' + 'will consider it.', ); - return undefined; + return; } const dir = installDir(); if (!dir) { return; + } else { + await stat(dir).catch(() => mkdir(dir, { recursive: true })); } - await ensureDir(dir); const dest = path.join(dir, binaryName); const exists = await stat(dest).catch(() => false); if (!exists) { - await state.updateReleaseTag(undefined); - } else if (state.releaseTag === config.package.releaseTag) { - return dest; + await state.updateInstalledRelease(undefined); } - if (config.askBeforeDownload) { - const userResponse = await vs.window.showInformationMessage( - `${ - state.releaseTag && state.releaseTag !== config.package.releaseTag - ? `You seem to have installed release \`${state.releaseTag}\` but requested a different one.` - : '' - } - Release \`${ - config.package.releaseTag - }\` of rust-analyzer is not installed.\n - Install to ${dir}?`, - 'Download', - ); - if (userResponse !== 'Download') { + const now = Date.now(); + if (state.installedRelease?.tag === config.package.releaseTag) { + // Release tags that are *moving* - these are expected to point to different + // commits and update as the time goes on. Make sure to poll the GitHub API + // (at most once per hour) to see if we need to update. + const MOVING_TAGS = ['nightly']; + const POLL_INTERVAL = 60 * 60 * 1000; + + const shouldCheckForNewRelease = MOVING_TAGS.includes( + config.package.releaseTag, + ) + ? state.installedRelease === undefined || + now - (state.lastCheck ?? 0) > POLL_INTERVAL + : false; + + if (!shouldCheckForNewRelease) { return dest; } } @@ -121,11 +118,35 @@ export async function getServer( 'rust-analyzer', config.package.releaseTag, ); + + if (state.installedRelease?.id === release.id) { + return dest; + } + const artifact = release.assets.find(asset => asset.name === binaryName); if (!artifact) { throw new Error(`Bad release: ${JSON.stringify(release)}`); } + if (config.askBeforeDownload) { + const userResponse = await vs.window.showInformationMessage( + `${ + state.installedRelease && + state.installedRelease.tag !== config.package.releaseTag + ? `You seem to have installed release \`${state.installedRelease?.tag}\` but requested a different one.` + : '' + } + Release \`${config.package.releaseTag}\` of rust-analyzer ${ + !state.installedRelease ? 'is not installed' : 'can be updated' + }.\n + Install to ${dir}?`, + 'Download', + ); + if (userResponse !== 'Download') { + return exists ? dest : undefined; + } + } + await download({ url: artifact.browser_download_url, dest, @@ -133,7 +154,11 @@ export async function getServer( mode: 0o755, }); - await state.updateReleaseTag(config.package.releaseTag); + await state.updateLastCheck(now); + await state.updateInstalledRelease({ + id: release.id, + tag: config.package.releaseTag, + }); return dest; } @@ -167,14 +192,18 @@ export async function createLanguageClient( await rustup.ensureComponents(config.rustup, REQUIRED_COMPONENTS); } - if (!config.rustAnalyzer.path) { - await getServer( + const binPath = + config.rustAnalyzer.path || + (await getServer( { askBeforeDownload: true, package: { releaseTag: config.rustAnalyzer.releaseTag }, }, new PersistentState(state), - ); + )); + + if (!binPath) { + throw new Error("Couldn't fetch Rust Analyzer binary"); } if (INSTANCE) { @@ -182,19 +211,6 @@ export async function createLanguageClient( } const serverOptions: lc.ServerOptions = async () => { - const binPath = - config.rustAnalyzer.path || - (await getServer( - { - package: { releaseTag: config.rustAnalyzer.releaseTag }, - }, - new PersistentState(state), - )); - - if (!binPath) { - throw new Error("Couldn't fetch Rust Analyzer binary"); - } - const childProcess = child_process.exec(binPath); if (config.logToFile) { const logPath = path.join(folder.uri.fsPath, `ra-${Date.now()}.log`);