aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-03-16 18:23:38 +0000
committerAleksey Kladov <[email protected]>2020-03-16 21:02:11 +0000
commitae662617a2bc49d025adaf9c4a8ff2dfa557d36c (patch)
treef321ad0a529283bbb276095da7644d8f1ba663d2
parent2e9b6320e66cb764b9683a38c284a06b7c35aab6 (diff)
Separate persistent mutable state from config
That way, we clearly see which things are not change, and we also clearly see which things are persistent.
-rw-r--r--editors/code/src/commands/server_version.ts2
-rw-r--r--editors/code/src/config.ts41
-rw-r--r--editors/code/src/ctx.ts6
-rw-r--r--editors/code/src/installation/extension.ts16
-rw-r--r--editors/code/src/installation/server.ts21
-rw-r--r--editors/code/src/main.ts10
-rw-r--r--editors/code/src/persistent_state.ts49
7 files changed, 80 insertions, 65 deletions
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts
index c4d84b443..83b1acf67 100644
--- a/editors/code/src/commands/server_version.ts
+++ b/editors/code/src/commands/server_version.ts
@@ -5,7 +5,7 @@ import { spawnSync } from 'child_process';
5 5
6export function serverVersion(ctx: Ctx): Cmd { 6export function serverVersion(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
8 const binaryPath = await ensureServerBinary(ctx.config); 8 const binaryPath = await ensureServerBinary(ctx.config, ctx.state);
9 9
10 if (binaryPath == null) { 10 if (binaryPath == null) {
11 throw new Error( 11 throw new Error(
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index f63e1d20e..bd8096dd6 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -182,13 +182,6 @@ export class Config {
182 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG); 182 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
183 } 183 }
184 184
185 readonly installedNightlyExtensionReleaseDate = new DateStorage(
186 "installed-nightly-extension-release-date",
187 this.ctx.globalState
188 );
189 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
190 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
191
192 // We don't do runtime config validation here for simplicity. More on stackoverflow: 185 // We don't do runtime config validation here for simplicity. More on stackoverflow:
193 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension 186 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
194 187
@@ -232,37 +225,3 @@ export class Config {
232 // for internal use 225 // for internal use
233 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } 226 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; }
234} 227}
235
236export class Storage<T> {
237 constructor(
238 private readonly key: string,
239 private readonly storage: vscode.Memento,
240 private readonly defaultVal: T
241 ) { }
242
243 get(): T {
244 const val = this.storage.get(this.key, this.defaultVal);
245 log.debug(this.key, "==", val);
246 return val;
247 }
248 async set(val: T) {
249 log.debug(this.key, "=", val);
250 await this.storage.update(this.key, val);
251 }
252}
253export class DateStorage {
254 inner: Storage<null | string>;
255
256 constructor(key: string, storage: vscode.Memento) {
257 this.inner = new Storage(key, storage, null);
258 }
259
260 get(): null | Date {
261 const dateStr = this.inner.get();
262 return dateStr ? new Date(dateStr) : null;
263 }
264
265 async set(date: null | Date) {
266 await this.inner.set(date ? date.toString() : null);
267 }
268}
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 25ef38aed..c929ab063 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -4,19 +4,21 @@ import * as lc from 'vscode-languageclient';
4import { Config } from './config'; 4import { Config } from './config';
5import { createClient } from './client'; 5import { createClient } from './client';
6import { isRustEditor, RustEditor } from './util'; 6import { isRustEditor, RustEditor } from './util';
7import { PersistentState } from './persistent_state';
7 8
8export class Ctx { 9export class Ctx {
9 private constructor( 10 private constructor(
10 readonly config: Config, 11 readonly config: Config,
12 readonly state: PersistentState,
11 private readonly extCtx: vscode.ExtensionContext, 13 private readonly extCtx: vscode.ExtensionContext,
12 readonly client: lc.LanguageClient 14 readonly client: lc.LanguageClient
13 ) { 15 ) {
14 16
15 } 17 }
16 18
17 static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { 19 static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
18 const client = await createClient(config, serverPath); 20 const client = await createClient(config, serverPath);
19 const res = new Ctx(config, extCtx, client); 21 const res = new Ctx(config, state, extCtx, client);
20 res.pushCleanup(client.start()); 22 res.pushCleanup(client.start());
21 await client.onReady(); 23 await client.onReady();
22 return res; 24 return res;
diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts
index eea6fded2..a1db96f05 100644
--- a/editors/code/src/installation/extension.ts
+++ b/editors/code/src/installation/extension.ts
@@ -7,6 +7,7 @@ import { Config, UpdatesChannel } from "../config";
7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces"; 7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
8import { downloadArtifactWithProgressUi } from "./downloads"; 8import { downloadArtifactWithProgressUi } from "./downloads";
9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; 9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
10import { PersistentState } from "../persistent_state";
10 11
11const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; 12const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
12 13
@@ -14,7 +15,7 @@ const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
14 * Installs `stable` or latest `nightly` version or does nothing if the current 15 * Installs `stable` or latest `nightly` version or does nothing if the current
15 * extension version is what's needed according to `desiredUpdateChannel`. 16 * extension version is what's needed according to `desiredUpdateChannel`.
16 */ 17 */
17export async function ensureProperExtensionVersion(config: Config): Promise<never | void> { 18export async function ensureProperExtensionVersion(config: Config, state: PersistentState): Promise<never | void> {
18 // User has built lsp server from sources, she should manage updates manually 19 // User has built lsp server from sources, she should manage updates manually
19 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return; 20 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
20 21
@@ -23,7 +24,7 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
23 24
24 if (currentUpdChannel === UpdatesChannel.Stable) { 25 if (currentUpdChannel === UpdatesChannel.Stable) {
25 // Release date is present only when we are on nightly 26 // Release date is present only when we are on nightly
26 await config.installedNightlyExtensionReleaseDate.set(null); 27 await state.installedNightlyExtensionReleaseDate.set(null);
27 } 28 }
28 29
29 if (desiredUpdChannel === UpdatesChannel.Stable) { 30 if (desiredUpdChannel === UpdatesChannel.Stable) {
@@ -39,10 +40,10 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
39 if (currentUpdChannel === UpdatesChannel.Stable) { 40 if (currentUpdChannel === UpdatesChannel.Stable) {
40 if (!await askToDownloadProperExtensionVersion(config)) return; 41 if (!await askToDownloadProperExtensionVersion(config)) return;
41 42
42 return await tryDownloadNightlyExtension(config); 43 return await tryDownloadNightlyExtension(config, state);
43 } 44 }
44 45
45 const currentExtReleaseDate = config.installedNightlyExtensionReleaseDate.get(); 46 const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get();
46 47
47 if (currentExtReleaseDate === null) { 48 if (currentExtReleaseDate === null) {
48 void vscode.window.showErrorMessage( 49 void vscode.window.showErrorMessage(
@@ -66,9 +67,9 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
66 return; 67 return;
67 } 68 }
68 69
69 await tryDownloadNightlyExtension(config, releaseInfo => { 70 await tryDownloadNightlyExtension(config, state, releaseInfo => {
70 assert( 71 assert(
71 currentExtReleaseDate.getTime() === config.installedNightlyExtensionReleaseDate.get()?.getTime(), 72 currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(),
72 "Other active VSCode instance has reinstalled the extension" 73 "Other active VSCode instance has reinstalled the extension"
73 ); 74 );
74 75
@@ -111,6 +112,7 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "")
111 */ 112 */
112const tryDownloadNightlyExtension = notReentrant(async ( 113const tryDownloadNightlyExtension = notReentrant(async (
113 config: Config, 114 config: Config,
115 state: PersistentState,
114 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true 116 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
115): Promise<never | void> => { 117): Promise<never | void> => {
116 const vsixSource = config.nightlyVsixSource; 118 const vsixSource = config.nightlyVsixSource;
@@ -124,7 +126,7 @@ const tryDownloadNightlyExtension = notReentrant(async (
124 const vsixPath = path.join(vsixSource.dir, vsixSource.file); 126 const vsixPath = path.join(vsixSource.dir, vsixSource.file);
125 127
126 await vscodeInstallExtensionFromVsix(vsixPath); 128 await vscodeInstallExtensionFromVsix(vsixPath);
127 await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate); 129 await state.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
128 await fs.unlink(vsixPath); 130 await fs.unlink(vsixPath);
129 131
130 await vscodeReloadWindow(); // never returns 132 await vscodeReloadWindow(); // never returns
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
index 05730a778..05d326131 100644
--- a/editors/code/src/installation/server.ts
+++ b/editors/code/src/installation/server.ts
@@ -7,8 +7,9 @@ import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
7import { downloadArtifactWithProgressUi } from "./downloads"; 7import { downloadArtifactWithProgressUi } from "./downloads";
8import { log, assert, notReentrant } from "../util"; 8import { log, assert, notReentrant } from "../util";
9import { Config, NIGHTLY_TAG } from "../config"; 9import { Config, NIGHTLY_TAG } from "../config";
10import { PersistentState } from "../persistent_state";
10 11
11export async function ensureServerBinary(config: Config): Promise<null | string> { 12export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
12 const source = config.serverSource; 13 const source = config.serverSource;
13 14
14 if (!source) { 15 if (!source) {
@@ -37,7 +38,7 @@ export async function ensureServerBinary(config: Config): Promise<null | string>
37 return null; 38 return null;
38 } 39 }
39 case ArtifactSource.Type.GithubRelease: { 40 case ArtifactSource.Type.GithubRelease: {
40 if (!shouldDownloadServer(source, config)) { 41 if (!shouldDownloadServer(state, source)) {
41 return path.join(source.dir, source.file); 42 return path.join(source.dir, source.file);
42 } 43 }
43 44
@@ -50,24 +51,24 @@ export async function ensureServerBinary(config: Config): Promise<null | string>
50 if (userResponse !== "Download now") return null; 51 if (userResponse !== "Download now") return null;
51 } 52 }
52 53
53 return await downloadServer(source, config); 54 return await downloadServer(state, source);
54 } 55 }
55 } 56 }
56} 57}
57 58
58function shouldDownloadServer( 59function shouldDownloadServer(
60 state: PersistentState,
59 source: ArtifactSource.GithubRelease, 61 source: ArtifactSource.GithubRelease,
60 config: Config
61): boolean { 62): boolean {
62 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true; 63 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
63 64
64 const installed = { 65 const installed = {
65 tag: config.serverReleaseTag.get(), 66 tag: state.serverReleaseTag.get(),
66 date: config.serverReleaseDate.get() 67 date: state.serverReleaseDate.get()
67 }; 68 };
68 const required = { 69 const required = {
69 tag: source.tag, 70 tag: source.tag,
70 date: config.installedNightlyExtensionReleaseDate.get() 71 date: state.installedNightlyExtensionReleaseDate.get()
71 }; 72 };
72 73
73 log.debug("Installed server:", installed, "required:", required); 74 log.debug("Installed server:", installed, "required:", required);
@@ -86,16 +87,16 @@ function shouldDownloadServer(
86 * Enforcing no reentrancy for this is best-effort. 87 * Enforcing no reentrancy for this is best-effort.
87 */ 88 */
88const downloadServer = notReentrant(async ( 89const downloadServer = notReentrant(async (
90 state: PersistentState,
89 source: ArtifactSource.GithubRelease, 91 source: ArtifactSource.GithubRelease,
90 config: Config,
91): Promise<null | string> => { 92): Promise<null | string> => {
92 try { 93 try {
93 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); 94 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
94 95
95 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server"); 96 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
96 await Promise.all([ 97 await Promise.all([
97 config.serverReleaseTag.set(releaseInfo.releaseName), 98 state.serverReleaseTag.set(releaseInfo.releaseName),
98 config.serverReleaseDate.set(releaseInfo.releaseDate) 99 state.serverReleaseDate.set(releaseInfo.releaseDate)
99 ]); 100 ]);
100 } catch (err) { 101 } catch (err) {
101 log.downloadError(err, "language server", source.repo.name); 102 log.downloadError(err, "language server", source.repo.name);
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index bd4661a36..94ecd4dab 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -9,6 +9,7 @@ import { ensureServerBinary } from './installation/server';
9import { Config } from './config'; 9import { Config } from './config';
10import { log } from './util'; 10import { log } from './util';
11import { ensureProperExtensionVersion } from './installation/extension'; 11import { ensureProperExtensionVersion } from './installation/extension';
12import { PersistentState } from './persistent_state';
12 13
13let ctx: Ctx | undefined; 14let ctx: Ctx | undefined;
14 15
@@ -34,13 +35,14 @@ export async function activate(context: vscode.ExtensionContext) {
34 context.subscriptions.push(defaultOnEnter); 35 context.subscriptions.push(defaultOnEnter);
35 36
36 const config = new Config(context); 37 const config = new Config(context);
38 const state = new PersistentState(context);
37 39
38 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config).catch(log.error)); 40 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error));
39 41
40 // Don't await the user response here, otherwise we will block the lsp server bootstrap 42 // Don't await the user response here, otherwise we will block the lsp server bootstrap
41 void ensureProperExtensionVersion(config).catch(log.error); 43 void ensureProperExtensionVersion(config, state).catch(log.error);
42 44
43 const serverPath = await ensureServerBinary(config); 45 const serverPath = await ensureServerBinary(config, state);
44 46
45 if (serverPath == null) { 47 if (serverPath == null) {
46 throw new Error( 48 throw new Error(
@@ -53,7 +55,7 @@ export async function activate(context: vscode.ExtensionContext) {
53 // registers its `onDidChangeDocument` handler before us. 55 // registers its `onDidChangeDocument` handler before us.
54 // 56 //
55 // This a horribly, horribly wrong way to deal with this problem. 57 // This a horribly, horribly wrong way to deal with this problem.
56 ctx = await Ctx.create(config, context, serverPath); 58 ctx = await Ctx.create(config, state, context, serverPath);
57 59
58 // Commands which invokes manually via command palette, shortcut, etc. 60 // Commands which invokes manually via command palette, shortcut, etc.
59 ctx.registerCommand('reload', (ctx) => { 61 ctx.registerCommand('reload', (ctx) => {
diff --git a/editors/code/src/persistent_state.ts b/editors/code/src/persistent_state.ts
new file mode 100644
index 000000000..13095b806
--- /dev/null
+++ b/editors/code/src/persistent_state.ts
@@ -0,0 +1,49 @@
1import * as vscode from 'vscode';
2import { log } from "./util";
3
4export class PersistentState {
5 constructor(private readonly ctx: vscode.ExtensionContext) {
6 }
7
8 readonly installedNightlyExtensionReleaseDate = new DateStorage(
9 "installed-nightly-extension-release-date",
10 this.ctx.globalState
11 );
12 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
13 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
14}
15
16
17export class Storage<T> {
18 constructor(
19 private readonly key: string,
20 private readonly storage: vscode.Memento,
21 private readonly defaultVal: T
22 ) { }
23
24 get(): T {
25 const val = this.storage.get(this.key, this.defaultVal);
26 log.debug(this.key, "==", val);
27 return val;
28 }
29 async set(val: T) {
30 log.debug(this.key, "=", val);
31 await this.storage.update(this.key, val);
32 }
33}
34export class DateStorage {
35 inner: Storage<null | string>;
36
37 constructor(key: string, storage: vscode.Memento) {
38 this.inner = new Storage(key, storage, null);
39 }
40
41 get(): null | Date {
42 const dateStr = this.inner.get();
43 return dateStr ? new Date(dateStr) : null;
44 }
45
46 async set(date: null | Date) {
47 await this.inner.set(date ? date.toString() : null);
48 }
49}