Skip to content

Commit 796fea1

Browse files
committed
feat(shire): enhance file handling utilities and streamline editor interactions #379
1 parent 1c145c2 commit 796fea1

File tree

16 files changed

+258
-57
lines changed

16 files changed

+258
-57
lines changed

core/src/main/kotlin/cc/unitmesh/devti/sketch/SketchInputListener.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@ import cc.unitmesh.devti.template.TemplateRender
1515
import cc.unitmesh.devti.util.AutoDevCoroutineScope
1616
import com.intellij.openapi.Disposable
1717
import com.intellij.openapi.application.ApplicationManager
18-
import com.intellij.openapi.application.invokeLater
19-
import com.intellij.openapi.application.runReadAction
2018
import com.intellij.openapi.diagnostic.logger
21-
import com.intellij.openapi.progress.ProgressManager
2219
import com.intellij.openapi.project.Project
23-
import kotlinx.coroutines.cancel
2420
import kotlinx.coroutines.flow.cancellable
2521
import kotlinx.coroutines.launch
26-
import kotlinx.coroutines.runBlocking
2722

2823
open class SketchInputListener(
2924
private val project: Project,

core/src/main/kotlin/cc/unitmesh/devti/util/DirUtil.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,3 @@ object DirUtil {
1818
}
1919

2020
}
21-
22-
val VirtualFile.isFile: Boolean get() = isValid && !isDirectory

core/src/main/kotlin/cc/unitmesh/devti/util/ProjectFileUtil.kt

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,26 @@
22
package cc.unitmesh.devti.util
33

44
import com.intellij.openapi.application.runReadAction
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.openapi.fileEditor.FileDocumentManager
57
import com.intellij.openapi.fileTypes.FileTypeManager
68
import com.intellij.openapi.project.Project
79
import com.intellij.openapi.project.guessProjectDir
810
import com.intellij.openapi.roots.ProjectFileIndex
911
import com.intellij.openapi.util.io.FileUtil
1012
import com.intellij.openapi.util.io.FileUtilRt
1113
import com.intellij.openapi.vcs.changes.VcsIgnoreManager
14+
import com.intellij.openapi.vfs.VfsUtilCore
1215
import com.intellij.openapi.vfs.VirtualFile
1316
import com.intellij.openapi.vfs.VirtualFileManager
1417
import com.intellij.psi.PsiManager
18+
import com.intellij.util.concurrency.annotations.RequiresReadLock
19+
import org.jetbrains.annotations.Contract
20+
import org.jetbrains.annotations.SystemIndependent
21+
import java.io.IOException
22+
import java.nio.file.Path
23+
import java.nio.file.Paths
24+
import kotlin.io.path.pathString
1525

