Skip to content
This repository was archived by the owner on Nov 18, 2022. It is now read-only.

Commit 8555126

Browse files
authored
Merge pull request #772 from rust-lang/progress-tweaks
Progress tweaks
2 parents dd1ec79 + 62d1ce7 commit 8555126

File tree

5 files changed

+151
-65
lines changed

5 files changed

+151
-65
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
### Unreleased
22

3+
* (!) Don't immediately start server instances for every already opened file
4+
* (!) Don't immediately start server instances for newly added workspace folders
5+
* Dynamically show progress only for the active client workspace
6+
* Correctly run tasks based on active text editor rather than last opened Rust file
37
* Use smooth, universally supported spinner in the status bar ⚙️
48

59
### 0.7.3 - 2020-04-21

src/extension.ts

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
IndentAction,
1010
languages,
1111
RelativePattern,
12-
TextDocument,
12+
TextEditor,
1313
Uri,
1414
window,
1515
workspace,
@@ -29,10 +29,8 @@ import { checkForRls, ensureToolchain, rustupUpdate } from './rustup';
2929
import { startSpinner, stopSpinner } from './spinner';
3030
import { activateTaskProvider, Execution, runRlsCommand } from './tasks';
3131
import { withWsl } from './utils/child_process';
32-
import {
33-
getOuterMostWorkspaceFolder,
34-
nearestParentWorkspace,
35-
} from './utils/workspace';
32+
import { Observable, Observer } from './utils/observable';
33+
import { nearestParentWorkspace } from './utils/workspace';
3634
import { uriWindowsToWsl, uriWslToWindows } from './utils/wslpath';
3735

3836
/**
@@ -48,17 +46,17 @@ interface ProgressParams {
4846
}
4947

5048
export async function activate(context: ExtensionContext) {
51-
context.subscriptions.push(configureLanguage());
52-
context.subscriptions.push(...registerCommands());
53-
54-
workspace.onDidOpenTextDocument(doc => whenOpeningTextDocument(doc));
55-
workspace.onDidChangeWorkspaceFolders(e => whenChangingWorkspaceFolders(e));
56-
window.onDidChangeActiveTextEditor(
57-
ed => ed && whenOpeningTextDocument(ed.document),
49+
context.subscriptions.push(
50+
...[
51+
configureLanguage(),
52+
...registerCommands(),
53+
workspace.onDidChangeWorkspaceFolders(whenChangingWorkspaceFolders),
54+
window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor),
55+
],
5856
);
59-
// Installed listeners don't fire immediately for already opened files, so
60-
// trigger an open event manually to fire up RLS instances where needed
61-
workspace.textDocuments.forEach(whenOpeningTextDocument);
57+
// Manually trigger the first event to start up server instance if necessary,
58+
// since VSCode doesn't do that on startup by itself.
59+
onDidChangeActiveTextEditor(window.activeTextEditor);
6260

6361
// Migrate the users of multi-project setup for RLS to disable the setting
6462
// entirely (it's always on now)
@@ -92,55 +90,38 @@ export async function deactivate() {
9290
return Promise.all([...workspaces.values()].map(ws => ws.stop()));
9391
}
9492

95-
// Taken from https://github.com/Microsoft/vscode-extension-samples/blob/master/lsp-multi-server-sample/client/src/extension.ts
96-
function whenOpeningTextDocument(document: TextDocument) {
97-
if (document.languageId !== 'rust' && document.languageId !== 'toml') {
98-
return;
99-
}
93+
/** Tracks dynamically updated progress for the active client workspace for UI purposes. */
94+
const progressObserver: Observer<{ message: string } | null> = new Observer();
10095

