diff options
Diffstat (limited to 'editors/code/src')
-rw-r--r-- | editors/code/src/client.ts | 22 | ||||
-rw-r--r-- | editors/code/src/config.ts | 56 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 6 | ||||
-rw-r--r-- | editors/code/src/installation/download_file.ts (renamed from editors/code/src/github/download_file.ts) | 2 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_latest_artifact_metadata.ts (renamed from editors/code/src/github/fetch_latest_artifact_metadata.ts) | 18 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 26 | ||||
-rw-r--r-- | editors/code/src/installation/language_server.ts | 119 |
7 files changed, 210 insertions, 39 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 7e7e909dd..7639ed44b 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -1,24 +1,18 @@ | |||
1 | import { homedir } from 'os'; | ||
2 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
3 | import { spawnSync } from 'child_process'; | ||
4 | 2 | ||
5 | import { window, workspace } from 'vscode'; | 3 | import { window, workspace } from 'vscode'; |
6 | import { Config } from './config'; | 4 | import { Config } from './config'; |
5 | import { ensureLanguageServerBinary } from './installation/language_server'; | ||
7 | 6 | ||
8 | export function createClient(config: Config): lc.LanguageClient { | 7 | export async function createClient(config: Config): Promise<null | lc.LanguageClient> { |
9 | // '.' Is the fallback if no folder is open | 8 | // '.' Is the fallback if no folder is open |
10 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). | 9 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). |
11 | // It might be a good idea to test if the uri points to a file. | 10 | // It might be a good idea to test if the uri points to a file. |
12 | const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; | 11 | const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; |
13 | 12 | ||
14 | const raLspServerPath = expandPathResolving(config.raLspServerPath); | 13 | const raLspServerPath = await ensureLanguageServerBinary(config.raLspServerSource); |
15 | if (spawnSync(raLspServerPath, ["--version"]).status !== 0) { | 14 | if (!raLspServerPath) return null; |
16 | window.showErrorMessage( | 15 | |
17 | `Unable to execute '${raLspServerPath} --version'\n\n` + | ||
18 | `Perhaps it is not in $PATH?\n\n` + | ||
19 | `PATH=${process.env.PATH}\n` | ||
20 | ); | ||
21 | } | ||
22 | const run: lc.Executable = { | 16 | const run: lc.Executable = { |
23 | command: raLspServerPath, | 17 | command: raLspServerPath, |
24 | options: { cwd: workspaceFolderPath }, | 18 | options: { cwd: workspaceFolderPath }, |
@@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient { | |||
87 | res.registerProposedFeatures(); | 81 | res.registerProposedFeatures(); |
88 | return res; | 82 | return res; |
89 | } | 83 | } |
90 | function expandPathResolving(path: string) { | ||
91 | if (path.startsWith('~/')) { | ||
92 | return path.replace('~', homedir()); | ||
93 | } | ||
94 | return path; | ||
95 | } | ||
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 524620433..aca5dab5a 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import * as os from "os"; | ||
1 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | import { BinarySource, BinarySourceType } from "./installation/interfaces"; | ||
2 | 4 | ||
3 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; | 5 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; |
4 | 6 | ||
@@ -16,10 +18,24 @@ export interface CargoFeatures { | |||
16 | } | 18 | } |
17 | 19 | ||
18 | export class Config { | 20 | export class Config { |
21 | readonly raLspServerGithubArtifactName = { | ||
22 | linux: "ra_lsp_server-linux", | ||
23 | darwin: "ra_lsp_server-mac", | ||
24 | win32: "ra_lsp_server-windows.exe", | ||
25 | aix: null, | ||
26 | android: null, | ||
27 | freebsd: null, | ||
28 | openbsd: null, | ||
29 | sunos: null, | ||
30 | cygwin: null, | ||
31 | netbsd: null, | ||
32 | }[process.platform]; | ||
33 | |||
34 | raLspServerSource!: null | BinarySource; | ||
35 | |||
19 | highlightingOn = true; | 36 | highlightingOn = true; |
20 | rainbowHighlightingOn = false; | 37 | rainbowHighlightingOn = false; |
21 | enableEnhancedTyping = true; | 38 | enableEnhancedTyping = true; |
22 | raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; | ||
23 | lruCapacity: null | number = null; | 39 | lruCapacity: null | number = null; |
24 | displayInlayHints = true; | 40 | displayInlayHints = true; |
25 | maxInlayHintLength: null | number = null; | 41 | maxInlayHintLength: null | number = null; |
@@ -45,11 +61,20 @@ export class Config { | |||
45 | private prevCargoWatchOptions: null | CargoWatchOptions = null; | 61 | private prevCargoWatchOptions: null | CargoWatchOptions = null; |
46 | 62 | ||
47 | constructor(ctx: vscode.ExtensionContext) { | 63 | constructor(ctx: vscode.ExtensionContext) { |
48 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions); | 64 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions); |
49 | this.refresh(); | 65 | this.refresh(ctx); |
66 | } | ||
67 | |||
68 | private static expandPathResolving(path: string) { | ||
69 | if (path.startsWith('~/')) { | ||
70 | return path.replace('~', os.homedir()); | ||
71 | } | ||
72 | return path; | ||
50 | } | 73 | } |
51 | 74 | ||
52 | private refresh() { | 75 | // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default |
76 | // values only in one place (i.e. remove default values from non-readonly members declarations) | ||
77 | private refresh(ctx: vscode.ExtensionContext) { | ||
53 | const config = vscode.workspace.getConfiguration('rust-analyzer'); | 78 | const config = vscode.workspace.getConfiguration('rust-analyzer'); |
54 | 79 | ||
55 | let requireReloadMessage = null; | 80 | let requireReloadMessage = null; |
@@ -82,9 +107,26 @@ export class Config { | |||
82 | this.prevEnhancedTyping = this.enableEnhancedTyping; | 107 | this.prevEnhancedTyping = this.enableEnhancedTyping; |
83 | } | 108 | } |
84 | 109 | ||
85 | if (config.has('raLspServerPath')) { | 110 | { |
86 | this.raLspServerPath = | 111 | const raLspServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath"); |
87 | RA_LSP_DEBUG || (config.get('raLspServerPath') as string); | 112 | if (raLspServerPath) { |
113 | this.raLspServerSource = { | ||
114 | type: BinarySourceType.ExplicitPath, | ||
115 | path: Config.expandPathResolving(raLspServerPath) | ||
116 | }; | ||
117 | } else if (this.raLspServerGithubArtifactName) { | ||
118 | this.raLspServerSource = { | ||
119 | type: BinarySourceType.GithubBinary, | ||
120 | dir: ctx.globalStoragePath, | ||
121 | file: this.raLspServerGithubArtifactName, | ||
122 | repo: { | ||
123 | name: "rust-analyzer", | ||
124 | owner: "rust-analyzer", | ||
125 | } | ||
126 | }; | ||
127 | } else { | ||
128 | this.raLspServerSource = null; | ||
129 | } | ||
88 | } | 130 | } |
89 | 131 | ||
90 | if (config.has('cargo-watch.enable')) { | 132 | if (config.has('cargo-watch.enable')) { |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index a4dcc3037..f0e2d72f7 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts | |||
@@ -29,7 +29,11 @@ export class Ctx { | |||
29 | await old.stop(); | 29 | await old.stop(); |
30 | } | 30 | } |
31 | this.client = null; | 31 | this.client = null; |
32 | const client = createClient(this.config); | 32 | const client = await createClient(this.config); |
33 | if (!client) { | ||
34 | throw new Error("Rust Analyzer Language Server is not available"); | ||
35 | } | ||
36 | |||
33 | this.pushCleanup(client.start()); | 37 | this.pushCleanup(client.start()); |
34 | await client.onReady(); | 38 | await client.onReady(); |
35 | 39 | ||
diff --git a/editors/code/src/github/download_file.ts b/editors/code/src/installation/download_file.ts index f40750be9..7b537e114 100644 --- a/editors/code/src/github/download_file.ts +++ b/editors/code/src/installation/download_file.ts | |||
@@ -7,7 +7,7 @@ export async function downloadFile( | |||
7 | destFilePath: fs.PathLike, | 7 | destFilePath: fs.PathLike, |
8 | onProgress: (readBytes: number, totalBytes: number) => void | 8 | onProgress: (readBytes: number, totalBytes: number) => void |
9 | ): Promise<void> { | 9 | ): Promise<void> { |
10 | onProgress = throttle(100, /* noTrailing: */ true, onProgress); | 10 | onProgress = throttle(1000, /* noTrailing: */ true, onProgress); |
11 | 11 | ||
12 | const response = await fetch(url); | 12 | const response = await fetch(url); |
13 | 13 | ||
diff --git a/editors/code/src/github/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts index 52641ca67..f07431aac 100644 --- a/editors/code/src/github/fetch_latest_artifact_metadata.ts +++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts | |||
@@ -1,25 +1,19 @@ | |||
1 | import fetch from "node-fetch"; | 1 | import fetch from "node-fetch"; |
2 | import { GithubRepo, ArtifactMetadata } from "./interfaces"; | ||
2 | 3 | ||
3 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | 4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; |
4 | 5 | ||
5 | export interface FetchLatestArtifactMetadataOpts { | 6 | export interface FetchLatestArtifactMetadataOpts { |
6 | repoName: string; | 7 | repo: GithubRepo; |
7 | repoOwner: string; | ||
8 | artifactFileName: string; | 8 | artifactFileName: string; |
9 | } | 9 | } |
10 | 10 | ||
11 | export interface ArtifactMetadata { | ||
12 | releaseName: string; | ||
13 | releaseDate: Date; | ||
14 | downloadUrl: string; | ||
15 | } | ||
16 | |||
17 | export async function fetchLatestArtifactMetadata( | 11 | export async function fetchLatestArtifactMetadata( |
18 | opts: FetchLatestArtifactMetadataOpts | 12 | opts: FetchLatestArtifactMetadataOpts |
19 | ): Promise<ArtifactMetadata | null> { | 13 | ): Promise<null | ArtifactMetadata> { |
20 | 14 | ||
21 | const repoOwner = encodeURIComponent(opts.repoOwner); | 15 | const repoOwner = encodeURIComponent(opts.repo.owner); |
22 | const repoName = encodeURIComponent(opts.repoName); | 16 | const repoName = encodeURIComponent(opts.repo.name); |
23 | 17 | ||
24 | const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; | 18 | const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; |
25 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | 19 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; |
@@ -35,14 +29,12 @@ export async function fetchLatestArtifactMetadata( | |||
35 | 29 | ||
36 | return !artifact ? null : { | 30 | return !artifact ? null : { |
37 | releaseName: response.name, | 31 | releaseName: response.name, |
38 | releaseDate: new Date(response.published_at), | ||
39 | downloadUrl: artifact.browser_download_url | 32 | downloadUrl: artifact.browser_download_url |
40 | }; | 33 | }; |
41 | 34 | ||
42 | // Noise denotes tremendous amount of data that we are not using here | 35 | // Noise denotes tremendous amount of data that we are not using here |
43 | interface GithubRelease { | 36 | interface GithubRelease { |
44 | name: string; | 37 | name: string; |
45 | published_at: Date; | ||
46 | assets: Array<{ | 38 | assets: Array<{ |
47 | browser_download_url: string; | 39 | browser_download_url: string; |
48 | 40 | ||
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts new file mode 100644 index 000000000..f54e24e26 --- /dev/null +++ b/editors/code/src/installation/interfaces.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | export interface GithubRepo { | ||
2 | name: string; | ||
3 | owner: string; | ||
4 | } | ||
5 | |||
6 | export interface ArtifactMetadata { | ||
7 | releaseName: string; | ||
8 | downloadUrl: string; | ||
9 | } | ||
10 | |||
11 | |||
12 | export enum BinarySourceType { ExplicitPath, GithubBinary } | ||
13 | |||
14 | export type BinarySource = EplicitPathSource | GithubBinarySource; | ||
15 | |||
16 | export interface EplicitPathSource { | ||
17 | type: BinarySourceType.ExplicitPath; | ||
18 | path: string; | ||
19 | } | ||
20 | |||
21 | export interface GithubBinarySource { | ||
22 | type: BinarySourceType.GithubBinary; | ||
23 | repo: GithubRepo; | ||
24 | dir: string; | ||
25 | file: string; | ||
26 | } | ||
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts new file mode 100644 index 000000000..2b3ce6621 --- /dev/null +++ b/editors/code/src/installation/language_server.ts | |||
@@ -0,0 +1,119 @@ | |||
1 | import { unwrapNotNil } from "ts-not-nil"; | ||
2 | import { spawnSync } from "child_process"; | ||
3 | import * as vscode from "vscode"; | ||
4 | import * as path from "path"; | ||
5 | import { strict as assert } from "assert"; | ||
6 | import { promises as fs } from "fs"; | ||
7 | |||
8 | import { BinarySource, BinarySourceType, GithubBinarySource } from "./interfaces"; | ||
9 | import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata"; | ||
10 | import { downloadFile } from "./download_file"; | ||
11 | |||
12 | export async function downloadLatestLanguageServer( | ||
13 | {file: artifactFileName, dir: installationDir, repo}: GithubBinarySource | ||
14 | ) { | ||
15 | const binaryMetadata = await fetchLatestArtifactMetadata({ artifactFileName, repo }); | ||
16 | |||
17 | const { | ||
18 | releaseName, | ||
19 | downloadUrl | ||
20 | } = unwrapNotNil(binaryMetadata, `Latest GitHub release lacks "${artifactFileName}" file`); | ||
21 | |||
22 | await fs.mkdir(installationDir).catch(err => assert.strictEqual( | ||
23 | err && err.code, | ||
24 | "EEXIST", | ||
25 | `Couldn't create directory "${installationDir}" to download `+ | ||
26 | `language server binary: ${err.message}` | ||
27 | )); | ||
28 | |||
29 | const installationPath = path.join(installationDir, artifactFileName); | ||
30 | |||
31 | await vscode.window.withProgress( | ||
32 | { | ||
33 | location: vscode.ProgressLocation.Notification, | ||
34 | cancellable: false, // FIXME: add support for canceling download? | ||
35 | title: `Downloading language server ${releaseName}` | ||
36 | }, | ||
37 | async (progress, _) => { | ||
38 | let lastPrecentage = 0; | ||
39 | await downloadFile(downloadUrl, installationPath, (readBytes, totalBytes) => { | ||
40 | const newPercentage = (readBytes / totalBytes) * 100; | ||
41 | progress.report({ | ||
42 | message: newPercentage.toFixed(0) + "%", | ||
43 | increment: newPercentage - lastPrecentage | ||
44 | }); | ||
45 | |||
46 | lastPrecentage = newPercentage; | ||
47 | }); | ||
48 | } | ||
49 | ); | ||
50 | |||
51 | await fs.chmod(installationPath, 111); // Set xxx permissions | ||
52 | } | ||
53 | export async function ensureLanguageServerBinary( | ||
54 | langServerSource: null | BinarySource | ||
55 | ): Promise<null | string> { | ||
56 | |||
57 | if (!langServerSource) { | ||
58 | vscode.window.showErrorMessage( | ||
59 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
60 | "You need to manually clone rust-analyzer repository and " + | ||
61 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
62 | "If you feel that your platform should be supported, please create an issue " + | ||
63 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
64 | "will consider it." | ||
65 | ); | ||
66 | return null; | ||
67 | } | ||
68 | |||
69 | switch (langServerSource.type) { | ||
70 | case BinarySourceType.ExplicitPath: { | ||
71 | if (isBinaryAvailable(langServerSource.path)) { | ||
72 | return langServerSource.path; | ||
73 | } | ||
74 | vscode.window.showErrorMessage( | ||
75 | `Unable to execute ${'`'}${langServerSource.path} --version${'`'}. ` + | ||
76 | "To use the bundled language server, set `rust-analyzer.raLspServerPath` " + | ||
77 | "value to `null` or remove it from the settings to use it by default." | ||
78 | ); | ||
79 | return null; | ||
80 | } | ||
81 | case BinarySourceType.GithubBinary: { | ||
82 | const bundledBinaryPath = path.join(langServerSource.dir, langServerSource.file); | ||
83 | |||
84 | if (!isBinaryAvailable(bundledBinaryPath)) { | ||
85 | const userResponse = await vscode.window.showInformationMessage( | ||
86 | `Language server binary for rust-analyzer was not found. ` + | ||
87 | `Do you want to download it now?`, | ||
88 | "Download now", "Cancel" | ||
89 | ); | ||
90 | if (userResponse !== "Download now") return null; | ||
91 | |||
92 | try { | ||
93 | await downloadLatestLanguageServer(langServerSource); | ||
94 | } catch (err) { | ||
95 | await vscode.window.showErrorMessage( | ||
96 | `Failed to download language server from ${langServerSource.repo.name} ` + | ||
97 | `GitHub repository: ${err.message}` | ||
98 | ); | ||
99 | return null; | ||
100 | } | ||
101 | |||
102 | |||
103 | assert( | ||
104 | isBinaryAvailable(bundledBinaryPath), | ||
105 | "Downloaded language server binary is not functional" | ||
106 | ); | ||
107 | |||
108 | vscode.window.showInformationMessage( | ||
109 | "Rust analyzer language server was successfully installed" | ||
110 | ); | ||
111 | } | ||
112 | return bundledBinaryPath; | ||
113 | } | ||
114 | } | ||
115 | |||
116 | function isBinaryAvailable(binaryPath: string) { | ||
117 | return spawnSync(binaryPath, ["--version"]).status === 0; | ||
118 | } | ||
119 | } | ||