Skip to content

Commit 36b6b74

Browse files
authored
Centralize vscodeProposed access via global module (#745)
Replace constructor injection of vscodeProposed with a global module that provides a lazy proxy to the proposed VS Code API. The proxy falls back to regular vscode before initialization, making it safe to use during tests and early startup.
1 parent c6fc438 commit 36b6b74

17 files changed

Lines changed: 136 additions & 81 deletions

src/commands.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ import { type Logger } from "./logging/logger";
2121
import { type LoginCoordinator } from "./login/loginCoordinator";
2222
import { maybeAskAgent, maybeAskUrl } from "./promptUtils";
2323
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";
24+
import { vscodeProposed } from "./vscodeProposed";
2425
import {
2526
AgentTreeItem,
2627
type OpenableTreeItem,
2728
WorkspaceTreeItem,
2829
} from "./workspace/workspacesProvider";
2930

3031
export class Commands {
31-
private readonly vscodeProposed: typeof vscode;
3232
private readonly logger: Logger;
3333
private readonly pathResolver: PathResolver;
3434
private readonly mementoManager: MementoManager;
@@ -53,7 +53,6 @@ export class Commands {
5353
private readonly extensionClient: CoderApi,
5454
private readonly deploymentManager: DeploymentManager,
5555
) {
56-
this.vscodeProposed = serviceContainer.getVsCodeProposed();
5756
this.logger = serviceContainer.getLogger();
5857
this.pathResolver = serviceContainer.getPathResolver();
5958
this.mementoManager = serviceContainer.getMementoManager();
@@ -492,7 +491,7 @@ export class Commands {
492491
if (!this.workspace || !this.remoteWorkspaceClient) {
493492
return;
494493
}
495-
const action = await this.vscodeProposed.window.showWarningMessage(
494+
const action = await vscodeProposed.window.showWarningMessage(
496495
"Update Workspace",
497496
{
498497
useCustom: true,

src/core/binaryLock.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as lockfile from "proper-lockfile";
33
import * as vscode from "vscode";
44

55
import { type Logger } from "../logging/logger";
6+
import { vscodeProposed } from "../vscodeProposed";
67

78
import * as downloadProgress from "./downloadProgress";
89

@@ -21,10 +22,7 @@ type LockRelease = () => Promise<void>;
2122
* VS Code windows downloading the same binary.
2223
*/
2324
export class BinaryLock {
24-
constructor(
25-
private readonly vscodeProposed: typeof vscode,
26-
private readonly output: Logger,
27-
) {}
25+
constructor(private readonly output: Logger) {}
2826

2927
/**
3028
* Acquire the lock, or wait for another process if the lock is held.
@@ -78,7 +76,7 @@ export class BinaryLock {
7876
binPath: string,
7977
progressLogPath: string,
8078
): Promise<LockRelease> {
81-
return await this.vscodeProposed.window.withProgress(
79+
return await vscodeProposed.window.withProgress(
8280
{
8381
location: vscode.ProgressLocation.Notification,
8482
title: "Another window is downloading the Coder CLI binary",

src/core/cliManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as vscode from "vscode";
1414
import { errToStr } from "../api/api-helper";
1515
import { type Logger } from "../logging/logger";
1616
import * as pgp from "../pgp";
17+
import { vscodeProposed } from "../vscodeProposed";
1718

1819
import { BinaryLock } from "./binaryLock";
1920
import * as cliUtils from "./cliUtils";
@@ -24,11 +25,10 @@ export class CliManager {
2425
private readonly binaryLock: BinaryLock;
2526

2627
constructor(
27-
private readonly vscodeProposed: typeof vscode,
2828
private readonly output: Logger,
2929
private readonly pathResolver: PathResolver,
3030
) {
31-
this.binaryLock = new BinaryLock(vscodeProposed, output);
31+
this.binaryLock = new BinaryLock(output);
3232
}
3333

3434
/**
@@ -200,7 +200,7 @@ export class CliManager {
200200
version: string,
201201
reason: string,
202202
): Promise<boolean> {
203-
const choice = await this.vscodeProposed.window.showErrorMessage(
203+
const choice = await vscodeProposed.window.showErrorMessage(
204204
`${reason}. Run version ${version} anyway?`,
205205
"Run",
206206
);
@@ -621,7 +621,7 @@ export class CliManager {
621621
options.push("Download signature");
622622
}
623623
options.push("Run without verification");
624-
const action = await this.vscodeProposed.window.showWarningMessage(
624+
const action = await vscodeProposed.window.showWarningMessage(
625625
status === 404 ? "Signature not found" : "Failed to download signature",
626626
{
627627
useCustom: true,
@@ -675,7 +675,7 @@ export class CliManager {
675675
this.output,
676676
);
677677
} catch (error) {
678-
const action = await this.vscodeProposed.window.showWarningMessage(
678+
const action = await vscodeProposed.window.showWarningMessage(
679679
// VerificationError should be the only thing that throws, but
680680
// unfortunately caught errors are always type unknown.
681681
error instanceof pgp.VerificationError

src/core/container.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@ export class ServiceContainer implements vscode.Disposable {
2222
private readonly contextManager: ContextManager;
2323
private readonly loginCoordinator: LoginCoordinator;
2424

25-
constructor(
26-
context: vscode.ExtensionContext,
27-
private readonly vscodeProposed: typeof vscode = vscode,
28-
) {
25+
constructor(context: vscode.ExtensionContext) {
2926
this.logger = vscode.window.createOutputChannel("Coder", { log: true });
3027
this.pathResolver = new PathResolver(
3128
context.globalStorageUri.fsPath,
@@ -37,25 +34,16 @@ export class ServiceContainer implements vscode.Disposable {
3734
context.globalState,
3835
this.logger,
3936
);
40-
this.cliManager = new CliManager(
41-
this.vscodeProposed,
42-
this.logger,
43-
this.pathResolver,
44-
);
37+
this.cliManager = new CliManager(this.logger, this.pathResolver);
4538
this.contextManager = new ContextManager(context);
4639
this.loginCoordinator = new LoginCoordinator(
4740
this.secretsManager,
4841
this.mementoManager,
49-
this.vscodeProposed,
5042
this.logger,
5143
context.extension.id,
5244
);
5345
}
5446

55-
getVsCodeProposed(): typeof vscode {
56-
return this.vscodeProposed;
57-
}
58-
5947
getPathResolver(): PathResolver {
6048
return this.pathResolver;
6149
}

src/error/certificateError.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as vscode from "vscode";
1+
import { vscodeProposed } from "../vscodeProposed";
22

33
/**
44
* Base class for certificate-related errors that can display notifications to users.
@@ -23,7 +23,7 @@ export abstract class CertificateError extends Error {
2323
const message =
2424
!modal && title ? `${title}: ${this.detail}` : title || this.detail;
2525

26-
return await vscode.window.showErrorMessage(
26+
return await vscodeProposed.window.showErrorMessage(
2727
message,
2828
{ modal, useCustom: modal, detail: this.detail },
2929
...(items ?? []),

src/extension.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { OAuthSessionManager } from "./oauth/sessionManager";
1818
import { Remote } from "./remote/remote";
1919
import { getRemoteSshExtension } from "./remote/sshExtension";
2020
import { registerUriHandler } from "./uri/uriHandler";
21+
import { initVscodeProposed } from "./vscodeProposed";
2122
import {
2223
WorkspaceProvider,
2324
WorkspaceQuery,
@@ -53,7 +54,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
5354
);
5455
}
5556

56-
const serviceContainer = new ServiceContainer(ctx, vscodeProposed);
57+
// Initialize the global vscodeProposed module for use throughout the extension
58+
initVscodeProposed(vscodeProposed);
59+
60+
const serviceContainer = new ServiceContainer(ctx);
5761
ctx.subscriptions.push(serviceContainer);
5862

5963
const output = serviceContainer.getLogger();
@@ -184,12 +188,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
184188
const commands = new Commands(serviceContainer, client, deploymentManager);
185189

186190
ctx.subscriptions.push(
187-
registerUriHandler(
188-
serviceContainer,
189-
deploymentManager,
190-
commands,
191-
vscodeProposed,
192-
),
191+
registerUriHandler(serviceContainer, deploymentManager, commands),
193192
vscode.commands.registerCommand(
194193
"coder.login",
195194
commands.login.bind(commands),

src/login/loginCoordinator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CertificateError } from "../error/certificateError";
88
import { OAuthAuthorizer } from "../oauth/authorizer";
99
import { buildOAuthTokenData } from "../oauth/utils";
1010
import { maybeAskAuthMethod, maybeAskUrl } from "../promptUtils";
11+
import { vscodeProposed } from "../vscodeProposed";
1112

1213
import type { User } from "coder/site/src/api/typesGenerated";
1314

@@ -37,7 +38,6 @@ export class LoginCoordinator implements vscode.Disposable {
3738
constructor(
3839
private readonly secretsManager: SecretsManager,
3940
private readonly mementoManager: MementoManager,
40-
private readonly vscodeProposed: typeof vscode,
4141
private readonly logger: Logger,
4242
extensionId: string,
4343
) {
@@ -78,7 +78,7 @@ export class LoginCoordinator implements vscode.Disposable {
7878
const { safeHostname, url, detailPrefix, message } = options;
7979
return this.executeWithGuard(async () => {
8080
// Show dialog promise
81-
const dialogPromise = this.vscodeProposed.window
81+
const dialogPromise = vscodeProposed.window
8282
.showErrorMessage(
8383
message || "Authentication Required",
8484
{
@@ -291,9 +291,11 @@ export class LoginCoordinator implements vscode.Disposable {
291291
if (isAutoLogin) {
292292
this.logger.warn("Failed to log in to Coder server:", message);
293293
} else if (err instanceof CertificateError) {
294-
void err.showNotification("Failed to log in to Coder server");
294+
void err.showNotification("Failed to log in to Coder server", {
295+
modal: true,
296+
});
295297
} else {
296-
void this.vscodeProposed.window.showErrorMessage(
298+
void vscodeProposed.window.showErrorMessage(
297299
"Failed to log in to Coder server",
298300
{
299301
detail: message,

src/remote/remote.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
expandPath,
4444
parseRemoteAuthority,
4545
} from "../util";
46+
import { vscodeProposed } from "../vscodeProposed";
4647
import { WorkspaceMonitor } from "../workspace/workspaceMonitor";
4748

4849
import {
@@ -62,8 +63,6 @@ export interface RemoteDetails extends vscode.Disposable {
6263
}
6364

6465
export class Remote {
65-
// We use the proposed API to get access to useCustom in dialogs.
66-
private readonly vscodeProposed: typeof vscode;
6766
private readonly logger: Logger;
6867
private readonly pathResolver: PathResolver;
6968
private readonly cliManager: CliManager;
@@ -76,7 +75,6 @@ export class Remote {
7675
private readonly commands: Commands,
7776
private readonly extensionContext: vscode.ExtensionContext,
7877
) {
79-
this.vscodeProposed = serviceContainer.getVsCodeProposed();
8078
this.logger = serviceContainer.getLogger();
8179
this.pathResolver = serviceContainer.getPathResolver();
8280
this.cliManager = serviceContainer.getCliManager();
@@ -268,7 +266,7 @@ export class Remote {
268266

269267
// Server versions before v0.14.1 don't support the vscodessh command!
270268
if (!featureSet.vscodessh) {
271-
await this.vscodeProposed.window.showErrorMessage(
269+
await vscodeProposed.window.showErrorMessage(
272270
"Incompatible Server",
273271
{
274272
detail:
@@ -304,16 +302,15 @@ export class Remote {
304302
}
305303
switch (error.response?.status) {
306304
case 404: {
307-
const result =
308-
await this.vscodeProposed.window.showInformationMessage(
309-
`That workspace doesn't exist!`,
310-
{
311-
modal: true,
312-
detail: `${workspaceName} cannot be found on ${baseUrlRaw}. Maybe it was deleted...`,
313-
useCustom: true,
314-
},
315-
"Open Workspace",
316-
);
305+
const result = await vscodeProposed.window.showInformationMessage(
306+
`That workspace doesn't exist!`,
307+
{
308+
modal: true,
309+
detail: `${workspaceName} cannot be found on ${baseUrlRaw}. Maybe it was deleted...`,
310+
useCustom: true,
311+
},
312+
"Open Workspace",
313+
);
317314
disposables.forEach((d) => {
318315
d.dispose();
319316
});
@@ -345,7 +342,6 @@ export class Remote {
345342
workspace,
346343
workspaceClient,
347344
this.logger,
348-
this.vscodeProposed,
349345
this.contextManager,
350346
);
351347
disposables.push(
@@ -366,12 +362,11 @@ export class Remote {
366362
featureSet,
367363
this.logger,
368364
this.pathResolver,
369-
this.vscodeProposed,
370365
);
371366
disposables.push(stateMachine);
372367

373368
try {
374-
workspace = await this.vscodeProposed.window.withProgress(
369+
workspace = await vscodeProposed.window.withProgress(
375370
{
376371
location: vscode.ProgressLocation.Notification,
377372
cancellable: false,
@@ -452,10 +447,10 @@ export class Remote {
452447

453448
// Do some janky setting manipulation.
454449
this.logger.info("Modifying settings...");
455-
const remotePlatforms = this.vscodeProposed.workspace
450+
const remotePlatforms = vscodeProposed.workspace
456451
.getConfiguration()
457452
.get<Record<string, string>>("remote.SSH.remotePlatform", {});
458-
const connTimeout = this.vscodeProposed.workspace
453+
const connTimeout = vscodeProposed.workspace
459454
.getConfiguration()
460455
.get<number | undefined>("remote.SSH.connectTimeout");
461456

@@ -885,7 +880,7 @@ export class Remote {
885880
continue;
886881
}
887882

888-
const result = await this.vscodeProposed.window.showErrorMessage(
883+
const result = await vscodeProposed.window.showErrorMessage(
889884
"Unexpected SSH Config Option",
890885
{
891886
useCustom: true,
@@ -1007,7 +1002,7 @@ export class Remote {
10071002
}
10081003
// VS Code caches resource label formatters in it's global storage SQLite database
10091004
// under the key "memento/cachedResourceLabelFormatters2".
1010-
return this.vscodeProposed.workspace.registerResourceLabelFormatter({
1005+
return vscodeProposed.workspace.registerResourceLabelFormatter({
10111006
scheme: "vscode-remote",
10121007
// authority is optional but VS Code prefers formatters that most
10131008
// accurately match the requested authority, so we include it.

src/remote/workspaceStateMachine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "../api/workspace";
77
import { maybeAskAgent } from "../promptUtils";
88
import { type AuthorityParts } from "../util";
9+
import { vscodeProposed } from "../vscodeProposed";
910

1011
import { TerminalSession } from "./terminalSession";
1112

@@ -44,7 +45,6 @@ export class WorkspaceStateMachine implements vscode.Disposable {
4445
private readonly featureSet: FeatureSet,
4546
private readonly logger: Logger,
4647
private readonly pathResolver: PathResolver,
47-
private readonly vscodeProposed: typeof vscode,
4848
) {
4949
this.terminal = new TerminalSession("Workspace Build");
5050
}
@@ -231,7 +231,7 @@ export class WorkspaceStateMachine implements vscode.Disposable {
231231
}
232232

233233
private async confirmStart(workspaceName: string): Promise<boolean> {
234-
const action = await this.vscodeProposed.window.showInformationMessage(
234+
const action = await vscodeProposed.window.showInformationMessage(
235235
`Unable to connect to the workspace ${workspaceName} because it is not running. Start the workspace?`,
236236
{
237237
useCustom: true,

src/uri/uriHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type DeploymentManager } from "../deployment/deploymentManager";
77
import { CALLBACK_PATH } from "../oauth/utils";
88
import { maybeAskUrl } from "../promptUtils";
99
import { toSafeHost } from "../util";
10+
import { vscodeProposed } from "../vscodeProposed";
1011

1112
interface UriRouteContext {
1213
params: URLSearchParams;
@@ -30,7 +31,6 @@ export function registerUriHandler(
3031
serviceContainer: ServiceContainer,
3132
deploymentManager: DeploymentManager,
3233
commands: Commands,
33-
vscodeProposed: typeof vscode,
3434
): vscode.Disposable {
3535
const output = serviceContainer.getLogger();
3636

0 commit comments

Comments
 (0)