feat: context editor improvements (file/folder picker). Target IDE shift (#1224)

- Target IDE build shifted to 262.* max supported (current EAP).
- Added context settings for file/folder picker to be more usable:
File filter settings, max files, max folders, display right options for better views, sorting.
This commit is contained in:
Roman Gromov 2026-05-26 18:55:11 +02:00 committed by GitHub
parent 00b0f62e07
commit 2b802a0879
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 670 additions and 42 deletions

View file

@ -8,7 +8,7 @@ pluginVersion = 3.8.1
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 261
pluginUntilBuild = 261.*
pluginUntilBuild = 262.*
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IC

View file

@ -29,6 +29,7 @@ public class ConfigurationComponent {
private final JBTextField temperatureField;
private final CodeCompletionConfigurationForm codeCompletionForm;
private final ChatCompletionConfigurationForm chatCompletionForm;
private final ContextSuggestionConfigurationForm contextSuggestionForm;
private final ScreenshotConfigurationForm screenshotForm;
public ConfigurationComponent(
@ -75,6 +76,7 @@ public class ConfigurationComponent {
codeCompletionForm = new CodeCompletionConfigurationForm();
chatCompletionForm = new ChatCompletionConfigurationForm();
contextSuggestionForm = new ContextSuggestionConfigurationForm();
screenshotForm = new ScreenshotConfigurationForm();
screenshotForm.loadState(configuration.getScreenshotWatchPaths());
@ -96,6 +98,9 @@ public class ConfigurationComponent {
.addComponent(new TitledSeparator(
CodeGPTBundle.get("configurationConfigurable.section.chatCompletion.title")))
.addComponent(chatCompletionForm.createPanel())
.addComponent(new TitledSeparator(
CodeGPTBundle.get("configurationConfigurable.section.contextSuggestions.title")))
.addComponent(contextSuggestionForm.createPanel())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
@ -114,6 +119,7 @@ public class ConfigurationComponent {
state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected());
state.setCodeCompletionSettings(codeCompletionForm.getFormState());
state.setChatCompletionSettings(chatCompletionForm.getFormState());
state.setContextSuggestionSettings(contextSuggestionForm.getFormState());
var screenshotPaths = screenshotForm.getState();
state.getScreenshotWatchPaths().clear();
@ -132,6 +138,7 @@ public class ConfigurationComponent {
autoFormattingCheckBox.setSelected(configuration.getAutoFormattingEnabled());
codeCompletionForm.resetForm(configuration.getCodeCompletionSettings());
chatCompletionForm.resetForm(configuration.getChatCompletionSettings());
contextSuggestionForm.resetForm(configuration.getContextSuggestionSettings());
screenshotForm.loadState(configuration.getScreenshotWatchPaths());
}

View file

@ -1,6 +1,7 @@
package ee.carlrobert.codegpt.settings.configuration
import com.intellij.openapi.components.*
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState
import kotlin.math.max
@ -36,6 +37,7 @@ class ConfigurationSettingsState : BaseState() {
var tableData by map<String, String>()
var chatCompletionSettings by property(ChatCompletionSettingsState())
var codeCompletionSettings by property(CodeCompletionSettingsState())
var contextSuggestionSettings by property(ContextSuggestionSettingsState())
init {
tableData.putAll(EditorActionsUtil.DEFAULT_ACTIONS)
@ -57,6 +59,59 @@ class ChatCompletionSettingsState : BaseState() {
var sendWithShiftEnter by property(false)
}
object ContextSuggestionSettings {
const val DEFAULT_MAX_FILE_SUGGESTIONS = 50
const val DEFAULT_MAX_DIRECTORY_SUGGESTIONS = 25
const val MAX_FILE_SUGGESTIONS = 500
const val MAX_DIRECTORY_SUGGESTIONS = 70
private const val MIN_SUGGESTIONS = 1
private const val LOOKUP_RESULT_HEADROOM = 32
fun normalizeMaxFileSuggestions(value: Int): Int =
value.coerceIn(MIN_SUGGESTIONS, MAX_FILE_SUGGESTIONS)
fun normalizeMaxDirectorySuggestions(value: Int): Int =
value.coerceIn(MIN_SUGGESTIONS, MAX_DIRECTORY_SUGGESTIONS)
fun maxLookupResults(state: ConfigurationSettingsState = ConfigurationSettings.getState()): Int {
val fileLimit = normalizeMaxFileSuggestions(state.contextSuggestionSettings.maxFileSuggestions)
val directoryLimit =
normalizeMaxDirectorySuggestions(state.contextSuggestionSettings.maxDirectorySuggestions)
return fileLimit + directoryLimit + LOOKUP_RESULT_HEADROOM
}
}
class ContextSuggestionSettingsState : BaseState() {
var maxFileSuggestions by property(ContextSuggestionSettings.DEFAULT_MAX_FILE_SUGGESTIONS)
var maxDirectorySuggestions by property(ContextSuggestionSettings.DEFAULT_MAX_DIRECTORY_SUGGESTIONS)
var blankFileSuggestionMode by enum(ContextSuggestionBlankFileSuggestionMode.OPEN_AND_RECENT)
var fileSortMode by enum(ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER)
var pathDetailsMode by enum(ContextSuggestionPathDetailsMode.FULL_PATH)
}
enum class ContextSuggestionBlankFileSuggestionMode(private val messageKey: String) {
OPEN_AND_RECENT("configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openAndRecent"),
OPEN_RECENT_AND_PROJECT("configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openRecentAndProject");
fun displayName(): String = CodeGPTBundle.get(messageKey)
}
enum class ContextSuggestionFileSortMode(private val messageKey: String) {
FILE_NAME_ASCENDING("configurationConfigurable.section.contextSuggestions.fileSortMode.option.fileNameAscending"),
FOLDER_THEN_FILE_ASCENDING("configurationConfigurable.section.contextSuggestions.fileSortMode.option.folderThenFileAscending"),
PRESERVE_CURRENT_ORDER("configurationConfigurable.section.contextSuggestions.fileSortMode.option.preserveCurrentOrder");
fun displayName(): String = CodeGPTBundle.get(messageKey)
}
enum class ContextSuggestionPathDetailsMode(private val messageKey: String) {
FULL_PATH("configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.fullPath"),
DIRECTORY_ONLY("configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.directoryOnly");
fun displayName(): String = CodeGPTBundle.get(messageKey)
}
class CodeCompletionSettingsState : BaseState() {
var treeSitterProcessingEnabled by property(true)
var gitDiffEnabled by property(true)

View file

@ -0,0 +1,193 @@
package ee.carlrobert.codegpt.settings.configuration
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.EnumComboBoxModel
import com.intellij.ui.components.fields.IntegerField
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
import java.awt.Component
import javax.swing.DefaultListCellRenderer
import javax.swing.JList
class ContextSuggestionConfigurationForm {
private val maxFileSuggestionsField = IntegerField(
"max_file_suggestions",
1,
ContextSuggestionSettings.MAX_FILE_SUGGESTIONS
).apply {
columns = 12
value = service<ConfigurationSettings>().state.contextSuggestionSettings.maxFileSuggestions
}
private val maxDirectorySuggestionsField = IntegerField(
"max_directory_suggestions",
1,
ContextSuggestionSettings.MAX_DIRECTORY_SUGGESTIONS
).apply {
columns = 12
value = service<ConfigurationSettings>().state.contextSuggestionSettings.maxDirectorySuggestions
}
private val blankFileSuggestionModeComboBox =
ComboBox(EnumComboBoxModel(ContextSuggestionBlankFileSuggestionMode::class.java)).apply {
selectedItem = service<ConfigurationSettings>().state.contextSuggestionSettings.blankFileSuggestionMode
renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
val displayValue =
(value as? ContextSuggestionBlankFileSuggestionMode)?.displayName() ?: value
return super.getListCellRendererComponent(
list,
displayValue,
index,
isSelected,
cellHasFocus
)
}
}
}
private val fileSortModeComboBox =
ComboBox(EnumComboBoxModel(ContextSuggestionFileSortMode::class.java)).apply {
selectedItem = service<ConfigurationSettings>().state.contextSuggestionSettings.fileSortMode
renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
val displayValue = (value as? ContextSuggestionFileSortMode)?.displayName() ?: value
return super.getListCellRendererComponent(
list,
displayValue,
index,
isSelected,
cellHasFocus
)
}
}
}
private val pathDetailsModeComboBox =
ComboBox(EnumComboBoxModel(ContextSuggestionPathDetailsMode::class.java)).apply {
selectedItem = service<ConfigurationSettings>().state.contextSuggestionSettings.pathDetailsMode
renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
val displayValue = (value as? ContextSuggestionPathDetailsMode)?.displayName() ?: value
return super.getListCellRendererComponent(
list,
displayValue,
index,
isSelected,
cellHasFocus
)
}
}
}
fun createPanel(): DialogPanel {
return panel {
row {
label(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.maxFileSuggestions.title"
)
)
cell(maxFileSuggestionsField)
.comment(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.maxFileSuggestions.description"
)
)
}
row {
label(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.title"
)
)
cell(maxDirectorySuggestionsField)
.comment(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.description"
)
)
}
row {
label(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.title"
)
)
cell(blankFileSuggestionModeComboBox)
.comment(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.description"
)
)
}
row {
label(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.fileSortMode.title"
)
)
cell(fileSortModeComboBox)
.comment(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.fileSortMode.description"
)
)
}
row {
label(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.pathDetailsMode.title"
)
)
cell(pathDetailsModeComboBox)
.comment(
CodeGPTBundle.get(
"configurationConfigurable.section.contextSuggestions.pathDetailsMode.description"
)
)
}
}.withBorder(JBUI.Borders.emptyLeft(16))
}
fun resetForm(prevState: ContextSuggestionSettingsState) {
maxFileSuggestionsField.value = prevState.maxFileSuggestions
maxDirectorySuggestionsField.value = prevState.maxDirectorySuggestions
blankFileSuggestionModeComboBox.selectedItem = prevState.blankFileSuggestionMode
fileSortModeComboBox.selectedItem = prevState.fileSortMode
pathDetailsModeComboBox.selectedItem = prevState.pathDetailsMode
}
fun getFormState(): ContextSuggestionSettingsState {
return ContextSuggestionSettingsState().apply {
maxFileSuggestions = maxFileSuggestionsField.value
maxDirectorySuggestions = maxDirectorySuggestionsField.value
blankFileSuggestionMode =
blankFileSuggestionModeComboBox.selectedItem as? ContextSuggestionBlankFileSuggestionMode
?: ContextSuggestionBlankFileSuggestionMode.OPEN_AND_RECENT
fileSortMode = fileSortModeComboBox.selectedItem as? ContextSuggestionFileSortMode
?: ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER
pathDetailsMode = pathDetailsModeComboBox.selectedItem as? ContextSuggestionPathDetailsMode
?: ContextSuggestionPathDetailsMode.FULL_PATH
}
}
}

