Skip to content

Commit 88781b5

Browse files
committed
feat(mpp-ui): implement file context management with indexed search
- Add SelectedFileItem, FileChip, TopToolbar, FileSearchPopup components - Add WorkspaceFileSearchProvider with pre-built file index for fast search - Add IndexingState enum for tracking indexing progress - Integrate file context into DevInEditorInput with buildAndSendMessage() - Add Prompt Enhancement button to BottomToolbar - Add AutoAwesome and History icons to AutoDevComposeIcons - Add FileContext to EditorCallbacks for file context submission Closes #35
1 parent 40c4593 commit 88781b5

File tree

10 files changed

+1193
-22
lines changed

10 files changed

+1193
-22
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
package cc.unitmesh.devins.editor
22

3+
/**
4+
* Represents a file in the context.
5+
* Used for passing file context information with submissions.
6+
*/
7+
data class FileContext(
8+
val name: String,
9+
val path: String,
10+
val relativePath: String = name,
11+
val isDirectory: Boolean = false
12+
)
13+
314
/**
415
* 编辑器回调接口
5-
*
16+
*
617
* 定义了编辑器的各种回调方法,用于响应编辑器事件
718
* 所有方法都有默认空实现,子类只需要重写感兴趣的方法
819
*/
@@ -11,6 +22,14 @@ interface EditorCallbacks {
1122
* 当用户提交内容时调用(例如按下 Cmd+Enter)
1223
*/
1324
fun onSubmit(text: String) {}
25+
26+
/**
27+
* 当用户提交内容时调用,包含文件上下文
28+
* 默认实现调用不带文件上下文的 onSubmit
29+
*/
30+
fun onSubmit(text: String, files: List<FileContext>) {
31+
onSubmit(text)
32+
}
1433

1534
/**
1635
* 当文本内容变化时调用

mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,18 @@ actual class DefaultFileSystem actual constructor(private val projectPath: Strin
105105

106106
actual override fun searchFiles(pattern: String, maxDepth: Int, maxResults: Int): List<String> {
107107
return try {
108+
println("[DefaultFileSystem] searchFiles called: pattern=$pattern, projectPath=$projectPath")
108109
val projectRoot = Path.of(projectPath)
109110
if (!projectRoot.exists() || !projectRoot.isDirectory()) {
111+
println("[DefaultFileSystem] Project root does not exist or is not a directory")
110112
return emptyList()
111113
}
112-
114+
113115
// Convert glob pattern to regex - handle ** and * differently
114116
// **/ should match zero or more directory levels (including root)
115117
// IMPORTANT: Use placeholders without * to avoid conflicts
116118
val regexPattern = pattern
117-
.replace("**/", "___RECURSIVE___") // Protect **/ first
119+
.replace("**/", "___RECURSIVE___") // Protect **/ first
118120
.replace("**", "___GLOBSTAR___") // Then protect **
119121
.replace(".", "\\.") // Escape dots
120122
.replace("?", "___QUESTION___") // Protect ? before converting braces
@@ -125,18 +127,22 @@ actual class DefaultFileSystem actual constructor(private val projectPath: Strin
125127
.replace("___RECURSIVE___", "(?:(?:.*/)|(?:))") // **/ matches zero or more directories
126128
.replace("___GLOBSTAR___", ".*") // ** without / matches anything
127129
.replace("___QUESTION___", ".") // Now replace ? with .
128-
130+
131+
println("[DefaultFileSystem] Regex pattern: $regexPattern")
129132
val regex = regexPattern.toRegex(RegexOption.IGNORE_CASE)
130-
133+
131134
val results = mutableListOf<String>()
132-
135+
133136
// 只保留最基本的排除目录(.git 必须排除,其他依赖 gitignore)
134137
// Add build to satisfy tests expecting no files under /build/; also pre-filter relative paths containing /build/
135138
val criticalExcludeDirs = setOf(".git", "build")
136-
139+
137140
// Reload gitignore patterns before search
141+
println("[DefaultFileSystem] Reloading gitignore...")
138142
gitIgnoreParser?.reload()
139-
143+
144+
println("[DefaultFileSystem] Starting Files.walk...")
145+
val startTime = System.currentTimeMillis()
140146
Files.walk(projectRoot, maxDepth).use { stream ->
141147
val iterator = stream
142148
.filter { path ->
@@ -177,9 +183,13 @@ actual class DefaultFileSystem actual constructor(private val projectPath: Strin
177183
}
178184
}
179185
}
180-
186+
187+
val elapsed = System.currentTimeMillis() - startTime
188+
println("[DefaultFileSystem] Files.walk completed in ${elapsed}ms, found ${results.size} results")
181189
results
182190
} catch (e: Exception) {
191+
println("[DefaultFileSystem] Error during search: ${e.message}")
192+
e.printStackTrace()
183193
emptyList()
184194
}
185195
}

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ fun BottomToolbar(
2525
isExecuting: Boolean = false,
2626
onStopClick: () -> Unit = {},
2727
onAtClick: () -> Unit = {},
28+
onEnhanceClick: () -> Unit = {},
29+
isEnhancing: Boolean = false,
2830
onSettingsClick: () -> Unit = {},
2931
workspacePath: String? = null,
3032
totalTokenInfo: cc.unitmesh.llm.compression.TokenInfo? = null,
@@ -149,6 +151,28 @@ fun BottomToolbar(
149151
)
150152
}
151153

