diff --git a/package.json b/package.json index 79cb94a8e..855006495 100644 --- a/package.json +++ b/package.json @@ -920,4 +920,4 @@ "ws": "^8.17.1", "ya-bbcode": "^4.0.0" } -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 1dcd1d7ba..508e551e7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,6 +12,7 @@ import { GDDocumentationProvider, GDDefinitionProvider, GDTaskProvider, + GDCodeActionProvider, } from "./providers"; import { ClientConnectionManager } from "./lsp"; import { ScenePreviewProvider } from "./scene_tools"; @@ -47,6 +48,7 @@ interface Extension { semanticTokensProvider?: GDSemanticTokensProvider; completionProvider?: GDCompletionItemProvider; tasksProvider?: GDTaskProvider; + codeActionProvider?: GDCodeActionProvider; } export const globals: Extension = {}; @@ -65,6 +67,7 @@ export function activate(context: vscode.ExtensionContext) { globals.formattingProvider = new FormattingProvider(context); globals.docsProvider = new GDDocumentationProvider(context); globals.definitionProvider = new GDDefinitionProvider(context); + globals.codeActionProvider = new GDCodeActionProvider(context); // globals.semanticTokensProvider = new GDSemanticTokensProvider(context); // globals.completionProvider = new GDCompletionItemProvider(context); // globals.tasksProvider = new GDTaskProvider(context); diff --git a/src/providers/code_action_provider.ts b/src/providers/code_action_provider.ts new file mode 100644 index 000000000..2037ae193 --- /dev/null +++ b/src/providers/code_action_provider.ts @@ -0,0 +1,274 @@ +import * as vscode from "vscode"; +import { + ExtensionContext +} from "vscode"; +import { tabString } from "../utils"; +/** + * @param $1 The variable name (including the : if it has it), + * @param $2 The variable type, + * @param $3 Everything else after the type + */ +const VARIABLE_REGEXP: RegExp = /^var\s*(\w+:?)\s*\s*(\w*)([\s\S\w\W]*)/; + +export class GDCodeActionProvider implements vscode.CodeActionProvider { + constructor(private context: ExtensionContext) { + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { language: 'gdscript' }, + this, + { + providedCodeActionKinds: GDCodeActionProvider.providedCodeActionKinds, + } + ) + ); + + } + // Define the kind of code actions this provider offers + public static readonly providedCodeActionKinds = [ + vscode.CodeActionKind.Refactor, + vscode.CodeActionKind.RefactorExtract, + vscode.CodeActionKind.RefactorRewrite, + vscode.CodeActionKind.RefactorInline, + ]; + + public provideCodeActions( + document: vscode.TextDocument, + range: vscode.Range, + context: vscode.CodeActionContext, + token: vscode.CancellationToken + ): vscode.ProviderResult { + + // Specify the edits to be applied + const _exportTheseVariables = exportTheseVariables(document); + const _extractFunction = extractFunction(document); + const _extractVariable = extractVariable(document); + + const _addExportToVariable = addExportToVariable(range, document); + const _addRangeExportToVariable = addRangeExportToVariable(range, document); + + return [_extractVariable, _extractFunction, _exportTheseVariables, _addExportToVariable, _addRangeExportToVariable]; + } + + +} + + + +function addExportToVariable(range: vscode.Range, document: vscode.TextDocument): vscode.CodeAction { + const startLine = document.lineAt(range.start.line); + const lineText = startLine.text; + + const exec: RegExpExecArray | null = VARIABLE_REGEXP.exec(lineText); + if (!exec) { + return undefined; + } + + const codeAction = new vscode.CodeAction( + "Export this variable", + vscode.CodeActionKind.RefactorInline + ); + + + codeAction.edit = new vscode.WorkspaceEdit(); + + const updatedText = lineText.replace(VARIABLE_REGEXP, "@export var $1 $2 $3"); + + codeAction.edit.replace( + document.uri, + startLine.range, + updatedText + ); + + return codeAction; +} + +function addRangeExportToVariable(range: vscode.Range, document: vscode.TextDocument): vscode.CodeAction { + const startLine = document.lineAt(range.start.line); + const lineText = startLine.text.trim(); + + const exec: RegExpExecArray | null = VARIABLE_REGEXP.exec(lineText); + if (!exec) { + return undefined; + } + + const [_, name, type, body] = exec; + + const is_number = type.trim() === "float" || type.trim() === "int"; + if (!is_number) return undefined; + + const codeAction = new vscode.CodeAction( + "Export as a range", + vscode.CodeActionKind.RefactorInline + ); + + codeAction.edit = new vscode.WorkspaceEdit(); + let updatedText = ""; + + if (type.trim() === "float") { + updatedText = lineText.replace(VARIABLE_REGEXP, `@export_range(0.0, 1.0) var ${name} ${type} ${body}`); + } else { + updatedText = lineText.replace(VARIABLE_REGEXP, `@export_range(0, 1) var ${name} ${type} ${body}`); + } + + codeAction.edit.replace( + document.uri, + startLine.range, + updatedText + ); + codeAction.isPreferred = true; + return codeAction; +} + +function extractVariable(document: vscode.TextDocument) { + const editor = vscode.window.activeTextEditor; + if (!editor) return undefined; + const selectedText = document.getText(editor.selection); + if (selectedText === "") return undefined; + const codeAction = new vscode.CodeAction( + "Extract Variable", + vscode.CodeActionKind.RefactorExtract, + ); + + const variableName = "_new_variable_name"; + + const pasteLine: number = editor.selection.start.line; + + /** Paste at the same indentation level, just above the selection */ + let indentation = ""; + + /** Look for the whitespace above at the start of the line */ + const exec = /^\s+/.exec(document.lineAt(pasteLine).text); + if (exec) { + indentation = exec[0]; + } + + //\t\t var xyz := x + y + z\n + const newVariable = new vscode.SnippetString(`${indentation}var ${variableName}$0 := ${selectedText}\n`); + + const position = new vscode.Position(document.lineAt(pasteLine).lineNumber, 0); + + const referenceNewVariable = vscode.TextEdit.replace( + editor.selection, + variableName + ); + const insertNewFunction = vscode.SnippetTextEdit.insert( + position, + newVariable + ); + + const edit = new vscode.WorkspaceEdit(); + + edit.set(document.uri, [ + referenceNewVariable, + insertNewFunction + ]); + codeAction.edit = edit; + codeAction.command = { + command: "editor.action.rename", + title: "Rename the new variable" + }; + + return codeAction; +} + +function extractFunction(document: vscode.TextDocument) { + const editor = vscode.window.activeTextEditor; + if (!editor) return undefined; + const selectedText = document.getText(editor.selection); + if (selectedText === "") return undefined; + const codeAction = new vscode.CodeAction( + "Extract function", + vscode.CodeActionKind.RefactorExtract, + ); + const tabOrSpace = tabString(); + // TODO: Maybe look for another name because if by any chance the user has this exact function name, it will mess things up + const functionName = "_new_function"; + const newFunction: string = `func ${functionName}():\n${selectedText + .split("\n") + .map((line) => tabOrSpace + line) + .join("\n")}\n`; + /** + * Look in each line, starting with this one and go down, + * if you find a line with a method declaration, go up one and paste the new function + * or the end of the document + */ + let pasteLine: number = editor.selection.end.line; + + for (let i = 0; i < document.lineCount; i++) { + if (i < pasteLine) continue; + const textLine = document.lineAt(i); + + if (textLine.text.startsWith("func")) { + break; + } else { + pasteLine++; + } + } + const position = new vscode.Position(Math.min(pasteLine, document.lineCount), 0); + + const edit = new vscode.WorkspaceEdit(); + + const callNewFunction = vscode.SnippetTextEdit.replace( + editor.selection, + new vscode.SnippetString(`${functionName}$0()`) + ); + const insertNewFunction = vscode.TextEdit.insert( + position, `\n${newFunction}` + ); + + edit.set(document.uri, [ + callNewFunction, + insertNewFunction + ]); + + codeAction.edit = edit; + + codeAction.command = { + command: "editor.action.rename", + title: "Rename the new function" + }; + + return codeAction; +} + +// TODO: When everything is done, this should export them to their designated exports +function exportTheseVariables(document: vscode.TextDocument): vscode.CodeAction { + const codeAction = new vscode.CodeAction( + "Export these variables", + vscode.CodeActionKind.RefactorRewrite, + ); + const editor = vscode.window.activeTextEditor; + const selectedText = document.getText(editor.selection); + + if (selectedText === "") return undefined; + + const individualLines = selectedText.split("\n"); + + let updatedText: string[] = []; + let nonVarElements = 0; + let varElements = 0; + + for (let i = 0; i < individualLines.length; i++) { + const element = individualLines[i]; + + if (!element.startsWith("var ")) { + updatedText = updatedText.concat(element); + nonVarElements++; + continue; + } + updatedText = updatedText.concat(element.replace(/^var/, "@export var")); + varElements++; + } + + if (varElements < 1) return undefined; + + const newText: string = updatedText.join("\n"); + codeAction.edit = new vscode.WorkspaceEdit(); + codeAction.edit.replace( + document.uri, + editor.selection, + newText, + ); + codeAction.isPreferred = true; + return codeAction; +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 4b50d703b..a403580fb 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -7,3 +7,4 @@ export * from "./hover"; export * from "./inlay_hints"; export * from "./semantic_tokens"; export * from "./tasks"; +export * from "./code_action_provider"; diff --git a/src/utils/vscode_utils.ts b/src/utils/vscode_utils.ts index 92e0ee154..b23f17ae2 100644 --- a/src/utils/vscode_utils.ts +++ b/src/utils/vscode_utils.ts @@ -28,3 +28,23 @@ export function register_command(command: string, callback: (...args: any[]) => export function get_extension_uri(...paths: string[]) { return vscode.Uri.joinPath(vscode.extensions.getExtension("geequlim.godot-tools").extensionUri, ...paths ?? ""); } + + +/** + * Returns either a tab or spaces depending on the user config + */ +export function tabString(document?: vscode.TextDocument): string { + const editor = vscode.window.activeTextEditor; + const doc = document ?? editor?.document; + + if (!doc) { + const editorConfig = vscode.workspace.getConfiguration("editor"); + const insertSpaces = editorConfig.get("insertSpaces", true); + const tabSize = editorConfig.get("tabSize", 4); + return insertSpaces ? " ".repeat(tabSize) : "\t"; + } + + const { insertSpaces, tabSize } = vscode.window.activeTextEditor?.options ?? {}; + const size = typeof tabSize === "number" ? tabSize : 4; + return insertSpaces ? " ".repeat(size) : "\t"; +} \ No newline at end of file diff --git a/syntaxes/examples/code_actions_example.gd b/syntaxes/examples/code_actions_example.gd new file mode 100644 index 000000000..fc715f17d --- /dev/null +++ b/syntaxes/examples/code_actions_example.gd @@ -0,0 +1,32 @@ +var simple_export +var range_export: float = 0.0 +var range_export_integer: int = 0 +var range_export_integer: int +var range_export_float: float + +var ex +var ex +const ex +var ex +var ex + + +func export_code_as_new_function(args): + if condition: + var a + var b + var c + else: + pass + pass + pass + if condition: + if condition: + if condition: + pass +func method(args): + position.x += (15 / 2) * 5 + var lambda = func (x): + print(x) + pass +