1626
// https://github.com/JetBrains/intellij-community/blob/master/platform/projectModel-impl/src/com/intellij/openapi/roots/impl/ProjectFileIndexImpl.java#L32
1727
fun isInProject(virtualFile: VirtualFile, project: Project): Boolean {
@@ -50,4 +60,104 @@ fun VirtualFile.relativePath(project: Project): String {
5060
fun isIgnoredByVcs(project: Project?, file: VirtualFile?): Boolean {
5161
val ignoreManager = VcsIgnoreManager.getInstance(project!!)
5262
return ignoreManager.isPotentiallyIgnoredFile(file!!)
53-
}
63+
}
64+
65+
fun virtualFile(editor: Editor?): VirtualFile? {
66+
if (editor == null) return null
67+
return FileDocumentManager.getInstance().getFile(editor.document)
68+
}
69+
70+
fun VirtualFile.validOrNull() = if (isValid) this else null
71+
72+
val VirtualFile.isFile: Boolean
73+
get() = isValid && !isDirectory
74+
75+
fun VirtualFile.readText(): String {
76+
return VfsUtilCore.loadText(this)
77+
}
78+
79+
@RequiresReadLock
80+
fun VirtualFile.findFileOrDirectory(relativePath: @SystemIndependent String): VirtualFile? {
81+
return getResolvedVirtualFile(relativePath) { name, _ ->
82+
findChild(name) ?: return null // return from findFileOrDirectory
83+
}
84+
}
85+
86+
@RequiresReadLock
87+
fun VirtualFile.findFile(relativePath: @SystemIndependent String): VirtualFile? {
88+
val file = findFileOrDirectory(relativePath) ?: return null
89+
if (!file.isFile) {
90+
throw IOException("""
91+
|Expected file instead of directory: $file
92+
| basePath = $path
93+
| relativePath = $relativePath
94+
""".trimMargin())
95+
}
96+
return file
97+
}
98+
99+
private inline fun VirtualFile.getResolvedVirtualFile(
100+
relativePath: String,
101+
getChild: VirtualFile.(String, Boolean) -> VirtualFile
102+
): VirtualFile {
103+
val (baseVirtualFile, normalizedRelativePath) = relativizeToClosestAncestor(relativePath)
104+
var virtualFile = baseVirtualFile
105+
if (normalizedRelativePath.pathString.isNotEmpty()) {
106+
val names = normalizedRelativePath.map { it.pathString }
107+
for ((i, name) in names.withIndex()) {
108+
if (!virtualFile.isDirectory) {
109+
throw IOException("""
110+
|Expected directory instead of file: $virtualFile
111+
| basePath = $path
112+
| relativePath = $relativePath
113+
""".trimMargin())
114+
}
115+
virtualFile = virtualFile.getChild(name, i == names.lastIndex)
116+
}
117+
}
118+
return virtualFile
119+
}
120+
121+
fun Path.relativizeToClosestAncestor(relativePath: String): Pair<Path, Path> {
122+
val normalizedPath = getResolvedPath(relativePath)
123+
val normalizedBasePath = checkNotNull(findAncestor(this, normalizedPath)) {
124+
"""
125+
|Cannot resolve normalized base path for: $normalizedPath
126+
| basePath = $this
127+
| relativePath = $relativePath
128+
""".trimMargin()
129+
}
130+
val normalizedRelativePath = normalizedBasePath.relativize(normalizedPath)
131+
return normalizedBasePath to normalizedRelativePath
132+
}
133+
134+
@Contract(pure = true)
135+
fun findAncestor(path1: Path, path2: Path): Path? {
136+
var ancestor: Path? = path1
137+
while (ancestor != null && !path2.startsWith(ancestor)) {
138+
ancestor = ancestor.getParent()
139+
}
140+
return ancestor
141+
}
142+
143+
private fun VirtualFile.relativizeToClosestAncestor(
144+
relativePath: String
145+
): Pair<VirtualFile, Path> {
146+
val basePath = Paths.get(path)
147+
val (normalizedBasePath, normalizedRelativePath) = basePath.relativizeToClosestAncestor(relativePath)
148+
var baseVirtualFile = this
149+
repeat(basePath.nameCount - normalizedBasePath.nameCount) {
150+
baseVirtualFile = checkNotNull(baseVirtualFile.parent) {
151+
"""
152+
|Cannot resolve base virtual file for $baseVirtualFile
153+
| basePath = $path
154+
| relativePath = $relativePath
155+
""".trimMargin()
156+
}
157+
}
158+
return baseVirtualFile to normalizedRelativePath
159+
}
160+
161+
fun Path.getResolvedPath(relativePath: String): Path {
162+
return resolve(relativePath).normalize()
163+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package cc.unitmesh.devti.language.startup
2+
3+
import cc.unitmesh.devti.custom.team.InteractionType
4+
import cc.unitmesh.devti.language.DevInFileType
5+
import cc.unitmesh.devti.language.ast.HobbitHole
6+
import cc.unitmesh.devti.language.psi.DevInFile
7+
import cc.unitmesh.devti.language.startup.third.ShireSonarLintToolWindowListener
8+
import com.intellij.openapi.actionSystem.ActionManager
9+
import com.intellij.openapi.actionSystem.Constraints
10+
import com.intellij.openapi.actionSystem.DefaultActionGroup
11+
import com.intellij.openapi.application.ApplicationManager
12+
import com.intellij.openapi.application.smartReadAction
13+
import com.intellij.openapi.project.Project
14+
import com.intellij.openapi.startup.StartupActivity
15+
import com.intellij.openapi.vfs.VirtualFile
16+
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
17+
import com.intellij.psi.PsiManager
18+
import com.intellij.psi.search.FileTypeIndex
19+
import com.intellij.psi.search.ProjectScope
20+
import kotlin.collections.forEach
21+
import cc.unitmesh.devti.util.AutoDevCoroutineScope
22+
import kotlinx.coroutines.flow.cancellable
23+
import kotlinx.coroutines.launch
24+
25+
class ShireActionStartupActivity : StartupActivity {
26+
override fun runActivity(project: Project) {
27+
AutoDevCoroutineScope.scope(project).launch {
28+
bindingShireActions(project)
29+
}
30+
}
31+
32+
private suspend fun bindingShireActions(project: Project) {
33+
GlobalShireFileChangesProvider.getInstance().startup(::attachCopyPasteAction)
34+
val changesProvider = ShireFileChangesProvider.getInstance(project)
35+
smartReadAction(project) {
36+
obtainShireFiles(project).forEach {
37+
changesProvider.onUpdated(it)
38+
}
39+
40+
// changesProvider.startup { shireConfig, shireFile ->
41+
// attachCopyPasteAction(shireConfig, shireFile)
42+
// }
43+
// attachTerminalAction()
44+
// attachDatabaseAction()
45+
// attachVcsLogAction()
46+
// attachExtensionActions(project)
47+
}
48+
}
49+
50+
private fun attachCopyPasteAction(shireConfig: HobbitHole, shireFile: DevInFile) {
51+
if (shireConfig.interaction == InteractionType.OnPaste) {
52+
PasteManagerService.getInstance()
53+
.registerPasteProcessor(shireConfig, shireFile)
54+
}
55+
}
56+
57+
/**
58+
* We make terminal plugin optional, so can't add to `TerminalToolwindowActionGroup` the plugin.xml.
59+
* So we add it manually here, if terminal plugin is not enabled, this action will not be shown.
60+
*/
61+
private fun attachTerminalAction() {
62+
val actionManager = ActionManager.getInstance()
63+
val toolsMenu = actionManager.getAction("TerminalToolwindowActionGroup") as? DefaultActionGroup ?: return
64+
65+
val action = actionManager.getAction("ShireTerminalAction")
66+
if (!toolsMenu.containsAction(action)) {
67+
toolsMenu.add(action)
68+
}
69+
}
70+
71+
private fun attachDatabaseAction() {
72+
val actionManager = ActionManager.getInstance()
73+
val toolsMenu = actionManager.getAction("DatabaseViewPopupMenu") as? DefaultActionGroup ?: return
74+
75+
val action = actionManager.getAction("ShireDatabaseAction")
76+
if (!toolsMenu.containsAction(action)) {
77+
toolsMenu.add(action, Constraints.LAST)
78+
}
79+
}
80+
81+
private fun attachVcsLogAction() {
82+
val actionManager = ActionManager.getInstance()
83+
val toolsMenu = actionManager.getAction("Vcs.Log.ContextMenu") as? DefaultActionGroup ?: return
84+
85+
val action = actionManager.getAction("ShireVcsLogAction")
86+
if (!toolsMenu.containsAction(action)) {
87+
toolsMenu.add(action, Constraints.FIRST)
88+
}
89+
}
90+
91+
private fun attachExtensionActions(project: Project) {
92+
project.messageBus.connect().subscribe(ToolWindowManagerListener.TOPIC, ShireSonarLintToolWindowListener());
93+
}
94+
95+
companion object {
96+
private fun obtainShireFiles(project: Project): List<DevInFile> {
97+
ApplicationManager.getApplication().assertReadAccessAllowed()
98+
val projectShire = obtainProjectShires(project).map {
99+
PsiManager.getInstance(project).findFile(it) as DevInFile
100+
}
101+
102+
return projectShire
103+
}
104+
105+
private fun obtainProjectShires(project: Project): List<VirtualFile> {
106+
val scope = ProjectScope.getContentScope(project)
107+
val projectShire = FileTypeIndex.getFiles(DevInFileType.Companion.INSTANCE, scope).mapNotNull {
108+
it
109+
}
110+
111+
return projectShire
112+
}
113+
114+
fun findShireFile(project: Project, filename: String): DevInFile? {
115+
return DynamicShireActionService.getInstance(project).getAllActions().map {
116+
it.devinFile
117+
}.firstOrNull {
118+
it.name == filename
119+
}
120+
}
121+
}
122+
}

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/startup/ShireActionStartupActivity.kt renamed to exts/devins-lang/src/233/main/kotlin/cc/unitmesh/devti/language/startup/ShireActionStartupActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.intellij.openapi.wm.ex.ToolWindowManagerListener
1717
import com.intellij.psi.PsiManager
1818
import com.intellij.psi.search.FileTypeIndex
1919
import com.intellij.psi.search.ProjectScope
20+
import kotlin.collections.forEach
2021

2122
class ShireActionStartupActivity : ProjectActivity {
2223
override suspend fun execute(project: Project) {
@@ -98,7 +99,7 @@ class ShireActionStartupActivity : ProjectActivity {
9899

99100
private fun obtainProjectShires(project: Project): List<VirtualFile> {
100101
val scope = ProjectScope.getContentScope(project)
101-
val projectShire = FileTypeIndex.getFiles(DevInFileType.INSTANCE, scope).mapNotNull {
102+
val projectShire = FileTypeIndex.getFiles(DevInFileType.Companion.INSTANCE, scope).mapNotNull {
102103
it
103104
}
104105

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/ast/action/PatternFuncProcessor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import com.intellij.openapi.project.Project
1919
import com.intellij.openapi.project.guessProjectDir
2020
import com.intellij.openapi.vfs.LocalFileSystem
2121
import com.intellij.openapi.vfs.VirtualFile
22-
import com.intellij.openapi.vfs.findFile
23-
import com.intellij.openapi.vfs.readText
2422
import cc.unitmesh.shirelang.compiler.execute.processor.CaptureProcessor
2523
import cc.unitmesh.devti.language.processor.ForeignFunctionProcessor
24+
import cc.unitmesh.devti.util.findFile
25+
import cc.unitmesh.devti.util.readText
2626
import kotlinx.coroutines.CoroutineScope
2727
import kotlinx.coroutines.launch
2828
import java.io.File

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/ast/variable/resolver/PsiContextVariableResolver.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.intellij.openapi.diagnostic.logger
88
import com.intellij.psi.PsiManager
99
import cc.unitmesh.devti.language.ast.variable.resolver.base.VariableResolver
1010
import cc.unitmesh.devti.language.ast.variable.resolver.base.VariableResolverContext
11+
import cc.unitmesh.devti.util.virtualFile
1112

1213
/**
1314
* Include ToolchainVariableProvider and PsiContextVariableProvider
@@ -17,7 +18,7 @@ class PsiContextVariableResolver(private val context: VariableResolverContext) :
1718

1819
init {
1920
val psiFile = runReadAction {
20-
PsiManager.getInstance(context.myProject).findFile(context.editor.virtualFile ?: return@runReadAction null)
21+
PsiManager.getInstance(context.myProject).findFile(virtualFile(context.editor) ?: return@runReadAction null)
2122
}
2223

2324
variableProvider = if (psiFile?.language != null) {

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/compiler/HobbitHoleParser.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,31 +66,32 @@ object HobbitHoleParser {
6666

6767
frontMatterEntries.forEach { entry ->
6868
entry.children.forEach { child ->
69+
val text = runReadAction { child.text }
6970
when (child.elementType) {
7071
DevInTypes.LIFECYCLE_ID,
7172
DevInTypes.FRONT_MATTER_KEY,
7273
-> {
73-
lastKey = child.text
74+
lastKey = text
7475
}
7576

7677
DevInTypes.FRONT_MATTER_VALUE -> {
7778
frontMatter[lastKey] = parseFrontMatterValue(child)
78-
?: FrontMatterType.STRING("FrontMatter value parsing failed: ${child.text}")
79+
?: FrontMatterType.STRING("FrontMatter value parsing failed: $text")
7980
}
8081

8182
DevInTypes.PATTERN_ACTION -> {
8283
frontMatter[lastKey] = parsePatternAction(child)
83-
?: FrontMatterType.STRING("Pattern action parsing failed: ${child.text}")
84+
?: FrontMatterType.STRING("Pattern action parsing failed: $text")
8485
}
8586

8687
DevInTypes.LOGICAL_AND_EXPR -> {
8788
frontMatter[lastKey] = parseLogicAndExprToType(child as DevInLogicalAndExpr)
88-
?: FrontMatterType.STRING("Logical expression parsing failed: ${child.text}")
89+
?: FrontMatterType.STRING("Logical expression parsing failed: ${text}")
8990
}
9091

9192
DevInTypes.LOGICAL_OR_EXPR -> {
9293
frontMatter[lastKey] = parseLogicOrExprToType(child as DevInLogicalOrExpr)
93-
?: FrontMatterType.STRING("Logical expression parsing failed: ${child.text}")
94+
?: FrontMatterType.STRING("Logical expression parsing failed: ${text}")
9495
}
9596

9697
DevInTypes.CALL_EXPR -> {

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/compiler/streaming/ProfilingStreamingService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ class ProfilingStreamingService : StreamingServiceProvider {
3434
}
3535

3636
private fun toMb(value: Long): Long {
37-
return value / IOUtil.MiB
37+
return value / 1048576
3838
}
3939
}

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/config/EditorInteractionProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import cc.unitmesh.devti.language.run.runner.cancelWithConsole
2121
import cc.unitmesh.devti.llms.LlmFactory
2222
import cc.unitmesh.devti.llms.cancelHandler
2323
import cc.unitmesh.devti.util.AutoDevCoroutineScope
24+
import cc.unitmesh.devti.util.virtualFile
2425
import com.intellij.execution.ui.ConsoleView
2526
import com.intellij.execution.ui.ConsoleViewContentType
2627
import com.intellij.openapi.application.invokeLater
@@ -41,7 +42,7 @@ class EditorInteractionProvider : LocationInteractionProvider {
4142
}
4243

4344
override fun execute(context: LocationInteractionContext, postExecute: PostFunction) {
44-
val targetFile = context.editor?.virtualFile
45+
val targetFile = virtualFile(context.editor)
4546

4647
when (context.interactionType) {
4748
InteractionType.AppendCursor,

0 commit comments

Comments
 (0)