Skip to content

Commit

Permalink
feat: auto insert semicolon on import completion (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
antico5 committed Sep 20, 2022
1 parent b3f404c commit ae62417
Show file tree
Hide file tree
Showing 30 changed files with 108 additions and 45 deletions.
2 changes: 1 addition & 1 deletion client/src/commands/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ExtensionState } from "../types";
export default abstract class Command {
constructor(public state: ExtensionState) {}

public abstract execute(commandArgs: unknown): Promise<unknown>;
public abstract execute(...commandArgs: unknown[]): Promise<unknown>;

public abstract name(): string;
}
26 changes: 26 additions & 0 deletions client/src/commands/InsertSemicolonCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import vscode, { Position } from "vscode";
import Command from "./Command";

export default class InsertSemicolonCommand extends Command {
public async execute(position: Position) {
const editor = vscode.window.activeTextEditor;

if (editor === undefined) return;

const document = editor.document;

const lineText = document.lineAt(position.line).text;

if (/.*;$/.test(lineText)) {
return;
}

await editor.edit((builder) =>
builder.insert(new Position(position.line, lineText.length), ";")
);
}

public name(): string {
return "insertSemicolon";
}
}
9 changes: 7 additions & 2 deletions client/src/setup/setupCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import { ExtensionState } from "../types";
import CompileCommand from "../commands/CompileCommand";
import FlattenCurrentFileCommand from "../commands/FlattenCurrentFileCommand";
import CleanCommand from "../commands/CleanCommand";
import InsertSemicolonCommand from "../commands/InsertSemicolonCommand";
import Command from "../commands/Command";

