IJPL-159035 SpotlightPainter - get rid of MergingUpdateQueue

GitOrigin-RevId: 5f6c5c4cc41aec3d829284ca4a511e6c6696ceb1
This commit is contained in:
Vladimir Krivosheev
2024-07-31 14:23:26 +02:00
committed by intellij-monorepo-bot
parent 3cef5eda0a
commit 696f7393dd
12 changed files with 149 additions and 122 deletions

View File

@@ -25,6 +25,7 @@ import com.intellij.testFramework.TestApplicationManager;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.Matcher;
import org.assertj.core.api.SoftAssertions;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
@@ -35,8 +36,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiPredicate;
import static org.assertj.core.api.Assertions.assertThat;
public class GotoActionTest extends LightJavaCodeInsightFixtureTestCase {
private static final DataKey<Boolean> SHOW_HIDDEN_KEY = DataKey.create("GotoActionTest.DataKey");
private static final Comparator<MatchedValue> MATCH_COMPARATOR = MatchedValue::compareWeights;
@@ -292,25 +291,22 @@ public class GotoActionTest extends LightJavaCodeInsightFixtureTestCase {
}
public void testNavigableSettingsOptionsAppearInResults() {
SearchEverywhereContributor<?> contributor = createActionContributor(getProject(), getTestRootDisposable());
List<String> patterns = List.of("support screen readers", "show line numbers", "tab placement");
List<Object> errors = new ArrayList<>();
for (String pattern : patterns) {
List<?> elements = ChooseByNameTest.calcContributorElements(contributor, pattern);
boolean result = false;
for (Object t : elements) {
if (isNavigableOption(((MatchedValue)t).value)) {
result = true;
break;
SoftAssertions.assertSoftly(softly -> {
SearchEverywhereContributor<?> contributor = createActionContributor(getProject(), getTestRootDisposable());
for (String pattern : List.of("support screen readers", "show line numbers", "tab placement")) {
List<?> elements = ChooseByNameTest.calcContributorElements(contributor, pattern);
boolean result = false;
for (Object t : elements) {
if (isNavigableOption(((MatchedValue)t).value)) {
result = true;
break;
}
}
if (!result) {
softly.fail("Failure for pattern '" + pattern + "' - " + elements);
}
}
if (!result) {
errors.add("Failure for pattern '" + pattern + "' - " + elements);
}
}
assertThat(errors).isEmpty();
});
}
public void testUseUpdatedPresentationForMatching() {

View File

@@ -1586,10 +1586,10 @@ a:com.intellij.codeInspection.ex.InspectionToolWrapper
- a:createCopy():com.intellij.codeInspection.ex.InspectionToolWrapper
- getDefaultEditorAttributes():java.lang.String
- getDefaultLevel():com.intellij.codeHighlighting.HighlightDisplayLevel
- getDescriptionContextClass():java.lang.Class
- f:getDescriptionContextClass():java.lang.Class
- getDisplayKey():com.intellij.codeInsight.daemon.HighlightDisplayKey
- getDisplayName():java.lang.String
- getExtension():com.intellij.codeInspection.InspectionEP
- f:getExtension():com.intellij.codeInspection.InspectionEP
- f:getFolderName():java.lang.String
- getGroupDisplayName():java.lang.String
- getGroupPath():java.lang.String[]

View File

@@ -10,8 +10,6 @@ import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.diagnostic.PluginException;
import com.intellij.l10n.LocalizationUtil;
import com.intellij.lang.Language;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.HtmlChunk;
@@ -174,15 +172,21 @@ public abstract class InspectionToolWrapper<T extends InspectionProfileEntry, E
}
public @Nls String loadDescription() {
final String description = getStaticDescription();
if (description != null) return description;
String description = getStaticDescription();
if (description != null) {
return description;
}
try {
InputStream descriptionStream = getDescriptionStream();
//noinspection HardCodedStringLiteral(IDEA-249976)
return descriptionStream != null ? insertAddendum(ResourceUtil.loadText(descriptionStream),
getTool().getDescriptionAddendum()) : null;
if (descriptionStream != null) {
//noinspection HardCodedStringLiteral(IDEA-249976)
return insertAddendum(ResourceUtil.loadText(descriptionStream), getTool().getDescriptionAddendum());
}
return null;
}
catch (IOException ignored) {
}
catch (IOException ignored) { }
return getTool().loadDescription();
}
@@ -200,11 +204,10 @@ public abstract class InspectionToolWrapper<T extends InspectionProfileEntry, E
}
private @Nullable InputStream getDescriptionStream() {
Application app = ApplicationManager.getApplication();
String path = INSPECTION_DESCRIPTIONS_FOLDER + "/" + getDescriptionFileName();
ClassLoader classLoader;
if (myEP == null || app.isUnitTestMode() || app.isHeadlessEnvironment()) {
classLoader = getDescriptionContextClass().getClassLoader();
if (myEP == null) {
classLoader = getTool().getClass().getClassLoader();
}
else {
classLoader = myEP.getPluginDescriptor().getPluginClassLoader();
@@ -220,7 +223,7 @@ public abstract class InspectionToolWrapper<T extends InspectionProfileEntry, E
return getShortName();
}
public @NotNull Class<? extends InspectionProfileEntry> getDescriptionContextClass() {
public final @NotNull Class<? extends InspectionProfileEntry> getDescriptionContextClass() {
return getTool().getClass();
}
@@ -228,7 +231,7 @@ public abstract class InspectionToolWrapper<T extends InspectionProfileEntry, E
return getTool().getMainToolId();
}
public E getExtension() {
public final E getExtension() {
return myEP;
}

View File

@@ -90,9 +90,9 @@ object LocalizationUtil {
@Internal
fun getResourceAsStream(classLoader: ClassLoader?, path: String, specialLocale: Locale? = null): InputStream? {
if (classLoader != null) {
val locale = specialLocale ?: getLocaleOrNullForDefault()
if (classLoader != null && locale != null) {
try {
val locale = specialLocale ?: getLocaleOrNullForDefault()
for (localizedPath in getLocalizedPaths(path, locale)) {
classLoader.getResourceAsStream(localizedPath)?.let { return it }
}
@@ -101,7 +101,8 @@ object LocalizationUtil {
thisLogger().error("Cannot find localized resource: $path", e)
}
}
return getPluginClassLoader()?.getResourceAsStream(path) ?: classLoader?.getResourceAsStream(path)
return locale?.let { getPluginClassLoader(defaultLoader = null, locale = it)?.getResourceAsStream(path) }
?: classLoader?.getResourceAsStream(path)
}
@Internal

View File

@@ -304,6 +304,7 @@ internal class ActionAsyncProvider(private val model: GotoActionModel) {
val map = model.configurablesNames
val registrar = serviceAsync<SearchableOptionsRegistrar>() as SearchableOptionsRegistrarImpl
registrar.initialize()
val words = registrar.getProcessedWords(pattern)
val filterOutInspections = Registry.`is`("go.to.action.filter.out.inspections", true)
@@ -323,12 +324,12 @@ internal class ActionAsyncProvider(private val model: GotoActionModel) {
var registrarDescriptions: MutableSet<OptionDescription>? = null
for (word in words) {
val descriptions = registrar.findAcceptableDescriptions(word)?.toHashSet()
descriptions?.removeIf {
@Suppress("HardCodedStringLiteral")
it.path == "ActionManager" || filterOutInspections && it.groupName == "Inspections"
}
val descriptions = registrar.findAcceptableDescriptions(word)
?.filter {
@Suppress("HardCodedStringLiteral")
!(it.path == "ActionManager" || filterOutInspections && it.groupName == "Inspections")
}
?.toHashSet()
if (descriptions.isNullOrEmpty()) {
registrarDescriptions = null
break

View File

@@ -6,9 +6,9 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
data class ConfigurableHit(
@JvmField val nameHits: Set<Configurable>,
@JvmField val nameFullHits: Set<Configurable>,
@JvmField val contentHits: Set<Configurable>,
@JvmField val nameHits: Collection<Configurable>,
@JvmField val nameFullHits: Collection<Configurable>,
@JvmField val contentHits: List<Configurable>,
@JvmField val spotlightFilter: String,
) {
val all: Set<Configurable>

View File

@@ -9,6 +9,7 @@ import com.intellij.openapi.util.Disposer;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.Activatable;
import com.intellij.util.ui.update.UiNotifyConnector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -37,28 +38,34 @@ public final class IdeGlassPaneUtil {
}
public static void installPainter(@NotNull JComponent target, @NotNull Painter painter, @NotNull Disposable parent) {
final UiNotifyConnector connector = UiNotifyConnector.installOn(target, new Activatable() {
private IdeGlassPane myPane;
private Disposable myPanePainterListeners = Disposer.newDisposable();
Activatable listeners = createPainterActivatable(target, painter);
UiNotifyConnector connector = UiNotifyConnector.installOn(target, listeners);
Disposer.register(parent, connector);
}
@ApiStatus.Internal
public static @NotNull Activatable createPainterActivatable(@NotNull JComponent target, @NotNull Painter painter) {
return new Activatable() {
private Disposable panePainterListeners;
@Override
public void showNotify() {
IdeGlassPane pane = find(target);
if (myPane != null && myPane != pane) {
Disposer.dispose(myPanePainterListeners);
if (panePainterListeners != null) {
Disposer.dispose(panePainterListeners);
}
myPane = pane;
myPanePainterListeners = Disposer.newDisposable("PanePainterListeners");
Disposer.register(parent, myPanePainterListeners);
myPane.addPainter(target, painter, myPanePainterListeners);
panePainterListeners = Disposer.newDisposable("PanePainterListeners");
pane.addPainter(target, painter, panePainterListeners);
}
@Override
public void hideNotify() {
Disposer.dispose(myPanePainterListeners);
if (panePainterListeners != null) {
Disposer.dispose(panePainterListeners);
}
}
});
Disposer.register(parent, connector);
};
}
public static boolean canBePreprocessed(@NotNull MouseEvent e) {

View File

@@ -10,7 +10,8 @@ import javax.swing.*;
* Gets notified when a component is found and should be highlighted.
*/
public interface ComponentHighlightingListener {
Topic<ComponentHighlightingListener> TOPIC = Topic.create("highlightComponent", ComponentHighlightingListener.class);
Topic<ComponentHighlightingListener> TOPIC =
new Topic<>("highlightComponent", ComponentHighlightingListener.class, Topic.BroadcastDirection.NONE);
void highlight(@NotNull JComponent component, @NotNull String searchString);
}

View File

@@ -63,9 +63,8 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
stopWords = emptySet()
}
else {
startLoading()
stopWords = loadStopWords()
startLoading()
app.getMessageBus().simpleConnect().subscribe<DynamicPluginListener>(DynamicPluginListener.TOPIC, object : DynamicPluginListener {
override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) {
@@ -152,12 +151,12 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
try {
extension.instance?.contribute(processor)
}
catch (e: Throwable) {
LOG.error(PluginException(e, extension.pluginDescriptor.pluginId))
}
catch (e: CancellationException) {
throw e
}
catch (e: Throwable) {
LOG.error(PluginException(e, extension.pluginDescriptor.pluginId))
}
}
coroutineContext.ensureActive()
@@ -198,13 +197,20 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
val identifierTable = identifierTable!!
val groupName = if (_groupName == Short.MAX_VALUE.toInt()) null else identifierTable.fromId(_groupName)
val configurableId = identifierTable.fromId(_id).toString()
val hit = if (_hit == Short.MAX_VALUE.toInt()) null else identifierTable.fromId(_hit).toString()
val path = if (_path == Short.MAX_VALUE.toInt()) null else identifierTable.fromId(_path).toString()
val configurableId = identifierTable.fromId(_id)
val hit = if (_hit == Short.MAX_VALUE.toInt()) null else identifierTable.fromId(_hit)
val path = if (_path == Short.MAX_VALUE.toInt()) null else identifierTable.fromId(_path)
return OptionDescription(_option = null, configurableId = configurableId, hit = hit, path = path, groupName = groupName)
}
@Suppress("LocalVariableName")
private fun unpackConfigurableId(data: Long): String {
val _id = (data shr 32 and 0xffffL).toInt()
assert(_id < Short.Companion.MAX_VALUE)
return identifierTable!!.fromId(_id)
}
override fun getConfigurables(
groups: List<ConfigurableGroup>,
type: DocumentEvent.EventType?,
@@ -276,7 +282,7 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
return ConfigurableHit(
nameHits = nameHits,
nameFullHits = nameFullHits,
contentHits = emptySet(),
contentHits = emptyList(),
spotlightFilter = option,
)
}
@@ -296,28 +302,39 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
return ConfigurableHit(
nameHits = nameHits,
nameFullHits = nameFullHits,
contentHits = LinkedHashSet(contentHits),
contentHits = contentHits,
spotlightFilter = option,
)
}
}
private fun findConfigurablesByDescriptions(descriptionOptions: Set<String>): MutableSet<String>? {
var helpIds: MutableSet<String>? = null
var result: MutableSet<String>? = null
for (prefix in descriptionOptions) {
val ids = (findAcceptableDescriptions(prefix) ?: return null).mapTo(HashSet()) { it.configurableId!! }
if (helpIds == null) {
helpIds = ids
val ids = HashSet<String>()
for (longs in findAcceptablePackedDescriptions(prefix) ?: return null) {
for (l in longs) {
ids.add(unpackConfigurableId(l))
}
}
if (result == null) {
result = ids
}
else {
result.retainAll(ids)
}
helpIds.retainAll(ids)
}
return helpIds
return result
}
fun findAcceptableDescriptions(prefix: String): Sequence<OptionDescription>? {
return findAcceptablePackedDescriptions(prefix)?.flatMap { data -> data.asSequence().map { unpack(it)} }
}
private fun findAcceptablePackedDescriptions(prefix: String): Sequence<LongArray>? {
val storage = storage?.takeIf { it.isCompleted }?.getCompleted()
if (storage == null) {
LOG.warn("Not yet initialized")
LOG.error("Not yet initialized")
return null
}
@@ -336,9 +353,7 @@ class SearchableOptionsRegistrarImpl(private val coroutineScope: CoroutineScope)
}
}
for (description in entry.value) {
yield(unpack(description))
}
yield(entry.value)
}
}
}
@@ -496,7 +511,7 @@ private fun findGroupsByPath(groups: List<ConfigurableGroup>, path: String): Con
lastMatchedIndex = i
if (matched is Configurable.Composite) {
current = (matched as Configurable.Composite).getConfigurables().asList()
current = matched.getConfigurables().asList()
}
else {
break
@@ -513,7 +528,7 @@ private fun findGroupsByPath(groups: List<ConfigurableGroup>, path: String): Con
""
}
val hits = setOf(lastMatched)
val hits = listOf(lastMatched)
return ConfigurableHit(nameHits = hits, nameFullHits = hits, contentHits = hits, spotlightFilter = spotlightFilter)
}

View File

@@ -103,7 +103,7 @@ public class SettingsDialog extends DialogWrapper implements UiCompatibleDataPro
@NotNull Disposable parent,
@NotNull Function1<? super SpotlightPainter, Unit> updater
) {
return new SpotlightPainter(target, parent, updater);
return new SpotlightPainter(target, updater);
}
private void init(@Nullable Configurable configurable, @Nullable Project project) {

View File

@@ -244,7 +244,7 @@ private fun getUnnamedConfigurable(candidate: Configurable): UnnamedConfigurable
return if (candidate is ConfigurableWrapper) candidate.getConfigurable() else candidate
}
private fun findConfigurable(configurables: Set<Configurable>, hits: Set<Configurable>?): Configurable? {
private fun findConfigurable(configurables: Collection<Configurable>, hits: Collection<Configurable>?): Configurable? {
var candidate: Configurable? = null
for (configurable in configurables) {
if (hits != null && hits.contains(configurable)) {

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.
@file:Suppress("ReplaceGetOrSet")
package com.intellij.openapi.options.newEditor
import com.intellij.ide.ui.search.ComponentHighlightingListener
import com.intellij.ide.ui.search.SearchUtil.lightOptions
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SearchableConfigurable
@@ -13,8 +14,12 @@ import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.IdeGlassPaneUtil
import com.intellij.ui.ClientProperty
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.update.MergingUpdateQueue
import com.intellij.util.ui.update.Update
import com.intellij.util.ui.showingScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import org.jetbrains.annotations.ApiStatus
import java.awt.Component
import java.awt.Graphics2D
@@ -26,27 +31,47 @@ import javax.swing.SwingUtilities
private val DO_NOT_SCROLL = Key.create<Boolean?>("SpotlightPainter.DO_NOT_SCROLL")
@OptIn(FlowPreview::class)
@ApiStatus.Internal
open class SpotlightPainter constructor(
open class SpotlightPainter(
private val target: JComponent,
parent: Disposable,
private val updater: (SpotlightPainter) -> Unit,
) : AbstractPainter(), ComponentHighlightingListener {
) : AbstractPainter() {
private val configurableOption = HashMap<String?, String?>()
private val queue = MergingUpdateQueue(
name = "SettingsSpotlight",
mergingTimeSpan = 200,
isActive = false,
modalityStateComponent = target,
parent = parent,
activationComponent = target,
)
private val glassPanel = GlassPanel(target)
private var isVisible: Boolean = false
private val updateRequests = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
init {
IdeGlassPaneUtil.installPainter(target, this, parent)
ApplicationManager.getApplication().getMessageBus().connect(parent).subscribe(ComponentHighlightingListener.TOPIC, this)
val activatable = IdeGlassPaneUtil.createPainterActivatable(target, this)
target.showingScope("SpotlightPainter") {
activatable.showNotify()
try {
ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(ComponentHighlightingListener.TOPIC, object : ComponentHighlightingListener {
override fun highlight(component: JComponent, searchString: String) {
// If several spotlight painters exist, they will receive each other updates,
// because they share one message bus (ComponentHighlightingListener.TOPIC).
// The painter should only draw spotlights for components in the hierarchy of `myTarget`
if (UIUtil.isAncestor(target, component)) {
glassPanel.addSpotlight(component)
if (target.getClientProperty(DO_NOT_SCROLL) != true && center(component)) {
target.putClientProperty(DO_NOT_SCROLL, true)
}
}
}
})
updateRequests
.debounce(200)
.collectLatest {
updateNow()
}
}
finally {
activatable.hideNotify()
}
}
}
companion object {
@@ -64,11 +89,7 @@ open class SpotlightPainter constructor(
override fun needsRepaint(): Boolean = true
fun updateLater() {
queue.queue(object : Update(this) {
override fun run() {
updateNow()
}
})
updateRequests.tryEmit(Unit)
}
fun updateNow() {
@@ -104,26 +125,8 @@ open class SpotlightPainter constructor(
fireNeedsRepaint(glassPanel)
}
override fun highlight(component: JComponent, searchString: String) {
// If several spotlight painters exist, they will receive each other updates,
// because they share one message bus (ComponentHighlightingListener.TOPIC).
// The painter should only draw spotlights for components in the hierarchy of `myTarget`
if (UIUtil.isAncestor(target, component)) {
glassPanel.addSpotlight(component)
if (isScrollingEnabled(target) && center(component)) {
disableScrolling(target)
}
}
}
}
private fun disableScrolling(target: JComponent) {
target.putClientProperty(DO_NOT_SCROLL, true)
}
private fun isScrollingEnabled(target: JComponent): Boolean = !ClientProperty.isTrue(target, DO_NOT_SCROLL)
private fun center(component: JComponent): Boolean {
var scrollPane: JScrollPane? = null
var c: Component? = component