Skip to content

Commit c3aad2c

Browse files
authoredNov 23, 2023
Add support for scopes in hh autocomplete (#4593)
1 parent a792e82 commit c3aad2c

File tree

8 files changed

+384
-54
lines changed

8 files changed

+384
-54
lines changed
 

‎.changeset/giant-books-tie.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Added support for scopes in hh autocomplete.

‎packages/hardhat-core/src/internal/cli/autocomplete.ts

+136-48
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import findup from "find-up";
22
import * as fs from "fs-extra";
33
import * as path from "path";
44

5-
import { HardhatRuntimeEnvironment } from "../../types";
5+
import { HardhatRuntimeEnvironment, TaskDefinition } from "../../types";
66
import { HARDHAT_PARAM_DEFINITIONS } from "../core/params/hardhat-params";
77
import { getCacheDir } from "../util/global-dir";
88
import { createNonCryptographicHashBasedIdentifier } from "../util/hash";
@@ -22,19 +22,30 @@ interface CompletionEnv {
2222
point: number;
2323
}
2424

25+
interface Task {
26+
name: string;
27+
description: string;
28+
isSubtask: boolean;
29+
paramDefinitions: {
30+
[paramName: string]: {
31+
name: string;
32+
description: string;
33+
isFlag: boolean;
34+
};
35+
};
36+
}
37+
2538
interface CompletionData {
2639
networks: string[];
2740
tasks: {
28-
[taskName: string]: {
41+
[taskName: string]: Task;
42+
};
43+
scopes: {
44+
[scopeName: string]: {
2945
name: string;
3046
description: string;
31-
isSubtask: boolean;
32-
paramDefinitions: {
33-
[paramName: string]: {
34-
name: string;
35-
description: string;
36-
isFlag: boolean;
37-
};
47+
tasks: {
48+
[taskName: string]: Task;
3849
};
3950
};
4051
};
@@ -63,12 +74,12 @@ export async function complete({
6374
return [];
6475
}
6576

66-
const { networks, tasks } = completionData;
77+
const { networks, tasks, scopes } = completionData;
6778

6879
const words = line.split(/\s+/).filter((x) => x.length > 0);
6980

7081
const wordsBeforeCursor = line.slice(0, point).split(/\s+/);
71-
// examples:
82+
// 'prev' and 'last' variables examples:
7283
// `hh compile --network|` => prev: "compile" last: "--network"
7384
// `hh compile --network |` => prev: "--network" last: ""
7485
// `hh compile --network ha|` => prev: "--network" last: "ha"
@@ -83,28 +94,58 @@ export async function complete({
8394
}))
8495
.filter((x) => !words.includes(x.name));
8596

86-
// check if the user entered a task
87-
let task: string | undefined;
97+
// Get the task or scope if the user has entered one
98+
let taskName: string | undefined;
99+
let scopeName: string | undefined;
100+
88101
let index = 1;
89102
while (index < words.length) {
90-
if (isGlobalFlag(words[index])) {
103+
const word = words[index];
104+
105+
if (isGlobalFlag(word)) {
91106
index += 1;
92-
} else if (isGlobalParam(words[index])) {
107+
} else if (isGlobalParam(word)) {
93108
index += 2;
94-
} else if (words[index].startsWith("--")) {
109+
} else if (word.startsWith("--")) {
95110
index += 1;
96111
} else {
97-
task = words[index];
98-
break;
112+
// Possible scenarios:
113+
// - no task or scope: `hh `
114+
// - only a task: `hh task `
115+
// - only a scope: `hh scope `
116+
// - both a scope and a task (the task always follow the scope): `hh scope task `
117+
// Between a scope and a task there could be other words, e.g.: `hh scope --flag task `
118+
if (scopeName === undefined) {
119+
if (tasks[word] !== undefined) {
120+
taskName = word;
121+
break;
122+
} else if (scopes[word] !== undefined) {
123+
scopeName = word;
124+
}
125+
} else {
126+
taskName = word;
127+
break;
128+
}
129+
130+
index += 1;
99131
}
100132
}
101133

102-
// if a task was found but it's equal to the last word, it means
103-
// that the cursor is after the task, we ignore the task in this
104-
// case because if you have a task `foo` and `foobar` and the
105-
// line is: `hh foo|`, we want tasks to be suggested
106-
if (task === last) {
107-
task = undefined;
134+
// If a task or a scope is found and it is equal to the last word,
135+
// this indicates that the cursor is positioned after the task or scope.
136+
// In this case, we ignore the task or scope. For instance, if you have a task or a scope named 'foo' and 'foobar',
137+
// and the line is 'hh foo|', we want to suggest the value for 'foo' and 'foobar'.
138+
// Possible scenarios:
139+
// - no task or scope: `hh ` -> task and scope already undefined
140+
// - only a task: `hh task ` -> task set to undefined, scope already undefined
141+
// - only a scope: `hh scope ` -> scope set to undefined, task already undefined
142+
// - both a scope and a task (the task always follow the scope): `hh scope task ` -> task set to undefined, scope stays defined
143+
if (taskName === last || scopeName === last) {
144+
if (taskName !== undefined && scopeName !== undefined) {
145+
[taskName, scopeName] = [undefined, scopeName];
146+
} else {
147+
[taskName, scopeName] = [undefined, undefined];
148+
}
108149
}
109150

110151
if (prev === "--network") {
@@ -114,6 +155,16 @@ export async function complete({
114155
}));
115156
}
116157

158+
const scopeDefinition =
159+
scopeName === undefined ? undefined : scopes[scopeName];
160+
161+
const taskDefinition =
162+
taskName === undefined
163+
? undefined
164+
: scopeDefinition === undefined
165+
? tasks[taskName]
166+
: scopeDefinition.tasks[taskName];
167+
117168
// if the previous word is a param, then a value is expected
118169
// we don't complete anything here
119170
if (prev.startsWith("-")) {
@@ -125,39 +176,60 @@ export async function complete({
125176
}
126177

127178
const isTaskParam =
128-
task !== undefined &&
129-
tasks[task]?.paramDefinitions[paramName]?.isFlag === false;
179+
taskDefinition?.paramDefinitions[paramName]?.isFlag === false;
130180

131181
if (isTaskParam) {
132182
return HARDHAT_COMPLETE_FILES;
133183
}
134184
}
135185

136-
// if there's no task, we complete either tasks or params
137-
if (task === undefined || tasks[task] === undefined) {
186+
// If there's no task or scope, we complete either tasks and scopes or params
187+
if (taskDefinition === undefined && scopeDefinition === undefined) {
188+
if (last.startsWith("-")) {
189+
return coreParams.filter((param) => startsWithLast(param.name));
190+
}
191+
138192
const taskSuggestions = Object.values(tasks)
139193
.filter((x) => !x.isSubtask)
140194
.map((x) => ({
141195
name: x.name,
142196
description: x.description,
143197
}));
144-
if (last.startsWith("-")) {
145-
return coreParams.filter((param) => startsWithLast(param.name));
146-
}
147-
return taskSuggestions.filter((x) => startsWithLast(x.name));
198+
199+
const scopeSuggestions = Object.values(scopes).map((x) => ({
200+
name: x.name,
201+
description: x.description,
202+
}));
203+
204+
return taskSuggestions
205+
.concat(scopeSuggestions)
206+
.filter((x) => startsWithLast(x.name));
207+
}
208+
209+
// If there's a scope but not a task, we complete with the scopes'tasks
210+
if (taskDefinition === undefined && scopeDefinition !== undefined) {
211+
return Object.values(scopes[scopeName!].tasks)
212+
.filter((x) => !x.isSubtask)
213+
.map((x) => ({
214+
name: x.name,
215+
description: x.description,
216+
}))
217+
.filter((x) => startsWithLast(x.name));
148218
}
149219

150220
if (!last.startsWith("-")) {
151221
return HARDHAT_COMPLETE_FILES;
152222
}
153223

154-
// if there's a task and the last word starts with -, we complete its params and the global params
155-
const taskParams = Object.values(tasks[task].paramDefinitions)
156-
.map((param) => ({
157-
name: ArgumentsParser.paramNameToCLA(param.name),
158-
description: param.description,
159-
}))
160-
.filter((x) => !words.includes(x.name));
224+
const taskParams =
225+
taskDefinition === undefined
226+
? []
227+
: Object.values(taskDefinition.paramDefinitions)
228+
.map((param) => ({
229+
name: ArgumentsParser.paramNameToCLA(param.name),
230+
description: param.description,
231+
}))
232+
.filter((x) => !words.includes(x.name));
161233

162234
return [...taskParams, ...coreParams].filter((suggestion) =>
163235
startsWithLast(suggestion.name)
@@ -195,27 +267,43 @@ async function getCompletionData(): Promise<CompletionData | undefined> {
195267

196268
// we extract the tasks data explicitly to make sure everything
197269
// is serializable and to avoid saving unnecessary things from the HRE
198-
const tasks: CompletionData["tasks"] = mapValues(hre.tasks, (task) => ({
199-
name: task.name,
200-
description: task.description ?? "",
201-
isSubtask: task.isSubtask,
202-
paramDefinitions: mapValues(task.paramDefinitions, (paramDefinition) => ({
203-
name: paramDefinition.name,
204-
description: paramDefinition.description ?? "",
205-
isFlag: paramDefinition.isFlag,
206-
})),
270+
const tasks: CompletionData["tasks"] = mapValues(hre.tasks, (task) =>
271+
getTaskFromTaskDefinition(task)
272+
);
273+
274+
const scopes: CompletionData["scopes"] = mapValues(hre.scopes, (scope) => ({
275+
name: scope.name,
276+
description: scope.description ?? "",
277+
tasks: mapValues(scope.tasks, (task) => getTaskFromTaskDefinition(task)),
207278
}));
208279

209280
const completionData: CompletionData = {
210281
networks,
211282
tasks,
283+
scopes,
212284
};
213285

214286
await saveCachedCompletionData(projectId, completionData, mtimes);
215287

216288
return completionData;
217289
}
218290

291+
function getTaskFromTaskDefinition(taskDef: TaskDefinition): Task {
292+
return {
293+
name: taskDef.name,
294+
description: taskDef.description ?? "",
295+
isSubtask: taskDef.isSubtask,
296+
paramDefinitions: mapValues(
297+
taskDef.paramDefinitions,
298+
(paramDefinition) => ({
299+
name: paramDefinition.name,
300+
description: paramDefinition.description ?? "",
301+
isFlag: paramDefinition.isFlag,
302+
})
303+
),
304+
};
305+
}
306+
219307
function getProjectId(): string | undefined {
220308
const packageJsonPath = findup.sync("package.json");
221309

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const scope1 = scope("scope1");
2+
scope1.task("task1", "task1 description").setAction(async () => {});
3+
scope1
4+
.task("task2", "task2 description")
5+
.setAction(async () => {})
6+
.addFlag("flag1", "flag1 description")
7+
.addFlag("flag2")
8+
.addFlag("tmpflag");
9+
10+
const scope2 = scope("scope-2", "scope-2 description");
11+
12+
const scope3 = scope("scope-3", "scope-3 description");
13+
scope3.task("scope-3", "task description").setAction(async () => {});
14+
scope3.task("compile").setAction(async () => {});
15+
16+
module.exports = {
17+
solidity: "0.7.3",
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "custom-scopes"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "overridden-task"
3+
}

‎packages/hardhat-core/test/fixture-projects/autocomplete/overriden-task/package.json

-3
This file was deleted.

‎packages/hardhat-core/test/internal/cli/autocomplete.ts

+219-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ const coreTasks = [
6666
description: "Runs mocha tests",
6767
name: "test",
6868
},
69+
// 'vars' is a scope
70+
{
71+
description: "Manage your configuration variables",
72+
name: "vars",
73+
},
6974
];
7075

7176
const verboseParam = {
@@ -151,6 +156,17 @@ describe("autocomplete", function () {
151156
expect(suggestions).to.have.deep.members(coreTasks);
152157
});
153158

159+
it("should suggest all tasks' names that starts with the correct letters", async () => {
160+
const suggestions = await complete("hh t");
161+
162+
expect(suggestions).same.deep.members([
163+
{
164+
name: "test",
165+
description: "Runs mocha tests",
166+
},
167+
]);
168+
});
169+
154170
it("should suggest all core params after a -", async () => {
155171
const suggestions = await complete("hh -");
156172

@@ -347,6 +363,14 @@ describe("autocomplete", function () {
347363

348364
expect(suggestions).to.equal(HARDHAT_COMPLETE_FILES);
349365
});
366+
367+
it("should not confuse arguments as scopes", async () => {
368+
// "flatten" should not be identified as scope and "contracts/foo.sol" should
369+
// not be identified as task
370+
const suggestions = await complete("hh flatten contracts/foo.sol");
371+
372+
expect(suggestions).to.equal(HARDHAT_COMPLETE_FILES);
373+
});
350374
});
351375

352376
describe("custom tasks", () => {
@@ -402,14 +426,14 @@ describe("autocomplete", function () {
402426
});
403427
});
404428

405-
describe("overriden task", () => {
406-
useFixtureProject("autocomplete/overriden-task");
429+
describe("overridden task", () => {
430+
useFixtureProject("autocomplete/overridden-task");
407431

408432
after(() => {
409433
resetHardhatContext();
410434
});
411435

412-
it("should work when a task is overriden", async () => {
436+
it("should work when a task is overridden", async () => {
413437
const suggestions = await complete("hh ");
414438
expect(suggestions).to.have.deep.members(coreTasks);
415439
});
@@ -420,4 +444,196 @@ describe("autocomplete", function () {
420444
expect(suggestions).to.have.deep.members(coreTasks);
421445
});
422446
});
447+
448+
describe("scopes", () => {
449+
describe("autocomplete the scope'tasks", () => {
450+
useFixtureProject("autocomplete/basic-project");
451+
452+
it("should suggest the tasks assigned to a scope", async () => {
453+
const suggestions = await complete("hh vars ");
454+
455+
expect(suggestions).same.deep.members([
456+
{
457+
description: "Set the value of a configuration variable",
458+
name: "set",
459+
},
460+
{
461+
description: "Get the value of a configuration variable",
462+
name: "get",
463+
},
464+
{
465+
description: "List all the configuration variables",
466+
name: "list",
467+
},
468+
{
469+
description: "Delete a configuration variable",
470+
name: "delete",
471+
},
472+
{
473+
description:
474+
"Show the path of the file where all the configuration variables are stored",
475+
name: "path",
476+
},
477+
{
478+
description:
479+
"Show how to setup the configuration variables used by this project",
480+
name: "setup",
481+
},
482+
]);
483+
});
484+
});
485+
486+
describe("custom scopes", () => {
487+
useFixtureProject("autocomplete/custom-scopes");
488+
489+
after(() => {
490+
resetHardhatContext();
491+
});
492+
493+
it("should include custom scopes", async () => {
494+
const suggestions = await complete("hh ");
495+
496+
expect(suggestions).to.have.deep.members([
497+
...coreTasks,
498+
{
499+
name: "scope1",
500+
description: "",
501+
},
502+
{
503+
name: "scope-2",
504+
description: "scope-2 description",
505+
},
506+
{
507+
name: "scope-3",
508+
description: "scope-3 description",
509+
},
510+
]);
511+
});
512+
513+
it("should complete scopes after a - in the middle of the scope name", async () => {
514+
const suggestions = await complete("hh scope-");
515+
516+
expect(suggestions).to.have.deep.members([
517+
{
518+
name: "scope-2",
519+
description: "scope-2 description",
520+
},
521+
{
522+
name: "scope-3",
523+
description: "scope-3 description",
524+
},
525+
]);
526+
});
527+
528+
it("should autocomplete with the scope's tasks", async () => {
529+
const suggestions = await complete("hh scope1 ");
530+
531+
expect(suggestions).to.have.deep.members([
532+
{
533+
name: "task1",
534+
description: "task1 description",
535+
},
536+
{
537+
name: "task2",
538+
description: "task2 description",
539+
},
540+
]);
541+
});
542+
543+
it("should not autocomplete with the scope's tasks, because there are no tasks assigned", async () => {
544+
const suggestions = await complete("hh scope-2 ");
545+
546+
expect(suggestions).to.have.deep.members([]);
547+
});
548+
549+
it("should autocomplete with the scope's task's flags and the core parameters", async () => {
550+
const suggestions = await complete("hh scope1 task2 --flag --");
551+
552+
expect(suggestions).to.have.deep.members([
553+
{
554+
name: "--flag1",
555+
description: "flag1 description",
556+
},
557+
{
558+
name: "--flag2",
559+
description: "",
560+
},
561+
{
562+
name: "--tmpflag",
563+
description: "",
564+
},
565+
...coreParams,
566+
]);
567+
});
568+
569+
it("should autocomplete with the matching flags", async () => {
570+
const suggestions = await complete("hh scope1 task2 --fla");
571+
572+
expect(suggestions).to.have.deep.members([
573+
{
574+
name: "--flag1",
575+
description: "flag1 description",
576+
},
577+
{
578+
name: "--flag2",
579+
description: "",
580+
},
581+
{
582+
description: "Generate a flamegraph of your Hardhat tasks",
583+
name: "--flamegraph",
584+
},
585+
]);
586+
});
587+
588+
it("should autocomplete with the matching flags that haven't been used yet", async () => {
589+
const suggestions = await complete(
590+
"hh scope1 --flamegraph task2 --fla"
591+
);
592+
593+
expect(suggestions).to.have.deep.members([
594+
{
595+
name: "--flag1",
596+
description: "flag1 description",
597+
},
598+
{
599+
name: "--flag2",
600+
description: "",
601+
},
602+
]);
603+
});
604+
605+
it("should autocomplete with a scope's task that has the same name as the scope itself", async () => {
606+
const suggestions = await complete("hh scope-3 s");
607+
608+
expect(suggestions).to.have.deep.members([
609+
{
610+
name: "scope-3",
611+
description: "task description",
612+
},
613+
]);
614+
});
615+
616+
it("should autocomplete with a scope's task that has the same name as a stand alone task", async () => {
617+
const suggestions = await complete("hh scope-3 c");
618+
619+
expect(suggestions).to.have.deep.members([
620+
{
621+
name: "compile",
622+
description: "",
623+
},
624+
]);
625+
});
626+
627+
it("should autocomplete with a scope's task that with the task being declared after a flag", async () => {
628+
const suggestions = await complete("hh scope-3 --verbose c");
629+
630+
expect(suggestions).to.have.deep.members([
631+
{
632+
name: "compile",
633+
description: "",
634+
},
635+
]);
636+
});
637+
});
638+
});
423639
});

0 commit comments

Comments
 (0)
Please sign in to comment.