Skip to content
This repository was archived by the owner on Dec 25, 2023. It is now read-only.

Commit 7124ac9

Browse files
authored
tsserver preferences & better debugging (#283)
* Lots o work - Add explicit debug settings - Add support configuring tsserver user preferences - Workaround bug with excessive detection of server crashes - Fix failure to re-dispose handlers during reloads * README & Changelog * Fix lint
1 parent 9d2f907 commit 7124ac9

File tree

11 files changed

+706
-74
lines changed

11 files changed

+706
-74
lines changed

.nova/Configuration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"apexskier.typescript.config.organizeImportsOnSave": "true",
2+
"apexskier.typescript.config.organizeImportsOnSave": "null",
33
"editor.default_syntax": "typescript",
44
"workspace.name": "TypeScript Extension"
55
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This extension is written in TypeScript. To support this I've contributed Nova e
1212

1313
## Notes
1414

15-
Nova's language server support conforms to the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). Unfortunately, the [built in language server](https://github.com/Microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29) in the TypeScript project doesn't ([but might in the future - follow this ticket](https://github.com/microsoft/TypeScript/issues/39459)). I've used a language server from Theia IDE that uses `tsserver` internally, which I think is the best approach. (list of [alternatives](https://microsoft.github.io/language-server-protocol/implementors/servers/), [Sourcegraph](https://github.com/sourcegraph/javascript-typescript-langserver) doesn't support TypeScript syntax correctly).
15+
Nova's language server support conforms to the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). Unfortunately, [TypeScript's server](https://github.com/Microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29) doesn't ([but might in the future - follow this ticket](https://github.com/microsoft/TypeScript/issues/39459)). This extension uses [`typescript-language-server`](https://github.com/theia-ide/typescript-language-server/) to translate between the Language Server Protocol and `tsserver`.
1616

1717
## Images
1818

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ module.exports = {
22
preset: "ts-jest",
33
setupFiles: ["./src/test.setup.ts"],
44
collectCoverageFrom: ["./src/**"],
5+
coverageReporters: ["html"],
56
};

src/commands/signatureHelp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type * as lspTypes from "vscode-languageserver-protocol";
2-
import { wrapCommand } from "../novaUtils";
32
import { rangeToLspRange } from "../lspNovaConversions";
3+
import { wrapCommand } from "../novaUtils";
44

55
// @Panic: this is totally decoupled from typescript, so it could totally be native to Nova
66

src/main.test.ts

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import * as informationViewModule from "./informationView";
2-
31
jest.mock("./informationView");
42
jest.mock("./tsLibPath", () => ({
53
getTsLibPath: () => "/tsLibPath",
64
}));
5+
jest.mock("./tsUserPreferences", () => ({
6+
setupUserPreferences: jest.fn(),
7+
getUserPreferences: () => "user preferences",
8+
}));
79
jest.mock("./isEnabledForJavascript", () => ({
810
isEnabledForJavascript: () => true,
911
}));
@@ -16,6 +18,9 @@ jest.useFakeTimers();
1618
register: jest.fn(),
1719
invoke: jest.fn(),
1820
},
21+
config: {
22+
["get"]: jest.fn(),
23+
},
1924
workspace: {
2025
path: "/workspace",
2126
onDidAddTextEditor: jest.fn(),
@@ -38,28 +43,27 @@ global.console.log = jest.fn((...args) => {
3843
if (
3944
args[0] === "activating..." ||
4045
args[0] === "activated" ||
41-
args[0] === "reloading..."
46+
args[0] === "reloading..." ||
47+
args[0] === "deactivate"
4248
) {
4349
return;
4450
}
4551
originalLog(...args);
4652
});
4753
global.console.info = jest.fn();
4854

49-
const CompositeDisposableMock: jest.Mock<Partial<CompositeDisposable>> = jest
50-
.fn()
51-
.mockImplementation(() => ({ add: jest.fn(), dispose: jest.fn() }));
55+
const CompositeDisposableMock: jest.Mock<Partial<CompositeDisposable>> =
56+
jest.fn();
5257
(global as any).CompositeDisposable = CompositeDisposableMock;
5358
const ProcessMock: jest.Mock<Partial<Process>> = jest.fn();
5459
(global as any).Process = ProcessMock;
5560
const LanguageClientMock: jest.Mock<Partial<LanguageClient>> = jest.fn();
5661
(global as any).LanguageClient = LanguageClientMock;
5762

5863
describe("test suite", () => {
59-
// dynamically require so global mocks are setup before top level code execution
60-
const { activate, deactivate } = require("./main");
64+
beforeEach(() => {
65+
jest.resetModules();
6166

62-
function resetMocks() {
6367
const {
6468
dependencyManagement: { installWrappedDependencies },
6569
} = require("nova-extension-utils");
@@ -69,6 +73,7 @@ describe("test suite", () => {
6973
nova.fs.access = jest.fn().mockReturnValue(true);
7074
(nova.commands.register as jest.Mock).mockReset();
7175
(nova.commands.invoke as jest.Mock).mockReset();
76+
(nova.config.get as jest.Mock).mockReset();
7277
LanguageClientMock.mockReset().mockImplementation(() => ({
7378
onRequest: jest.fn(),
7479
onNotification: jest.fn(),
@@ -85,15 +90,18 @@ describe("test suite", () => {
8590
}),
8691
start: jest.fn(),
8792
}));
88-
(informationViewModule.InformationView as jest.Mock).mockReset();
93+
const { InformationView } = require("./informationView");
94+
(InformationView as jest.Mock).mockReset();
8995
(nova.workspace.onDidAddTextEditor as jest.Mock).mockReset();
90-
}
91-
92-
const reload = (nova.commands.register as jest.Mock).mock.calls.find(
93-
([command]) => command == "apexskier.typescript.reload"
94-
)[1];
96+
CompositeDisposableMock.mockReset().mockImplementation(() => ({
97+
add: jest.fn(),
98+
dispose: jest.fn(),
99+
}));
100+
});
95101

96102
test("global behavior", () => {
103+
require("./main");
104+
97105
expect(nova.commands.register).toBeCalledTimes(2);
98106
expect(nova.commands.register).toBeCalledWith(
99107
"apexskier.typescript.openWorkspaceConfig",
@@ -115,28 +123,46 @@ describe("test suite", () => {
115123
});
116124

117125
function assertActivationBehavior() {
118-
expect(nova.commands.register).toBeCalledTimes(5);
119-
expect(nova.commands.register).toBeCalledWith(
120-
"apexskier.typescript.rename",
126+
expect(nova.commands.register).toBeCalledTimes(7);
127+
expect(nova.commands.register).nthCalledWith(
128+
1,
129+
"apexskier.typescript.openWorkspaceConfig",
121130
expect.any(Function)
122131
);
123-
expect(nova.commands.register).toBeCalledWith(
124-
"apexskier.typescript.findSymbol",
132+
expect(nova.commands.register).nthCalledWith(
133+
2,
134+
"apexskier.typescript.reload",
125135
expect.any(Function)
126136
);
127-
expect(nova.commands.register).toBeCalledWith(
137+
expect(nova.commands.register).nthCalledWith(
138+
3,
128139
"apexskier.typescript.findReferences",
129140
expect.any(Function)
130141
);
131-
expect(nova.commands.register).toBeCalledWith(
142+
expect(nova.commands.register).nthCalledWith(
143+
4,
144+
"apexskier.typescript.findSymbol",
145+
expect.any(Function)
146+
);
147+
expect(nova.commands.register).nthCalledWith(
148+
5,
149+
"apexskier.typescript.rename",
150+
expect.any(Function)
151+
);
152+
expect(nova.commands.register).nthCalledWith(
153+
6,
132154
"apexskier.typescript.commands.organizeImports",
133155
expect.any(Function)
134156
);
135-
expect(nova.commands.register).toBeCalledWith(
157+
expect(nova.commands.register).nthCalledWith(
158+
7,
136159
"apexskier.typescript.commands.formatDocument",
137160
expect.any(Function)
138161
);
139162

163+
const tsUserPreferencesModule = require("./tsUserPreferences");
164+
expect(tsUserPreferencesModule.setupUserPreferences).toBeCalled();
165+
140166
// installs dependencies
141167

142168
const {
@@ -156,23 +182,45 @@ describe("test suite", () => {
156182
});
157183

158184
expect(LanguageClientMock).toBeCalledTimes(1);
185+
expect(LanguageClientMock).toBeCalledWith(
186+
"apexskier.typescript",
187+
"TypeScript Language Server",
188+
{
189+
env: {
190+
DEBUG: "FALSE",
191+
DEBUG_BREAK: "FALSE",
192+
DEBUG_PORT: "undefined",
193+
INSTALL_DIR: undefined,
194+
TSLIB_PATH: "/tsLibPath",
195+
WORKSPACE_DIR: "/workspace",
196+
},
197+
path: "/extension/run.sh",
198+
type: "stdio",
199+
},
200+
{
201+
initializationOptions: { preferences: "user preferences" },
202+
syntaxes: ["typescript", "tsx", "javascript", "jsx"],
203+
}
204+
);
159205
const languageClient: LanguageClient =
160206
LanguageClientMock.mock.results[0].value;
161207
expect(languageClient.start).toBeCalledTimes(1);
162208

163209
expect(languageClient.onRequest).not.toBeCalled();
164210

165-
expect(informationViewModule.InformationView).toBeCalledTimes(1);
211+
const { InformationView } = require("./informationView");
212+
expect(InformationView).toBeCalledTimes(1);
166213
const informationView = (
167-
informationViewModule.InformationView as jest.Mock<informationViewModule.InformationView>
214+
InformationView as jest.Mock<typeof InformationView>
168215
).mock.instances[0];
169216
expect(informationView.status).toBe("Running");
170217
expect(informationView.reload).toBeCalledTimes(1);
171218
}
172219

173220
describe("activate and deactivate", () => {
174221
it("installs dependencies, runs the server, gets the ts version", async () => {
175-
resetMocks();
222+
// dynamically require so global mocks are setup before top level code execution
223+
const { activate, deactivate } = require("./main");
176224

177225
(ProcessMock as jest.Mock<Partial<Process>>)
178226
.mockImplementationOnce(() => ({
@@ -199,8 +247,9 @@ describe("test suite", () => {
199247
assertActivationBehavior();
200248

201249
// typescript version is reported in the information view
250+
const { InformationView } = require("./informationView");
202251
const informationView = (
203-
informationViewModule.InformationView as jest.Mock<informationViewModule.InformationView>
252+
InformationView as jest.Mock<typeof InformationView>
204253
).mock.instances[0];
205254
expect(informationView.tsVersion).toBeUndefined();
206255
const tsVersionProcess: Process = ProcessMock.mock.results[1].value;
@@ -221,7 +270,9 @@ describe("test suite", () => {
221270
});
222271

223272
it("shows an error if activation fails", async () => {
224-
resetMocks();
273+
// dynamically require so global mocks are setup before top level code execution
274+
const { activate } = require("./main");
275+
225276
global.console.error = jest.fn();
226277
global.console.warn = jest.fn();
227278
nova.workspace.showErrorMessage = jest.fn();
@@ -241,7 +292,9 @@ describe("test suite", () => {
241292
});
242293

243294
it("handles unexpected crashes", async () => {
244-
resetMocks();
295+
// dynamically require so global mocks are setup before top level code execution
296+
const { activate } = require("./main");
297+
245298
nova.workspace.showActionPanel = jest.fn();
246299

247300
await activate();
@@ -265,8 +318,9 @@ describe("test suite", () => {
265318
`);
266319
expect(actionPanelCall[1].buttons).toHaveLength(2);
267320

321+
const { InformationView } = require("./informationView");
268322
const informationView = (
269-
informationViewModule.InformationView as jest.Mock<informationViewModule.InformationView>
323+
InformationView as jest.Mock<typeof InformationView>
270324
).mock.instances[0];
271325
expect(informationView.status).toBe("Stopped");
272326

@@ -285,19 +339,32 @@ describe("test suite", () => {
285339
});
286340

287341
test("reload", async () => {
288-
resetMocks();
342+
// dynamically require so global mocks are setup before top level code execution
343+
require("./main");
344+
345+
const reload = (nova.commands.register as jest.Mock).mock.calls.find(
346+
([command]) => command == "apexskier.typescript.reload"
347+
)[1];
348+
349+
expect(CompositeDisposableMock).toBeCalledTimes(1);
289350

290351
await reload();
291352

292-
const compositeDisposable: CompositeDisposable =
353+
expect(CompositeDisposableMock).toBeCalledTimes(2);
354+
355+
const compositeDisposable1: CompositeDisposable =
293356
CompositeDisposableMock.mock.results[0].value;
294-
expect(compositeDisposable.dispose).toBeCalledTimes(2);
357+
expect(compositeDisposable1.dispose).toBeCalledTimes(1);
358+
const compositeDisposable2: CompositeDisposable =
359+
CompositeDisposableMock.mock.results[1].value;
360+
expect(compositeDisposable2.dispose).not.toBeCalled();
295361

296362
assertActivationBehavior();
297363
});
298364

299365
test("watches files to apply post-save actions", async () => {
300-
resetMocks();
366+
// dynamically require so global mocks are setup before top level code execution
367+
const { activate } = require("./main");
301368

302369
await activate();
303370

0 commit comments

Comments
 (0)