From 689fe17d8cca8002bf2ca7f816d1925101d80734 Mon Sep 17 00:00:00 2001 From: Roman Gromov <35900229+sapphirepro@users.noreply.github.com> Date: Sun, 17 May 2026 03:20:24 +0200 Subject: [PATCH] Context editor improvements (file/folder picker). Target IDE shift - 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. --- gradle.properties | 2 +- .../configuration/ConfigurationComponent.java | 7 + .../configuration/ConfigurationSettings.kt | 55 +++++ .../ContextSuggestionConfigurationForm.kt | 193 ++++++++++++++++++ .../codegpt/ui/textarea/PromptTextField.kt | 3 +- .../codegpt/ui/textarea/SearchManager.kt | 3 +- .../ContextSuggestionPathPresentation.kt | 33 +++ .../lookup/action/FolderActionItem.kt | 8 +- .../lookup/action/files/FileActionItem.kt | 9 +- .../textarea/lookup/group/FilesGroupItem.kt | 156 ++++++++++++-- .../resources/messages/codegpt.properties | 18 ++ .../resources/messages/codegpt_zh.properties | 18 ++ .../ContextSuggestionSettingsTest.kt | 46 +++++ .../IgnoreRulesTagManagerIntegrationTest.kt | 159 ++++++++++++++- 14 files changed, 669 insertions(+), 41 deletions(-) create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionConfigurationForm.kt create mode 100644 src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/ContextSuggestionPathPresentation.kt create mode 100644 src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionSettingsTest.kt diff --git a/gradle.properties b/gradle.properties index 3f7864f67..04a867ffe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java index f11cfe16b..dd30c290a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java @@ -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 void changedUpdate(DocumentEvent e) { codeCompletionForm = new CodeCompletionConfigurationForm(); chatCompletionForm = new ChatCompletionConfigurationForm(); + contextSuggestionForm = new ContextSuggestionConfigurationForm(); screenshotForm = new ScreenshotConfigurationForm(); screenshotForm.loadState(configuration.getScreenshotWatchPaths()); @@ -96,6 +98,9 @@ public void changedUpdate(DocumentEvent e) { .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 ConfigurationSettingsState getCurrentFormState() { 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 void resetForm() { autoFormattingCheckBox.setSelected(configuration.getAutoFormattingEnabled()); codeCompletionForm.resetForm(configuration.getCodeCompletionSettings()); chatCompletionForm.resetForm(configuration.getChatCompletionSettings()); + contextSuggestionForm.resetForm(configuration.getContextSuggestionSettings()); screenshotForm.loadState(configuration.getScreenshotWatchPaths()); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt index 8e7e33973..f66283d15 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt @@ -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() 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) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionConfigurationForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionConfigurationForm.kt new file mode 100644 index 000000000..d0cbaafad --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionConfigurationForm.kt @@ -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().state.contextSuggestionSettings.maxFileSuggestions + } + + private val maxDirectorySuggestionsField = IntegerField( + "max_directory_suggestions", + 1, + ContextSuggestionSettings.MAX_DIRECTORY_SUGGESTIONS + ).apply { + columns = 12 + value = service().state.contextSuggestionSettings.maxDirectorySuggestions + } + private val blankFileSuggestionModeComboBox = + ComboBox(EnumComboBoxModel(ContextSuggestionBlankFileSuggestionMode::class.java)).apply { + selectedItem = service().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().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().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 + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index 02e34a1c5..052722f23 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -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() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt index b4ea92ea4..b505f5738 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/SearchManager.kt @@ -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 { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/ContextSuggestionPathPresentation.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/ContextSuggestionPathPresentation.kt new file mode 100644 index 000000000..ff8e7b218 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/ContextSuggestionPathPresentation.kt @@ -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) +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/FolderActionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/FolderActionItem.kt index d602dccff..5037c4e9c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/FolderActionItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/FolderActionItem.kt @@ -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 } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/FileActionItem.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/FileActionItem.kt index 53aa7f4b6..b50e40777 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/FileActionItem.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/lookup/action/files/FileActionItem.kt @@ -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 } 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 813504ed5..7dcf711bf 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 @@ -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() @Volatile + private var cachedProjectFiles: List = emptyList() + @Volatile + private var cachedProjectFileStructureModCount: Long = -1L + @Volatile + private var cachedProjectFileRootModCount: Long = -1L + @Volatile private var cachedFolders: List = emptyList() @Volatile private var cachedFolderStructureModCount: Long = -1L @@ -45,7 +56,10 @@ class FilesGroupItem( override suspend fun getLookupItems(searchText: String): List { 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() 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, - recentFiles: List + recentFiles: List, + projectFiles: List ): List { - val prioritizedOpenFiles = openFiles.take(MAX_OPEN_FILE_SUGGESTIONS) - val openFilePaths = prioritizedOpenFiles.mapTo(mutableSetOf()) { it.file.path } + val baseCandidates = when (blankFileSuggestionMode()) { + ContextSuggestionBlankFileSuggestionMode.OPEN_AND_RECENT -> + openFiles + recentFiles + + 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 openFilePaths } - .take(MAX_DEFAULT_FILE_SUGGESTIONS - prioritizedOpenFiles.size) + .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 + return prioritizedOpenFiles + recentBackfill + projectBackfill + } + + private suspend fun getDefaultProjectFileCandidates(): List { + return readAction { + val projectFileIndex = project.service() + getProjectFiles(projectFileIndex) + .asSequence() + .filter { file -> isVisibleProjectFile(file, projectFileIndex) } + .map { file -> + FileSearchCandidate( + file = file, + source = FileSearchSource.NATIVE + ) + } + .toList() + } } private suspend fun getFolderSuggestions(searchText: String): List { 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() @@ -236,7 +284,7 @@ class FilesGroupItem( } private fun Iterable.toFileSuggestions(searchText: String): List { - 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 { + val structureModCount = VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS.modificationCount + val rootModCount = ProjectRootManager.getInstance(project).modificationCount + if (cachedProjectFileStructureModCount == structureModCount && + cachedProjectFileRootModCount == rootModCount + ) { + return cachedProjectFiles + } + + val files = mutableListOf() + 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): List { + return when (fileSortMode()) { + ContextSuggestionFileSortMode.PRESERVE_CURRENT_ORDER -> candidates + ContextSuggestionFileSortMode.FILE_NAME_ASCENDING -> + candidates.sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { + it.file.name + }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.file.path } + ) + + ContextSuggestionFileSortMode.FOLDER_THEN_FILE_ASCENDING -> + candidates.sortedWith( + compareBy(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 } + ) + } + } } diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 173270a67..0e5fe35e9 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -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 diff --git a/src/main/resources/messages/codegpt_zh.properties b/src/main/resources/messages/codegpt_zh.properties index 666277eaa..e47de2d29 100644 --- a/src/main/resources/messages/codegpt_zh.properties +++ b/src/main/resources/messages/codegpt_zh.properties @@ -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 diff --git a/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionSettingsTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionSettingsTest.kt new file mode 100644 index 000000000..047be0fe7 --- /dev/null +++ b/src/test/kotlin/ee/carlrobert/codegpt/settings/configuration/ContextSuggestionSettingsTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt index 8dbbca93b..05c8006b7 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/ui/textarea/IgnoreRulesTagManagerIntegrationTest.kt @@ -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() + + 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() - 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() + + 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() + + 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 + } + } }