Skip to content

Commit 7979bb3

Browse files
authored
feat(vscode): provide Debug CodeLens on route handlers (#973)
1 parent ca4c2b4 commit 7979bb3

File tree

5 files changed

+304
-29
lines changed

5 files changed

+304
-29
lines changed

extensions/vscode/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Dart Frog can be installed from the [VS Code Marketplace](https://marketplace.vi
2424
| `Dart Frog: Start Daemon` | Starts the Dart Frog daemon | Command palette |
2525
| `Dart Frog: Start Development Server` | Starts a Dart Frog server | Command palette and CodeLens |
2626
| `Dart Frog: Debug Development Server` | Debugs a Dart Frog server | Command palette |
27-
| `Dart Frog: Start and Debug Development Server` | Starts and debugs a Dart Frog server | Command palette |
27+
| `Dart Frog: Start and Debug Development Server` | Starts and debugs a Dart Frog server | Command palette and CodeLens |
2828
| `Dart Frog: Stop Development Server` | Stops a Dart Frog server | Command palette |
2929

3030
## Configuration

extensions/vscode/src/code-lens/on-request-code-lens.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,22 @@ export class RunOnRequestCodeLensProvider extends OnRequestCodeLensProvider {
114114
return codeLens;
115115
}
116116
}
117+
118+
/**
119+
* Shows a "Debug" CodeLens on route handlers, which allows starting and
120+
* debugging a development server.
121+
*/
122+
export class DebugOnRequestCodeLensProvider extends OnRequestCodeLensProvider {
123+
public resolveCodeLens?(codeLens: CodeLens): ProviderResult<CodeLens> {
124+
if (!super.resolveCodeLens!(codeLens)) {
125+
return undefined;
126+
}
127+
128+
codeLens.command = {
129+
title: "Debug",
130+
tooltip: "Starts and debugs a development server",
131+
command: "dart-frog.start-debug-dev-server",
132+
};
133+
return codeLens;
134+
}
135+
}

extensions/vscode/src/extension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import * as vscode from "vscode";
2+
import {
3+
DebugOnRequestCodeLensProvider,
4+
RunOnRequestCodeLensProvider,
5+
} from "./code-lens";
26
import {
37
create,
48
debugDevServer,
@@ -19,7 +23,6 @@ import {
1923
readLatestDartFrogCLIVersion,
2024
suggestInstallingDartFrogCLI,
2125
} from "./utils";
22-
import { RunOnRequestCodeLensProvider } from "./code-lens";
2326

2427
/**
2528
* This method is called when the extension is activated.
@@ -42,6 +45,10 @@ export function activate(
4245
ensureCompatibleCLI();
4346
}
4447

48+
vscode.languages.registerCodeLensProvider(
49+
"dart",
50+
new DebugOnRequestCodeLensProvider()
51+
);
4552
vscode.languages.registerCodeLensProvider(
4653
"dart",
4754
new RunOnRequestCodeLensProvider()

extensions/vscode/src/test/suite/code-lens/on-request-code-lens.test.ts

Lines changed: 239 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ import * as assert from "assert";
55
import { CodeLens, Position, workspace } from "vscode";
66
import { afterEach, beforeEach } from "mocha";
77

8+
/**
9+
* The content of a route file.
10+
*/
11+
const routeContent = `
12+
import 'package:dart_frog/dart_frog.dart';
13+
14+
Response onRequest(RequestContext context) {
15+
return Response(body: 'Welcome to Dart Frog!');
16+
}
17+
18+
`;
19+
20+
/**
21+
* The content of a route file with a dynamic route.
22+
*/
23+
const dynamicRouteContent = `
24+
import 'package:dart_frog/dart_frog.dart';
25+
26+
Response onRequest(RequestContext context, String id) {
27+
return Response(body: 'Welcome to Dart Frog!');
28+
}
29+
30+
`;
31+
32+
/**
33+
* The content of something that looks like a route file but isn't.
34+
*/
35+
const invalidRouteContent = `
36+
import 'package:dart_frog/dart_frog.dart';
37+
38+
Response notOnRequest(RequestContext context) {
39+
return Response(body: 'Welcome to Dart Frog!');
40+
}
41+
42+
`;
43+
844
suite("RunOnRequestCodeLensProvider", () => {
945
let vscodeStub: any;
1046
let utilsStub: any;
@@ -22,8 +58,7 @@ suite("RunOnRequestCodeLensProvider", () => {
2258
};
2359
workspaceConfiguration = sinon.stub();
2460
vscodeStub.workspace.getConfiguration.returns(workspaceConfiguration);
25-
const getConfiguration = sinon.stub();
26-
workspaceConfiguration.get = getConfiguration;
61+
const getConfiguration = (workspaceConfiguration.get = sinon.stub());
2762
getConfiguration.withArgs("enableCodeLens", true).returns(true);
2863

2964
utilsStub = {
@@ -134,14 +169,7 @@ suite("RunOnRequestCodeLensProvider", () => {
134169
});
135170

136171
test("returns the correct CodeLenses", async () => {
137-
const content = `
138-
import 'package:dart_frog/dart_frog.dart';
139-
140-
Response onRequest(RequestContext context) {
141-
return Response(body: 'Welcome to Dart Frog!');
142-
}
143-
144-
`;
172+
const content = routeContent;
145173
const textDocument = await workspace.openTextDocument({
146174
language: "text",
147175
content,
@@ -168,14 +196,7 @@ Response onRequest(RequestContext context) {
168196
});
169197

170198
test("returns the correct CodeLenses on a dynamic route", async () => {
171-
const content = `
172-
import 'package:dart_frog/dart_frog.dart';
173-
174-
Response onRequest(RequestContext context, String id) {
175-
return Response(body: 'Welcome to Dart Frog!');
176-
}
177-
178-
`;
199+
const content = dynamicRouteContent;
179200
const textDocument = await workspace.openTextDocument({
180201
language: "text",
181202
content,
@@ -202,15 +223,154 @@ Response onRequest(RequestContext context, String id) {
202223
});
203224

204225
test("returns no CodeLenses on a non route file", async () => {
205-
const content = `
206-
import 'package:dart_frog/dart_frog.dart';
226+
const content = invalidRouteContent;
227+
const textDocument = await workspace.openTextDocument({
228+
language: "text",
229+
content,
230+
});
231+
document.getText = textDocument.getText.bind(textDocument);
232+
document.positionAt = textDocument.positionAt.bind(textDocument);
233+
document.lineAt = textDocument.lineAt.bind(textDocument);
234+
document.getWordRangeAtPosition =
235+
textDocument.getWordRangeAtPosition.bind(textDocument);
207236

208-
Response notOnRequest(RequestContext context) {
209-
return Response(body: 'Welcome to Dart Frog!');
210-
}
237+
const provider = new RunOnRequestCodeLensProvider();
238+
const result = await provider.provideCodeLenses(document);
211239

212-
`;
240+
assert.strictEqual(result.length, 0);
241+
});
242+
});
243+
});
244+
245+
suite("DebugOnRequestCodeLensProvider", () => {
246+
let vscodeStub: any;
247+
let utilsStub: any;
248+
// eslint-disable-next-line @typescript-eslint/naming-convention
249+
let DebugOnRequestCodeLensProvider: any;
250+
let document: any;
251+
let workspaceConfiguration: any;
252+
253+
beforeEach(() => {
254+
vscodeStub = {
255+
workspace: {
256+
onDidChangeConfiguration: sinon.stub(),
257+
getConfiguration: sinon.stub(),
258+
},
259+
};
260+
workspaceConfiguration = sinon.stub();
261+
vscodeStub.workspace.getConfiguration.returns(workspaceConfiguration);
262+
const getConfiguration = (workspaceConfiguration.get = sinon.stub());
263+
getConfiguration.withArgs("enableCodeLens", true).returns(true);
264+
265+
utilsStub = {
266+
nearestDartFrogProject: sinon.stub(),
267+
};
268+
utilsStub.nearestDartFrogProject.returns("/home/dart_frog");
269+
270+
DebugOnRequestCodeLensProvider = proxyquire(
271+
"../../../code-lens/on-request-code-lens",
272+
{
273+
vscode: vscodeStub,
274+
// eslint-disable-next-line @typescript-eslint/naming-convention
275+
"../utils": utilsStub,
276+
}
277+
).DebugOnRequestCodeLensProvider;
278+
279+
document = sinon.stub();
280+
document.languageId = "dart";
281+
document.uri = {
282+
fsPath: "/home/dart_frog/routes/index.dart",
283+
};
284+
});
285+
286+
afterEach(() => {
287+
sinon.restore();
288+
});
289+
290+
test("onDidChangeCodeLenses fires when configuration changes", () => {
291+
const provider = new DebugOnRequestCodeLensProvider();
292+
const onDidChangeCodeLenses = sinon.stub();
293+
provider.onDidChangeCodeLenses(onDidChangeCodeLenses);
294+
295+
vscodeStub.workspace.onDidChangeConfiguration.callArg(0);
296+
297+
sinon.assert.calledOnce(onDidChangeCodeLenses);
298+
});
299+
300+
suite("resolveCodeLens", () => {
301+
test("returns the CodeLens when configuration is enabled", async () => {
302+
workspaceConfiguration.get.withArgs("enableCodeLens", true).returns(true);
213303

304+
const provider = new DebugOnRequestCodeLensProvider();
305+
const codeLens = new CodeLens(sinon.stub());
306+
const result = await provider.resolveCodeLens(codeLens, sinon.stub());
307+
308+
assert.strictEqual(result, codeLens);
309+
sinon.assert.match(result.command, {
310+
title: "Debug",
311+
tooltip: "Starts and debugs a development server",
312+
command: "dart-frog.start-debug-dev-server",
313+
});
314+
});
315+
316+
test("returns undefined when configuration is disabled", async () => {
317+
workspaceConfiguration.get
318+
.withArgs("enableCodeLens", true)
319+
.returns(false);
320+
321+
const provider = new DebugOnRequestCodeLensProvider();
322+
const codeLens = new CodeLens(sinon.stub());
323+
const result = await provider.resolveCodeLens(codeLens, sinon.stub());
324+
325+
assert.strictEqual(result, undefined);
326+
});
327+
});
328+
329+
suite("providesCodeLenses", () => {
330+
suite("returns undefined if the document is not", () => {
331+
test("a Dart file", () => {
332+
document.languageId = "not-dart";
333+
334+
const provider = new DebugOnRequestCodeLensProvider();
335+
const result = provider.provideCodeLenses(document);
336+
337+
assert.strictEqual(result, undefined);
338+
});
339+
340+
test("in a Dart Frog project", () => {
341+
utilsStub.nearestDartFrogProject.returns(undefined);
342+
343+
const provider = new DebugOnRequestCodeLensProvider();
344+
const result = provider.provideCodeLenses(document);
345+
346+
assert.strictEqual(result, undefined);
347+
});
348+
349+
test("in the routes folder", () => {
350+
document.uri = {
351+
fsPath: "/home/dart_frog/not-routes/route.dart",
352+
};
353+
354+
const provider = new DebugOnRequestCodeLensProvider();
355+
const result = provider.provideCodeLenses(document);
356+
357+
assert.strictEqual(result, undefined);
358+
});
359+
360+
test("codeLens configuration is disabled", () => {
361+
workspaceConfiguration.get
362+
.withArgs("enableCodeLens", true)
363+
.returns(false);
364+
365+
const provider = new DebugOnRequestCodeLensProvider();
366+
const result = provider.provideCodeLenses(document);
367+
368+
assert.strictEqual(result, undefined);
369+
});
370+
});
371+
372+
test("returns the correct CodeLenses", async () => {
373+
const content = routeContent;
214374
const textDocument = await workspace.openTextDocument({
215375
language: "text",
216376
content,
@@ -221,7 +381,61 @@ Response notOnRequest(RequestContext context) {
221381
document.getWordRangeAtPosition =
222382
textDocument.getWordRangeAtPosition.bind(textDocument);
223383

224-
const provider = new RunOnRequestCodeLensProvider();
384+
const provider = new DebugOnRequestCodeLensProvider();
385+
const result = await provider.provideCodeLenses(document);
386+
387+
assert.strictEqual(result.length, 1);
388+
389+
const codeLens = result[0];
390+
391+
const range = document.getWordRangeAtPosition(
392+
new Position(3, 0),
393+
/Response onRequest\(RequestContext context\) {/
394+
)!;
395+
396+
sinon.assert.match(codeLens, new CodeLens(range));
397+
});
398+
399+
test("returns the correct CodeLenses on a dynamic route", async () => {
400+
const content = dynamicRouteContent;
401+
const textDocument = await workspace.openTextDocument({
402+
language: "text",
403+
content,
404+
});
405+
document.getText = textDocument.getText.bind(textDocument);
406+
document.positionAt = textDocument.positionAt.bind(textDocument);
407+
document.lineAt = textDocument.lineAt.bind(textDocument);
408+
document.getWordRangeAtPosition =
409+
textDocument.getWordRangeAtPosition.bind(textDocument);
410+
411+
const provider = new DebugOnRequestCodeLensProvider();
412+
const result = await provider.provideCodeLenses(document);
413+
414+
assert.strictEqual(result.length, 1);
415+
416+
const codeLens = result[0];
417+
418+
const range = document.getWordRangeAtPosition(
419+
new Position(3, 0),
420+
/Response onRequest\(RequestContext context, String id\) {/
421+
)!;
422+
423+
sinon.assert.match(codeLens, new CodeLens(range));
424+
});
425+
426+
test("returns no CodeLenses on a non route file", async () => {
427+
const content = invalidRouteContent;
428+
const textDocument = await workspace.openTextDocument({
429+
language: "text",
430+
content,
431+
});
432+
document.getText = textDocument.getText.bind(textDocument);
433+
document.positionAt = textDocument.positionAt.bind(textDocument);
434+
document.lineAt = textDocument.lineAt.bind(textDocument);
435+
document.getWordRangeAtPosition =
436+
textDocument.getWordRangeAtPosition.bind(textDocument);
437+
438+
const provider = new DebugOnRequestCodeLensProvider();
225439
const result = await provider.provideCodeLenses(document);
226440

227441
assert.strictEqual(result.length, 0);

0 commit comments

Comments
 (0)