mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-19 16:28:46 +00:00
fix: respect ignorelist when dealing with any search/read operations (relates #1126)
This commit is contained in:
parent
b456b53752
commit
6a7107c6e7
18 changed files with 446 additions and 136 deletions
|
|
@ -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<VirtualFile> 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<VirtualFile> 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;
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<ReferencedFile> getReferencedFiles(List<? extends TagDetails> 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<VirtualFile> collectVisibleFiles(
|
||||
List<VirtualFile> inputFiles,
|
||||
ProxyAISettingsService settingsService) {
|
||||
var visibleFiles = new LinkedHashSet<VirtualFile>();
|
||||
inputFiles.forEach(file -> appendVisibleFiles(file, settingsService, visibleFiles));
|
||||
return visibleFiles.stream().toList();
|
||||
}
|
||||
|
||||
private void appendVisibleFiles(
|
||||
VirtualFile file,
|
||||
ProxyAISettingsService settingsService,
|
||||
LinkedHashSet<VirtualFile> 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<UUID> getConversationHistoryIds(List<? extends TagDetails> tags) {
|
||||
return tags.stream()
|
||||
.map(it -> {
|
||||
|
|
@ -383,10 +415,14 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
}
|
||||
|
||||
public void includeFiles(List<VirtualFile> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<VirtualFile> getReferencedFiles() {
|
||||
var checkedNodes = getCheckedNodes(VirtualFile.class, Objects::nonNull);
|
||||
if (checkedNodes.length > 1024) {
|
||||
var files = new LinkedHashSet<VirtualFile>();
|
||||
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<VirtualFile> 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;
|
||||
|
|
|
|||
|
|
@ -607,6 +607,8 @@ class BashTool(
|
|||
lastWasReader = tokenIsReader
|
||||
}
|
||||
val settingsService = project.service<ProxyAISettingsService>()
|
||||
// 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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("\\", "/")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<McpSessionManager>()
|
||||
private val pendingApprovals = ConcurrentHashMap<String, CompletableFuture<Boolean>>()
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<CachedSettings?>(null)
|
||||
private val isWindows = System.getProperty("os.name")?.lowercase()?.contains("windows") == true
|
||||
|
||||
fun getSubagents(): List<ProxyAISubagent> {
|
||||
|
|
@ -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<String> {
|
||||
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<String>,
|
||||
val permissions: Permissions,
|
||||
val ignore: List<String> = DEFAULT_IGNORE_PATTERNS,
|
||||
val permissions: Permissions = Permissions(allow = DEFAULT_ALLOW_RULES),
|
||||
val subagents: List<ProxyAISubagent> = 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()
|
||||
|
|
|
|||
|
|
@ -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<ProxyAISettingsService>()
|
||||
|
||||
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<ProxyAISettingsService>()
|
||||
|
||||
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<ProxyAISettingsService>()
|
||||
|
||||
override fun process(
|
||||
message: Message,
|
||||
promptBuilder: StringBuilder
|
||||
|
|
@ -138,6 +153,9 @@ class FolderTagProcessor(
|
|||
|
||||
private fun processFolder(folder: VirtualFile, referencedFilePaths: MutableList<String>) {
|
||||
folder.children.forEach { child ->
|
||||
if (!settingsService.isVirtualFileVisible(child)) {
|
||||
return@forEach
|
||||
}
|
||||
when {
|
||||
child.isDirectory -> processFolder(child, referencedFilePaths)
|
||||
else -> referencedFilePaths.add(child.path)
|
||||
|
|
|
|||
|
|
@ -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<VirtualFile>) {
|
||||
val settingsService = project.service<ProxyAISettingsService>()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<ProxyAISettingsService>()
|
||||
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<TagPanel>().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
|
||||
|
|
|
|||
|
|
@ -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<FileTagDetails>()
|
||||
val settingsService = project.service<ProxyAISettingsService>()
|
||||
project.service<FileEditorManager>().openFiles
|
||||
.filter { openFile ->
|
||||
fileTags.none { it.virtualFile == openFile }
|
||||
settingsService.isVirtualFileVisible(openFile) &&
|
||||
fileTags.none { it.virtualFile == openFile }
|
||||
}
|
||||
.forEach {
|
||||
userInputPanel.addTag(FileTagDetails(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ProxyAISettingsService>()
|
||||
|
||||
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<ProjectFileIndex>().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<FileEditorManager>().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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ProxyAISettingsService>()
|
||||
|
||||
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<ProjectFileIndex>().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<VirtualFile>()
|
||||
project.service<ProjectFileIndex>().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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>): 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FileActionItem>()
|
||||
|
||||
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<FileActionItem>()
|
||||
|
||||
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<FolderActionItem>()
|
||||
.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<String>): 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue