From 6a7107c6e759d906a13457fd2ad9e5fa4dadfbe0 Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Fri, 6 Feb 2026 18:15:58 +0000 Subject: [PATCH] fix: respect ignorelist when dealing with any search/read operations (relates #1126) --- .../actions/IncludeFilesInContextAction.java | 38 +++-- .../chat/ChatToolWindowTabPanel.java | 48 ++++++- .../ui/checkbox/VirtualFileCheckboxTree.java | 59 ++++++-- .../codegpt/agent/tools/BashTool.kt | 2 + .../codegpt/agent/tools/EditTool.kt | 2 +- .../agent/tools/McpDynamicToolRegistry.kt | 4 +- .../codegpt/agent/tools/ReadTool.kt | 2 +- .../codegpt/agent/tools/WriteTool.kt | 2 +- .../codegpt/mcp/McpToolCallHandler.kt | 7 +- .../settings/ProxyAISettingsService.kt | 134 ++++++++++-------- .../ui/textarea/TagProcessorFactory.kt | 24 +++- .../codegpt/ui/textarea/UserInputPanel.kt | 15 +- .../textarea/header/UserInputHeaderPanel.kt | 38 ++++- .../files/IncludeOpenFilesActionItem.kt | 7 +- .../textarea/lookup/group/FilesGroupItem.kt | 8 +- .../textarea/lookup/group/FoldersGroupItem.kt | 16 ++- .../codegpt/agent/IgnoreRulesTest.kt | 69 +++++++-- .../IgnoreRulesTagManagerIntegrationTest.kt | 107 ++++++++++++++ 18 files changed, 446 insertions(+), 136 deletions(-) create mode 100644 src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java index 757a856e..53cc814b 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.actions; import static com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE_ARRAY; +import static com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE; import static com.intellij.openapi.ui.DialogWrapper.OK_EXIT_CODE; import static ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_PROMPT_TEMPLATE; import static ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT; @@ -26,6 +27,7 @@ import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.EncodingManager; import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.settings.IncludedFilesSettings; +import ee.carlrobert.codegpt.settings.ProxyAISettingsService; import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree; @@ -33,8 +35,6 @@ import ee.carlrobert.codegpt.ui.checkbox.VirtualFileCheckboxTree; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.Dimension; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.List; import javax.swing.JButton; import javax.swing.JComponent; @@ -57,7 +57,7 @@ public class IncludeFilesInContextAction extends AnAction { return; } - var checkboxTree = getCheckboxTree(e.getDataContext()); + var checkboxTree = getCheckboxTree(project, e.getDataContext()); if (checkboxTree == null) { throw new RuntimeException("Could not obtain file tree"); } @@ -92,10 +92,19 @@ public class IncludeFilesInContextAction extends AnAction { } } - private @Nullable FileCheckboxTree getCheckboxTree(DataContext dataContext) { + private @Nullable FileCheckboxTree getCheckboxTree(Project project, DataContext dataContext) { + var settingsService = project.getService(ProxyAISettingsService.class); var selectedVirtualFiles = VIRTUAL_FILE_ARRAY.getData(dataContext); - if (selectedVirtualFiles != null) { - return new VirtualFileCheckboxTree(selectedVirtualFiles); + if (selectedVirtualFiles != null && selectedVirtualFiles.length > 0) { + return new VirtualFileCheckboxTree( + selectedVirtualFiles, + settingsService); + } + var currentVirtualFile = VIRTUAL_FILE.getData(dataContext); + if (currentVirtualFile != null) { + return new VirtualFileCheckboxTree( + new VirtualFile[]{currentVirtualFile}, + settingsService); } return null; @@ -103,13 +112,15 @@ public class IncludeFilesInContextAction extends AnAction { private static class TotalTokensLabel extends JBLabel { - private static final EncodingManager encodingManager = EncodingManager.getInstance(); + private final EncodingManager encodingManager = EncodingManager.getInstance(); private int fileCount; private int totalTokens; TotalTokensLabel(List referencedFiles) { - fileCount = referencedFiles.size(); + fileCount = (int) referencedFiles.stream() + .filter(file -> file != null && !file.isDirectory() && file.isValid()) + .count(); totalTokens = calculateTotalTokens(referencedFiles); updateText(); } @@ -147,13 +158,13 @@ public class IncludeFilesInContextAction extends AnAction { return null; } - private String getVirtualFileContent(VirtualFile virtualFile) { + private @Nullable String getVirtualFileContent(VirtualFile virtualFile) { try { - if (!virtualFile.isDirectory()) { - return new String(Files.readAllBytes(Paths.get(virtualFile.getPath()))); + if (virtualFile.isValid() && !virtualFile.isDirectory()) { + return new String(virtualFile.contentsToByteArray(), virtualFile.getCharset()); } } catch (IOException ex) { - LOG.error(ex); + LOG.error("Failed to read file content: " + virtualFile.getPath(), ex); } return null; } @@ -168,12 +179,13 @@ public class IncludeFilesInContextAction extends AnAction { private int calculateTotalTokens(List referencedFiles) { return referencedFiles.stream() + .filter(file -> !file.isDirectory() && file.isValid()) .mapToInt(file -> { try { return encodingManager.countTokens( new String(file.contentsToByteArray(), file.getCharset())); } catch (IOException e) { - LOG.error("Failed to read file {} content", file.getName()); + LOG.error("Failed to read file content: " + file.getPath(), e); return 0; } }) diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 782b539b..88d4074f 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -35,6 +35,7 @@ import ee.carlrobert.codegpt.mcp.McpSessionManager; import ee.carlrobert.codegpt.mcp.McpTool; import ee.carlrobert.codegpt.psistructure.PsiStructureProvider; import ee.carlrobert.codegpt.psistructure.models.ClassStructure; +import ee.carlrobert.codegpt.settings.ProxyAISettingsService; import ee.carlrobert.codegpt.settings.service.FeatureType; import ee.carlrobert.codegpt.telemetry.TelemetryAction; import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction; @@ -66,7 +67,9 @@ import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -284,14 +287,43 @@ public class ChatToolWindowTabPanel implements Disposable { } private List getReferencedFiles(List tags) { - return tags.stream() - .map(this::getVirtualFile) - .filter(Objects::nonNull) - .distinct() + var settingsService = project.getService(ProxyAISettingsService.class); + var visibleFiles = collectVisibleFiles( + tags.stream() + .map(this::getVirtualFile) + .filter(Objects::nonNull) + .toList(), + settingsService + ); + + return visibleFiles.stream() .map(ReferencedFile::from) .toList(); } + private List collectVisibleFiles( + List inputFiles, + ProxyAISettingsService settingsService) { + var visibleFiles = new LinkedHashSet(); + inputFiles.forEach(file -> appendVisibleFiles(file, settingsService, visibleFiles)); + return visibleFiles.stream().toList(); + } + + private void appendVisibleFiles( + VirtualFile file, + ProxyAISettingsService settingsService, + LinkedHashSet output) { + if (!file.isValid() || !settingsService.isVirtualFileVisible(file)) { + return; + } + if (!file.isDirectory()) { + output.add(file); + return; + } + Arrays.stream(file.getChildren()) + .forEach(child -> appendVisibleFiles(child, settingsService, output)); + } + private List getConversationHistoryIds(List tags) { return tags.stream() .map(it -> { @@ -383,10 +415,14 @@ public class ChatToolWindowTabPanel implements Disposable { } public void includeFiles(List referencedFiles) { - userInputPanel.includeFiles(referencedFiles); + var settingsService = project.getService(ProxyAISettingsService.class); + var visibleReferencedFiles = collectVisibleFiles(referencedFiles, settingsService); + + userInputPanel.includeFiles(new ArrayList<>(visibleReferencedFiles)); ReadAction.nonBlocking(() -> { var encodingManager = EncodingManager.getInstance(); - return referencedFiles.stream() + return visibleReferencedFiles.stream() + .filter(file -> !file.isDirectory()) .mapToInt(it -> encodingManager.countTokens(ReferencedFile.from(it).fileContent())) .sum(); } diff --git a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java index 93fbea00..5caace88 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/checkbox/VirtualFileCheckboxTree.java @@ -5,46 +5,75 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.CheckedTreeNode; import com.intellij.util.PlatformIcons; -import ee.carlrobert.codegpt.ReferencedFile; +import com.intellij.util.ui.tree.TreeUtil; +import ee.carlrobert.codegpt.settings.ProxyAISettingsService; import ee.carlrobert.codegpt.ui.OverlayUtil; -import java.io.File; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.NotNull; public class VirtualFileCheckboxTree extends FileCheckboxTree { - public VirtualFileCheckboxTree(@NotNull VirtualFile[] rootFiles) { - super(createFileTypesRenderer(), createRootNode(rootFiles)); + private final ProxyAISettingsService settingsService; + + public VirtualFileCheckboxTree( + @NotNull VirtualFile[] rootFiles, + @NotNull ProxyAISettingsService settingsService) { + super(createFileTypesRenderer(), new CheckedTreeNode(null)); + this.settingsService = settingsService; + var rootNode = (CheckedTreeNode) getModel().getRoot(); + for (VirtualFile file : rootFiles) { + var childNode = createNode(file); + if (childNode != null) { + rootNode.add(childNode); + } + } + setRootVisible(false); + setShowsRootHandles(true); + TreeUtil.expandAll(this); } public List getReferencedFiles() { var checkedNodes = getCheckedNodes(VirtualFile.class, Objects::nonNull); - if (checkedNodes.length > 1024) { + var files = new LinkedHashSet(); + Arrays.stream(checkedNodes) + .filter(Objects::nonNull) + .forEach(node -> collectVisibleFiles(node, files)); + if (files.size() > 1024) { OverlayUtil.showNotification("Too many files selected.", NotificationType.ERROR); throw new RuntimeException("Too many files selected"); } - - return Arrays.stream(checkedNodes) - .filter(Objects::nonNull) + return files.stream() .toList(); } - private static CheckedTreeNode createRootNode(VirtualFile[] files) { - CheckedTreeNode rootNode = new CheckedTreeNode(null); - for (VirtualFile file : files) { - rootNode.add(createNode(file)); + private void collectVisibleFiles(VirtualFile file, LinkedHashSet output) { + if (!file.isValid() || !settingsService.isVirtualFileVisible(file)) { + return; } - return rootNode; + if (!file.isDirectory()) { + output.add(file); + return; + } + Arrays.stream(file.getChildren()) + .forEach(child -> collectVisibleFiles(child, output)); } - private static CheckedTreeNode createNode(VirtualFile file) { + private CheckedTreeNode createNode(VirtualFile file) { + if (!settingsService.isVirtualFileVisible(file)) { + return null; + } + CheckedTreeNode node = new CheckedTreeNode(file); if (file.isDirectory()) { VirtualFile[] children = file.getChildren(); for (VirtualFile child : children) { - node.add(createNode(child)); + var childNode = createNode(child); + if (childNode != null) { + node.add(childNode); + } } } return node; diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/BashTool.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/BashTool.kt index 040dca86..88a3b0ab 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/BashTool.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/BashTool.kt @@ -607,6 +607,8 @@ class BashTool( lastWasReader = tokenIsReader } val settingsService = project.service() + // TODO(PROXYAI-IGNORE): Replace deny-style bash path checks with visibility filtering. + // Bash output and directory listings should hide ignored paths instead of returning policy-denied. return paths.any { candidate -> settingsService.isPathIgnored(toAbsolute(candidate)) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/EditTool.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/EditTool.kt index 417d4b8c..1801c17c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/EditTool.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/EditTool.kt @@ -135,7 +135,7 @@ class EditTool( if (svc.isPathIgnored(args.filePath)) { return Result.Error( filePath = args.filePath, - error = ".proxyai ignore rules block editing this path" + error = "File not found: ${args.filePath}" ) } val normalizedPath = args.filePath.replace("\\", "/") diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/McpDynamicToolRegistry.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/McpDynamicToolRegistry.kt index 2593fc98..32939f07 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/McpDynamicToolRegistry.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/McpDynamicToolRegistry.kt @@ -6,6 +6,7 @@ import ai.koog.agents.core.tools.ToolParameterDescriptor import ai.koog.agents.core.tools.ToolParameterType import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.agent.AgentMcpContext import ee.carlrobert.codegpt.mcp.ConnectionStatus import ee.carlrobert.codegpt.mcp.McpSessionAttachment @@ -174,7 +175,8 @@ private class SessionBoundMcpTool( private fun runTool(client: McpSyncClient, args: JsonObject): McpTool.Result { return runCatching { - val request = McpSchema.CallToolRequest(sourceTool.name, args.toMcpArguments()) + val callArgs = args.toMcpArguments() + val request = McpSchema.CallToolRequest(sourceTool.name, callArgs) val result = client.callTool(request) val content = result.formatMcpContent() if (result.isError == true) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/ReadTool.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/ReadTool.kt index 7e1856e0..1ae5e186 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/ReadTool.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/ReadTool.kt @@ -118,7 +118,7 @@ class ReadTool( if (settingsService.isPathIgnored(args.filePath)) { return@runReadAction Result.Error( filePath = args.filePath, - error = "Access to this path is blocked by .proxyai ignore rules" + error = "File not found: ${args.filePath}" ) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/WriteTool.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/WriteTool.kt index 08eb7cc5..8289fe17 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/WriteTool.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/tools/WriteTool.kt @@ -101,7 +101,7 @@ class WriteTool( if (svc.isPathIgnored(args.filePath) || svc.isPathIgnored(filePath)) { return Result.Error( filePath = filePath, - error = ".proxyai ignore rules block writing to this path" + error = "File not found: $filePath" ) } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/mcp/McpToolCallHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/mcp/McpToolCallHandler.kt index 16571cb5..39396a81 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/mcp/McpToolCallHandler.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/mcp/McpToolCallHandler.kt @@ -26,8 +26,8 @@ import javax.swing.JPanel * Handles MCP tool call execution with user approval workflow. * Manages the lifecycle of tool calls from request to result display. */ -@Service -class McpToolCallHandler { +@Service(Service.Level.PROJECT) +class McpToolCallHandler(private val project: Project) { private val sessionManager = service() private val pendingApprovals = ConcurrentHashMap>() @@ -202,7 +202,7 @@ class McpToolCallHandler { return "Tool execution failed: $errorMessage" } - val resultContent = when { + val rawResultContent = when { toolResult.content != null -> { try { when (val content = toolResult.content) { @@ -227,6 +227,7 @@ class McpToolCallHandler { toolResult.isError -> "Error: Tool execution failed" else -> "Tool executed successfully (no content returned)" } + val resultContent = rawResultContent statusPanels[toolCall.id]?.let { panel -> runInEdt { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/ProxyAISettingsService.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/ProxyAISettingsService.kt index 6a23e4f2..f2b8c189 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/ProxyAISettingsService.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/ProxyAISettingsService.kt @@ -5,8 +5,10 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.settings.agents.SubagentDefaults import ee.carlrobert.codegpt.settings.hooks.HookConfiguration +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -15,7 +17,6 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardOpenOption -import java.util.concurrent.atomic.AtomicReference import kotlin.io.path.inputStream import kotlin.io.path.notExists @@ -33,7 +34,6 @@ class ProxyAISettingsService(private val project: Project) { private val store: ProxyAISettingsStore by lazy { ProxyAISettingsStore(settingsFile, json, logger) } - private val cache = AtomicReference(null) private val isWindows = System.getProperty("os.name")?.lowercase()?.contains("windows") == true fun getSubagents(): List { @@ -96,6 +96,14 @@ class ProxyAISettingsService(private val project: Project) { return snapshot().ignoreMatcher.matches(path, basePath) } + fun isPathVisible(path: String): Boolean { + return !isPathIgnored(path) + } + + fun isVirtualFileVisible(file: VirtualFile): Boolean { + return isPathVisible(file.path) + } + private fun permissionTargets(target: String): List { val normalized = try { Paths.get(target).normalize().toString().replace('\\', '/') @@ -121,32 +129,21 @@ class ProxyAISettingsService(private val project: Project) { }.distinct() } - private fun snapshot(): CachedSettings { - val cached = cache.get() - val lastModified = store.lastModified() - if (cached != null && cached.lastModified == lastModified) { - return cached - } + private fun snapshot(): SettingsSnapshot { val settings = store.load() ?: ProxyAISettings.default() val ignoreMatcher = IgnoreMatcher.from(settings.ignore, isWindows) - val snapshot = CachedSettings(settings, lastModified, ignoreMatcher) - cache.set(snapshot) - return snapshot + return SettingsSnapshot(settings, ignoreMatcher) } private fun updateSettings(transform: (ProxyAISettings) -> ProxyAISettings) { val current = snapshot().settings val updated = transform(current) - if (!store.save(updated)) return - val lastModified = store.lastModified() - val ignoreMatcher = IgnoreMatcher.from(updated.ignore, isWindows) - cache.set(CachedSettings(updated, lastModified, ignoreMatcher)) + store.save(updated) } } -private data class CachedSettings( +private data class SettingsSnapshot( val settings: ProxyAISettings, - val lastModified: Long, val ignoreMatcher: IgnoreMatcher ) @@ -155,6 +152,7 @@ private class ProxyAISettingsStore( private val json: Json, private val logger: Logger ) { + @OptIn(ExperimentalSerializationApi::class) fun load(): ProxyAISettings? { if (settingsFile.notExists()) return null return try { @@ -167,6 +165,7 @@ private class ProxyAISettingsStore( } } + @OptIn(ExperimentalSerializationApi::class) fun save(settings: ProxyAISettings): Boolean { return try { settingsFile.parent?.let { Files.createDirectories(it) } @@ -185,14 +184,6 @@ private class ProxyAISettingsStore( } } - fun lastModified(): Long { - return try { - if (settingsFile.notExists()) -1 else Files.getLastModifiedTime(settingsFile).toMillis() - } catch (e: Exception) { - logger.warn("Failed to read ProxyAI settings modified time from $settingsFile", e) - -1 - } - } } private class IgnoreMatcher( @@ -229,10 +220,18 @@ private class IgnoreMatcher( return try { var g = glob.trim() if (g.isEmpty()) return null + val deepDirSuffix = g.endsWith("/**") val dirSuffix = g.endsWith("/") + if (deepDirSuffix) { + g = g.removeSuffix("/**") + } g = g.trimEnd('/') val sb = StringBuilder() sb.append('^') + if (!g.startsWith("/")) { + // Match from project root or any nested directory, similar to gitignore-style rules. + sb.append("(?:.*/)?") + } var i = 0 while (i < g.length) { when (val c = g[i]) { @@ -257,7 +256,10 @@ private class IgnoreMatcher( } i += 1 } - if (dirSuffix) sb.append("(/.*)?") + when { + deepDirSuffix -> sb.append("(/.*)?") + dirSuffix -> sb.append("(/.*)?") + } sb.append('$') if (ignoreCase) Regex( sb.toString(), @@ -278,10 +280,17 @@ private class IgnoreMatcher( } private fun simpleMatch(path: String, pattern: String): Boolean { - val p = path.trimStart('/') - val pat = pattern.trim() + val p = path.trimStart('/').replace('\\', '/') + val pat = pattern.trim().replace('\\', '/') if (pat == p) return true - if (pat.endsWith("/") && p.startsWith(pat.trimEnd('/'))) return true + if (pat.endsWith("/**")) { + val dir = pat.removeSuffix("/**").trimEnd('/') + if (p == dir || p.startsWith("$dir/") || p.contains("/$dir/")) return true + } + if (pat.endsWith("/")) { + val dir = pat.trimEnd('/') + if (p == dir || p.startsWith("$dir/") || p.contains("/$dir/")) return true + } if (!pat.contains('/') && pat.startsWith('.')) { val fileName = p.substringAfterLast('/') if (fileName == pat) return true @@ -292,46 +301,47 @@ private class IgnoreMatcher( @Serializable data class ProxyAISettings( - val ignore: List, - val permissions: Permissions, + val ignore: List = DEFAULT_IGNORE_PATTERNS, + val permissions: Permissions = Permissions(allow = DEFAULT_ALLOW_RULES), val subagents: List = emptyList(), val hooks: HookConfiguration? = null ) { companion object { + private val DEFAULT_IGNORE_PATTERNS = listOf( + ".idea/", + "*.iml", + ".git/", + ) + + private val DEFAULT_ALLOW_RULES = listOf( + "Bash(rg *)", + "Bash(grep *)", + "Bash(find *)", + "Bash(ls *)", + "Bash(sed *)", + "Bash(rm *)", + "Bash(cat *)", + "Bash(head *)", + "Bash(tail *)", + "Bash(diff *)", + "Bash(du *)", + "Bash(file *)", + "Bash(sort *)", + "Bash(stat *)", + "Bash(tree *)", + "Bash(uniq *)", + "Bash(wc *)", + "Bash(whereis *)", + "Bash(which *)", + "Bash(less *)", + "Bash(more *)", + ) + fun default(): ProxyAISettings { return ProxyAISettings( - ignore = listOf( - ".idea/", - "*.iml", - "build/", - "dist/", - "node_modules/", - ".git/", - ), + ignore = DEFAULT_IGNORE_PATTERNS, permissions = Permissions( - allow = listOf( - "Bash(rg *)", - "Bash(grep *)", - "Bash(find *)", - "Bash(ls *)", - "Bash(sed *)", - "Bash(rm *)", - "Bash(cat *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(diff *)", - "Bash(du *)", - "Bash(file *)", - "Bash(sort *)", - "Bash(stat *)", - "Bash(tree *)", - "Bash(uniq *)", - "Bash(wc *)", - "Bash(whereis *)", - "Bash(which *)", - "Bash(less *)", - "Bash(more *)", - ) + allow = DEFAULT_ALLOW_RULES ), subagents = SubagentDefaults.defaults(), hooks = HookConfiguration() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt index 7f278561..0fbbf6f9 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/TagProcessorFactory.kt @@ -19,6 +19,7 @@ import ee.carlrobert.codegpt.completions.CompletionRequestUtil import ee.carlrobert.codegpt.conversations.Conversation import ee.carlrobert.codegpt.conversations.ConversationsState import ee.carlrobert.codegpt.conversations.message.Message +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.ui.textarea.header.tag.* import ee.carlrobert.codegpt.ui.textarea.lookup.action.HistoryActionItem import ee.carlrobert.codegpt.util.EditorUtil @@ -30,18 +31,18 @@ object TagProcessorFactory { fun getProcessor(project: Project, tagDetails: TagDetails): TagProcessor { return when (tagDetails) { - is FileTagDetails -> FileTagProcessor(tagDetails) + is FileTagDetails -> FileTagProcessor(project, tagDetails) is SelectionTagDetails -> SelectionTagProcessor(project, tagDetails) is EditorSelectionTagDetails -> EditorSelectionTagProcessor(project, tagDetails) is HistoryTagDetails -> ConversationTagProcessor(tagDetails) is DocumentationTagDetails -> DocumentationTagProcessor(tagDetails) is PersonaTagDetails -> PersonaTagProcessor(tagDetails) - is FolderTagDetails -> FolderTagProcessor(tagDetails) + is FolderTagDetails -> FolderTagProcessor(project, tagDetails) is WebTagDetails -> WebTagProcessor() is McpTagDetails -> McpTagProcessor() is GitCommitTagDetails -> GitCommitTagProcessor(project, tagDetails) is CurrentGitChangesTagDetails -> CurrentGitChangesTagProcessor(project) - is EditorTagDetails -> EditorTagProcessor(tagDetails) + is EditorTagDetails -> EditorTagProcessor(project, tagDetails) is ImageTagDetails -> ImageTagProcessor(tagDetails) is EmptyTagDetails -> TagProcessor { _, _ -> } is CodeAnalyzeTagDetails -> TagProcessor { _, _ -> } @@ -51,9 +52,15 @@ object TagProcessorFactory { } class FileTagProcessor( + project: Project, private val tagDetails: FileTagDetails, ) : TagProcessor { + private val settingsService = project.service() + override fun process(message: Message, promptBuilder: StringBuilder) { + if (!settingsService.isVirtualFileVisible(tagDetails.virtualFile)) { + return + } if (message.referencedFilePaths == null) { message.referencedFilePaths = mutableListOf() } @@ -62,10 +69,15 @@ class FileTagProcessor( } class EditorTagProcessor( + project: Project, private val tagDetails: EditorTagDetails, ) : TagProcessor { + private val settingsService = project.service() override fun process(message: Message, promptBuilder: StringBuilder) { + if (!settingsService.isVirtualFileVisible(tagDetails.virtualFile)) { + return + } if (message.referencedFilePaths == null) { message.referencedFilePaths = mutableListOf() } @@ -123,8 +135,11 @@ class PersonaTagProcessor( } class FolderTagProcessor( + project: Project, private val tagDetails: FolderTagDetails, ) : TagProcessor { + private val settingsService = project.service() + override fun process( message: Message, promptBuilder: StringBuilder @@ -138,6 +153,9 @@ class FolderTagProcessor( private fun processFolder(folder: VirtualFile, referencedFilePaths: MutableList) { folder.children.forEach { child -> + if (!settingsService.isVirtualFileVisible(child)) { + return@forEach + } when { child.isDirectory -> processFolder(child, referencedFilePaths) else -> referencedFilePaths.add(child.path) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt index 8275b828..e0b99991 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt @@ -29,6 +29,7 @@ import com.intellij.util.ui.components.BorderLayoutPanel import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.agent.PromptEnhancer +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.settings.configuration.ChatMode import ee.carlrobert.codegpt.settings.models.ModelRegistry import ee.carlrobert.codegpt.settings.service.FeatureType @@ -303,7 +304,7 @@ class UserInputPanel @JvmOverloads constructor( runInEdt { EditorUtil.getSelectedEditor(project)?.let { editor -> if (EditorUtil.hasSelection(editor)) { - tagManager.addTag( + addTag( EditorSelectionTagDetails(editor.virtualFile, editor.selectionModel) ) } @@ -375,7 +376,11 @@ class UserInputPanel @JvmOverloads constructor( } fun includeFiles(referencedFiles: MutableList) { + val settingsService = project.service() referencedFiles.forEach { vf -> + if (!settingsService.isVirtualFileVisible(vf)) { + return@forEach + } if (vf.isDirectory) { userInputHeaderPanel.addTag(FolderTagDetails(vf)) } else { @@ -613,14 +618,6 @@ class UserInputPanel @JvmOverloads constructor( repaint() } - fun setApplyVisible(visible: Boolean) { - userInputHeaderPanel.setApplyVisible(visible) - } - - fun setApplyEnabled(enabled: Boolean) { - userInputHeaderPanel.setApplyEnabled(enabled) - } - fun setThinkingVisible(visible: Boolean, text: String = CodeGPTBundle.get("shared.thinking")) { thinkingLabel.text = text thinkingIcon.isVisible = visible diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt index 88cc0033..ddd5ee50 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/header/UserInputHeaderPanel.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runUndoTransparentWriteAction +import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorKind import com.intellij.openapi.editor.SelectionModel @@ -21,6 +22,7 @@ import com.intellij.util.ui.JBUI import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.EditorNotifier import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.settings.service.FeatureType import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel @@ -114,6 +116,8 @@ class UserInputHeaderPanel( ).apply { isVisible = getMarkdownContent != null } private val backgroundScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val settingsService = project.service() + private var purgingHiddenTags = false init { tagManager.addListener(this) @@ -131,6 +135,9 @@ class UserInputHeaderPanel( } fun addTag(tagDetails: TagDetails) { + if (!isTagVisible(tagDetails)) { + return + } tagManager.addTag(tagDetails) } @@ -230,6 +237,16 @@ class UserInputHeaderPanel( } private fun onTagsChanged() { + if (!purgingHiddenTags) { + val hiddenTags = tagManager.getTags().filterNot(::isTagVisible) + if (hiddenTags.isNotEmpty()) { + purgingHiddenTags = true + hiddenTags.forEach { tagManager.remove(it) } + purgingHiddenTags = false + return + } + } + components.filterIsInstance().forEach { remove(it) } val allTags = tagManager.getTags() @@ -333,7 +350,7 @@ class UserInputHeaderPanel( if (autoTaggingEnabled) { val selectedFile = getSelectedEditor(project)?.virtualFile if (selectedFile != null) { - tagManager.addTag( + addTag( EditorTagDetails( selectedFile, isRemovable = withRemovableSelectedEditorTag @@ -345,7 +362,7 @@ class UserInputHeaderPanel( .filterNot { it == selectedFile } .take(INITIAL_VISIBLE_FILES) .forEach { - tagManager.addTag(EditorTagDetails(it).apply { selected = false }) + addTag(EditorTagDetails(it).apply { selected = false }) } } } @@ -410,7 +427,7 @@ class UserInputHeaderPanel( if (hasSelectionTag) { tagManager.updateSelectionTag(virtualFile, selectionModel) } else { - tagManager.addTag(EditorSelectionTagDetails(virtualFile, selectionModel)) + addTag(EditorSelectionTagDetails(virtualFile, selectionModel)) } } else { tagManager.remove(EditorSelectionTagDetails(virtualFile, selectionModel)) @@ -439,10 +456,10 @@ class UserInputHeaderPanel( .firstOrNull { it.virtualFile == newFile } if (existing == null) { - tagManager.addTag(EditorTagDetails(newFile).apply { selected = false }) + addTag(EditorTagDetails(newFile).apply { selected = false }) } else if (!existing.selected) { tagManager.remove(existing) - tagManager.addTag(EditorTagDetails(newFile).apply { selected = false }) + addTag(EditorTagDetails(newFile).apply { selected = false }) } emptyText.isVisible = false @@ -450,6 +467,17 @@ class UserInputHeaderPanel( } } + private fun isTagVisible(tagDetails: TagDetails): Boolean { + return when (tagDetails) { + is FileTagDetails -> settingsService.isVirtualFileVisible(tagDetails.virtualFile) + is EditorTagDetails -> settingsService.isVirtualFileVisible(tagDetails.virtualFile) + is SelectionTagDetails -> settingsService.isVirtualFileVisible(tagDetails.virtualFile) + is EditorSelectionTagDetails -> settingsService.isVirtualFileVisible(tagDetails.virtualFile) + is FolderTagDetails -> settingsService.isVirtualFileVisible(tagDetails.folder) + else -> true + } + } + private inner class TagPopupMenu : JBPopupMenu() { private fun resolveTagPanel(from: Component): TagPanel? = when (from) { is TagPanel -> from diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/IncludeOpenFilesActionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/IncludeOpenFilesActionItem.kt index 89ea9e29..22d6c104 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/IncludeOpenFilesActionItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/IncludeOpenFilesActionItem.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.ui.textarea.UserInputPanel import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails import ee.carlrobert.codegpt.ui.textarea.lookup.action.AbstractLookupActionItem @@ -17,12 +18,14 @@ class IncludeOpenFilesActionItem : AbstractLookupActionItem() { override fun execute(project: Project, userInputPanel: UserInputPanel) { val fileTags = userInputPanel.getSelectedTags().filterIsInstance() + val settingsService = project.service() project.service().openFiles .filter { openFile -> - fileTags.none { it.virtualFile == openFile } + settingsService.isVirtualFileVisible(openFile) && + fileTags.none { it.virtualFile == openFile } } .forEach { userInputPanel.addTag(FileTagDetails(it)) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt index ed7b79fd..7fe395b1 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FilesGroupItem.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.codeStyle.NameUtil import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager import ee.carlrobert.codegpt.ui.textarea.header.tag.TagUtil @@ -26,6 +27,7 @@ class FilesGroupItem( private val project: Project, private val tagManager: TagManager ) : AbstractLookupGroupItem(), DynamicLookupGroupItem { + private val settingsService = project.service() override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.files.displayName") override val icon = AllIcons.FileTypes.Any_type @@ -33,7 +35,7 @@ class FilesGroupItem( override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) { withContext(Dispatchers.Default) { project.service().iterateContent { - if (!it.isDirectory && !containsTag(it)) { + if (!it.isDirectory && settingsService.isVirtualFileVisible(it) && !containsTag(it)) { val actionItem = FileActionItem(project, it) runInEdt { LookupUtil.addLookupItem(lookup, actionItem) @@ -52,6 +54,7 @@ class FilesGroupItem( projectFileIndex.iterateContent { file -> if (!file.isDirectory && + settingsService.isVirtualFileVisible(file) && !containsTag(file) && (searchText.isEmpty() || matcher.matchingDegree(file.name) != Int.MIN_VALUE) ) { @@ -63,6 +66,7 @@ class FilesGroupItem( val openFiles = project.service().openFiles .filter { projectFileIndex.isInContent(it) && + settingsService.isVirtualFileVisible(it) && !containsTag(it) && (searchText.isEmpty() || matcher.matchingDegree(it.name) != Int.MIN_VALUE) } @@ -80,4 +84,4 @@ class FilesGroupItem( return filter { file -> selectedFileTags.none { it.virtualFile == file } } .map { FileActionItem(project, it) } + listOf(IncludeOpenFilesActionItem()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt index 79622e5e..c3e55b5d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/group/FoldersGroupItem.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.settings.ProxyAISettingsService import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem @@ -20,6 +21,7 @@ class FoldersGroupItem( private val project: Project, private val tagManager: TagManager ) : AbstractLookupGroupItem(), DynamicLookupGroupItem { + private val settingsService = project.service() override val displayName: String = CodeGPTBundle.get("suggestionGroupItem.folders.displayName") override val icon = AllIcons.Nodes.Folder @@ -27,7 +29,11 @@ class FoldersGroupItem( override suspend fun updateLookupList(lookup: LookupImpl, searchText: String) { withContext(Dispatchers.Default) { project.service().iterateContent { - if (it.isDirectory && !it.name.startsWith(".") && !tagManager.containsTag(it)) { + if (it.isDirectory && + settingsService.isVirtualFileVisible(it) && + !it.name.startsWith(".") && + !tagManager.containsTag(it) + ) { runInEdt { LookupUtil.addLookupItem(lookup, FolderActionItem(project, it)) } @@ -52,7 +58,11 @@ class FoldersGroupItem( private suspend fun getProjectFolders(project: Project) = withContext(Dispatchers.IO) { val folders = mutableSetOf() project.service().iterateContent { file: VirtualFile -> - if (file.isDirectory && !file.name.startsWith(".") && !tagManager.containsTag(file)) { + if (file.isDirectory && + settingsService.isVirtualFileVisible(file) && + !file.name.startsWith(".") && + !tagManager.containsTag(file) + ) { val folderPath = file.path if (folders.none { it.path.startsWith(folderPath) }) { folders.removeAll { it.path.startsWith(folderPath) } @@ -63,4 +73,4 @@ class FoldersGroupItem( } folders.toList() } -} \ No newline at end of file +} diff --git a/src/test/kotlin/ee/carlrobert/codegpt/agent/IgnoreRulesTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/agent/IgnoreRulesTest.kt index c0a6fec5..88d69a44 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/agent/IgnoreRulesTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/agent/IgnoreRulesTest.kt @@ -25,7 +25,7 @@ class IgnoreRulesTest : IntegrationTest() { assertThat(result).isInstanceOf(ReadTool.Result.Error::class.java) val error = result as ReadTool.Result.Error - assertThat(error.error).isEqualTo("Access to this path is blocked by .proxyai ignore rules") + assertThat(error.error).isEqualTo("File not found: ${envFile.absolutePath}") } fun testWriteBlockedByIgnoreRule() { @@ -39,7 +39,7 @@ class IgnoreRulesTest : IntegrationTest() { assertThat(result).isInstanceOf(WriteTool.Result.Error::class.java) val error = result as WriteTool.Result.Error - assertThat(error.error).isEqualTo(".proxyai ignore rules block writing to this path") + assertThat(error.error).isEqualTo("File not found: ${pemFile.absolutePath}") } fun testBashBlockedByIgnoreRule() { @@ -83,17 +83,68 @@ class IgnoreRulesTest : IntegrationTest() { assertThat(result).isInstanceOf(ReadTool.Result.Success::class.java) } + fun testReadBlockedWithIgnoreOnlySettingsFile() { + val file = File(project.basePath, "app/src/main/Test.kt").apply { + parentFile.mkdirs() + writeText("secret") + } + writeSettingsRaw("""{"ignore":["app/src/main/**"]}""") + + val result = runBlocking { + ReadTool(project, HookManager(project), "ignore-test") + .execute(ReadTool.Args(file.absolutePath)) + } + + assertThat(result).isInstanceOf(ReadTool.Result.Error::class.java) + val error = result as ReadTool.Result.Error + assertThat(error.error).isEqualTo("File not found: ${file.absolutePath}") + } + + fun testReadBlockedByNestedBuildDirectoryRule() { + val file = File(project.basePath, "app/build/reports/test.txt").apply { + parentFile.mkdirs() + writeText("secret") + } + writeSettings(ignoreEntries = listOf("build/")) + + val result = runBlocking { + ReadTool(project, HookManager(project), "ignore-test") + .execute(ReadTool.Args(file.absolutePath)) + } + + assertThat(result).isInstanceOf(ReadTool.Result.Error::class.java) + val error = result as ReadTool.Result.Error + assertThat(error.error).isEqualTo("File not found: ${file.absolutePath}") + } + + fun testWriteBlockedByNestedExtensionRule() { + val file = File(project.basePath, "app/certs/private.pem") + writeSettings(ignoreEntries = listOf("*.pem")) + + val result = runBlocking { + WriteTool(project, HookManager(project)) + .execute(WriteTool.Args(file.absolutePath, "PRIVATE KEY")) + } + + assertThat(result).isInstanceOf(WriteTool.Result.Error::class.java) + val error = result as WriteTool.Result.Error + assertThat(error.error).isEqualTo("File not found: ${file.absolutePath}") + } + private fun writeSettings(ignoreEntries: List): File { val ignoreJson = ignoreEntries.joinToString(",") { "\"$it\"" } val file = File(project.basePath, ".proxyai/settings.json") file.parentFile.mkdirs() - file.writeText( - """{"ignore":[$ignoreJson],"permissions":{"allow":[],"ask":[],"deny":[]},"hooks":{}}""" - ) - Files.setLastModifiedTime( - file.toPath(), - FileTime.fromMillis(System.currentTimeMillis() + 1000) - ) + file.writeText("""{"ignore":[$ignoreJson],"permissions":{"allow":[],"ask":[],"deny":[]},"hooks":{}}""") + Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(System.currentTimeMillis() + 1000)) + return file + } + + private fun writeSettingsRaw(rawJson: String): File { + val file = File(project.basePath, ".proxyai/settings.json") + file.parentFile.mkdirs() + file.writeText(rawJson) + Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(System.currentTimeMillis() + 1000)) return file } } diff --git a/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt new file mode 100644 index 00000000..67496a21 --- /dev/null +++ b/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt @@ -0,0 +1,107 @@ +package ee.carlrobert.codegpt.ui.textarea + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import ee.carlrobert.codegpt.conversations.message.Message +import ee.carlrobert.codegpt.ui.textarea.header.tag.FolderTagDetails +import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager +import ee.carlrobert.codegpt.ui.textarea.lookup.action.FolderActionItem +import ee.carlrobert.codegpt.ui.textarea.lookup.action.files.FileActionItem +import ee.carlrobert.codegpt.ui.textarea.lookup.group.FilesGroupItem +import ee.carlrobert.codegpt.ui.textarea.lookup.group.FoldersGroupItem +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import testsupport.IntegrationTest +import java.io.File +import java.nio.file.Files +import java.nio.file.attribute.FileTime + +class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() { + + fun `test files group should not suggest ignored files`() { + myFixture.addFileToProject("app/src/main/Hidden.kt", "class Hidden") + myFixture.addFileToProject("app/src/test/Visible.kt", "class Visible") + writeSettings(ignoreEntries = listOf("app/src/main/**")) + val filesGroupItem = FilesGroupItem(project, TagManager()) + + val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("kt") } + .filterIsInstance() + + assertThat(fileSuggestions.map { it.file.path }) + .noneMatch { it.contains("/app/src/main/") } + .anyMatch { it.contains("/app/src/test/Visible.kt") } + } + + fun `test files group should not suggest ignored open files`() { + val ignoredOpenFile = + myFixture.addFileToProject("app/src/main/OpenHidden.kt", "class OpenHidden") + .virtualFile + myFixture.addFileToProject("app/src/test/OpenVisible.kt", "class OpenVisible") + writeSettings(ignoreEntries = listOf("app/src/main/**")) + ApplicationManager.getApplication().invokeAndWait { + FileEditorManager.getInstance(project).openFile(ignoredOpenFile, true) + } + val filesGroupItem = FilesGroupItem(project, TagManager()) + + val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("open") } + .filterIsInstance() + + assertThat(fileSuggestions.map { it.file.path }) + .noneMatch { it.contains("/app/src/main/OpenHidden.kt") } + } + + fun `test folders group should not suggest ignored folders`() { + myFixture.addFileToProject("app/src/main/Hidden.kt", "class Hidden") + myFixture.addFileToProject("app/src/test/Visible.kt", "class Visible") + writeSettings(ignoreEntries = listOf("app/src/main/**")) + val foldersGroupItem = FoldersGroupItem(project, TagManager()) + + val folderSuggestions = runBlocking { foldersGroupItem.getLookupItems("src") } + .filterIsInstance() + .mapNotNull { extractFolderPath(it) } + + assertThat(folderSuggestions) + .noneMatch { it.endsWith("/app/src/main") } + .anyMatch { it.endsWith("/app/src/test") } + } + + fun `test folder tag processor should skip ignored child files`() { + val hiddenFile = + myFixture.addFileToProject("app/src/main/Hidden.kt", "class Hidden").virtualFile + myFixture.addFileToProject("app/src/test/Visible.kt", "class Visible") + writeSettings(ignoreEntries = listOf("app/src/main/**")) + val folderTagProcessor = FolderTagProcessor( + project = project, + tagDetails = FolderTagDetails(hiddenFile.parent.parent.parent.parent) + ) + val message = Message("prompt") + + folderTagProcessor.process(message, StringBuilder()) + + assertThat(message.referencedFilePaths.orEmpty()) + .noneMatch { it.endsWith("/app/src/main/Hidden.kt") } + .anyMatch { it.endsWith("/app/src/test/Visible.kt") } + } + + private fun extractFolderPath(item: FolderActionItem): String? { + return runCatching { + val field = FolderActionItem::class.java.getDeclaredField("folder") + field.isAccessible = true + (field.get(item) as? com.intellij.openapi.vfs.VirtualFile)?.path + }.getOrNull() + } + + private fun writeSettings(ignoreEntries: List): File { + val ignoreJson = ignoreEntries.joinToString(",") { "\"$it\"" } + val file = File(project.basePath, ".proxyai/settings.json") + file.parentFile.mkdirs() + file.writeText( + """{"ignore":[$ignoreJson],"permissions":{"allow":[],"ask":[],"deny":[]},"hooks":{}}""" + ) + Files.setLastModifiedTime( + file.toPath(), + FileTime.fromMillis(System.currentTimeMillis() + 1000) + ) + return file + } +}