[find in files] IJ-CR-168030 IJPL-186012 Introduce FrontendScopeChooserCombo for remote development

- Added APIs and services to allow the backend to expose available search scopes to the frontend (ScopeModelService)
- created ScopesStateService that keeps map scopeId to ScopeDescriptor
- added scopeId to FindModel for getting scope on the backend using ScopesStateService
- refactored FindPopupScopeUIImpl for using FrontendScopeChooserCombo instead of ScopeChooserCombo in case when FindKey is enabled

(cherry picked from commit 4d44d7aaadff23a0a3bb4262ea4d6f5a7dfe1f85)

GitOrigin-RevId: 7a1174256fc723c3373c7924a4d028fd6e3d1285
This commit is contained in:
Vera Petrenkova
2025-06-26 11:54:53 +02:00
committed by intellij-monorepo-bot
parent 88830c6ae4
commit 4648976c82
23 changed files with 567 additions and 88 deletions

View File

@@ -12,6 +12,7 @@ import com.intellij.find.replaceInProject.ReplaceInProjectManager
import com.intellij.ide.ui.colors.rpcId
import com.intellij.ide.ui.icons.rpcId
import com.intellij.ide.ui.toSerializableTextChunk
import com.intellij.ide.util.scopeChooser.ScopesStateService
import com.intellij.ide.vfs.VirtualFileId
import com.intellij.ide.vfs.rpcId
import com.intellij.ide.vfs.virtualFile
@@ -56,6 +57,12 @@ internal class FindRemoteApiImpl : FindRemoteApi {
return@coroutineScope
}
val filesToScanInitially = filesToScanInitially.mapNotNull { it.virtualFile() }.toSet()
// SearchScope is not serializable, so we will get it by id from the client
findModel.customScopeId?.let { scopeId ->
ScopesStateService.getInstance(project).getScopeById(scopeId)?.let {
findModel.customScope = it
}
}
//read action is necessary in case of the loading from a directory
val scope = readAction { FindInProjectUtil.getGlobalSearchScope(project, findModel) }
FindInProjectUtil.findUsages(findModel, project, progressIndicator, presentation, filesToScanInitially) { usageInfo ->

View File

@@ -3,7 +3,7 @@ c:com.intellij.find.FindModel
- java.lang.Cloneable
- sf:Companion:com.intellij.find.FindModel$Companion
- <init>():V
- b:<init>(I,java.lang.String,java.lang.String,Z,Z,Z,com.intellij.find.FindModel$SearchContext,Z,Z,Z,Z,I,Z,Z,Z,Z,Z,Z,Z,java.lang.String,java.lang.String,Z,java.lang.String,java.lang.String,Z,Z,Z,Z,kotlinx.serialization.internal.SerializationConstructorMarker):V
- b:<init>(I,java.lang.String,java.lang.String,Z,Z,Z,com.intellij.find.FindModel$SearchContext,Z,Z,Z,Z,I,Z,Z,Z,Z,Z,Z,Z,java.lang.String,java.lang.String,Z,java.lang.String,java.lang.String,java.lang.String,Z,Z,Z,Z,kotlinx.serialization.internal.SerializationConstructorMarker):V
- f:addObserver(com.intellij.find.FindModel$FindModelObserver):V
- clone():com.intellij.find.FindModel
- f:compileRegExp():java.util.regex.Pattern

View File

@@ -15,6 +15,7 @@ import com.intellij.util.containers.ContainerUtil
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.intellij.lang.annotations.MagicConstant
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.UnknownNullability
import java.util.regex.Pattern
@@ -371,6 +372,15 @@ open class FindModel : UserDataHolder, Cloneable {
}
}
@ApiStatus.Internal
var customScopeId: String? = null
set(value) {
if (value != field) {
field = value
notifyObservers()
}
}
@get:JvmName("isCustomScope")
@set:JvmName("setCustomScope")
var isCustomScope: Boolean = false
@@ -581,6 +591,7 @@ open class FindModel : UserDataHolder, Cloneable {
moduleName = model.moduleName
customScopeName = model.customScopeName
customScope = model.customScope
customScopeId = model.customScopeId
isCustomScope = model.isCustomScope
isFindAll = model.isFindAll
searchContext = model.searchContext
@@ -622,6 +633,7 @@ open class FindModel : UserDataHolder, Cloneable {
if (isWholeWordsOnly != findModel.isWholeWordsOnly) return false
if (isWithSubdirectories != findModel.isWithSubdirectories) return false
if (if (customScope != null) (customScope != findModel.customScope) else findModel.customScope != null) return false
if (if (customScopeId != null) (customScopeId != findModel.customScopeId) else findModel.customScopeId != null) return false
if (if (customScopeName != null) (customScopeName != findModel.customScopeName) else findModel.customScopeName != null) return false
if (if (directoryName != null) (directoryName != findModel.directoryName) else findModel.directoryName != null) return false
if (if (fileFilter != null) (fileFilter != findModel.fileFilter) else findModel.fileFilter != null) return false
@@ -663,6 +675,7 @@ open class FindModel : UserDataHolder, Cloneable {
result = 31 * result + (fileFilter?.hashCode() ?: 0)
result = 31 * result + (customScopeName?.hashCode() ?: 0)
result = 31 * result + (customScope?.hashCode() ?: 0)
result = 31 * result + (customScopeId?.hashCode() ?: 0)
result = 31 * result + (if (isCustomScope) 1 else 0)
result = 31 * result + (if (isMultiline) 1 else 0)
result = 31 * result + (if (isPreserveCase) 1 else 0)

View File

@@ -1628,6 +1628,7 @@ public final class FindPopupPanel extends JBPanel<FindPopupPanel> implements Fin
model.setModuleName(null);
model.setCustomScopeName(null);
model.setCustomScope(null);
model.setCustomScopeId(null);
model.setCustomScope(false);
myScopeUI.applyTo(model, mySelectedScope);

View File

@@ -1,9 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.find.impl;
import com.intellij.find.FindBundle;
import com.intellij.find.FindModel;
import com.intellij.find.FindSettings;
import com.intellij.ide.util.scopeChooser.FrontendScopeChooserCombo;
import com.intellij.ide.util.scopeChooser.ScopeChooserCombo;
import com.intellij.ide.util.scopeChooser.ScopeDescriptor;
import com.intellij.openapi.module.Module;
@@ -48,6 +49,7 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
private ComboBox<String> myModuleComboBox;
private FindPopupDirectoryChooser myDirectoryChooser;
private ScopeChooserCombo myScopeCombo;
private FrontendScopeChooserCombo newScopeCombo;
FindPopupScopeUIImpl(@NotNull FindPopupPanel panel) {
myHelper = panel.getHelper();
@@ -61,8 +63,8 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
? ContainerUtil.ar(new Pair<>(PROJECT, new JLabel()),
new Pair<>(MODULE, shrink(myModuleComboBox)),
new Pair<>(DIRECTORY, myDirectoryChooser),
new Pair<>(SCOPE, shrink(myScopeCombo)))
: ContainerUtil.ar(new Pair<>(SCOPE, shrink(myScopeCombo)),
new Pair<>(SCOPE, shrink(getScopeChooser())))
: ContainerUtil.ar(new Pair<>(SCOPE, shrink(getScopeChooser())),
new Pair<>(DIRECTORY, myDirectoryChooser));
}
@@ -80,47 +82,85 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
myDirectoryChooser = new FindPopupDirectoryChooser(myFindPopupPanel);
myScopeCombo = new ScopeChooserCombo();
Object selection = ObjectUtils.coalesce(myHelper.getModel().getCustomScopeName(), FindSettings.getInstance().getDefaultScopeName());
myScopeCombo.init(myProject, true, true, selection, new Condition<>() {
//final String projectFilesScopeName = PsiBundle.message("psi.search.scope.project");
final String moduleFilesScopeName;
initScopeCombo(restartSearchListener);
}
{
String moduleScopeName = IndexingBundle.message("search.scope.module", "");
final int ind = moduleScopeName.indexOf(' ');
moduleFilesScopeName = moduleScopeName.substring(0, ind + 1);
}
private JComponent getScopeChooser() {
return FindKey.isEnabled() ? newScopeCombo : myScopeCombo;
}
@Override
public boolean value(ScopeDescriptor descriptor) {
final String display = descriptor.getDisplayName();
return /*!projectFilesScopeName.equals(display) &&*/ !display.startsWith(moduleFilesScopeName);
}
});
myScopeCombo.setBrowseListener(new ScopeChooserCombo.BrowseListener() {
private ComboBox<ScopeDescriptor> getScopeCombo() {
return FindKey.isEnabled() ? newScopeCombo: myScopeCombo.getComboBox();
}
private FindModel myModelSnapshot;
private void initScopeCombo(ActionListener restartSearchListener) {
if (FindKey.isEnabled()) {
newScopeCombo = new FrontendScopeChooserCombo(myProject);
Disposer.register(myFindPopupPanel.getDisposable(), newScopeCombo);
}
else {
myScopeCombo = new ScopeChooserCombo();
Object selection = ObjectUtils.coalesce(myHelper.getModel().getCustomScopeName(), FindSettings.getInstance().getDefaultScopeName());
myScopeCombo.init(myProject, true, true, selection, new Condition<>() {
//final String projectFilesScopeName = PsiBundle.message("psi.search.scope.project");
final String moduleFilesScopeName;
@Override
public void onBeforeBrowseStarted() {
myModelSnapshot = myHelper.getModel();
myFindPopupPanel.getCanClose().set(false);
}
@Override
public void onAfterBrowseFinished() {
if (myModelSnapshot != null) {
SearchScope scope = myScopeCombo.getSelectedScope();
if (scope != null) {
myModelSnapshot.setCustomScope(scope);
}
myFindPopupPanel.getCanClose().set(true);
{
String moduleScopeName = IndexingBundle.message("search.scope.module", "");
final int ind = moduleScopeName.indexOf(' ');
moduleFilesScopeName = moduleScopeName.substring(0, ind + 1);
}
}
});
myScopeCombo.getComboBox().addActionListener(restartSearchListener);
Disposer.register(myFindPopupPanel.getDisposable(), myScopeCombo);
@Override
public boolean value(ScopeDescriptor descriptor) {
final String display = descriptor.getDisplayName();
return /*!projectFilesScopeName.equals(display) &&*/ !display.startsWith(moduleFilesScopeName);
}
});
myScopeCombo.setBrowseListener(new ScopeChooserCombo.BrowseListener() {
private FindModel myModelSnapshot;
@Override
public void onBeforeBrowseStarted() {
myModelSnapshot = myHelper.getModel();
myFindPopupPanel.getCanClose().set(false);
}
@Override
public void onAfterBrowseFinished() {
if (myModelSnapshot != null) {
SearchScope scope = myScopeCombo.getSelectedScope();
if (scope != null) {
myModelSnapshot.setCustomScope(scope);
}
myFindPopupPanel.getCanClose().set(true);
}
}
});
Disposer.register(myFindPopupPanel.getDisposable(), myScopeCombo);
}
getScopeCombo().addActionListener(restartSearchListener);
}
private String getSelectedScopeName() {
if (FindKey.isEnabled()) {
return newScopeCombo.getSelectedScopeName();
}
return myScopeCombo.getSelectedScopeName();
}
private void applyScopeTo(FindModel findModel) {
if (FindKey.isEnabled()) {
findModel.setCustomScopeId(newScopeCombo.getSelectedScopeId());
findModel.setCustomScopeName(newScopeCombo.getSelectedScopeName());
}
else {
SearchScope selectedCustomScope = myScopeCombo.getSelectedScope();
String customScopeName = selectedCustomScope == null ? null : selectedCustomScope.getDisplayName();
findModel.setCustomScopeName(customScopeName);
findModel.setCustomScope(selectedCustomScope);
}
}
@Override
@@ -130,7 +170,7 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
@Override
public void applyTo(@NotNull FindSettings findSettings, @NotNull FindPopupScopeUI.ScopeType selectedScope) {
findSettings.setDefaultScopeName(myScopeCombo.getSelectedScopeName());
findSettings.setDefaultScopeName(getSelectedScopeName());
}
@Override
@@ -146,10 +186,7 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
findModel.setModuleName((String)myModuleComboBox.getSelectedItem());
}
else if (selectedScope == SCOPE) {
SearchScope selectedCustomScope = myScopeCombo.getSelectedScope();
String customScopeName = selectedCustomScope == null ? null : selectedCustomScope.getDisplayName();
findModel.setCustomScopeName(customScopeName);
findModel.setCustomScope(selectedCustomScope);
applyScopeTo(findModel);
findModel.setCustomScope(true);
}
}
@@ -164,7 +201,7 @@ final class FindPopupScopeUIImpl implements FindPopupScopeUI {
@Override
public boolean hideAllPopups() {
final JComboBox[] candidates = { myModuleComboBox, myScopeCombo.getComboBox(), myDirectoryChooser.getComboBox() };
final JComboBox[] candidates = { myModuleComboBox, getScopeCombo(), myDirectoryChooser.getComboBox() };
for (JComboBox candidate : candidates) {
if (candidate.isPopupVisible()) {
candidate.hidePopup();

View File

@@ -0,0 +1,132 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser
import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.popup.ListSeparator
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.components.fields.ExtendableTextComponent
import com.intellij.ui.components.fields.ExtendableTextField
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.awt.Dimension
import java.util.*
import javax.swing.Icon
import javax.swing.JTextField
import javax.swing.plaf.basic.BasicComboBoxEditor
import kotlin.math.min
/**
* Instances of `ScopeChooserCombo` **must be disposed** when the corresponding dialog or settings page is closed. Otherwise,
* listeners registered in `init()` cause memory leak.<br></br><br></br>
* Example: if `ScopeChooserCombo` is used in a
* `DialogWrapper` subclass, call `Disposer.register(getDisposable(), myScopeChooserCombo)`, where
* `getDisposable()` is `DialogWrapper`'s method.
*/
@ApiStatus.Internal
class FrontendScopeChooserCombo(project: Project) : ComboBox<ScopeDescriptor>(), Disposable {
private val scopeService = ScopeModelService.getInstance(project)
private val modelId = UUID.randomUUID().toString()
private var loadingTextComponent: ExtendableTextField? = null
private var scopesMap: Map<String, ScopeDescriptor> = emptyMap()
private val scopeToSeparator: MutableMap<ScopeDescriptor, ListSeparator> = mutableMapOf()
private val browseExtension: ExtendableTextComponent.Extension = ExtendableTextComponent.Extension.create(AllIcons.General.ArrowDown, "", //TODO()
{ TODO() })
private val loadingExtension: ExtendableTextComponent.Extension = ExtendableTextComponent.Extension.create(AnimatedIcon.Default(), "", { TODO() })
init {
loadItemsAsync()
setEditor(object : BasicComboBoxEditor() {
override fun createEditorComponent(): JTextField {
val ecbEditor = ExtendableTextField()
ecbEditor.addExtension(browseExtension)
ecbEditor.setBorder(null)
loadingTextComponent = ecbEditor
return ecbEditor
}
}.apply {
renderer = createScopeDescriptorRenderer { descriptor -> scopeToSeparator[descriptor] }
})
}
private fun setLoading(loading: Boolean) {
isEnabled = !loading
loadingTextComponent?.let { editor ->
editor.removeExtension(loadingExtension)
editor.removeExtension(browseExtension)
if (loading) { // Add loading indicator extension
editor.addExtension(loadingExtension)
}
editor.addExtension(browseExtension)
editor.repaint()
}
}
private fun loadItemsAsync() {
setLoading(true)
scopeService.loadItemsAsync(modelId, onFinished = { scopeIdToScopeDescriptor ->
scopesMap = scopeIdToScopeDescriptor ?: emptyMap()
val items = scopesMap.values
withContext(Dispatchers.EDT) {
removeAllItems()
items.filterOutSeparators().forEach { addItem(it) }
setLoading(false)
}
})
}
private fun Collection<ScopeDescriptor>.filterOutSeparators(): List<ScopeDescriptor> {
var lastItem: ScopeDescriptor? = null
return this.filter { item ->
if (item is ScopeSeparator) {
if (lastItem != null) {
scopeToSeparator[lastItem] = ListSeparator(item.text)
}
}
lastItem = item
item !is ScopeSeparator
}
}
override fun getPreferredSize(): Dimension? {
if (isPreferredSizeSet) {
return super.getPreferredSize()
}
val preferredSize = super.getPreferredSize()
return Dimension(min(400, preferredSize.width), preferredSize.height)
}
fun getSelectedScopeId(): String? {
val scopeDescriptor = selectedItem as? ScopeDescriptor
return scopesMap.entries.firstOrNull { it.value == scopeDescriptor }?.key
}
@Nls
fun getSelectedScopeName(): String? {
return (selectedItem as? ScopeDescriptor)?.displayName
}
override fun dispose() {
scopeService.disposeModel(modelId) // ActionListener[] listeners = myBrowseButton.getActionListeners();
// for (ActionListener listener : listeners) {
// myBrowseButton.removeActionListener(listener);
// }
}
}
@ApiStatus.Internal
data class SearchScopeUiInfo(val id: String, val name: String, val icon: Icon?, val isSeparator: Boolean)

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser;
import com.intellij.openapi.Disposable;
@@ -107,7 +107,8 @@ public class ScopeChooserCombo extends ComboboxWithBrowseButton implements Dispo
ComboBox<ScopeDescriptor> combo = getComboBox();
combo.setMinimumAndPreferredWidth(JBUIScale.scale(300));
combo.setRenderer(ScopeSeparatorKt.createScopeDescriptorRenderer(() -> scopes));
combo.setRenderer(
ScopeSeparatorKt.createScopeDescriptorRenderer(scopes == null ? null : (descriptor) -> scopes.getSeparatorFor(descriptor)));
combo.setSwingPopup(false);
if (selection != null) {

View File

@@ -0,0 +1,24 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface ScopeModelService {
fun loadItemsAsync(modelId: String, onFinished: suspend (Map<String, ScopeDescriptor>?) -> Unit)
fun disposeModel(modelId: String)
fun getScopeById(scopeId: String): ScopeDescriptor?
companion object {
@JvmStatic
fun getInstance(project: Project): ScopeModelService {
return project.service<ScopeModelService>()
}
}
}

View File

@@ -1,10 +1,10 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser
import com.intellij.openapi.ui.popup.ListSeparator
import com.intellij.ui.dsl.listCellRenderer.listCellRenderer
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.util.function.Supplier
import javax.swing.ListCellRenderer
@ApiStatus.Internal
@@ -15,15 +15,14 @@ class ScopeSeparator @ApiStatus.Internal constructor(@Nls val text: String) : Sc
}
}
internal fun createScopeDescriptorRenderer(scopesSupplier: Supplier<ScopesSnapshot?>): ListCellRenderer<ScopeDescriptor?> {
internal fun createScopeDescriptorRenderer(separatorProvider: ((ScopeDescriptor) -> ListSeparator?)?): ListCellRenderer<ScopeDescriptor?> {
return listCellRenderer("") {
value.icon?.let {
icon(it)
}
text(value.displayName ?: "")
val scopes = scopesSupplier.get()
scopes?.getSeparatorFor(value)?.let {
separatorProvider?.invoke(value)?.let {
separator {
text = it.text
}

View File

@@ -0,0 +1,41 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util.scopeChooser
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.psi.search.SearchScope
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class ScopesStateService(val project: Project) {
private var scopesState: ScopesState? = null
fun getScopeById(scopeId: String): SearchScope? {
return scopesState?.scopeIdToDescriptor[scopeId]?.let { return it.scope }
}
fun getOrCreateScopesState(): ScopesState {
if (scopesState != null) return scopesState!!
val state = ScopesState(project)
scopesState = state
return state
}
companion object {
@JvmStatic
fun getInstance(project: Project): ScopesStateService {
return project.service<ScopesStateService>()
}
}
}
@ApiStatus.Internal
class ScopesState internal constructor(val project: Project) {
var scopeIdToDescriptor: Map<String, ScopeDescriptor> = mapOf()
fun updateScopes(scopesStateMap: Map<String, ScopeDescriptor>) {
scopeIdToDescriptor = scopesStateMap
}
}

View File

@@ -22,6 +22,10 @@ jvm_library(
"@lib//:kotlinx-serialization-core",
"@lib//:kotlinx-serialization-json",
"//platform/lang-impl",
"//platform/util:util-ui",
"//platform/project/shared:project",
"//platform/kernel/shared:kernel",
"//platform/util/coroutines",
],
runtime_deps = [":scopes_resources"]
)

View File

@@ -1,8 +0,0 @@
c:com.intellij.ide.util.scopeChooser.ScopeDescriptor
- com.intellij.openapi.util.ColoredItem
- <init>(com.intellij.psi.search.SearchScope):V
- getColor():java.awt.Color
- getDisplayName():java.lang.String
- getIcon():javax.swing.Icon
- getScope():com.intellij.psi.search.SearchScope
- scopeEquals(com.intellij.psi.search.SearchScope):Z

View File

@@ -12,7 +12,17 @@ jvm_library(
module_name = "intellij.platform.scopes.backend",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java"], allow_empty = True),
deps = ["@lib//:kotlin-stdlib"],
deps = [
"@lib//:kotlin-stdlib",
"//platform/scopes",
"//platform/project/shared:project",
"//platform/kernel/backend",
"//platform/core-api:core",
"//platform/util",
"//platform/lang-impl",
"//platform/core-ui",
"//platform/util/coroutines",
],
runtime_deps = [
":backend_resources",
"//platform/backend",

View File

@@ -14,7 +14,11 @@
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
<args>
<arg>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</arg>
<arg>$MAVEN_REPOSITORY$/jetbrains/fleet/rhizomedb-compiler-plugin/2.2.0-RC2-0.2/rhizomedb-compiler-plugin-2.2.0-RC2-0.2.jar</arg>
<arg>$MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.2.0-RC2-0.1/rpc-compiler-plugin-2.2.0-RC2-0.1.jar</arg>
</args>
</arrayArg>
<arrayArg name="pluginOptions" />
</arrayArguments>
@@ -32,5 +36,13 @@
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.backend" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.scopes" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.kernel.backend" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
</component>
</module>

View File

@@ -2,5 +2,9 @@
<dependencies>
<module name="intellij.platform.backend"/>
<module name="intellij.platform.scopes"/>
<module name="intellij.platform.kernel.backend"/>
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.backend.remoteApiProvider implementation="com.intellij.platform.scopes.backend.ScopesStateApiProvider"/>/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,95 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.scopes.backend
import com.intellij.ide.util.scopeChooser.*
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProjectOrNull
import com.intellij.platform.rpc.backend.RemoteApiProvider
import com.intellij.platform.scopes.ScopeModelApi
import com.intellij.platform.scopes.SearchScopeData
import com.intellij.platform.scopes.SearchScopesInfo
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import java.util.*
private val LOG = logger<ScopesModelApiImpl>()
internal class ScopesModelApiImpl : ScopeModelApi {
private val modelIdToModel = mutableMapOf<String, AbstractScopeModel>()
private val modelIdToScopes = mutableMapOf<String, ScopesState>()
override suspend fun createModelAndSubscribe(projectId: ProjectId, modelId: String): Flow<SearchScopesInfo>? {
val project = projectId.findProjectOrNull()
if (project == null) {
LOG.warn("Project not found for projectId: $projectId")
return null
}
val model = project.getService(ScopeService::class.java)
.createModel(EnumSet.of(
ScopeOption.FROM_SELECTION,
ScopeOption.USAGE_VIEW,
ScopeOption.LIBRARIES,
ScopeOption.SEARCH_RESULTS
))
modelIdToModel[modelId] = model
val flow = subscribeToModelUpdates(model, modelId, project)
model.refreshScopes(null)
return flow
}
private fun subscribeToModelUpdates(model: AbstractScopeModel, modelId: String, project: Project): Flow<SearchScopesInfo> {
val flow = channelFlow {
model.addScopeModelListener(object : ScopeModelListener {
override fun scopesUpdated(scopes: ScopesSnapshot) {
val scopesStateMap = mutableMapOf<String, ScopeDescriptor>()
val scopesData = scopes.scopeDescriptors.mapNotNull { descriptor ->
val scopeId = UUID.randomUUID().toString()
val scopeData = SearchScopeData.from(descriptor, scopeId) ?: return@mapNotNull null
scopesStateMap[scopeData.scopeId] = descriptor
scopeData
}
var scopeState = modelIdToScopes[modelId]
if (scopeState == null) {
scopeState = ScopesStateService.getInstance(project).getOrCreateScopesState(project)
modelIdToScopes[modelId] = scopeState
}
scopeState.updateScopes(scopesStateMap)
val searchScopesInfo = SearchScopesInfo(scopesData, null, null, null)
launch {
send(searchScopesInfo)
}
}
})
awaitClose {}
}
return flow
}
override suspend fun updateModel(modelId: String, scopesInfo: SearchScopesInfo): Flow<SearchScopesInfo> {
TODO("Not yet implemented")
}
override suspend fun dispose(modelId: String) {
modelIdToScopes.remove(modelId)
val model = modelIdToModel[modelId] ?: return
Disposer.dispose(model)
modelIdToModel.remove(modelId)
}
}
private class ScopesStateApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<ScopeModelApi>()) {
ScopesModelApiImpl()
}
}
}

View File

@@ -14,7 +14,11 @@
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
<args>
<arg>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</arg>
<arg>$MAVEN_REPOSITORY$/jetbrains/fleet/rhizomedb-compiler-plugin/2.2.0-RC2-0.2/rhizomedb-compiler-plugin-2.2.0-RC2-0.2.jar</arg>
<arg>$MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.2.0-RC2-0.1/rpc-compiler-plugin-2.2.0-RC2-0.1.jar</arg>
</args>
</arrayArg>
<arrayArg name="pluginOptions" />
</arrayArguments>
@@ -39,5 +43,9 @@
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.platform.project" />
<orderEntry type="module" module-name="intellij.platform.kernel" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
</component>
</module>

View File

@@ -1,2 +1,8 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<!--suppress PluginXmlRegistrationCheck -->
<projectService serviceInterface="com.intellij.ide.util.scopeChooser.ScopeModelService"
serviceImplementation="com.intellij.platform.scopes.service.ScopeModelServiceImpl"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,26 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.scopes
import com.intellij.platform.project.ProjectId
import com.intellij.platform.rpc.RemoteApiProviderService
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.coroutines.flow.Flow
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Rpc
interface ScopeModelApi : RemoteApi<Unit> {
suspend fun createModelAndSubscribe(projectId: ProjectId, modelId: String): Flow<SearchScopesInfo>?
suspend fun updateModel(modelId: String, scopesInfo: SearchScopesInfo): Flow<SearchScopesInfo>
suspend fun dispose(modelId: String)
companion object {
@JvmStatic
suspend fun getInstance(): ScopeModelApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<ScopeModelApi>())
}
}
}

View File

@@ -2,14 +2,22 @@
package com.intellij.platform.scopes
import com.intellij.ide.ui.colors.ColorId
import com.intellij.ide.ui.colors.color
import com.intellij.ide.ui.colors.rpcId
import com.intellij.ide.ui.icons.IconId
import com.intellij.ide.ui.icons.icon
import com.intellij.ide.ui.icons.rpcId
import com.intellij.ide.util.scopeChooser.ScopeDescriptor
import com.intellij.ide.util.scopeChooser.ScopeSeparator
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.awt.Color
import java.util.*
import javax.swing.Icon
@ApiStatus.Internal
@Serializable
@@ -31,4 +39,22 @@ class SearchScopeData(val scopeId: String, val name: @Nls String, val iconId: Ic
class SearchScopesInfo(val scopes: List<SearchScopeData>,
val selectedScopeId: String?,
val projectScopeId: String?,
val everywhereScopeId: String?)
val everywhereScopeId: String?) {
fun getScopeDescriptors(): Map<String, ScopeDescriptor> {
return scopes.associate {
val descriptor =
if (it.isSeparator) ScopeSeparator(it.name)
else object : ScopeDescriptor(object : GlobalSearchScope() {
override fun contains(file: VirtualFile): Boolean = throw IllegalStateException("Should not be called")
override fun isSearchInModuleContent(aModule: Module): Boolean = throw IllegalStateException("Should not be called")
override fun isSearchInLibraries(): Boolean = throw IllegalStateException("Should not be called")
}) {
override fun getColor(): Color? = it.colorId?.color()
override fun getDisplayName(): @Nls(capitalization = Nls.Capitalization.Sentence) String = it.name
override fun getIcon(): Icon? = it.iconId?.icon()
}
it.scopeId to descriptor
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.scopes.service
import com.intellij.ide.util.scopeChooser.ScopeDescriptor
import com.intellij.ide.util.scopeChooser.ScopeModelService
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.platform.project.projectId
import com.intellij.platform.scopes.ScopeModelApi
import com.intellij.platform.util.coroutines.childScope
import fleet.rpc.client.RpcTimeoutException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
private val LOG = logger<ScopeModelServiceImpl>()
@ApiStatus.Internal
private class ScopeModelServiceImpl(private val project: Project, private val coroutineScope: CoroutineScope) : ScopeModelService {
private var scopeIdToDescriptor = mapOf<String, ScopeDescriptor>()
override fun loadItemsAsync(modelId: String, onFinished: suspend (Map<String, ScopeDescriptor>?) -> Unit) {
coroutineScope.childScope("ScopesStateService.subscribeToScopeStates").launch {
try {
val scopesFlow = ScopeModelApi.getInstance().createModelAndSubscribe(project.projectId(), modelId)
if (scopesFlow == null) {
LOG.warn("Failed to subscribe to model updates for modelId: $modelId")
onFinished(null)
return@launch
}
scopesFlow.collect { scopesInfo ->
val fetchedScopes = scopesInfo.getScopeDescriptors()
onFinished(fetchedScopes)
scopeIdToDescriptor = fetchedScopes
}
}
catch (e: RpcTimeoutException) {
LOG.warn("Failed to subscribe to model updates for modelId: $modelId", e)
onFinished(null)
}
}
}
override fun disposeModel(modelId: String) {
coroutineScope.launch {
try {
ScopeModelApi.getInstance().dispose(modelId)
}
catch (e: RpcTimeoutException) {
LOG.warn("Failed to dispose model for modelId: $modelId", e)
}
}
}
override fun getScopeById(scopeId: String): ScopeDescriptor? {
scopeIdToDescriptor.get(scopeId)?.let { return it }
return null
}
}

View File

@@ -2,39 +2,17 @@
package com.intellij.platform.searchEverywhere.frontend.tabs.target
import com.intellij.ide.actions.searcheverywhere.ScopeChooserAction
import com.intellij.ide.ui.colors.color
import com.intellij.ide.ui.icons.icon
import com.intellij.ide.util.scopeChooser.ScopeDescriptor
import com.intellij.ide.util.scopeChooser.ScopeSeparator
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.scopes.SearchScopesInfo
import com.intellij.platform.searchEverywhere.frontend.AutoToggleAction
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.Processor
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.awt.Color
import javax.swing.Icon
@ApiStatus.Internal
class SeScopeChooserActionProvider(val scopesInfo: SearchScopesInfo, private val onSelectedScopeChanged: (String?) -> Unit) {
private val descriptors: Map<String, ScopeDescriptor> = scopesInfo.scopes.associate {
val descriptor =
if (it.isSeparator) ScopeSeparator(it.name)
else object : ScopeDescriptor(object : GlobalSearchScope() {
override fun contains(file: VirtualFile): Boolean = throw IllegalStateException("Should not be called")
override fun isSearchInModuleContent(aModule: Module): Boolean = throw IllegalStateException("Should not be called")
override fun isSearchInLibraries(): Boolean = throw IllegalStateException("Should not be called")
}) {
override fun getColor(): Color? = it.colorId?.color()
override fun getDisplayName(): @Nls(capitalization = Nls.Capitalization.Sentence) String = it.name
override fun getIcon(): Icon? = it.iconId?.icon()
}
it.scopeId to descriptor
}
private val descriptors: Map<String, ScopeDescriptor> = scopesInfo.getScopeDescriptors()
var selectedScopeId: String? = scopesInfo.selectedScopeId
private set(value) {

View File

@@ -1,4 +1,7 @@
<idea-plugin package="com.intellij.platform.searchEverywhere">
<dependencies>
<module name="intellij.platform.scopes"/>
</dependencies>
<extensionPoints>
<extensionPoint name="searchEverywhere.itemsProviderFactory"
interface="com.intellij.platform.searchEverywhere.SeItemsProviderFactory"