154+
// Prompt Enhancement button (Ctrl+P)
155+
IconButton(
156+
onClick = onEnhanceClick,
157+
enabled = !isEnhancing,
158+
modifier = Modifier.size(36.dp)
159+
) {
160+
if (isEnhancing) {
161+
CircularProgressIndicator(
162+
modifier = Modifier.size(16.dp),
163+
strokeWidth = 2.dp,
164+
color = MaterialTheme.colorScheme.primary
165+
)
166+
} else {
167+
Icon(
168+
imageVector = AutoDevComposeIcons.AutoAwesome,
169+
contentDescription = "Enhance Prompt (Ctrl+P)",
170+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
171+
modifier = Modifier.size(20.dp)
172+
)
173+
}
174+
}
175+
152176
IconButton(
153177
onClick = onSettingsClick,
154178
modifier = Modifier.size(36.dp)

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ import cc.unitmesh.devins.completion.CompletionItem
3333
import cc.unitmesh.devins.completion.CompletionManager
3434
import cc.unitmesh.devins.completion.CompletionTriggerType
3535
import cc.unitmesh.devins.editor.EditorCallbacks
36+
import cc.unitmesh.devins.editor.FileContext
3637
import cc.unitmesh.devins.ui.compose.config.ToolConfigDialog
3738
import cc.unitmesh.devins.ui.compose.editor.changes.FileChangeSummary
3839
import cc.unitmesh.devins.ui.compose.editor.completion.CompletionPopup
3940
import cc.unitmesh.devins.ui.compose.editor.completion.CompletionTrigger
41+
import cc.unitmesh.devins.ui.compose.editor.context.FileSearchPopup
42+
import cc.unitmesh.devins.ui.compose.editor.context.FileSearchProvider
43+
import cc.unitmesh.devins.ui.compose.editor.context.SelectedFileItem
44+
import cc.unitmesh.devins.ui.compose.editor.context.TopToolbar
45+
import cc.unitmesh.devins.ui.compose.editor.context.WorkspaceFileSearchProvider
4046
import cc.unitmesh.devins.ui.compose.editor.highlighting.DevInSyntaxHighlighter
4147
import cc.unitmesh.devins.ui.config.ConfigManager
4248
import cc.unitmesh.devins.ui.compose.sketch.getUtf8FontFamily
@@ -74,7 +80,8 @@ fun DevInEditorInput(
7480
modifier: Modifier = Modifier,
7581
onModelConfigChange: (ModelConfig) -> Unit = {},
7682
dismissKeyboardOnSend: Boolean = true,
77-
renderer: cc.unitmesh.devins.ui.compose.agent.ComposeRenderer? = null
83+
renderer: cc.unitmesh.devins.ui.compose.agent.ComposeRenderer? = null,
84+
fileSearchProvider: FileSearchProvider? = null
7885
) {
7986
var textFieldValue by remember { mutableStateOf(TextFieldValue(initialText)) }
8087
var highlightedText by remember { mutableStateOf(initialText) }
@@ -94,6 +101,43 @@ fun DevInEditorInput(
94101
var mcpServers by remember { mutableStateOf<Map<String, McpServerConfig>>(emptyMap()) }
95102
val mcpClientManager = remember { McpClientManager() }
96103

104+
// File context state (for TopToolbar)
105+
var selectedFiles by remember { mutableStateOf<List<SelectedFileItem>>(emptyList()) }
106+
var autoAddCurrentFile by remember { mutableStateOf(true) }
107+
108+
// File search provider - use WorkspaceFileSearchProvider as default if not provided
109+
val effectiveSearchProvider = remember { fileSearchProvider ?: WorkspaceFileSearchProvider() }
110+
111+
// Helper function to convert SelectedFileItem to FileContext
112+
fun getFileContexts(): List<FileContext> = selectedFiles.map { file ->
113+
FileContext(
114+
name = file.name,
115+
path = file.path,
116+
relativePath = file.relativePath,
117+
isDirectory = file.isDirectory
118+
)
119+
}
120+
121+
/**
122+
* Build and send message with file references (like IDEA's buildAndSendMessage).
123+
* Appends DevIns commands for selected files to the message.
124+
*/
125+
fun buildAndSendMessage(text: String) {
126+
if (text.isBlank()) return
127+
128+
// Generate DevIns commands for selected files
129+
val filesText = selectedFiles.joinToString("\n") { it.toDevInsCommand() }
130+
val fullText = if (filesText.isNotEmpty()) "$text\n$filesText" else text
131+
132+
// Send with file contexts
133+
callbacks?.onSubmit(fullText, getFileContexts())
134+
135+
// Clear input and files
136+
textFieldValue = TextFieldValue("")
137+
selectedFiles = emptyList()
138+
showCompletion = false
139+
}
140+
97141
val highlighter = remember { DevInSyntaxHighlighter() }
98142
val manager = completionManager ?: remember { CompletionManager() }
99143
val focusRequester = remember { FocusRequester() }
@@ -268,9 +312,7 @@ fun DevInEditorInput(
268312
) {
269313
scope.launch {
270314
delay(100) // Small delay to ensure UI updates
271-
callbacks?.onSubmit(trimmedText)
272-
textFieldValue = TextFieldValue("")
273-
showCompletion = false
315+
buildAndSendMessage(trimmedText)
274316
}
275317
return
276318
}
@@ -393,9 +435,7 @@ fun DevInEditorInput(
393435
// 桌面端:Enter 发送消息(但不在移动端拦截)
394436
!isAndroid && !Platform.isIOS && event.key == Key.Enter && !event.isShiftPressed -> {
395437
if (textFieldValue.text.isNotBlank()) {
396-
callbacks?.onSubmit(textFieldValue.text)
397-
textFieldValue = TextFieldValue("")
398-
showCompletion = false
438+
buildAndSendMessage(textFieldValue.text)
399439
if (dismissKeyboardOnSend) {
400440
focusManager.clearFocus()
401441
}
@@ -448,6 +488,21 @@ fun DevInEditorInput(
448488
Column(
449489
modifier = Modifier.fillMaxWidth()
450490
) {
491+
// Top toolbar with file context management (desktop only)
492+
if (!isMobile) {
493+
TopToolbar(
494+
selectedFiles = selectedFiles,
495+
onAddFile = { file -> selectedFiles = selectedFiles + file },
496+
onRemoveFile = { file ->
497+
selectedFiles = selectedFiles.filter { it.path != file.path }
498+
},
499+
onClearFiles = { selectedFiles = emptyList() },
500+
autoAddCurrentFile = autoAddCurrentFile,
501+
onToggleAutoAdd = { autoAddCurrentFile = !autoAddCurrentFile },
502+
searchProvider = effectiveSearchProvider
503+
)
504+
}
505+
451506
Box(
452507
modifier =
453508
Modifier
@@ -497,9 +552,7 @@ fun DevInEditorInput(
497552
keyboardActions = KeyboardActions(
498553
onSend = {
499554
if (textFieldValue.text.isNotBlank()) {
500-
callbacks?.onSubmit(textFieldValue.text)
501-
textFieldValue = TextFieldValue("")
502-
showCompletion = false
555+
buildAndSendMessage(textFieldValue.text)
503556
if (dismissKeyboardOnSend) {
504557
focusManager.clearFocus()
505558
}
@@ -577,9 +630,7 @@ fun DevInEditorInput(
577630
BottomToolbar(
578631
onSendClick = {
579632
if (textFieldValue.text.isNotBlank()) {
580-
callbacks?.onSubmit(textFieldValue.text)
581-
textFieldValue = TextFieldValue("")
582-
showCompletion = false
633+
buildAndSendMessage(textFieldValue.text)
583634
// Force dismiss keyboard on mobile
584635
if (isMobile) {
585636
focusManager.clearFocus()
@@ -620,6 +671,8 @@ fun DevInEditorInput(
620671
}
621672
}
622673
},
674+
onEnhanceClick = { enhanceCurrentInput() },
675+
isEnhancing = isEnhancing,
623676
onSettingsClick = {
624677
showToolConfig = true
625678
},

0 commit comments

Comments
 (0)