package cc.unitmesh.devins.idea.editor import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.devins.idea.compose.IdeaLaunchedEffect import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Tooltip /** * Top toolbar for the input section. * Contains @ trigger, file selection, and other context-related actions. * * Layout: * - Collapsed mode: Add Button | [Horizontal scrollable file chips] | Expand button * - Expanded mode: Add Button | [Vertical list of all files] | Collapse button * * Features: * - Integrates with IdeaContextManager for state management * - Shows selected files as chips with remove button on hover * - Horizontal scroll in collapsed mode, vertical list in expanded mode * - Shows context indicator when default context or rules are active */ @Composable fun IdeaTopToolbar( project: Project? = null, onAtClick: () -> Unit = {}, onSlashClick: () -> Unit = {}, onAddFileClick: () -> Unit = {}, selectedFiles: List = emptyList(), onRemoveFile: (SelectedFileItem) -> Unit = {}, onFilesSelected: (List) -> Unit = {}, modifier: Modifier = Modifier ) { var showFileSearchPopup by remember { mutableStateOf(false) } var isExpanded by remember { mutableStateOf(false) } // Get context manager state if project is available // Use manual state collection to avoid ClassLoader conflicts with collectAsState() val contextManager = remember(project) { project?.let { IdeaContextManager.getInstance(it) } } var hasDefaultContext by remember { mutableStateOf>(emptyList()) } var rules by remember { mutableStateOf>(emptyList()) } var relatedFiles by remember { mutableStateOf>(emptyList()) } IdeaLaunchedEffect(contextManager) { contextManager?.defaultContextFiles?.collect { hasDefaultContext = it } } IdeaLaunchedEffect(contextManager) { contextManager?.rules?.collect { rules = it } } IdeaLaunchedEffect(contextManager) { contextManager?.relatedFiles?.collect { relatedFiles = it } } Column( modifier = modifier .fillMaxWidth() .animateContentSize() ) { // Main toolbar row Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 4.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { // Left side: Add button with popup Row( horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically ) { // File search popup with trigger button if (project != null) { IdeaFileSearchPopup( project = project, showPopup = showFileSearchPopup, onShowPopupChange = { showFileSearchPopup = it }, onFilesSelected = { files -> onFilesSelected(files) showFileSearchPopup = false } ) } else { ToolbarIconButton( onClick = { onAddFileClick() }, tooltip = "Add File to Context" ) { Icon( imageVector = IdeaComposeIcons.Add, contentDescription = "Add File", modifier = Modifier.size(16.dp), tint = JewelTheme.globalColors.text.normal ) } } // Context indicator: show if default context or rules are active if (hasDefaultContext.isNotEmpty() || rules.isNotEmpty()) { ContextIndicator( hasDefaultContext = hasDefaultContext.isNotEmpty(), rulesCount = rules.size ) } } // Separator if (selectedFiles.isNotEmpty() || relatedFiles.isNotEmpty()) { Box( Modifier .width(1.dp) .height(20.dp) .background(JewelTheme.globalColors.borders.normal.copy(alpha = 0.5f)) ) } // Selected files - horizontal scrollable in collapsed mode if (!isExpanded && selectedFiles.isNotEmpty()) { val scrollState = rememberScrollState() Row( modifier = Modifier .weight(1f) .horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { selectedFiles.forEach { file -> FileChip(file = file, onRemove = { onRemoveFile(file) }) } } } else if (!isExpanded) { Spacer(Modifier.weight(1f)) } else { Spacer(Modifier.weight(1f)) } // Expand/Collapse button - only show if there are files if (selectedFiles.size > 1) { ToolbarIconButton( onClick = { isExpanded = !isExpanded }, tooltip = if (isExpanded) "Collapse file list" else "Expand file list" ) { Icon( imageVector = if (isExpanded) IdeaComposeIcons.ExpandLess else IdeaComposeIcons.ExpandMore, contentDescription = if (isExpanded) "Collapse" else "Expand", modifier = Modifier.size(16.dp), tint = JewelTheme.globalColors.text.normal ) } } } // Expanded view - vertical list of all files if (isExpanded && selectedFiles.isNotEmpty()) { Column( modifier = Modifier .fillMaxWidth() .padding(start = 32.dp, end = 8.dp, bottom = 4.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { selectedFiles.forEach { file -> FileChipExpanded(file = file, onRemove = { onRemoveFile(file) }) } } } } } /** * Context indicator showing active default context or rules */ @Composable private fun ContextIndicator( hasDefaultContext: Boolean, rulesCount: Int ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() val tooltipText = buildString { if (hasDefaultContext) append("Default context active") if (hasDefaultContext && rulesCount > 0) append(" | ") if (rulesCount > 0) append("$rulesCount rule(s) active") } Tooltip(tooltip = { Text(tooltipText) }) { Row( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .hoverable(interactionSource = interactionSource) .background( if (isHovered) JewelTheme.globalColors.borders.normal.copy(alpha = 0.3f) else JewelTheme.globalColors.panelBackground.copy(alpha = 0.5f) ) .padding(horizontal = 4.dp, vertical = 2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically ) { if (hasDefaultContext) { Icon( imageVector = IdeaComposeIcons.Book, contentDescription = "Default context", modifier = Modifier.size(12.dp), tint = JewelTheme.globalColors.text.info ) } if (rulesCount > 0) { Icon( imageVector = IdeaComposeIcons.Settings, contentDescription = "Rules", modifier = Modifier.size(12.dp), tint = JewelTheme.globalColors.text.info ) Text( text = rulesCount.toString(), style = JewelTheme.defaultTextStyle.copy( fontSize = 10.sp, color = JewelTheme.globalColors.text.info ) ) } } } } @Composable private fun ToolbarIconButton( onClick: () -> Unit, tooltip: String, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() Tooltip(tooltip = { Text(tooltip) }) { Box( modifier = modifier .size(28.dp) .clip(RoundedCornerShape(4.dp)) .hoverable(interactionSource = interactionSource) .background( if (isHovered) JewelTheme.globalColors.borders.normal.copy(alpha = 0.3f) else androidx.compose.ui.graphics.Color.Transparent ) .clickable(onClick = onClick), contentAlignment = Alignment.Center ) { content() } } } @Composable private fun FileChip(file: SelectedFileItem, onRemove: () -> Unit, modifier: Modifier = Modifier) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() Row( modifier = modifier .clip(RoundedCornerShape(4.dp)) .hoverable(interactionSource = interactionSource) .background(JewelTheme.globalColors.panelBackground.copy(alpha = 0.8f)) .border(1.dp, JewelTheme.globalColors.borders.normal, RoundedCornerShape(4.dp)) .padding(horizontal = 6.dp, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( imageVector = if (file.isDirectory) IdeaComposeIcons.Folder else (file.icon ?: IdeaComposeIcons.InsertDriveFile), contentDescription = null, modifier = Modifier.size(14.dp), tint = JewelTheme.globalColors.text.normal ) Text(text = file.name, style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), maxLines = 1) if (isHovered) { Icon( imageVector = IdeaComposeIcons.Close, contentDescription = "Remove", modifier = Modifier.size(14.dp).clickable(onClick = onRemove), tint = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f) ) } } } /** * Expanded file chip showing full path - used in vertical expanded mode */ @Composable private fun FileChipExpanded(file: SelectedFileItem, onRemove: () -> Unit, modifier: Modifier = Modifier) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() Row( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(4.dp)) .hoverable(interactionSource = interactionSource) .background( if (isHovered) JewelTheme.globalColors.panelBackground else JewelTheme.globalColors.panelBackground.copy(alpha = 0.6f) ) .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( imageVector = if (file.isDirectory) IdeaComposeIcons.Folder else (file.icon ?: IdeaComposeIcons.InsertDriveFile), contentDescription = null, modifier = Modifier.size(16.dp), tint = JewelTheme.globalColors.text.normal ) Column(modifier = Modifier.weight(1f)) { Text( text = file.name, style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = file.path, style = JewelTheme.defaultTextStyle.copy( fontSize = 10.sp, color = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f) ), maxLines = 1, overflow = TextOverflow.Ellipsis ) } Icon( imageVector = IdeaComposeIcons.Close, contentDescription = "Remove", modifier = Modifier .size(16.dp) .clickable(onClick = onRemove), tint = if (isHovered) JewelTheme.globalColors.text.normal else JewelTheme.globalColors.text.normal.copy(alpha = 0.4f) ) } } data class SelectedFileItem( val name: String, val path: String, val icon: androidx.compose.ui.graphics.vector.ImageVector? = null, val virtualFile: com.intellij.openapi.vfs.VirtualFile? = null, val isDirectory: Boolean = false ) { /** * Generate the DevIns command for this file/folder. * Uses /dir: for directories and /file: for files. */ fun toDevInsCommand(): String { return if (isDirectory) "/dir:$path" else "/file:$path" } }