Skip to content

Commit eb00b69

Browse files
committed
Handle renv issues and suggested commands on the UI
1 parent 0e5d582 commit eb00b69

File tree

7 files changed

+513
-1
lines changed

7 files changed

+513
-1
lines changed

extensions/vscode/src/eventErrors.ts

+21
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ type renvPackageEvtErr = baseEvtErr & {
3636
libraryVersion: string;
3737
};
3838

39+
export type RenvAction =
40+
| "renvsetup"
41+
| "renvinit"
42+
| "renvsnapshot"
43+
| "renvstatus";
44+
45+
export type renvSetupEvtErr = baseEvtErr & {
46+
command: string;
47+
action: RenvAction;
48+
actionLabel: string;
49+
};
50+
3951
export const isEvtErrDeploymentFailed = (
4052
emsg: EventStreamMessageErrorCoded,
4153
): emsg is EventStreamMessageErrorCoded<baseEvtErr> => {
@@ -60,6 +72,15 @@ export const isEvtErrRenvPackageSourceMissing = (
6072
return emsg.errCode === "renvPackageSourceMissing";
6173
};
6274

75+
export const isEvtErrRenvEnvironmentSetup = (
76+
emsg: EventStreamMessageErrorCoded,
77+
): emsg is EventStreamMessageErrorCoded<renvSetupEvtErr> => {
78+
return (
79+
emsg.errCode === "renvPackageNotInstalledError" ||
80+
emsg.errCode === "renvActionRequiredError"
81+
);
82+
};
83+
6384
export const isEvtErrRequirementsReadingFailed = (
6485
emsg: EventStreamMessageErrorCoded,
6586
): emsg is EventStreamMessageErrorCoded<requirementsReadingEvtErr> => {

extensions/vscode/src/utils/errorTypes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export type ErrorCode =
1313
| "renvPackageVersionMismatch"
1414
| "renvPackageSourceMissing"
1515
| "renvlockPackagesReadingError"
16+
| "renvPackageNotInstalledError"
17+
| "renvActionRequiredError"
1618
| "requirementsFileReadingError"
1719
| "deployedContentNotRunning"
1820
| "tomlValidationError"
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (C) 2025 by Posit Software, PBC.
2+
3+
import { describe, expect, beforeEach, test, vi } from "vitest";
4+
import { window } from "vscode";
5+
import {
6+
showErrorMessageWithTroubleshoot,
7+
showInformationMsg,
8+
taskWithProgressMsg,
9+
runTerminalCommand,
10+
} from "./window";
11+
12+
const terminalMock = {
13+
sendText: vi.fn(),
14+
show: vi.fn(),
15+
exitStatus: {
16+
code: 0,
17+
},
18+
};
19+
20+
vi.mock("vscode", () => {
21+
// mock Disposable
22+
const disposableMock = vi.fn();
23+
disposableMock.prototype.dispose = vi.fn();
24+
25+
// mock window
26+
const windowMock = {
27+
showErrorMessage: vi.fn(),
28+
showWarningMessage: vi.fn(),
29+
showInformationMessage: vi.fn(),
30+
withProgress: vi.fn().mockImplementation((_, progressCallback) => {
31+
progressCallback();
32+
}),
33+
createTerminal: vi.fn().mockImplementation(() => {
34+
terminalMock.sendText = vi.fn();
35+
terminalMock.show = vi.fn();
36+
return terminalMock;
37+
}),
38+
onDidCloseTerminal: vi.fn().mockImplementation((fn) => {
39+
setTimeout(() => fn(terminalMock), 100);
40+
return new disposableMock();
41+
}),
42+
};
43+
44+
return {
45+
Disposable: disposableMock,
46+
window: windowMock,
47+
ProgressLocation: {
48+
SourceControl: 1,
49+
Window: 10,
50+
Notification: 15,
51+
},
52+
};
53+
});
54+
55+
describe("Consumers of vscode window", () => {
56+
beforeEach(() => {
57+
terminalMock.exitStatus.code = 0;
58+
});
59+
60+
test("showErrorMessageWithTroubleshoot", () => {
61+
showErrorMessageWithTroubleshoot("Bad things happened");
62+
expect(window.showErrorMessage).toHaveBeenCalledWith(
63+
"Bad things happened. See [Troubleshooting docs](https://github.com/posit-dev/publisher/blob/main/docs/troubleshooting.md) for help.",
64+
);
65+
66+
showErrorMessageWithTroubleshoot(
67+
"Bad things happened.",
68+
"one",
69+
"two",
70+
"three",
71+
);
72+
expect(window.showErrorMessage).toHaveBeenCalledWith(
73+
"Bad things happened. See [Troubleshooting docs](https://github.com/posit-dev/publisher/blob/main/docs/troubleshooting.md) for help.",
74+
"one",
75+
"two",
76+
"three",
77+
);
78+
});
79+
80+
test("showInformationMsg", () => {
81+
showInformationMsg("Good thing happened");
82+
expect(window.showInformationMessage).toHaveBeenCalledWith(
83+
"Good thing happened",
84+
);
85+
86+
showInformationMsg("Good thing happened", "one", "two", "three");
87+
expect(window.showInformationMessage).toHaveBeenCalledWith(
88+
"Good thing happened",
89+
"one",
90+
"two",
91+
"three",
92+
);
93+
});
94+
95+
test("taskWithProgressMsg", () => {
96+
const taskMock = vi.fn();
97+
taskWithProgressMsg(
98+
"Running a task with a progress notification",
99+
taskMock,
100+
);
101+
expect(window.withProgress).toHaveBeenCalledWith(
102+
{
103+
location: 15,
104+
title: "Running a task with a progress notification",
105+
cancellable: false,
106+
},
107+
expect.any(Function),
108+
);
109+
expect(taskMock).toHaveBeenCalled();
110+
});
111+
112+
describe("runTerminalCommand", () => {
113+
test("showing the terminal", async () => {
114+
await runTerminalCommand("stat somefile.txt", true);
115+
expect(terminalMock.sendText).toHaveBeenCalledWith("stat somefile.txt");
116+
expect(terminalMock.show).toHaveBeenCalled();
117+
// For terminals that we open, we don't track close events
118+
expect(window.onDidCloseTerminal).not.toHaveBeenCalled();
119+
});
120+
121+
test("NOT showing the terminal", async () => {
122+
await runTerminalCommand("stat somefile.txt");
123+
expect(terminalMock.sendText).toHaveBeenCalledWith("stat somefile.txt");
124+
// For terminals that we DO NOT open, we DO track close events
125+
expect(terminalMock.show).not.toHaveBeenCalled();
126+
expect(window.onDidCloseTerminal).toHaveBeenCalled();
127+
});
128+
129+
test("catch non zero exit status", async () => {
130+
terminalMock.exitStatus.code = 1;
131+
try {
132+
await runTerminalCommand("stat somefile.txt");
133+
} catch (_) {
134+
expect(terminalMock.sendText).toHaveBeenCalledWith("stat somefile.txt");
135+
// For terminals that we DO NOT open, we DO track close events
136+
expect(terminalMock.show).not.toHaveBeenCalled();
137+
expect(window.onDidCloseTerminal).toHaveBeenCalled();
138+
}
139+
});
140+
});
141+
});

extensions/vscode/src/utils/window.ts

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (C) 2025 by Posit Software, PBC.
22

3-
import { window } from "vscode";
3+
import { window, ProgressLocation, Progress, CancellationToken } from "vscode";
44

55
export function showErrorMessageWithTroubleshoot(
66
message: string,
@@ -14,3 +14,67 @@ export function showErrorMessageWithTroubleshoot(
1414
" See [Troubleshooting docs](https://github.com/posit-dev/publisher/blob/main/docs/troubleshooting.md) for help.";
1515
return window.showErrorMessage(msg, ...items);
1616
}
17+
18+
export function showInformationMsg(msg: string, ...items: string[]) {
19+
return window.showInformationMessage(msg, ...items);
20+
}
21+
22+
type taskFunc = <T>(p: Progress<T>, t: CancellationToken) => Promise<void>;
23+
const progressCallbackHandlerFactory =
24+
(
25+
task: taskFunc,
26+
cancellable: boolean = false,
27+
onCancel?: () => void,
28+
): taskFunc =>
29+
(progress, token) => {
30+
if (cancellable && onCancel) {
31+
token.onCancellationRequested(() => {
32+
onCancel();
33+
});
34+
}
35+
return task(progress, token);
36+
};
37+
38+
export function taskWithProgressMsg(
39+
msg: string,
40+
task: taskFunc,
41+
cancellable: boolean = false,
42+
onCancel?: () => void,
43+
) {
44+
return window.withProgress(
45+
{
46+
location: ProgressLocation.Notification,
47+
title: msg,
48+
cancellable,
49+
},
50+
progressCallbackHandlerFactory(task, cancellable, onCancel),
51+
);
52+
}
53+
54+
export function runTerminalCommand(
55+
cmd: string,
56+
show: boolean = false,
57+
): Promise<void> {
58+
const term = window.createTerminal();
59+
term.sendText(cmd);
60+
61+
// If terminal is shown, there is no need to track exit status for it
62+
// everything will be visible on it.
63+
if (show) {
64+
term.show();
65+
return Promise.resolve();
66+
}
67+
68+
return new Promise((resolve, reject) => {
69+
const disposeToken = window.onDidCloseTerminal((closedTerminal) => {
70+
if (closedTerminal === term) {
71+
disposeToken.dispose();
72+
if (term.exitStatus && term.exitStatus.code === 0) {
73+
resolve();
74+
} else {
75+
reject();
76+
}
77+
}
78+
});
79+
});
80+
}

0 commit comments

Comments
 (0)