const commandClasses = [
type ICommandClass = new (state: ExtensionState) => Command;

const commandClasses: ICommandClass[] = [
CompileCommand,
FlattenCurrentFileCommand,
CleanCommand,
InsertSemicolonCommand,
];

export async function setupCommands(state: ExtensionState) {
for (const commandClass of commandClasses) {
const command = new commandClass(state);
const disposable = vscode.commands.registerCommand(
`hardhat.solidity.${command.name()}`,
() => command.execute()
(...args) => command.execute(...args)
);
state.context.subscriptions.push(disposable);
}
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@
"command": "hardhat.solidity.clean",
"title": "Hardhat: Clear cache and artifacts "
},
{
"command": "hardhat.solidity.insertSemicolon",
"title": "Insert semicolon",
"enablement": "false"
},
{
"command": "hardhat.solidity.flattenCurrentFile",
"title": "Hardhat: Flatten this file and its dependencies",
Expand Down
19 changes: 16 additions & 3 deletions server/src/services/completion/getImportPathCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export function getImportPathCompletion(
const currentImport = node.astNode.path.replace("_;", "");
const importPath = toUnixStyle(path.join(node.realUri, "..", currentImport));

let items: CompletionItem[];

if (currentImport === "") {
const relativeImports = getRelativeImportPathCompletions(
position,
Expand All @@ -33,18 +35,29 @@ export function getImportPathCompletion(
const indexNodeModuleFolders =
getIndexedNodeModuleFolderCompletions(projCtx);

return relativeImports.concat(indexNodeModuleFolders);
items = relativeImports.concat(indexNodeModuleFolders);
} else if (isRelativeImport(currentImport)) {
return getRelativeImportPathCompletions(
items = getRelativeImportPathCompletions(
position,
currentImport,
importPath,
node,
logger
);
} else {
return getDirectImportPathCompletions(position, currentImport, projCtx);
items = getDirectImportPathCompletions(position, currentImport, projCtx);
}

// Trigger auto-insertion of semicolon after import completion
for (const item of items) {
item.command = {
command: "hardhat.solidity.insertSemicolon",
arguments: [position],
title: "",
};
}

return items;
}

function isRelativeImport(currentImport: string) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/completion/onCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function doComplete(
}

if (
context?.triggerCharacter === '"' &&
['"', "'"].includes(context?.triggerCharacter ?? "") &&
(!definitionNode || !isImportDirectiveNode(definitionNode))
) {
return null;
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/initialization/onInitialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const onInitialize = (serverState: ServerState) => {
textDocumentSync: TextDocumentSyncKind.Incremental,
// Tell the client that this server supports code completion.
completionProvider: {
triggerCharacters: [".", "/", '"'],
triggerCharacters: [".", "/", '"', "'"],
},
signatureHelpProvider: {
triggerCharacters: ["(", ","],
Expand Down
2 changes: 1 addition & 1 deletion server/test/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("Solidity Language Server", () => {
describe("completions", () => {
it("advertises capability", () =>
assert.deepStrictEqual(capabilities.completionProvider, {
triggerCharacters: [".", "/", '"'],
triggerCharacters: [".", "/", '"', "'"],
}));

it("registers onCompletion", () =>
Expand Down
11 changes: 9 additions & 2 deletions server/test/services/completion/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import {
import { forceToUnixStyle } from "../../helpers/forceToUnixStyle";
import { prependWithSlash } from "../../helpers/prependWithSlash";

const semicolonCommand = (position: VSCodePosition) => ({
command: {
arguments: [position],
command: "hardhat.solidity.insertSemicolon",
title: "",
},
});

describe("Parser", () => {
const workspaceFolder = prependWithSlash(
forceToUnixStyle(path.join(__dirname, "../../../test"))
Expand Down Expand Up @@ -591,8 +599,6 @@ describe("Parser", () => {
});

describe("direct", function () {
this.timeout(5000);

before(async () => {
const openzepplinUri1 = forceToUnixStyle(
path.join(
Expand Down Expand Up @@ -796,6 +802,7 @@ const assertImportCompletion = async (
.map((comp) => ({
...comp,
documentation: "Imports the package",
...semicolonCommand(position),
}))
.sort((left, right) => left.label.localeCompare(right.label));

Expand Down
8 changes: 8 additions & 0 deletions test/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import path from "path";
import Mocha from "mocha";
// eslint-disable-next-line import/no-extraneous-dependencies
import glob from "glob";
import { sleep } from "./helpers/sleep";

export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: "tdd",
color: true,
rootHooks: {
beforeAll: async () => {
await sleep(5000); // Wait for the extension to be loaded
},
},
timeout: 30000,
retries: 1,
});

const testsRoot = path.resolve(__dirname, "tests");
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - add license identifier", function () {
this.timeout(30000);

test("add MIT license identifier", async () => {
const uri = getTestContractUri("main/contracts/codeactions/NoLicense.sol");
const editor = await openFileInEditor(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - add multi override specifier", function () {
this.timeout(30000);

test("add multi override on multiple occurrences", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/AddMultioverrideSpecifier.sol"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - add override specifier", function () {
this.timeout(30000);

test("add override on multiple occurrences", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/AddOverrideSpecifier.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/codeactions/addPragmaVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - add solidity pragma", function () {
this.timeout(30000);

test("add version of current compiler", async () => {
const uri = getTestContractUri("main/contracts/codeactions/NoPragma.sol");
const editor = await openFileInEditor(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - add virtual specifier", function () {
this.timeout(30000);

test("add virtual specifier on multiple occurrences", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/AddVirtualSpecifier.sol"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - constrain mutability", function () {
this.timeout(30000);

test("add view modifier", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/ConstrainMutabilityView.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/codeactions/markAbstract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - mark abstract", function () {
this.timeout(30000);

test("add missing functions from interfaces", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/MarkAbstract.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/codeactions/specifyVisibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "../../helpers/editor";

suite("codeactions - specify visibility", function () {
this.timeout(30000);

test("specify public", async () => {
const uri = getTestContractUri(
"main/contracts/codeactions/SpecifyVisibility.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/commands/clean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { sleep } from "../../helpers/sleep";
import { getRootPath } from "../../helpers/workspace";

suite("task - clean", function () {
this.timeout(30000);

test("run clean task", async () => {
// Close any active editor - since running a task saves the current file apparently
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/commands/flatten.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { getTestContractUri } from "../../helpers/getTestContract";
import { openFileInEditor, waitForUI } from "../../helpers/editor";

suite("commands - flatten", function () {
this.timeout(30000);

test("flatten via command palette", async () => {
const uri = getTestContractUri("main/contracts/commands/Importer.sol");
await openFileInEditor(uri);
Expand Down
32 changes: 32 additions & 0 deletions test/integration/tests/completion/completion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as assert from "assert";
import vscode from "vscode";
import { openFileInEditor, waitForUI } from "../../helpers/editor";
import { type } from "../../helpers/commands";
import { sleep } from "../../helpers/sleep";
import { getTestContractUri } from "../../helpers/getTestContract";

suite("completion", function () {
test("[completion] - add semicolon automatically after import", async () => {
const uri = getTestContractUri(
"main/contracts/completion/AddSemicolon.sol"
);
const editor = await openFileInEditor(uri);
const document = editor.document;

await type(document, "import '");
await sleep(2000);
await vscode.commands.executeCommand("acceptSelectedSuggestion");
await waitForUI();
assert.equal(document.getText(), "import '@openzeppelin';");

await type(document, "/");
await sleep(1000);
await vscode.commands.executeCommand("acceptSelectedSuggestion");

await waitForUI();
assert.equal(
document.getText(),
"import '@openzeppelin/contracts/access/AccessControl.sol';"
);
});
});
2 changes: 1 addition & 1 deletion test/integration/tests/configuration/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ suite("Configuration", function () {
textDocumentSync: 2,
codeActionProvider: true,
completionProvider: {
triggerCharacters: [".", "/", '"'],
triggerCharacters: [".", "/", '"', "'"],
},
signatureHelpProvider: {
triggerCharacters: ["(", ","],
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/definition/definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { getTestContractUri } from "../../helpers/getTestContract";
import { getRootPath } from "../../helpers/workspace";

suite("Single-file Navigation", function () {
this.timeout(10000);

const testUri = getTestContractUri("main/contracts/definition/Test.sol");
const importTestUri = getTestContractUri(
"main/contracts/definition/ImportTest.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/diagnostics/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { getTestContractUri } from "../../helpers/getTestContract";
import { openFileInEditor } from "../../helpers/editor";

suite("diagnostics", function () {
this.timeout(30000);

test("[diagnostics] missing semicolon", async () => {
const uri = getTestContractUri(
"main/contracts/diagnostics/MissingSemicolon.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/implementation/implementation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { assertLspCommand } from "../../common/assertLspCommand";
import { getTestContractUri } from "../../helpers/getTestContract";

suite("Single-file Navigation", function () {
this.timeout(10000);

const testUri = getTestContractUri("main/contracts/implementation/Test.sol");

let client!: Client;
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/references/references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { assertLspCommand } from "../../common/assertLspCommand";
import { getTestContractUri } from "../../helpers/getTestContract";

suite("Single-file Navigation", function () {
this.timeout(10000);

const testUri = getTestContractUri("main/contracts/references/Test.sol");
const importedUri = getTestContractUri(
"main/contracts/references/Imported.sol"
Expand Down
1 change: 0 additions & 1 deletion test/integration/tests/remappings/remappings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { assertPositionEqual } from "../../helpers/assertions";

suite("remappings", function () {
this.timeout(30000);
test("[remappings] multiple navigations", async () => {
const importerUri = getTestContractUri("remappings/src/Importer.sol");
const importedUri = getTestContractUri("remappings/lib/myLib/Imported.sol");
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/rename/rename.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { assertLspCommand } from "../../common/assertLspCommand";
import { getTestContractUri } from "../../helpers/getTestContract";

suite("Single-file Navigation", function () {
this.timeout(10000);

const testUri = getTestContractUri("main/contracts/rename/Test.sol");
const importTestUri = getTestContractUri(
"main/contracts/rename/MultiImport.sol"
Expand Down
2 changes: 0 additions & 2 deletions test/integration/tests/typedefinition/typedefinition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { assertLspCommand } from "../../common/assertLspCommand";
import { getTestContractUri } from "../../helpers/getTestContract";

suite("Single-file Navigation", function () {
this.timeout(10000);

const testUri = getTestContractUri("main/contracts/typedefinition/Test.sol");
const importedUri = getTestContractUri(
"main/contracts/typedefinition/Imported.sol"
Expand Down

0 comments on commit ae62417

Please sign in to comment.