diff --git a/CHANGELOG.md b/CHANGELOG.md index 8533bb6d5..12fa9b0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Complete `%todo`. https://github.com/rescript-lang/rescript-vscode/pull/981 - Add code action for extracting a locally defined module into its own file. https://github.com/rescript-lang/rescript-vscode/pull/983 - Add code action for expanding catch-all patterns. https://github.com/rescript-lang/rescript-vscode/pull/987 +- Add code actions for removing unused code (per item and for an entire file), driven by `reanalyze`. https://github.com/rescript-lang/rescript-vscode/pull/989 ## 1.50.0 diff --git a/client/src/commands/code_analysis.ts b/client/src/commands/code_analysis.ts index 557747882..fc0dffcca 100644 --- a/client/src/commands/code_analysis.ts +++ b/client/src/commands/code_analysis.ts @@ -34,6 +34,22 @@ export type DiagnosticsResultFormat = Array<{ }; }>; +enum ClassifiedMessage { + Removable, + Default, +} + +let classifyMessage = (msg: string) => { + if ( + msg.endsWith(" is never used") || + msg.endsWith(" has no side effects and can be removed") + ) { + return ClassifiedMessage.Removable; + } + + return ClassifiedMessage.Default; +}; + let resultsToDiagnostics = ( results: DiagnosticsResultFormat, diagnosticsResultCodeActions: DiagnosticsResultCodeActionsMap @@ -91,8 +107,6 @@ let resultsToDiagnostics = ( let codeActionEdit = new WorkspaceEdit(); - // In the future, it would be cool to have an additional code action - // here for automatically removing whatever the thing that's dead is. codeActionEdit.replace( Uri.parse(item.file), // Make sure the full line is replaced @@ -119,6 +133,38 @@ let resultsToDiagnostics = ( } } } + + // This heuristic below helps only target dead code that can be removed + // safely by just removing its text. + if (classifyMessage(item.message) === ClassifiedMessage.Removable) { + { + let codeAction = new CodeAction("Remove unused"); + codeAction.kind = CodeActionKind.RefactorRewrite; + + let codeActionEdit = new WorkspaceEdit(); + + codeActionEdit.replace( + Uri.parse(item.file), + new Range( + new Position(item.range[0], item.range[1]), + new Position(item.range[2], item.range[3]) + ), + "" + ); + + codeAction.edit = codeActionEdit; + + if (diagnosticsResultCodeActions.has(item.file)) { + diagnosticsResultCodeActions + .get(item.file) + .push({ range: issueLocationRange, codeAction }); + } else { + diagnosticsResultCodeActions.set(item.file, [ + { range: issueLocationRange, codeAction }, + ]); + } + } + } } }); diff --git a/client/src/extension.ts b/client/src/extension.ts index 5e34dae37..00b60e50b 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -9,6 +9,9 @@ import { Uri, Range, Position, + CodeAction, + WorkspaceEdit, + CodeActionKind, } from "vscode"; import { @@ -16,8 +19,7 @@ import { LanguageClientOptions, ServerOptions, State, - Executable, - TransportKind + TransportKind, } from "vscode-languageclient/node"; import * as customCommands from "./commands"; @@ -91,7 +93,11 @@ export function activate(context: ExtensionContext) { // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used let serverOptions: ServerOptions = { - run: { module: serverModule, args: ["--node-ipc"], transport: TransportKind.ipc }, + run: { + module: serverModule, + args: ["--node-ipc"], + transport: TransportKind.ipc, + }, debug: { module: serverModule, args: ["--node-ipc"], @@ -189,12 +195,31 @@ export function activate(context: ExtensionContext) { let availableActions = diagnosticsResultCodeActions.get(document.uri.fsPath) ?? []; - return availableActions + const allRemoveActionEdits = availableActions.filter( + ({ codeAction }) => codeAction.title === "Remove unused" + ); + + const actions: CodeAction[] = availableActions .filter( ({ range }) => range.contains(rangeOrSelection) || range.isEqual(rangeOrSelection) ) .map(({ codeAction }) => codeAction); + + if (allRemoveActionEdits.length > 0) { + const removeAllCodeAction = new CodeAction("Remove all unused in file"); + const edit = new WorkspaceEdit(); + allRemoveActionEdits.forEach((subEdit) => { + subEdit.codeAction.edit.entries().forEach(([uri, [textEdit]]) => { + edit.replace(uri, textEdit.range, textEdit.newText); + }); + }); + removeAllCodeAction.kind = CodeActionKind.RefactorRewrite; + removeAllCodeAction.edit = edit; + actions.push(removeAllCodeAction); + } + + return actions; }, });