101-
let folder = workspace.getWorkspaceFolder(document.uri);
102-
if (!folder) {
96+
function onDidChangeActiveTextEditor(editor: TextEditor | undefined) {
97+
if (!editor || !editor.document) {
10398
return;
10499
}
100+
const { languageId, uri } = editor.document;
105101

106-
folder = nearestParentWorkspace(folder, document.uri.fsPath);
107-
108-
if (!folder) {
109-
stopSpinner(`RLS: Cargo.toml missing`);
102+
const workspace = clientWorkspaceForUri(uri, {
103+
startIfMissing: languageId === 'rust' || languageId === 'toml',
104+
});
105+
if (!workspace) {
110106
return;
111107
}
112108

113-
const folderPath = folder.uri.toString();
109+
activeWorkspace = workspace;
114110

115-
if (!workspaces.has(folderPath)) {
116-
const workspace = new ClientWorkspace(folder);
117-
activeWorkspace = workspace;
118-
workspaces.set(folderPath, workspace);
119-
workspace.start();
120-
} else {
121-
const ws = workspaces.get(folderPath);
122-
activeWorkspace = typeof ws === 'undefined' ? null : ws;
123-
}
111+
const updateProgress = (progress: { message: string } | null) => {
112+
if (progress) {
113+
startSpinner(`[${workspace.folder.name}] ${progress.message}`);
114+
} else {
115+
stopSpinner(`[${workspace.folder.name}]`);
116+
}
117+
};
118+
119+
progressObserver.bind(activeWorkspace.progress, updateProgress);
120+
// Update UI ourselves immediately and don't wait for value update callbacks
121+
updateProgress(activeWorkspace.progress.value);
124122
}
125123

126124
function whenChangingWorkspaceFolders(e: WorkspaceFoldersChangeEvent) {
127-
// If a VSCode workspace has been added, check to see if it is part of an existing one, and
128-
// if not, and it is a Rust project (i.e., has a Cargo.toml), then create a new client.
129-
for (let folder of e.added) {
130-
folder = getOuterMostWorkspaceFolder(folder);
131-
if (workspaces.has(folder.uri.toString())) {
132-
continue;
133-
}
134-
for (const f of fs.readdirSync(folder.uri.fsPath)) {
135-
if (f === 'Cargo.toml') {
136-
const workspace = new ClientWorkspace(folder);
137-
workspaces.set(folder.uri.toString(), workspace);
138-
workspace.start();
139-
break;
140-
}
141-
}
142-
}
143-
144125
// If a workspace is removed which is a Rust workspace, kill the client.
145126
for (const folder of e.removed) {
146127
const ws = workspaces.get(folder.uri.toString());
@@ -154,25 +135,58 @@ function whenChangingWorkspaceFolders(e: WorkspaceFoldersChangeEvent) {
154135
// Don't use URI as it's unreliable the same path might not become the same URI.
155136
const workspaces: Map<string, ClientWorkspace> = new Map();
156137

138+
/**
139+
* Fetches a `ClientWorkspace` for a given URI. If missing and `startIfMissing`
140+
* option was provided, it is additionally initialized beforehand, if applicable.
141+
*/
142+
function clientWorkspaceForUri(
143+
uri: Uri,
144+
options?: { startIfMissing: boolean },
145+
): ClientWorkspace | undefined {
146+
const rootFolder = workspace.getWorkspaceFolder(uri);
147+
if (!rootFolder) {
148+
return;
149+
}
150+
151+
const folder = nearestParentWorkspace(rootFolder, uri.fsPath);
152+
if (!folder) {
153+
return undefined;
154+
}
155+
156+
const existing = workspaces.get(folder.uri.toString());
157+
if (!existing && options && options.startIfMissing) {
158+
const workspace = new ClientWorkspace(folder);
159+
workspaces.set(folder.uri.toString(), workspace);
160+
workspace.start();
161+
}
162+
163+
return workspaces.get(folder.uri.toString());
164+
}
165+
157166
// We run one RLS and one corresponding language client per workspace folder
158167
// (VSCode workspace, not Cargo workspace). This class contains all the per-client
159168
// and per-workspace stuff.
160169
class ClientWorkspace {
170+
public readonly folder: WorkspaceFolder;
161171
// FIXME(#233): Don't only rely on lazily initializing it once on startup,
162172
// handle possible `rust-client.*` value changes while extension is running
163173
private readonly config: RLSConfiguration;
164174
private lc: LanguageClient | null = null;
165-
private readonly folder: WorkspaceFolder;
166175
private disposables: Disposable[];
176+
private _progress: Observable<{ message: string } | null>;
177+
get progress() {
178+
return this._progress;
179+
}
167180

168181
constructor(folder: WorkspaceFolder) {
169182
this.config = RLSConfiguration.loadFromWorkspace(folder.uri.fsPath);
170183
this.folder = folder;
171184
this.disposables = [];
185+
this._progress = new Observable<{ message: string } | null>(null);
172186
}
173187

174188
public async start() {
175-
startSpinner('RLS', 'Starting');
189+
this._progress.value = { message: 'Starting' };
176190

177191
const serverOptions: ServerOptions = async () => {
178192
await this.autoUpdate();
@@ -273,7 +287,6 @@ class ClientWorkspace {
273287

274288
const runningProgress: Set<string> = new Set();
275289
await this.lc.onReady();
276-
stopSpinner('RLS');
277290

278291
this.lc.onNotification(
279292
new NotificationType<ProgressParams, void>('window/progress'),
@@ -292,9 +305,9 @@ class ClientWorkspace {
292305
} else if (progress.title) {
293306
status = `[${progress.title.toLowerCase()}]`;
294307
}
295-
startSpinner('RLS', status);
308+
this._progress.value = { message: status };
296309
} else {
297-
stopSpinner('RLS');
310+
this._progress.value = null;
298311
}
299312
},
300313
);

src/rustup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface RustupConfig {
2121
}
2222

2323
export async function rustupUpdate(config: RustupConfig) {
24-
startSpinner('RLS', 'Updating…');
24+
startSpinner('Updating…');
2525

2626
try {
2727
const { stdout } = await withWsl(config.useWSL).exec(
@@ -103,7 +103,7 @@ async function hasToolchain(config: RustupConfig): Promise<boolean> {
103103
}
104104

105105
async function tryToInstallToolchain(config: RustupConfig) {
106-
startSpinner('RLS', 'Installing toolchain…');
106+
startSpinner('Installing toolchain…');
107107
try {
108108
const { command, args } = withWsl(config.useWSL).modifyArgs(config.path, [
109109
'toolchain',
@@ -154,7 +154,7 @@ async function hasRlsComponents(config: RustupConfig): Promise<boolean> {
154154
}
155155

156156
async function installRlsComponents(config: RustupConfig) {
157-
startSpinner('RLS', 'Installing components…');
157+
startSpinner('Installing components…');
158158

159159
for (const component of REQUIRED_COMPONENTS) {
160160
try {

src/spinner.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { window } from 'vscode';
22

3-
export function startSpinner(prefix: string, postfix: string) {
4-
window.setStatusBarMessage(`${prefix} $(settings-gear~spin) ${postfix}`);
3+
export function startSpinner(message: string) {
4+
window.setStatusBarMessage(`RLS $(settings-gear~spin) ${message}`);
55
}
66

7-
export function stopSpinner(message: string) {
8-
window.setStatusBarMessage(message);
7+
export function stopSpinner(message?: string) {
8+
window.setStatusBarMessage(`RLS ${message || ''}`);
99
}

src/utils/observable.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* A wrapper around a value of type `T` that can be subscribed to whenever the
3+
* underlying value changes.
4+
*/
5+
export class Observable<T> {
6+
private _listeners: Set<(arg: T) => void> = new Set();
7+
private _value: T;
8+
/** Returns the current value. */
9+
get value() {
10+
return this._value;
11+
}
12+
/** Every change to the value triggers all the registered callbacks. */
13+
set value(value: T) {
14+
this._value = value;
15+
this._listeners.forEach(fn => fn(value));
16+
}
17+
18+
constructor(value: T) {
19+
this._value = value;
20+
}
21+
22+
/**
23+
* Registers a listener function that's called whenever the underlying value
24+
* changes.
25+
* @returns a function that unregisters the listener when called.
26+
*/
27+
public observe(fn: (arg: T) => void): () => void {
28+
this._listeners.add(fn);
29+
30+
return () => this._listeners.delete(fn);
31+
}
32+
}
33+
34+
/**
35+
* Capable of observing an `Observable<T>` type.
36+
*
37+
* Convenient when using a single observer that potentially binds multiple times
38+
* to different observables, where it automatically unregisters from previous
39+
* observables.
40+
*/
41+
// tslint:disable-next-line: max-classes-per-file
42+
export class Observer<T> {
43+
private _observable?: Observable<T>;
44+
private _stopObserving?: () => void;
45+
/** Returns the current value of a bound observable, if there is one. */
46+
get value() {
47+
return this._observable && this._observable.value;
48+
}
49+
/**
50+
* Binds to an observable value, along with the provided listener that's
51+
* called whenever the underlying value changes.
52+
*/
53+
public bind(observable: Observable<T>, handler: (arg: T) => void) {
54+
this.stop();
55+
56+
this._observable = observable;
57+
this._stopObserving = observable.observe(handler);
58+
}
59+
/** Unbinds from the observable, deregistering the previously bound callback. */
60+
public stop() {
61+
if (this._stopObserving) {
62+
this._stopObserving();
63+
delete this._stopObserving;
64+
}
65+
if (this._observable) {
66+
delete this._observable;
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)