View file

@ -38,6 +38,7 @@ import com.intellij.util.IncorrectOperationException
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys.IS_PROMPT_TEXT_FIELD_DOCUMENT
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
@ -1005,7 +1006,7 @@ class PromptTextField(
) {
val reusableItems = lookupItems
.filterNot { it is LoadingLookupItem || it is StatusLookupItem }
.take(PromptTextFieldConstants.MAX_SEARCH_RESULTS)
.take(ContextSuggestionSettings.maxLookupResults())
if (reusableItems.isEmpty()) {
if (!lookupSearchLoading) {
clearVisibleLookupItems()

View file

@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.ui.textarea
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.MinusculeMatcher
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionSettings
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
@ -202,7 +203,7 @@ class SearchManager(
}
.sortedByDescending { it.second }
.map { it.first }
.take(PromptTextFieldConstants.MAX_SEARCH_RESULTS)
.take(ContextSuggestionSettings.maxLookupResults())
}
private fun createMatcher(searchText: String): MinusculeMatcher {

View file

@ -0,0 +1,33 @@
package ee.carlrobert.codegpt.ui.textarea.lookup.action
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionPathDetailsMode
internal fun contextSuggestionTypeText(project: Project, file: VirtualFile): String? {
val projectDir = project.guessProjectDir()
return when (ConfigurationSettings.getState().contextSuggestionSettings.pathDetailsMode) {
ContextSuggestionPathDetailsMode.FULL_PATH -> projectRelativePath(projectDir, file) ?: file.path
ContextSuggestionPathDetailsMode.DIRECTORY_ONLY -> parentDirectoryPath(projectDir, file)
}
}
private fun parentDirectoryPath(projectDir: VirtualFile?, file: VirtualFile): String? {
val parent = file.parent ?: return null
return if (projectDir != null) {
val relativeParentPath = VfsUtil.getRelativePath(parent, projectDir) ?: return parent.path
if (relativeParentPath.isEmpty()) "." else relativeParentPath
} else {
parent.path
}
}
private fun projectRelativePath(projectDir: VirtualFile?, file: VirtualFile): String? {
if (projectDir == null) {
return null
}
return VfsUtil.getRelativePath(file, projectDir)
}

View file

@ -20,13 +20,7 @@ class FolderActionItem(
override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) {
super.setPresentation(element, presentation)
val projectDir = project.guessProjectDir()
presentation.typeText = if (projectDir != null) {
VfsUtil.getRelativePath(folder, projectDir) ?: folder.path
} else {
folder.path
}
presentation.typeText = contextSuggestionTypeText(project, folder)
presentation.isTypeGrayed = true
}

View file

@ -11,6 +11,7 @@ import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
import ee.carlrobert.codegpt.ui.textarea.FileSearchSource
import ee.carlrobert.codegpt.ui.textarea.header.tag.FileTagDetails
import ee.carlrobert.codegpt.ui.textarea.lookup.action.AbstractLookupActionItem
import ee.carlrobert.codegpt.ui.textarea.lookup.action.contextSuggestionTypeText
class FileActionItem(
private val project: Project,
@ -23,13 +24,7 @@ class FileActionItem(
override fun setPresentation(element: LookupElement, presentation: LookupElementPresentation) {
super.setPresentation(element, presentation)
val projectDir = project.guessProjectDir()
presentation.typeText = if (projectDir != null) {
VfsUtil.getRelativePath(file, projectDir) ?: file.path
} else {
file.path
}
presentation.typeText = contextSuggestionTypeText(project, file)
presentation.isTypeGrayed = true
}

View file

@ -13,6 +13,10 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.codeStyle.MinusculeMatcher
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.settings.ProxyAISettingsService
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionBlankFileSuggestionMode
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionFileSortMode
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionSettings
import ee.carlrobert.codegpt.ui.textarea.FileSearchCandidate
import ee.carlrobert.codegpt.ui.textarea.FileSearchProvider
import ee.carlrobert.codegpt.ui.textarea.FileSearchSource
@ -25,6 +29,7 @@ import ee.carlrobert.codegpt.ui.textarea.lookup.LookupMatchers
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.action.files.IncludeOpenFilesActionItem
import kotlin.math.max
class FilesGroupItem(
private val project: Project,
@ -33,6 +38,12 @@ class FilesGroupItem(
) : AbstractLookupGroupItem(), DynamicLookupGroupItem {
private val settingsService = project.service<ProxyAISettingsService>()
@Volatile
private var cachedProjectFiles: List<VirtualFile> = emptyList()
@Volatile
private var cachedProjectFileStructureModCount: Long = -1L
@Volatile
private var cachedProjectFileRootModCount: Long = -1L
@Volatile
private var cachedFolders: List<VirtualFile> = emptyList()
@Volatile
private var cachedFolderStructureModCount: Long = -1L
@ -45,7 +56,10 @@ class FilesGroupItem(
override suspend fun getLookupItems(searchText: String): List<LookupActionItem> {
val normalizedSearchText = searchText.trim()
val openFiles = getOpenFileCandidates(normalizedSearchText)
val providerMatches = fileSearchProvider.search(normalizedSearchText, MAX_SEARCH_FILES)
val providerMatches = fileSearchProvider.search(
normalizedSearchText,
maxFileSearchCandidates()
)
val visibleProviderMatches = readAction {
val projectFileIndex = project.service<ProjectFileIndex>()
providerMatches.filter { candidate ->
@ -58,7 +72,8 @@ class FilesGroupItem(
val orderedCandidates = if (normalizedSearchText.isEmpty()) {
buildDefaultCandidates(
openFiles = openFiles,
recentFiles = getRecentFileCandidates(normalizedSearchText)
recentFiles = getRecentFileCandidates(normalizedSearchText),
projectFiles = getDefaultProjectFileCandidates()
)
} else {
val recentFiles = if (openFiles.isEmpty()) {
@ -73,15 +88,12 @@ class FilesGroupItem(
}.distinctBy { it.file.path }
}
return orderedCandidates.toFileSuggestions(normalizedSearchText) + folderItems
return sortCandidates(orderedCandidates).toFileSuggestions(normalizedSearchText) + folderItems
}
companion object {
private const val MAX_SEARCH_FILES = 200
private const val MAX_DEFAULT_FILE_SUGGESTIONS = 25
private const val DEFAULT_SEARCH_FILES = 200
private const val MAX_SEARCH_FOLDERS = 200
private const val MAX_DEFAULT_FOLDER_SUGGESTIONS = 10
private const val MAX_OPEN_FILE_SUGGESTIONS = 15
}
private fun createMatcher(searchText: String): MinusculeMatcher {
@ -115,7 +127,7 @@ class FilesGroupItem(
.filter { file ->
matchingDegree(file, projectFileIndex, matcher) != Int.MIN_VALUE
}
.take(MAX_SEARCH_FILES)
.take(maxFileSearchCandidates())
.map { file ->
FileSearchCandidate(
file = file,
@ -128,24 +140,60 @@ class FilesGroupItem(
private fun buildDefaultCandidates(
openFiles: List<FileSearchCandidate>,
recentFiles: List<FileSearchCandidate>
recentFiles: List<FileSearchCandidate>,
projectFiles: List<FileSearchCandidate>
): List<FileSearchCandidate> {
val prioritizedOpenFiles = openFiles.take(MAX_OPEN_FILE_SUGGESTIONS)
val openFilePaths = prioritizedOpenFiles.mapTo(mutableSetOf()) { it.file.path }
val recentBackfill = recentFiles
.filterNot { it.file.path in openFilePaths }
.take(MAX_DEFAULT_FILE_SUGGESTIONS - prioritizedOpenFiles.size)
val baseCandidates = when (blankFileSuggestionMode()) {
ContextSuggestionBlankFileSuggestionMode.OPEN_AND_RECENT ->
openFiles + recentFiles
return prioritizedOpenFiles + recentBackfill
ContextSuggestionBlankFileSuggestionMode.OPEN_RECENT_AND_PROJECT ->
openFiles + recentFiles + projectFiles
}
if (fileSortMode() != ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER) {
return baseCandidates.distinctBy { it.file.path }
}
val maxFileSuggestions = maxFileSuggestions()
val prioritizedOpenFiles = openFiles.take(maxFileSuggestions)
val selectedFilePaths = prioritizedOpenFiles.mapTo(mutableSetOf()) { it.file.path }
val recentBackfill = recentFiles
.filterNot { it.file.path in selectedFilePaths }
.take((maxFileSuggestions - prioritizedOpenFiles.size).coerceAtLeast(0))
selectedFilePaths.addAll(recentBackfill.map { it.file.path })
val projectBackfill = if (
blankFileSuggestionMode() == ContextSuggestionBlankFileSuggestionMode.OPEN_RECENT_AND_PROJECT
) {
projectFiles
.filterNot { it.file.path in selectedFilePaths }
.take((maxFileSuggestions - prioritizedOpenFiles.size - recentBackfill.size).coerceAtLeast(0))
} else {
emptyList()
}
return prioritizedOpenFiles + recentBackfill + projectBackfill
}
private suspend fun getDefaultProjectFileCandidates(): List<FileSearchCandidate> {
return readAction {
val projectFileIndex = project.service<ProjectFileIndex>()
getProjectFiles(projectFileIndex)
.asSequence()
.filter { file -> isVisibleProjectFile(file, projectFileIndex) }
.map { file ->
FileSearchCandidate(
file = file,
source = FileSearchSource.NATIVE
)
}
.toList()
}
}
private suspend fun getFolderSuggestions(searchText: String): List<FolderActionItem> {
val matcher = createMatcher(searchText)
val resultLimit = if (searchText.isEmpty()) {
MAX_DEFAULT_FOLDER_SUGGESTIONS
} else {
MAX_SEARCH_FOLDERS
}
val resultLimit = minOf(maxDirectorySuggestions(), MAX_SEARCH_FOLDERS)
return readAction {
val projectFileIndex = project.service<ProjectFileIndex>()
@ -236,7 +284,7 @@ class FilesGroupItem(
}
private fun Iterable<FileSearchCandidate>.toFileSuggestions(searchText: String): List<LookupActionItem> {
val fileItems = map { candidate ->
val fileItems = take(maxFileSuggestions()).map { candidate ->
FileActionItem(project, candidate.file, candidate.source)
}
@ -272,4 +320,72 @@ class FilesGroupItem(
cachedFolderRootModCount = rootModCount
return folders
}
@Synchronized
private fun getProjectFiles(projectFileIndex: ProjectFileIndex): List<VirtualFile> {
val structureModCount = VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS.modificationCount
val rootModCount = ProjectRootManager.getInstance(project).modificationCount
if (cachedProjectFileStructureModCount == structureModCount &&
cachedProjectFileRootModCount == rootModCount
) {
return cachedProjectFiles
}
val files = mutableListOf<VirtualFile>()
projectFileIndex.iterateContent { file ->
if (file.isValid && !file.isDirectory) {
files += file
}
true
}
cachedProjectFiles = files
cachedProjectFileStructureModCount = structureModCount
cachedProjectFileRootModCount = rootModCount
return files
}
private fun maxFileSuggestions(): Int {
return ContextSuggestionSettings.normalizeMaxFileSuggestions(
ConfigurationSettings.getState().contextSuggestionSettings.maxFileSuggestions
)
}
private fun maxDirectorySuggestions(): Int {
return ContextSuggestionSettings.normalizeMaxDirectorySuggestions(
ConfigurationSettings.getState().contextSuggestionSettings.maxDirectorySuggestions
)
}
private fun maxFileSearchCandidates(): Int {
return max(DEFAULT_SEARCH_FILES, maxFileSuggestions())
}
private fun blankFileSuggestionMode(): ContextSuggestionBlankFileSuggestionMode {
return ConfigurationSettings.getState().contextSuggestionSettings.blankFileSuggestionMode
}
private fun fileSortMode(): ContextSuggestionFileSortMode {
return ConfigurationSettings.getState().contextSuggestionSettings.fileSortMode
}
private fun sortCandidates(candidates: List<FileSearchCandidate>): List<FileSearchCandidate> {
return when (fileSortMode()) {
ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER -> candidates
ContextSuggestionFileSortMode.FILE_NAME_ASCENDING ->
candidates.sortedWith(
compareBy<FileSearchCandidate, String>(String.CASE_INSENSITIVE_ORDER) {
it.file.name
}.thenBy(String.CASE_INSENSITIVE_ORDER) { it.file.path }
)
ContextSuggestionFileSortMode.FOLDER_THEN_FILE_ASCENDING ->
candidates.sortedWith(
compareBy<FileSearchCandidate, String>(String.CASE_INSENSITIVE_ORDER) {
it.file.parent?.path.orEmpty()
}
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.file.name }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.file.path }
)
}
}
}

View file

@ -173,6 +173,24 @@ configurationConfigurable.section.chatCompletion.clickableLinks.title=Show click
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
configurationConfigurable.section.chatCompletion.sendMessageShortcut.title=Send Message Shortcut
configurationConfigurable.section.chatCompletion.sendMessageShortcut.description=If none are selected, 'Enter' by itself sends the message. If any are selected, the chosen shortcut plus 'Enter' sends the message and 'Enter' by itself inserts a newline.
configurationConfigurable.section.contextSuggestions.title=Context Suggestions
configurationConfigurable.section.contextSuggestions.maxFileSuggestions.title=Maximum file suggestions:
configurationConfigurable.section.contextSuggestions.maxFileSuggestions.description=Controls how many file suggestions are shown in the @-context picker for chat and agent prompts. Default: 50. Range: 1-500.
configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.title=Maximum directory suggestions:
configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.description=Controls how many directory suggestions are shown in the @-context picker for chat and agent prompts. Default: 25. Range: 1-70.
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.title=Default file suggestion scope:
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.description=Controls what is shown in the @-context picker before you type a file name. Default: Open and recent files.
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openAndRecent=Open and recent files
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openRecentAndProject=Open and recent files + project files
configurationConfigurable.section.contextSuggestions.fileSortMode.title=File list sorting:
configurationConfigurable.section.contextSuggestions.fileSortMode.description=Controls how file suggestions are ordered in the @-context picker for chat and agent prompts. Default: Preserve current order.
configurationConfigurable.section.contextSuggestions.fileSortMode.option.fileNameAscending=Files alphabetically
configurationConfigurable.section.contextSuggestions.fileSortMode.option.folderThenFileAscending=Folder -> files alphabetically
configurationConfigurable.section.contextSuggestions.fileSortMode.option.preserveCurrentOrder=Preserve current order
configurationConfigurable.section.contextSuggestions.pathDetailsMode.title=Path details format:
configurationConfigurable.section.contextSuggestions.pathDetailsMode.description=Controls what is shown on the right side of file and directory suggestions in the @-context picker. Default: Full path.
configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.fullPath=Full path
configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.directoryOnly=Directory only
settingsConfigurable.service.llama.predefinedModel.comment=Download and use vetted models from HuggingFace.
settingsConfigurable.service.llama.customModel.comment=Use your own GGUF model file from a local path on your computer.
settingsConfigurable.service.custom.openai.testConnection.label=Test Connection

View file

@ -168,6 +168,24 @@ configurationConfigurable.section.chatCompletion.psiStructure.analyzeDepth.comme
configurationConfigurable.section.chatCompletion.psiStructure.description=\u5982\u679C\u542F\u7528\uFF0C\u9644\u52A0\u6587\u4EF6\u5BFC\u5165\u4E2D\u5B58\u5728\u7684\u7C7B\u7ED3\u6784\u5C06\u88AB\u6DFB\u52A0\u5230\u5BF9\u8BDD\u7684\u4E0A\u4E0B\u6587\u4E2D\u3002\u7ED3\u6784\u6307\u7684\u662F\u6587\u4EF6\u4E2D\u5305\u542B\u6784\u9020\u51FD\u6570\u3001\u5B57\u6BB5\u548C\u65B9\u6CD5\u7684\u6E90\u4EE3\u7801\uFF0C\u5305\u62EC\u6240\u6709\u4FEE\u9970\u7B26\u3001\u53C2\u6570\u548C\u8FD4\u56DE\u7C7B\u578B\uFF0C\u4F46\u4E0D\u5305\u62EC\u5B9E\u73B0\u3002\u4E3A\u4E86\u5728\u9AD8\u8D28\u91CF\u804A\u5929\u4E0A\u4E0B\u6587\u548C\u8282\u7701\u6807\u8BB0\u4E4B\u95F4\u627E\u5230\u5E73\u8861\uFF0C\u6545\u610F\u6392\u9664\u4E86\u4F9D\u8D56\u7684\u5B9E\u73B0\u3002
configurationConfigurable.section.chatCompletion.clickableLinks.title=Show clickable links for classes and methods
configurationConfigurable.section.chatCompletion.clickableLinks.description=If enabled, code references in answers become clickable so you can jump to them in your IDE.
configurationConfigurable.section.contextSuggestions.title=\u4E0A\u4E0B\u6587\u5EFA\u8BAE
configurationConfigurable.section.contextSuggestions.maxFileSuggestions.title=\u6700\u5927\u6587\u4EF6\u5EFA\u8BAE\u6570:
configurationConfigurable.section.contextSuggestions.maxFileSuggestions.description=\u63A7\u5236\u5728 chat \u548C agent \u63D0\u793A\u7684 @ \u4E0A\u4E0B\u6587\u9009\u62E9\u5668\u4E2D\u663E\u793A\u591A\u5C11\u6587\u4EF6\u5EFA\u8BAE\u3002\u9ED8\u8BA4\u503C: 50\u3002\u8303\u56F4: 1-500\u3002
configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.title=\u6700\u5927\u76EE\u5F55\u5EFA\u8BAE\u6570:
configurationConfigurable.section.contextSuggestions.maxDirectorySuggestions.description=\u63A7\u5236\u5728 chat \u548C agent \u63D0\u793A\u7684 @ \u4E0A\u4E0B\u6587\u9009\u62E9\u5668\u4E2D\u663E\u793A\u591A\u5C11\u76EE\u5F55\u5EFA\u8BAE\u3002\u9ED8\u8BA4\u503C: 25\u3002\u8303\u56F4: 1-70\u3002
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.title=\u9ED8\u8BA4\u6587\u4EF6\u5EFA\u8BAE\u8303\u56F4:
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.description=\u63A7\u5236\u5728\u60A8\u8FD8\u6CA1\u6709\u8F93\u5165\u6587\u4EF6\u540D\u65F6\uFF0C@ \u4E0A\u4E0B\u6587\u9009\u62E9\u5668\u4E2D\u663E\u793A\u4EC0\u4E48\u5185\u5BB9\u3002\u9ED8\u8BA4\u503C: \u6253\u5F00\u548C\u6700\u8FD1\u4F7F\u7528\u7684\u6587\u4EF6\u3002
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openAndRecent=\u6253\u5F00\u548C\u6700\u8FD1\u4F7F\u7528\u7684\u6587\u4EF6
configurationConfigurable.section.contextSuggestions.blankFileSuggestionMode.option.openRecentAndProject=\u6253\u5F00\u548C\u6700\u8FD1\u4F7F\u7528\u7684\u6587\u4EF6 + \u9879\u76EE\u6587\u4EF6
configurationConfigurable.section.contextSuggestions.fileSortMode.title=\u6587\u4EF6\u5217\u8868\u6392\u5E8F:
configurationConfigurable.section.contextSuggestions.fileSortMode.description=\u63A7\u5236\u5728 chat \u548C agent \u63D0\u793A\u7684 @ \u4E0A\u4E0B\u6587\u9009\u62E9\u5668\u4E2D\u5982\u4F55\u6392\u5217\u6587\u4EF6\u5EFA\u8BAE\u3002\u9ED8\u8BA4\u503C: \u4FDD\u6301\u5F53\u524D\u987A\u5E8F\u3002
configurationConfigurable.section.contextSuggestions.fileSortMode.option.fileNameAscending=\u6309\u6587\u4EF6\u540D\u5B57\u6BCD\u6392\u5E8F
configurationConfigurable.section.contextSuggestions.fileSortMode.option.folderThenFileAscending=\u6587\u4EF6\u5939 -> \u6587\u4EF6\u6309\u5B57\u6BCD\u6392\u5E8F
configurationConfigurable.section.contextSuggestions.fileSortMode.option.preserveCurrentOrder=\u4FDD\u6301\u5F53\u524D\u987A\u5E8F
configurationConfigurable.section.contextSuggestions.pathDetailsMode.title=\u8DEF\u5F84\u8BE6\u60C5\u683C\u5F0F:
configurationConfigurable.section.contextSuggestions.pathDetailsMode.description=\u63A7\u5236\u5728 chat \u548C agent \u63D0\u793A\u7684 @ \u4E0A\u4E0B\u6587\u9009\u62E9\u5668\u4E2D\uFF0C\u6587\u4EF6\u548C\u76EE\u5F55\u5EFA\u8BAE\u53F3\u4FA7\u663E\u793A\u4EC0\u4E48\u8DEF\u5F84\u4FE1\u606F\u3002\u9ED8\u8BA4\u503C: \u5B8C\u6574\u8DEF\u5F84\u3002
configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.fullPath=\u5B8C\u6574\u8DEF\u5F84
configurationConfigurable.section.contextSuggestions.pathDetailsMode.option.directoryOnly=\u4EC5\u76EE\u5F55
settingsConfigurable.service.llama.predefinedModel.comment=\u4ECEHuggingFace\u4E0B\u8F7D\u5E76\u4F7F\u7528\u7ECF\u8FC7\u5BA1\u67E5\u7684\u6A21\u578B\u3002
settingsConfigurable.service.llama.customModel.comment=\u4F7F\u7528\u60A8\u8BA1\u7B97\u673A\u4E0A\u672C\u5730\u8DEF\u5F84\u4E2D\u7684GGUF\u6A21\u578B\u6587\u4EF6\u3002
settingsConfigurable.service.custom.openai.testConnection.label=\u6D4B\u8BD5\u8FDE\u63A5

View file

@ -0,0 +1,46 @@
package ee.carlrobert.codegpt.settings.configuration
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class ContextSuggestionSettingsTest {
@Test
fun `defaults use the new production limits`() {
val state = ContextSuggestionSettingsState()
assertThat(state.maxFileSuggestions)
.isEqualTo(ContextSuggestionSettings.DEFAULT_MAX_FILE_SUGGESTIONS)
assertThat(state.maxDirectorySuggestions)
.isEqualTo(ContextSuggestionSettings.DEFAULT_MAX_DIRECTORY_SUGGESTIONS)
assertThat(state.blankFileSuggestionMode)
.isEqualTo(ContextSuggestionBlankFileSuggestionMode.OPEN_AND_RECENT)
assertThat(state.fileSortMode)
.isEqualTo(ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER)
assertThat(state.pathDetailsMode)
.isEqualTo(ContextSuggestionPathDetailsMode.FULL_PATH)
}
@Test
fun `normalization clamps values to configured bounds`() {
assertThat(ContextSuggestionSettings.normalizeMaxFileSuggestions(999))
.isEqualTo(ContextSuggestionSettings.MAX_FILE_SUGGESTIONS)
assertThat(ContextSuggestionSettings.normalizeMaxDirectorySuggestions(999))
.isEqualTo(ContextSuggestionSettings.MAX_DIRECTORY_SUGGESTIONS)
assertThat(ContextSuggestionSettings.normalizeMaxFileSuggestions(-5)).isEqualTo(1)
assertThat(ContextSuggestionSettings.normalizeMaxDirectorySuggestions(0)).isEqualTo(1)
}
@Test
fun `lookup result cap leaves headroom above configured file and directory limits`() {
val state = ConfigurationSettingsState().apply {
contextSuggestionSettings = ContextSuggestionSettingsState().apply {
maxFileSuggestions = ContextSuggestionSettings.MAX_FILE_SUGGESTIONS
maxDirectorySuggestions = ContextSuggestionSettings.MAX_DIRECTORY_SUGGESTIONS
}
}
assertThat(ContextSuggestionSettings.maxLookupResults(state)).isEqualTo(602)
}
}

View file

@ -3,7 +3,12 @@ package ee.carlrobert.codegpt.ui.textarea
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.codeInsight.lookup.LookupElementPresentation
import ee.carlrobert.codegpt.conversations.message.Message
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionBlankFileSuggestionMode
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionFileSortMode
import ee.carlrobert.codegpt.settings.configuration.ContextSuggestionPathDetailsMode
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
@ -79,7 +84,7 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
assertThat(fileSuggestions[1].source).isEqualTo(FileSearchSource.RECENT)
}
fun `test files group should cap blank suggestions at 15 open files and backfill to 25 with recent files`() {
fun `test files group should cap blank suggestions at configured max and backfill with project files`() {
val openProjectFiles = (1..20).map { index ->
myFixture.addFileToProject("app/src/test/Open$index.kt", "class Open$index").virtualFile
}
@ -89,6 +94,47 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
"class Recent$index"
).virtualFile
}
(1..120).forEach { index ->
myFixture.addFileToProject(
"app/src/test/Project$index.kt",
"class Project$index"
)
}
recentFiles.forEach { file -> openThenCloseFiles(file) }
openFiles(*openProjectFiles.toTypedArray())
withBlankFileSuggestionMode(ContextSuggestionBlankFileSuggestionMode.OPEN_RECENT_AND_PROJECT) {
val filesGroupItem = FilesGroupItem(project, TagManager())
val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("") }
.filterIsInstance<FileActionItem>()
assertThat(fileSuggestions).hasSize(50)
assertThat(fileSuggestions.take(20).map { it.source })
.allMatch { it == FileSearchSource.OPEN }
assertThat(fileSuggestions.drop(20).take(20).map { it.source })
.allMatch { it == FileSearchSource.RECENT }
assertThat(fileSuggestions.drop(40).map { it.source })
.allMatch { it == FileSearchSource.NATIVE }
}
}
fun `test default blank suggestions should only show open and recent files`() {
val openProjectFiles = (1..20).map { index ->
myFixture.addFileToProject("defaultscope/Open$index.kt", "class Open$index").virtualFile
}
val recentFiles = (1..20).map { index ->
myFixture.addFileToProject(
"defaultscope/Recent$index.kt",
"class Recent$index"
).virtualFile
}
(1..120).forEach { index ->
myFixture.addFileToProject(
"defaultscope/Project$index.kt",
"class Project$index"
)
}
recentFiles.forEach { file -> openThenCloseFiles(file) }
openFiles(*openProjectFiles.toTypedArray())
@ -97,13 +143,76 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("") }
.filterIsInstance<FileActionItem>()
assertThat(fileSuggestions).hasSize(25)
assertThat(fileSuggestions.take(15).map { it.source })
assertThat(fileSuggestions).hasSize(40)
assertThat(fileSuggestions.take(20).map { it.source })
.allMatch { it == FileSearchSource.OPEN }
assertThat(fileSuggestions.drop(15).map { it.source })
assertThat(fileSuggestions.drop(20).map { it.source })
.allMatch { it == FileSearchSource.RECENT }
}
fun `test files group should sort files alphabetically by file name when configured`() {
myFixture.addFileToProject("sorting/zeta/SortBeta.kt", "class SortBeta")
myFixture.addFileToProject("sorting/alpha/SortAlpha.kt", "class SortAlpha")
myFixture.addFileToProject("sorting/beta/SortGamma.kt", "class SortGamma")
withFileSortMode(ContextSuggestionFileSortMode.FILE_NAME_ASCENDING) {
val filesGroupItem = FilesGroupItem(project, TagManager())
val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("Sort") }
.filterIsInstance<FileActionItem>()
assertThat(fileSuggestions.take(3).map { it.file.name })
.containsExactly("SortAlpha.kt", "SortBeta.kt", "SortGamma.kt")
}
}
fun `test files group should sort files by folder then file name when configured`() {
myFixture.addFileToProject("sorting/zeta/FolderSortAlpha.kt", "class FolderSortAlpha")
myFixture.addFileToProject("sorting/alpha/FolderSortGamma.kt", "class FolderSortGamma")
myFixture.addFileToProject("sorting/alpha/FolderSortBeta.kt", "class FolderSortBeta")
withFileSortMode(ContextSuggestionFileSortMode.FOLDER_THEN_FILE_ASCENDING) {
val filesGroupItem = FilesGroupItem(project, TagManager())
val fileSuggestions = runBlocking { filesGroupItem.getLookupItems("FolderSort") }
.filterIsInstance<FileActionItem>()
assertThat(fileSuggestions.take(3).map { it.file.path.substringAfter("/sorting/") })
.containsExactly(
"alpha/FolderSortBeta.kt",
"alpha/FolderSortGamma.kt",
"zeta/FolderSortAlpha.kt"
)
}
}
fun `test file suggestion can show directory only in path details`() {
val file =
myFixture.addFileToProject("display/path/FileDisplay.kt", "class FileDisplay").virtualFile
withPathDetailsMode(ContextSuggestionPathDetailsMode.DIRECTORY_ONLY) {
val presentation = LookupElementPresentation()
FileActionItem(project, file).createLookupElement("", null).renderElement(presentation)
assertThat(presentation.itemText).isEqualTo("FileDisplay.kt")
assertThat(presentation.typeText).isEqualTo("display/path")
}
}
fun `test folder suggestion can show parent directory only in path details`() {
val folder =
myFixture.addFileToProject("display/folder/Inner/Visible.kt", "class Visible")
.virtualFile.parent
withPathDetailsMode(ContextSuggestionPathDetailsMode.DIRECTORY_ONLY) {
val presentation = LookupElementPresentation()
FolderActionItem(project, folder).createLookupElement("", null).renderElement(presentation)
assertThat(presentation.itemText).isEqualTo("Inner")
assertThat(presentation.typeText).isEqualTo("display/folder")
}
}
fun `test files group typed search should include closed project files even when files are open`() {
val openFile =
myFixture.addFileToProject("app/src/test/OpenDocument.kt", "class OpenDocument")
@ -317,4 +426,46 @@ class IgnoreRulesTagManagerIntegrationTest : IntegrationTest() {
)
return file
}
private fun withFileSortMode(
sortMode: ContextSuggestionFileSortMode,
block: () -> Unit
) {
val settings = ConfigurationSettings.getState().contextSuggestionSettings
val previousSortMode = settings.fileSortMode
try {
settings.fileSortMode = sortMode
block()
} finally {
settings.fileSortMode = previousSortMode
}
}
private fun withBlankFileSuggestionMode(
blankFileSuggestionMode: ContextSuggestionBlankFileSuggestionMode,
block: () -> Unit
) {
val settings = ConfigurationSettings.getState().contextSuggestionSettings
val previousBlankFileSuggestionMode = settings.blankFileSuggestionMode
try {
settings.blankFileSuggestionMode = blankFileSuggestionMode
block()
} finally {
settings.blankFileSuggestionMode = previousBlankFileSuggestionMode
}
}
private fun withPathDetailsMode(
pathDetailsMode: ContextSuggestionPathDetailsMode,
block: () -> Unit
) {
val settings = ConfigurationSettings.getState().contextSuggestionSettings
val previousPathDetailsMode = settings.pathDetailsMode
try {
settings.pathDetailsMode = pathDetailsMode
block()
} finally {
settings.pathDetailsMode = previousPathDetailsMode
}
}
}