diff --git a/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolContext.kt b/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolContext.kt new file mode 100644 index 000000000..b226872de --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolContext.kt @@ -0,0 +1,10 @@ +package com.phodal.shirelang.agenttool + +import com.intellij.openapi.project.Project + +class AgentToolContext( + val project: Project, + val argument: String +) { + +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolResult.kt b/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolResult.kt new file mode 100644 index 000000000..527a0a508 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/agenttool/AgentToolResult.kt @@ -0,0 +1,8 @@ +package com.phodal.shirelang.agenttool + +data class AgentToolResult( + val isSuccess: Boolean, + val output: String? = null +) { + +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/BrowseTool.kt b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/BrowseTool.kt new file mode 100644 index 000000000..225c6b5f9 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/BrowseTool.kt @@ -0,0 +1,32 @@ +package com.phodal.shirelang.agenttool.browse + +import com.phodal.shirelang.agenttool.AgentToolContext +import com.phodal.shirelang.provider.AgentTool +import com.phodal.shirelang.agenttool.AgentToolResult +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +class BrowseTool : AgentTool { + override val name: String get() = "Browse" + override val description: String = "Get the content of a given URL." + + override fun execute(context: AgentToolContext): AgentToolResult { + return AgentToolResult( + isSuccess = true, + output = parse(context.argument).body + ) + } + + companion object { + /** + * Doc for parseHtml + * + * Intellij API: [com.intellij.inspectopedia.extractor.utils.HtmlUtils.cleanupHtml] + */ + fun parse(url: String): DocumentContent { + val doc: Document = Jsoup.connect(url).get() + return DocumentCleaner().cleanHtml(doc) + } + } +} + diff --git a/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentCleaner.kt b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentCleaner.kt new file mode 100644 index 000000000..6e6ac1567 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentCleaner.kt @@ -0,0 +1,65 @@ +package com.phodal.shirelang.agenttool.browse + +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class DocumentCleaner { + fun cleanHtml(html: String): DocumentContent { + val doc = Jsoup.parse(html) + return cleanHtml(doc) + } + + fun cleanHtml(doc: Document): DocumentContent { + return DocumentContent( + title = doc.title(), + language = metaContent(doc, "http-equiv", "Content-Language"), + description = metaDescription(doc), + body = articleNode(doc) + ) + } + + fun metaDescription(doc: Document): String? { + val attributes = arrayOf(arrayOf("property", "description"), arrayOf("name", "description")) + return attributes + .asSequence() + .mapNotNull { (key, value) -> metaContent(doc, key, value) } + .firstOrNull() + } + + fun metaContent(doc: Document, key: String, value: String): String? { + val metaElements = doc.select("head meta[$key=$value]") + return metaElements + .map { it.attr("content").trim() } + .firstOrNull { it.isNotEmpty() } + } + + val ARTICLE_BODY_ATTR: Array> = arrayOf( + Pair("itemprop", "articleBody"), + Pair("data-testid", "article-body"), + Pair("name", "articleBody") + ) + + fun articleNode(doc: Document): String? { + var bodyElement: Element? = doc.select("html").select("body").first() + val firstBodyElement = bodyElement ?: return null + // the Microdata + for ((attr, value) in ARTICLE_BODY_ATTR) { + bodyElement = doc.selectFirst("[$attr=$value]") + if (bodyElement != null) { + return bodyElement.text() + } + } + + return trySelectBestCode(firstBodyElement) + } + + private fun trySelectBestCode(doc: Element): String { + val commonBestNodes = doc.select("article, main, #main, #content, #doc-content, #contents, .book-body") + if (commonBestNodes.isNotEmpty()) { + return commonBestNodes.first()?.text() ?: "" + } + + return doc.text() + } +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentContent.kt b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentContent.kt new file mode 100644 index 000000000..cbd60520a --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/agenttool/browse/DocumentContent.kt @@ -0,0 +1,9 @@ +package com.phodal.shirelang.agenttool.browse + +data class DocumentContent( + val title: String?, + val language: String?, + val description: String?, + val body: String? +) { +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compile/VariableTemplateCompiler.kt b/language/src/main/kotlin/com/phodal/shirelang/compile/VariableTemplateCompiler.kt index 8a973695b..6d1a99623 100644 --- a/language/src/main/kotlin/com/phodal/shirelang/compile/VariableTemplateCompiler.kt +++ b/language/src/main/kotlin/com/phodal/shirelang/compile/VariableTemplateCompiler.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiNameIdentifierOwner +import com.phodal.shirelang.completion.provider.CustomVariable import org.apache.velocity.VelocityContext import org.apache.velocity.app.Velocity import java.io.StringWriter diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/BrowseInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/BrowseInsCommand.kt new file mode 100644 index 000000000..765b3b1c1 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/BrowseInsCommand.kt @@ -0,0 +1,19 @@ +package com.phodal.shirelang.compiler + +import com.phodal.shirelang.agenttool.browse.BrowseTool +import com.phodal.shirelang.compiler.exec.InsCommand +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.project.Project + +class BrowseInsCommand(val myProject: Project, private val prop: String) : InsCommand { + override suspend fun execute(): String? { + var body: String? = null + runInEdt { + val parse = BrowseTool.parse(prop) + body = parse.body + } + + return body + } +} + diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/DevInsCompiler.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/DevInsCompiler.kt new file mode 100644 index 000000000..13f19f652 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/DevInsCompiler.kt @@ -0,0 +1,299 @@ +package com.phodal.shirelang.compiler + +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import com.phodal.shirelang.compile.VariableTemplateCompiler +import com.phodal.shirelang.compiler.exec.* +import com.phodal.shirelang.completion.dataprovider.BuiltinCommand +import com.phodal.shirelang.completion.dataprovider.CustomCommand +import com.phodal.shirelang.completion.dataprovider.ToolHubVariable +import com.phodal.shirelang.parser.CodeBlockElement +import com.phodal.shirelang.psi.ShireFile +import com.phodal.shirelang.psi.ShireTypes +import com.phodal.shirelang.psi.ShireUsed +import kotlinx.coroutines.runBlocking + +val CACHED_COMPILE_RESULT = mutableMapOf() + +class ShireCompiler( + private val myProject: Project, + private val file: ShireFile, + private val editor: Editor? = null, + private val element: PsiElement? = null +) { + private var skipNextCode: Boolean = false + private val logger = logger() + private val result = ShireCompiledResult() + private val output: StringBuilder = StringBuilder() + + /** + * Todo: build AST tree, then compile + */ + fun compile(): ShireCompiledResult { + result.input = file.text + file.children.forEach { + when (it.elementType) { + ShireTypes.TEXT_SEGMENT -> output.append(it.text) + ShireTypes.NEWLINE -> output.append("\n") + ShireTypes.CODE -> { + if (skipNextCode) { + skipNextCode = false + return@forEach + } + + output.append(it.text) + } + + ShireTypes.USED -> processUsed(it as ShireUsed) + ShireTypes.COMMENTS -> { + if (it.text.startsWith("[flow]:")) { + val fileName = it.text.substringAfter("[flow]:").trim() + val content = + myProject.guessProjectDir()?.findFileByRelativePath(fileName)?.let { virtualFile -> + virtualFile.inputStream.bufferedReader().use { reader -> reader.readText() } + } + + if (content != null) { + val devInFile = ShireFile.fromString(myProject, content) + result.nextJob = devInFile + } + } + } + + else -> { + output.append(it.text) + logger.warn("Unknown element type: ${it.elementType}") + } + } + } + + result.output = output.toString() + + CACHED_COMPILE_RESULT[file.name] = result + return result + } + + private fun processUsed(used: ShireUsed) { + val firstChild = used.firstChild + val id = firstChild.nextSibling + + when (firstChild.elementType) { + ShireTypes.COMMAND_START -> { + val command = BuiltinCommand.fromString(id?.text ?: "") + if (command == null) { + CustomCommand.fromString(myProject, id?.text ?: "")?.let { cmd -> + ShireFile.fromString(myProject, cmd.content).let { file -> + ShireCompiler(myProject, file).compile().let { + output.append(it.output) + result.hasError = it.hasError + } + } + + return + } + + + output.append(used.text) + logger.warn("Unknown command: ${id?.text}") + result.hasError = true + return + } + + if (!command.requireProps) { + processingCommand(command, "", used, fallbackText = used.text) + return + } + + val propElement = id.nextSibling?.nextSibling + val isProp = (propElement.elementType == ShireTypes.COMMAND_PROP) + if (!isProp) { + output.append(used.text) + logger.warn("No command prop found: ${used.text}") + result.hasError = true + return + } + + processingCommand(command, propElement!!.text, used, fallbackText = used.text) + } + + ShireTypes.AGENT_START -> { +// val agentId = id?.text +// val configs = CustomAgentConfig.loadFromProject(myProject).filter { +// it.name == agentId +// } + +// if (configs.isNotEmpty()) { +// result.executeAgent = configs.first() +// } + throw NotImplementedError("Not implemented yet") + } + + ShireTypes.VARIABLE_START -> { + val variableId = id?.text + val variable = ToolHubVariable.lookup(myProject, variableId) + if (variable.isNotEmpty()) { + output.append(variable.joinToString("\n") { it }) + return + } + + if (editor == null || element == null) { + output.append("$SHIRE_ERROR No context editor found for variable: ${used.text}") + result.hasError = true + return + } + + val file = element.containingFile + VariableTemplateCompiler(file.language, file, element, editor).compile(used.text).let { + output.append(it) + } + } + + else -> { + logger.warn("Unknown [cc.unitmesh.devti.language.psi.ShireUsed] type: ${firstChild.elementType}") + output.append(used.text) + } + } + } + + private fun processingCommand(commandNode: BuiltinCommand, prop: String, used: ShireUsed, fallbackText: String) { + val command: InsCommand = when (commandNode) { + BuiltinCommand.FILE -> { + FileInsCommand(myProject, prop) + } + + BuiltinCommand.REV -> { + RevInsCommand(myProject, prop) + } + + BuiltinCommand.SYMBOL -> { + result.isLocalCommand = true + SymbolInsCommand(myProject, prop) + } + + BuiltinCommand.WRITE -> { + result.isLocalCommand = true + val devInCode: CodeBlockElement? = lookupNextCode(used) + if (devInCode == null) { + PrintInsCommand("/" + commandNode.commandName + ":" + prop) + } else { + WriteInsCommand(myProject, prop, devInCode.text, used) + } + } + + BuiltinCommand.PATCH -> { + result.isLocalCommand = true + val devInCode: CodeBlockElement? = lookupNextCode(used) + if (devInCode == null) { + PrintInsCommand("/" + commandNode.commandName + ":" + prop) + } else { + PatchInsCommand(myProject, prop, devInCode.text) + } + } + + BuiltinCommand.COMMIT -> { + result.isLocalCommand = true + val devInCode: CodeBlockElement? = lookupNextCode(used) + if (devInCode == null) { + PrintInsCommand("/" + commandNode.commandName + ":" + prop) + } else { + CommitInsCommand(myProject, devInCode.text) + } + } + + BuiltinCommand.RUN -> { + result.isLocalCommand = true + RunInsCommand(myProject, prop) + } + + BuiltinCommand.FILE_FUNC -> { + result.isLocalCommand = true + FileFuncInsCommand(myProject, prop) + } + + BuiltinCommand.SHELL -> { + result.isLocalCommand = true + ShellInsCommand(myProject, prop) + } + + BuiltinCommand.BROWSE -> { + result.isLocalCommand = true + BrowseInsCommand(myProject, prop) + } + + BuiltinCommand.Refactor -> { + result.isLocalCommand = true + val nextTextSegment = lookupNextTextSegment(used) + RefactorInsCommand(myProject, prop, nextTextSegment) + } + + else -> { + PrintInsCommand("/" + commandNode.commandName + ":" + prop) + } + } + + val execResult = runBlocking { command.execute() } + + val isSucceed = execResult?.contains("$SHIRE_ERROR") == false + val result = if (isSucceed) { + val hasReadCodeBlock = commandNode in listOf( + BuiltinCommand.WRITE, + BuiltinCommand.PATCH, + BuiltinCommand.COMMIT + ) + + if (hasReadCodeBlock) { + skipNextCode = true + } + + execResult + } else { + execResult ?: fallbackText + } + + output.append(result) + } + + private fun lookupNextCode(used: ShireUsed): CodeBlockElement? { + val devInCode: CodeBlockElement? + var next: PsiElement? = used + while (true) { + next = next?.nextSibling + if (next == null) { + devInCode = null + break + } + + if (next.elementType == ShireTypes.CODE) { + devInCode = next as CodeBlockElement + break + } + } + + return devInCode + } + + private fun lookupNextTextSegment(used: ShireUsed): String { + val textSegment: StringBuilder = StringBuilder() + var next: PsiElement? = used + while (true) { + next = next?.nextSibling + if (next == null) { + break + } + + if (next.elementType == ShireTypes.TEXT_SEGMENT) { + textSegment.append(next.text) + break + } + } + + return textSegment.toString() + } +} + + diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/ShireCompiledResult.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/ShireCompiledResult.kt new file mode 100644 index 000000000..6d5013116 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/ShireCompiledResult.kt @@ -0,0 +1,23 @@ +package com.phodal.shirelang.compiler + +import com.phodal.shirelang.psi.ShireFile + +data class ShireCompiledResult( + /** + * The origin DevIns content + */ + var input: String = "", + /** + * Output String of a compile result + */ + var output: String = "", + var isLocalCommand: Boolean = false, + var hasError: Boolean = false, +// var executeAgent: CustomAgentConfig? = null, + var executeAgent: Any? = null, + /** + * Next job to be executed + */ + var nextJob: ShireFile? = null +) { +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/error/DevInError.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/error/DevInError.kt new file mode 100644 index 000000000..17ff0691c --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/error/DevInError.kt @@ -0,0 +1,3 @@ +package com.phodal.shirelang.compiler.error + +val SHIRE_ERROR = "" \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/CommitInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/CommitInsCommand.kt new file mode 100644 index 000000000..11ef74262 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/CommitInsCommand.kt @@ -0,0 +1,19 @@ +package com.phodal.shirelang.compiler.exec + +import com.intellij.openapi.project.Project +import com.phodal.shirelang.utils.Code + +class CommitInsCommand(val myProject: Project, val code: String) : InsCommand { + override suspend fun execute(): String { + val commitMsg = Code.parse(code).text + +// val changeListManager = ChangeListManager.getInstance(myProject) +// changeListManager.changeLists.forEach { +// val list: LocalChangeList = changeListManager.getChangeList(it.id) ?: return@forEach +// GitUtil.doCommit(myProject, list, commitMsg) + throw NotImplementedError("Not implemented") +// } + +// return "Committing..." + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileFuncInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileFuncInsCommand.kt new file mode 100644 index 000000000..6a2c95a54 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileFuncInsCommand.kt @@ -0,0 +1,67 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.phodal.shirelang.completion.dataprovider.FileFunc +import com.phodal.shirelang.utils.canBeAdded + +class FileFuncInsCommand(val myProject: Project, val prop: String) : InsCommand { + override suspend fun execute(): String? { + val (functionName, args) = parseRegex(prop) + ?: return """$SHIRE_ERROR: file-func is not in the format @file-func:(, , ...) + |Example: @file-func:regex(".*\.kt") + """.trimMargin() + + val fileFunction = FileFunc.fromString(functionName) ?: return "$SHIRE_ERROR: Unknown function: $functionName" + when (fileFunction) { + FileFunc.Regex -> { + try { + val regex = Regex(args[0]) + return regexFunction(regex, myProject).joinToString(", ") + } catch (e: Exception) { + return SHIRE_ERROR + ": ${e.message}" + } + } + } + } + + private fun regexFunction(regex: Regex, project: Project): MutableList { + val files: MutableList = mutableListOf() + ProjectFileIndex.getInstance(project).iterateContent { + if (it.canBeAdded()) { + if (regex.matches(it.path)) { + files.add(it) + } + } + + true + } + return files + } + + companion object { + /** + * Parses a given property string to extract the function name and its arguments. + * + * The property string is in the format (, , ...). + * + * @param prop The property string to parse. + * @return The function name and the list of arguments as a Pair object. + * @throws IllegalArgumentException if the property string has invalid regex pattern. + */ + fun parseRegex(prop: String): Pair>? { + val regexPattern = Regex("""(\w+)\(([^)]+)\)""") + val matchResult = regexPattern.find(prop) + + if (matchResult != null && matchResult.groupValues.size == 3) { + val functionName = matchResult.groupValues[1] + val args = matchResult.groupValues[2].split(',').map { it.trim() } + return Pair(functionName, args) + } else { + return null + } + } + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileInsCommand.kt new file mode 100644 index 000000000..83ad990a0 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/FileInsCommand.kt @@ -0,0 +1,60 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.model.LineInfo +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiManager +import com.phodal.shirelang.utils.lookupFile + +/** + * FileAutoCommand is responsible for reading a file and returning its contents. + * + * @param myProject the Project in which the file operations are performed + * @param prop the property string containing the file name and optional line range + * + */ +class FileInsCommand(private val myProject: Project, private val prop: String) : InsCommand { + private val logger = logger() + private val output = StringBuilder() + + override suspend fun execute(): String? { + val range: LineInfo? = LineInfo.fromString(prop) + + // prop name can be src/file.name#L1-L2 + val filename = prop.split("#")[0] + val virtualFile = myProject.lookupFile(filename) + + val contentsToByteArray = virtualFile?.contentsToByteArray() + if (contentsToByteArray == null) { + logger.warn("File not found: $virtualFile") + return null + } + + contentsToByteArray.let { bytes -> + val lang = virtualFile.let { + PsiManager.getInstance(myProject).findFile(it)?.language?.displayName + } ?: "" + + val content = bytes.toString(Charsets.UTF_8) + val fileContent = if (range != null) { + val subContent = try { + content.split("\n").slice(range.startLine - 1 until range.endLine) + .joinToString("\n") + } catch (e: StringIndexOutOfBoundsException) { + content + } + + subContent + } else { + content + } + + output.append("\n```$lang\n") + output.append(fileContent) + output.append("\n```\n") + } + + return output.toString() + } +} + diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/InsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/InsCommand.kt new file mode 100644 index 000000000..d87a001d8 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/InsCommand.kt @@ -0,0 +1,6 @@ +package com.phodal.shirelang.compiler.exec + +interface InsCommand { + suspend fun execute(): String? +} + diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PatchInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PatchInsCommand.kt new file mode 100644 index 000000000..1514c607d --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PatchInsCommand.kt @@ -0,0 +1,42 @@ +package com.phodal.shirelang.compiler.exec + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diff.impl.patch.FilePatch +import com.intellij.openapi.diff.impl.patch.PatchReader +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.changes.patch.AbstractFilePatchInProgress +import com.intellij.openapi.vcs.changes.patch.ApplyPatchDefaultExecutor +import com.intellij.openapi.vcs.changes.patch.MatchPatchPaths +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.containers.MultiMap +import com.phodal.shirelang.compiler.exec.InsCommand + +class PatchInsCommand(val myProject: Project, val prop: String, val codeContent: String) : InsCommand { + override suspend fun execute(): String? { + FileDocumentManager.getInstance().saveAllDocuments() + + val shelfExecutor = ApplyPatchDefaultExecutor(myProject) + + val myReader = PatchReader(codeContent) + myReader.parseAllPatches() + + val filePatches: MutableList = myReader.allPatches + + ApplicationManager.getApplication().invokeLater { + val matchedPatches = + MatchPatchPaths(myProject).execute(filePatches, true) + + val patchGroups = MultiMap>() + for (patchInProgress in matchedPatches) { + patchGroups.putValue(patchInProgress.base, patchInProgress) + } + + val additionalInfo = myReader.getAdditionalInfo(ApplyPatchDefaultExecutor.pathsFromGroups(patchGroups)) + shelfExecutor.apply(filePatches, patchGroups, null, prop, additionalInfo) + } + + return "Patch in Progress..." + } + +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PrintInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PrintInsCommand.kt new file mode 100644 index 000000000..6dbfedf7a --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/PrintInsCommand.kt @@ -0,0 +1,9 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.exec.InsCommand + +class PrintInsCommand(private val value: String) : InsCommand { + override suspend fun execute(): String { + return value + } +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RefactorInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RefactorInsCommand.kt new file mode 100644 index 000000000..71fc3ba84 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RefactorInsCommand.kt @@ -0,0 +1,97 @@ +package com.phodal.shirelang.compiler.exec + +import com.intellij.lang.Language +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.phodal.shirelang.completion.dataprovider.BuiltinRefactorCommand +import com.phodal.shirelang.provider.RefactoringTool +import com.phodal.shirelang.psi.ShireFile + +/** + * `RefactorInsCommand` is a class that implements the `InsCommand` interface. It is responsible for executing + * refactoring commands within a project based on the provided argument and text segment. + * + * The class has three private properties: + * - `myProject`: A `Project` instance representing the current project. + * - `argument`: A `String` containing the refactoring command to be executed. + * - `textSegment`: A `String` containing the text segment relevant to the refactoring command. + * + * The `execute` method is the main entry point for executing a refactoring command. It first attempts to parse the + * `argument` into a `BuiltinRefactorCommand` using the `fromString` method. If the command is not recognized, a + * message indicating that it is unknown is returned. + * + * Depending on the type of refactoring command, the `execute` method performs different actions: + * - For `BuiltinRefactorCommand.RENAME`: The method splits the `textSegment` using " to " and assigns the result to + * the variables `from` and `to`. It then performs a rename operation on a class in Java or Kotlin. The actual + * implementation of the rename operation is not provided in the code snippet, but it suggests using `RenameQuickFix`. + * @see com.intellij.jvm.analysis.quickFix.RenameQuickFix for kotlin + * @see com.intellij.spellchecker.quickfixes.RenameTo for by typos rename which will be better + * - For `BuiltinRefactorCommand.SAFEDELETE`: The method checks the usage of the symbol before deleting it. It + * suggests using `SafeDeleteFix` as an example. + * @see org.jetbrains.kotlin.idea.inspections.SafeDeleteFix for Kotlin + * @see com.intellij.codeInsight.daemon.impl.quickfix.SafeDeleteFix for Java + * - For `BuiltinRefactorCommand.DELETE`: The method does not perform any specific action, but it is expected to be + * implemented to handle the deletion of elements. + * @see com.intellij.codeInspection.LocalQuickFixOnPsiElement + * - For `BuiltinRefactorCommand.MOVE`: The method suggests using ` as an example for moving elements move package fix to a different package. + * @see com.intellij.codeInspection.MoveToPackageFix + * + * + * The `execute` method always returns `null`, indicating that the refactoring command has been executed, but the + * actual result of the refactoring is not returned. + * + * This class is designed to be used within a refactoring tool or plugin that provides built-in refactoring commands. + * It demonstrates how to handle different refactoring scenarios*/ +class RefactorInsCommand(val myProject: Project, private val argument: String, private val textSegment: String) : + InsCommand { + override suspend fun execute(): String? { + var currentEditFile: PsiFile? = null + val editor = FileEditorManager.getInstance(myProject).selectedTextEditor + if (editor != null) { + val currentFile = FileDocumentManager.getInstance().getFile(editor.document) ?: return "File not found" + val currentPsiFile = PsiManager.getInstance(myProject).findFile(currentFile) + + // will not handle the case where the current file is not a ShireFile + currentEditFile = if (currentPsiFile is ShireFile) { + null + } else { + currentPsiFile + } + } + + val language = currentEditFile?.language ?: Language.findLanguageByID("JAVA") ?: return "Language not found" + val refactoringTool = RefactoringTool.forLanguage(language) + ?: return "Refactoring tool not found for Java" + + val command = BuiltinRefactorCommand.fromString(argument) ?: return "Unknown refactor command: $argument" + + when (command) { + BuiltinRefactorCommand.RENAME -> { + val (from, to) = textSegment.split(" to ") + refactoringTool.rename(from.trim(), to.trim(), currentEditFile) + } + + BuiltinRefactorCommand.SAFE_DELETE -> { + val psiFile = refactoringTool.lookupFile(textSegment.trim()) ?: return "File not found" + refactoringTool.safeDelete(psiFile) + } + + BuiltinRefactorCommand.DELETE -> { + val psiFile = refactoringTool.lookupFile(textSegment.trim()) ?: return "File not found" + refactoringTool.safeDelete(psiFile) + } + + BuiltinRefactorCommand.MOVE -> { + val (from, to) = textSegment.split(" to ") + val psiFile = refactoringTool.lookupFile(from.trim()) ?: return "File not found" + refactoringTool.move(psiFile, to.trim()) + } + } + + return null + } +} + diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RevInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RevInsCommand.kt new file mode 100644 index 000000000..01a156378 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RevInsCommand.kt @@ -0,0 +1,52 @@ +package com.phodal.shirelang.compiler.exec + +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.changes.Change +import com.phodal.shirelang.ShireBundle +import git4idea.GitRevisionNumber +import git4idea.changes.GitCommittedChangeListProvider +import git4idea.repo.GitRepositoryManager +import kotlinx.coroutines.future.await +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CompletableFuture + + +/** + * RevAutoCommand is used to execute a command that retrieves the committed change list for a given revision using Git. + * + * @param myProject the Project instance associated with the command + * @param revision the Git revision for which the committed change list is to be retrieved + * + */ +class RevInsCommand(private val myProject: Project, private val revision: String) : InsCommand { + override suspend fun execute(): String? { + val repository = GitRepositoryManager.getInstance(myProject).repositories.firstOrNull() ?: return null + val future = CompletableFuture>() + + val task = object : Task.Backgroundable(myProject, ShireBundle.message("devin.ref.loading"), false) { + override fun run(indicator: ProgressIndicator) { + val committedChangeList = GitCommittedChangeListProvider.getCommittedChangeList( + myProject!!, repository.root, GitRevisionNumber(revision) + )?.changes?.toList() + + future.complete(committedChangeList) + } + } + + ProgressManager.getInstance() + .runProcessWithProgressAsynchronously(task, BackgroundableProcessIndicator(task)) + + + return runBlocking { +// val changes = future.await() +// val diffContext = myProject.service().prepareContext(changes) +// "\n```diff\n${diffContext}\n```\n" + throw NotImplementedError() + } + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RunInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RunInsCommand.kt new file mode 100644 index 000000000..19dd2ffda --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/RunInsCommand.kt @@ -0,0 +1,28 @@ +package com.phodal.shirelang.compiler.exec + +import com.intellij.openapi.project.Project +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.phodal.shirelang.utils.lookupFile + +/** + * The `RunAutoCommand` class is responsible for executing an auto command on a given project. + * + * @property myProject The project to execute the auto command on. + * @property argument The name of the file to find and run tests for. + * + */ +class RunInsCommand(val myProject: Project, private val argument: String) : InsCommand { + override suspend fun execute(): String? { + val virtualFile = myProject.lookupFile(argument.trim()) ?: return "$SHIRE_ERROR: File not found: $argument" + try { +// val psiFile: PsiFile = +// PsiManager.getInstance(myProject).findFile(virtualFile) ?: return "$SHIRE_ERROR: File not found: $argument" +// val testService = +// AutoTestService.context(psiFile) ?: return "$SHIRE_ERROR: No test service found for file: $argument" +// testService.runFile(myProject, virtualFile, null) + throw NotImplementedError() + } catch (e: Exception) { + return "$SHIRE_ERROR: ${e.message}" + } + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/ShellInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/ShellInsCommand.kt new file mode 100644 index 000000000..59ef54339 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/ShellInsCommand.kt @@ -0,0 +1,44 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiManager +import com.intellij.sh.psi.ShFile +import com.intellij.sh.run.ShRunner +import com.phodal.shirelang.utils.lookupFile + +/** + * A class that implements the `InsCommand` interface to execute a shell command within the IntelliJ IDEA environment. + * + * This class is designed to run a shell command specified by a given `prop` string, which is assumed to be the path to a file within the project. + * The command is executed in a shell runner service provided by IntelliJ IDEA, using the specified file's path and its parent directory as the working directory. + * + * @param myProject The current project context. + * @param argument The path to the file within the project whose content should be executed as a shell command. + */ +class ShellInsCommand(val myProject: Project, private val argument: String) : InsCommand { + override suspend fun execute(): String? { + val virtualFile = myProject.lookupFile(argument.trim()) ?: return "$SHIRE_ERROR: File not found: $argument" + val psiFile = PsiManager.getInstance(myProject).findFile(virtualFile) as? ShFile +// val settings: RunnerAndConfigurationSettings? = ShellRunService().createRunSettings(myProject, virtualFile, psiFile) + +// if (settings != null) { +// ShellRunService().runFile(myProject, virtualFile, psiFile) +// return "Running shell file: $argument" +// } + + + val workingDirectory = virtualFile.parent.path + val shRunner = ApplicationManager.getApplication().getService(ShRunner::class.java) + ?: return "$SHIRE_ERROR: Shell runner not found" + + if (shRunner.isAvailable(myProject)) { + shRunner.run(myProject, virtualFile.path, workingDirectory, "RunDevInsShell", true) + } + + throw NotImplementedError() + + return "Running shell command: $argument" + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/SymbolInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/SymbolInsCommand.kt new file mode 100644 index 000000000..296dc76ae --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/SymbolInsCommand.kt @@ -0,0 +1,22 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.intellij.openapi.project.Project +import com.phodal.shirelang.provider.ShireSymbolProvider + +class SymbolInsCommand(val myProject: Project, val prop: String) : + InsCommand { + override suspend fun execute(): String { + val result = ShireSymbolProvider.all().mapNotNull { + val found = it.resolveSymbol(myProject, prop) + if (found.isEmpty()) return@mapNotNull null + "```${it.language}\n${found.joinToString("\n")}\n```\n" + } + + if (result.isEmpty()) { + return "$SHIRE_ERROR No symbol found: $prop" + } + + return result.joinToString("\n") + } +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/WriteInsCommand.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/WriteInsCommand.kt new file mode 100644 index 000000000..8a5a62f50 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/exec/WriteInsCommand.kt @@ -0,0 +1,112 @@ +package com.phodal.shirelang.compiler.exec + +import com.phodal.shirelang.compiler.error.SHIRE_ERROR +import com.phodal.shirelang.compiler.model.LineInfo +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.phodal.shirelang.psi.ShireUsed +import com.phodal.shirelang.utils.Code +import com.phodal.shirelang.utils.lookupFile + +class WriteInsCommand(val myProject: Project, val argument: String, val content: String, val used: ShireUsed) : + InsCommand { + private val pathSeparator = "/" + + override suspend fun execute(): String? { + val content = Code.parse(content).text + + val range: LineInfo? = LineInfo.fromString(used.text) + val filepath = argument.split("#")[0] + val projectDir = myProject.guessProjectDir() ?: return "$SHIRE_ERROR: Project directory not found" + + val virtualFile = runReadAction { myProject.lookupFile(filepath) } + if (virtualFile == null) { + // filepath maybe just a file name, so we need to create parent directory + val hasChildPath = filepath.contains(pathSeparator) + if (hasChildPath) { + val parentPath = filepath.substringBeforeLast(pathSeparator) + var parentDir = projectDir.findChild(parentPath) + if (parentDir == null) { + // parentDir maybe multiple level, so we need to create all parent directory + val parentDirs = parentPath.split(pathSeparator) + parentDir = projectDir + for (dir in parentDirs) { + if (dir.isEmpty()) continue + + //check child folder if exist? if not, create it + if (parentDir?.findChild(dir) == null) { + parentDir = runWriteAction { parentDir?.createChildDirectory(this, dir) } + } else { + parentDir = parentDir?.findChild(dir) + } + } + + if (parentDir == null) { + return "$SHIRE_ERROR: Create Directory failed: $parentPath" + } + } + + return runWriteAction { + createNewContent(parentDir, filepath, content) + ?: return@runWriteAction "$SHIRE_ERROR: Create File failed: $argument" + + return@runWriteAction "Create file: $argument" + } + } else { + return runWriteAction { + createNewContent(projectDir, filepath, content) + ?: return@runWriteAction "$SHIRE_ERROR: Create File failed: $argument" + + return@runWriteAction "Create file: $argument" + } + } + } + + val psiFile = PsiManager.getInstance(myProject).findFile(virtualFile) + ?: return "$SHIRE_ERROR: File not found: $argument" + + return executeInsert(psiFile, range, content) + } + + private fun createNewContent(parentDir: VirtualFile, filepath: String, content: String): Document? { + val newFile = parentDir.createChildData(this, filepath.substringAfterLast(pathSeparator)) + val document = FileDocumentManager.getInstance().getDocument(newFile) ?: return null + + document.setText(content) + return document + } + + private fun executeInsert( + psiFile: PsiFile, + range: LineInfo?, + content: String + ): String { + val document = runReadAction { + PsiDocumentManager.getInstance(myProject).getDocument(psiFile) + } ?: return "$SHIRE_ERROR: File not found: $argument" + + val startLine = range?.startLine ?: 0 + val endLine = if (document.lineCount == 0) 1 else range?.endLine ?: document.lineCount + + try { + val startOffset = document.getLineStartOffset(startLine) + val endOffset = document.getLineEndOffset(endLine - 1) + WriteCommandAction.runWriteCommandAction(myProject) { + document.replaceString(startOffset, endOffset, content) + } + + return "Writing to file: $argument" + } catch (e: Exception) { + return "$SHIRE_ERROR: ${e.message}" + } + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/model/LineInfo.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/model/LineInfo.kt new file mode 100644 index 000000000..b81031750 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/model/LineInfo.kt @@ -0,0 +1,31 @@ +package com.phodal.shirelang.compiler.model + +data class LineInfo( + val startLine: Int, + val endLine: Int, + val startColumn: Int = 0, + val endColumn: Int = 0 +) { + companion object { + private val regex = Regex("""L(\d+)(?:C(\d+))?(?:-L(\d+)(?:C(\d+))?)?""") + + /** + * Convert a string to a `TextRange`, if possible. The string should be in the format: "filepath#L1-L12", + * where "filepath" is the path to the file, "#" is a hash symbol, "L1-L12" is a range of lines from line 1 to line 12. + * + * @param string The string to convert, in the format "filepath#L1-L12". + * @return A `LineInfo` object representing the range of lines, or `null` if the string is not in the correct format. + */ + fun fromString(input: String): LineInfo? { + val matchResult = regex.find(input) ?: return null + + val startLine = matchResult.groupValues[1].toIntOrNull() ?: return null + val startColumn = matchResult.groupValues[2].toIntOrNull() ?: 0 + val endLine = matchResult.groupValues[3].toIntOrNull() ?: return null + val endColumn = matchResult.groupValues[4].toIntOrNull() ?: 0 + + return LineInfo(startLine, endLine, startColumn, endColumn) + } + } + +} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compiler/service/ShellRunService.kt b/language/src/main/kotlin/com/phodal/shirelang/compiler/service/ShellRunService.kt new file mode 100644 index 000000000..7608c1f4c --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/compiler/service/ShellRunService.kt @@ -0,0 +1,24 @@ +package com.phodal.shirelang.compiler.service + +import com.intellij.execution.RunManager +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.execution.configurations.RunProfile +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiManager +import com.intellij.sh.psi.ShFile +import com.intellij.sh.run.ShConfigurationType +import com.intellij.sh.run.ShRunConfiguration + +//class ShellRunService : RunService { +// override fun runConfigurationClass(project: Project): Class = ShRunConfiguration::class.java +// override fun createConfiguration(project: Project, virtualFile: VirtualFile): RunConfiguration? { +// val psiFile = PsiManager.getInstance(project).findFile(virtualFile) as? ShFile ?: return null +// val configurationSetting = RunManager.getInstance(project) +// .createConfiguration(psiFile.name, ShConfigurationType.getInstance()) +// +// val configuration = configurationSetting.configuration as ShRunConfiguration +// configuration.scriptPath = virtualFile.path +// return configurationSetting.configuration +// } +//} \ No newline at end of file diff --git a/language/src/main/kotlin/com/phodal/shirelang/compile/CustomVariable.kt b/language/src/main/kotlin/com/phodal/shirelang/completion/provider/CustomVariable.kt similarity index 90% rename from language/src/main/kotlin/com/phodal/shirelang/compile/CustomVariable.kt rename to language/src/main/kotlin/com/phodal/shirelang/completion/provider/CustomVariable.kt index 99316e731..a00bd3881 100644 --- a/language/src/main/kotlin/com/phodal/shirelang/compile/CustomVariable.kt +++ b/language/src/main/kotlin/com/phodal/shirelang/completion/provider/CustomVariable.kt @@ -1,4 +1,6 @@ -package com.phodal.shirelang.compile +package com.phodal.shirelang.completion.provider + +import com.phodal.shirelang.compile.VariableTemplateCompiler enum class CustomVariable(val variable: String, val description: String) { SELECTION("selection", "The selected text"), diff --git a/language/src/main/kotlin/com/phodal/shirelang/completion/provider/VariableCompletionProvider.kt b/language/src/main/kotlin/com/phodal/shirelang/completion/provider/VariableCompletionProvider.kt index 55e827192..1691af084 100644 --- a/language/src/main/kotlin/com/phodal/shirelang/completion/provider/VariableCompletionProvider.kt +++ b/language/src/main/kotlin/com/phodal/shirelang/completion/provider/VariableCompletionProvider.kt @@ -6,7 +6,6 @@ import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.PrioritizedLookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.util.ProcessingContext -import com.phodal.shirelang.compile.CustomVariable class VariableCompletionProvider : CompletionProvider() { override fun addCompletions( diff --git a/language/src/main/kotlin/com/phodal/shirelang/provider/AgentTool.kt b/language/src/main/kotlin/com/phodal/shirelang/provider/AgentTool.kt new file mode 100644 index 000000000..b1269b605 --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/provider/AgentTool.kt @@ -0,0 +1,20 @@ +package com.phodal.shirelang.provider + +import com.intellij.openapi.extensions.ExtensionPointName +import com.phodal.shirelang.agenttool.AgentToolContext +import com.phodal.shirelang.agenttool.AgentToolResult + +interface AgentTool { + val name: String + val description: String + fun execute(context: AgentToolContext): AgentToolResult + + // extension point + companion object { + val EP_NAME = ExtensionPointName("com.phodal.shireAgentTool") + + fun allTools(): List { + return EP_NAME.extensionList + } + } +} diff --git a/language/src/main/kotlin/com/phodal/shirelang/provider/RefactoringTool.kt b/language/src/main/kotlin/com/phodal/shirelang/provider/RefactoringTool.kt new file mode 100644 index 000000000..51bc65aec --- /dev/null +++ b/language/src/main/kotlin/com/phodal/shirelang/provider/RefactoringTool.kt @@ -0,0 +1,174 @@ +package com.phodal.shirelang.provider + +//import com.intellij.codeInsight.daemon.impl.quickfix.SafeDeleteFix +//import com.intellij.codeInspection.MoveToPackageFix +import com.intellij.lang.Language +import com.intellij.lang.LanguageExtension +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.PsiUtilCore +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.listeners.RefactoringElementListener +import com.intellij.refactoring.rename.RenameProcessor +import com.intellij.refactoring.rename.RenameUtil +import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory +import com.intellij.refactoring.util.CommonRefactoringUtil +import com.intellij.usageView.UsageInfo +import com.intellij.util.ThrowableRunnable + +/** + * RefactoringTool is an interface that defines operations for performing various code refactoring tasks. + * It provides functionality to work with PsiFiles and PsiElements, such as looking up files, renaming, + * safely deleting elements, and moving elements or packages to a new location. + */ +interface RefactoringTool { + /** + * Looks up and retrieves a PsiFile from the given file path. + * + * @param path The path of the file to be looked up. It should be a valid file path within the project. + * @return A PsiFile object if the file is found and successfully loaded, or null if the file doesn't exist or cannot be loaded. + */ + fun lookupFile(path: String): PsiFile? + + /** + * Renames a given source name to a target name within the provided PSI file. + * + * @param sourceName The original name to be renamed. + * @param targetName The new name to replace the original name with. + * @param psiFile The PSI file where the renaming will take place; can be null if not applicable. + * @return A boolean value indicating whether the renaming was successful. Returns true if the + * renaming was successful, false otherwise. + */ + fun rename(sourceName: String, targetName: String, psiFile: PsiFile?): Boolean + + /** + * Deletes the given PsiElement in a safe manner, ensuring that no syntax errors or unexpected behavior occur as a result. + * The method performs checks before deletion to confirm that it is safe to remove the element from the code structure. + * + * @param element The PsiElement to be deleted. This should be a valid element within the PSI tree structure. + * @return true if the element was successfully deleted without any issues, false otherwise. This indicates whether + * the deletion was performed and considered safe. + */ + fun safeDelete(element: PsiElement): Boolean { +// val delete = SafeDeleteFix(element) +// try { +// delete.invoke(element.project, element.containingFile, element, element) +// } catch (e: Exception) { +// return false +// } + + return true + } + + /** + * Performs a refactoring rename operation on a given PSI element within the specified project. + * The method iterates through available renamer factories to find the appropriate one for the + * element to be renamed. It then collects usages of the element, checks read-only status, + * and performs the rename operation on all found usages, including those in comments if applicable. + * + * @param myProject The project in which the refactoring operation is taking place. + * @param elementToRename The PSI element (which must implement PsiNamedElement) that is to be renamed. + * @param newName The new name to assign to the element and its usages. + * + * Note: The method uses ProgressManager to search for usages and may prompt the user for read-only + * file access. It also uses ApplicationManager to schedule the rename operation on the EDT. + */ + fun performRefactoringRename(myProject: Project, elementToRename: PsiNamedElement, newName: String) { + for (renamerFactory in AutomaticRenamerFactory.EP_NAME.extensionList) { + if (!renamerFactory.isApplicable(elementToRename)) continue + val usages: List = ArrayList() + val renamer = renamerFactory.createRenamer(elementToRename, newName, ArrayList()) + if (!renamer.hasAnythingToRename()) continue + + val runnable = Runnable { + ApplicationManager.getApplication().runReadAction { + renamer.findUsages(usages, false, false) + } + } + + if (!ProgressManager.getInstance().runProcessWithProgressSynchronously( + runnable, RefactoringBundle.message("searching.for.variables"), true, myProject + ) + ) { + return + } + + if (!CommonRefactoringUtil.checkReadOnlyStatus( + myProject, + *PsiUtilCore.toPsiElementArray(renamer.elements) + ) + ) return + + val performAutomaticRename = ThrowableRunnable { + CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject) + val classified = RenameProcessor.classifyUsages(renamer.elements, usages) + for (element in renamer.elements) { + val newElementName = renamer.getNewName(element) + if (newElementName != null) { + val infos = classified[element] + RenameUtil.doRename( + element, + newElementName, + infos.toTypedArray(), + myProject, + RefactoringElementListener.DEAF + ) + } + } + } + + ApplicationManager.getApplication().invokeLater { + WriteCommandAction.writeCommandAction(myProject) + .withName("Rename").run(performAutomaticRename) + } + } + } + + /** + * In Java the canonicalName is the fully qualified name of the target package. + * In Kotlin the canonicalName is the fully qualified name of the target package or class. + */ + fun move(element: PsiElement, canonicalName: String): Boolean { +// val file = element.containingFile +// val fix = MoveToPackageFix(file, canonicalName) +// +// try { +// fix.invoke(file.project, file, element, element) +// } catch (e: Exception) { +// return false +// } + + return true + } + + companion object { + private val languageExtension: LanguageExtension = + LanguageExtension("com.phodal.shireRefactoringTool") + + fun forLanguage(language: Language): RefactoringTool? { + val refactoringTool = languageExtension.forLanguage(language) + if (refactoringTool != null) { + return refactoringTool + } + + // If no refactoring tool is found for the specified language, return java + val javaLanguage = Language.findLanguageByID("JAVA") ?: return null + return languageExtension.forLanguage(javaLanguage) + } + } +} + +data class RefactorInstElement( + val isClass: Boolean, + val isMethod: Boolean, + val methodName: String, + val canonicalName: String, + val className: String, + val pkgName: String +) diff --git a/language/src/main/resources/com.phodal.shirelang.xml b/language/src/main/resources/com.phodal.shirelang.xml index e95d4b286..edde4e4b3 100644 --- a/language/src/main/resources/com.phodal.shirelang.xml +++ b/language/src/main/resources/com.phodal.shirelang.xml @@ -1,4 +1,6 @@ + com.intellij.modules.platform + @@ -9,6 +11,15 @@ + + + + + + @@ -42,6 +53,7 @@ +