IJPL-188924 Rewrite Build for Remote Development

introduce FrontendBuildView

GitOrigin-RevId: 6160ceab9240b0c87252da2936ef8dc78b676afd
This commit is contained in:
Dmitry Batrak
2026-01-13 10:02:18 +00:00
committed by intellij-monorepo-bot
parent ab8f8bd3ef
commit 32a76c8067
9 changed files with 258 additions and 49 deletions
@@ -3,6 +3,7 @@ package com.intellij.platform.buildView.frontend
import com.intellij.build.*
import com.intellij.ide.IdeBundle
import com.intellij.ide.OccurenceNavigator
import com.intellij.ide.OccurenceNavigatorSupport
import com.intellij.ide.actions.OccurenceNavigatorActionBase
import com.intellij.ide.nls.NlsMessages
@@ -56,8 +57,12 @@ import javax.swing.tree.*
private val LOG = fileLogger()
internal class BuildTreeView(private val project: Project, parentScope: CoroutineScope, private val buildViewId: BuildViewId)
: JPanel(), UiDataProvider, ComponentContainer {
internal class BuildTreeView(
private val project: Project,
parentScope: CoroutineScope,
private val buildViewId: BuildViewId,
private val backendNavigationAndFiltering: Boolean = true,
) : JPanel(), UiDataProvider, ComponentContainer {
private val uiScope = parentScope.childScope("BuildTreeView", Dispatchers.UI + ModalityState.any().asContextElement())
private val model = BuildTreeViewModelProxy.getInstance(buildViewId)
@@ -80,6 +85,18 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
private val occurenceNavigatorSupport = MyOccurenceNavigatorSupport(tree)
internal var showingSuccessful: Boolean
get() = filteringState.showSuccessful
set(value) {
handleFilteringStateChange(filteringState.copy(showSuccessful = value))
}
internal var showingWarnings: Boolean
get() = filteringState.showWarnings
set(value) {
handleFilteringStateChange(filteringState.copy(showWarnings = value))
}
// A factor which can correct for the difference between frontend's and backend's clocks,
// in case we need to display a duration of a process, for which we know the start timestamp on the backend.
// It doesn't include the connection latency, but that seems acceptable in our case.
@@ -98,15 +115,17 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
handleTreeEvent(event, nodeMap)
}
}
uiScope.launch(Dispatchers.EDT /* Navigatable-s might expect WIL to be taken */) {
model.getNavigationFlow().collect {
handleNavigation(it.forward)
if (backendNavigationAndFiltering) {
uiScope.launch(Dispatchers.EDT /* Navigatable-s might expect WIL to be taken */) {
model.getNavigationFlow().collect {
handleNavigation(it.forward)
}
}
}
uiScope.launch {
navigationContext.collect {
LOG.debug { "Navigation context: $it" }
model.onNavigationContextChange(it)
uiScope.launch {
navigationContext.collect {
LOG.debug { "Navigation context: $it" }
model.onNavigationContextChange(it)
}
}
}
}
@@ -219,7 +238,7 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
val nodeId = event.nodeId
if (nodeId == null) {
LOG.debug { "Clearing selection" }
tree.clearSelection()
clearTreeSelection()
}
else {
val node = nodeMap[nodeId]
@@ -239,7 +258,9 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
}
}
is BuildTreeFilteringState -> {
handleFilteringStateChange(event)
if (backendNavigationAndFiltering) {
handleFilteringStateChange(event)
}
}
}
}
@@ -292,10 +313,12 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
}
private fun updateNavigationContext() {
val hasPrevNode = occurenceNavigatorSupport.hasPreviousOccurence()
val hasNextNode = occurenceNavigatorSupport.hasNextOccurence()
val hasAnyNode = hasNextNode || hasPrevNode || (tree.selectionPath?.lastPathComponent as? MyNode)?.occurrenceNavigatable != null
navigationContext.value = BuildTreeNavigationContext(hasPrevNode, hasNextNode, hasAnyNode)
if (backendNavigationAndFiltering) {
val hasPrevNode = occurenceNavigatorSupport.hasPreviousOccurence()
val hasNextNode = occurenceNavigatorSupport.hasNextOccurence()
val hasAnyNode = hasNextNode || hasPrevNode || (tree.selectionPath?.lastPathComponent as? MyNode)?.occurrenceNavigatable != null
navigationContext.value = BuildTreeNavigationContext(hasPrevNode, hasNextNode, hasAnyNode)
}
}
fun maybeExpand(path: TreePath?): Boolean {
@@ -337,6 +360,26 @@ internal class BuildTreeView(private val project: Project, parentScope: Coroutin
return if (navigatables.isEmpty()) null else navigatables.toTypedArray()
}
internal fun clearTreeSelection() {
tree.clearSelection()
}
internal fun hasNextOccurence(): Boolean {
return occurenceNavigatorSupport.hasNextOccurence()
}
internal fun hasPreviousOccurence(): Boolean {
return occurenceNavigatorSupport.hasPreviousOccurence()
}
internal fun goNextOccurence(): OccurenceNavigator.OccurenceInfo? {
return occurenceNavigatorSupport.goNextOccurence()
}
internal fun goPreviousOccurence(): OccurenceNavigator.OccurenceInfo? {
return occurenceNavigatorSupport.goPreviousOccurence()
}
private class MyOccurenceNavigatorSupport(tree: JTree) : OccurenceNavigatorSupport(tree) {
override fun createDescriptorForNode(node: DefaultMutableTreeNode): Navigatable? {
return (node as? MyNode)?.occurrenceNavigatable?.navigatable()
@@ -0,0 +1,119 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.buildView.frontend
import com.intellij.build.BuildTreeConsoleView
import com.intellij.build.BuildViewId
import com.intellij.build.SuccessfulStepsToggleAction
import com.intellij.build.WarningsToggleAction
import com.intellij.icons.AllIcons
import com.intellij.ide.OccurenceNavigator
import com.intellij.ide.rpc.ComponentDirectTransferId
import com.intellij.ide.rpc.getComponent
import com.intellij.ide.util.PropertiesComponent
import com.intellij.lang.LangBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.util.coroutines.childScope
import com.intellij.ui.OnePixelSplitter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import java.awt.BorderLayout
import java.lang.ref.Reference
import java.lang.ref.WeakReference
import javax.swing.JPanel
import kotlin.reflect.KMutableProperty1
internal class FrontendBuildView(
project: Project,
parentScope: CoroutineScope,
treeViewId: BuildViewId?,
consoleComponent: ComponentDirectTransferId,
) : JPanel(BorderLayout()), Disposable {
private val scope = parentScope.childScope("FrontendBuildView")
internal val treeView: BuildTreeView?
init {
val console = consoleComponent.getComponent()
if (treeViewId != null) {
treeView = BuildTreeView(project, scope, treeViewId, false)
restoreSavedFiltering(treeView)
val splitter = OnePixelSplitter(BuildTreeConsoleView.SPLITTER_PROPERTY, BuildTreeConsoleView.SPLITTER_DEFAULT_PROPORTION).apply {
firstComponent = treeView
secondComponent = console
}
add(splitter)
}
else {
treeView = null
add(console)
}
}
override fun dispose() {
scope.cancel()
}
fun hasNextOccurence(): Boolean {
return treeView?.hasNextOccurence() ?: false
}
fun hasPreviousOccurence(): Boolean {
return treeView?.hasPreviousOccurence() ?: false
}
fun goNextOccurence(): OccurenceNavigator.OccurenceInfo? {
return treeView?.goNextOccurence()
}
fun goPreviousOccurence(): OccurenceNavigator.OccurenceInfo? {
return treeView?.goPreviousOccurence()
}
fun createFilteringActionGroup(): ActionGroup {
val actionGroup = DefaultActionGroup(LangBundle.message("action.filters.text"), true)
actionGroup.templatePresentation.icon = AllIcons.Actions.Show
val treeViewRef = WeakReference(treeView)
actionGroup.add(FilteringToggleAction(LangBundle.message("build.tree.filters.show.warnings"), WarningsToggleAction.STATE_KEY,
BuildTreeView::showingWarnings, WarningsToggleAction.DEFAULT_STATE, treeViewRef))
actionGroup.add(FilteringToggleAction(LangBundle.message("build.tree.filters.show.successful"), SuccessfulStepsToggleAction.STATE_KEY,
BuildTreeView::showingSuccessful, SuccessfulStepsToggleAction.DEFAULT_STATE, treeViewRef))
return actionGroup
}
}
private fun restoreSavedFiltering(treeView: BuildTreeView) {
treeView.showingWarnings = PropertiesComponent.getInstance().getBoolean(WarningsToggleAction.STATE_KEY,
WarningsToggleAction.DEFAULT_STATE)
treeView.showingSuccessful = PropertiesComponent.getInstance().getBoolean(SuccessfulStepsToggleAction.STATE_KEY,
SuccessfulStepsToggleAction.DEFAULT_STATE)
}
private class FilteringToggleAction(
@NlsContexts.Command text: String,
private val stateKey: String,
private val property: KMutableProperty1<BuildTreeView, Boolean>,
private val defaultState: Boolean,
private val treeViewRef: Reference<BuildTreeView?>,
) : ToggleAction(text), DumbAware {
override fun getActionUpdateThread() = ActionUpdateThread.EDT
override fun isSelected(e: AnActionEvent): Boolean {
val presentation = e.presentation
val treeView = treeViewRef.get()
if (treeView == null) {
presentation.isEnabledAndVisible = false
return false
}
presentation.isEnabledAndVisible = true
return property.get(treeView)
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
val treeView = treeViewRef.get() ?: return
property.set(treeView, state)
PropertiesComponent.getInstance().setValue(stateKey, state, defaultState)
}
}
@@ -18,7 +18,6 @@ import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.OnePixelDivider
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.wm.ToolWindow
@@ -57,14 +56,14 @@ internal class FrontendMultipleBuildsView(
private val id = buildContent.id
private val name = buildContent.name
private val buildMap = mutableMapOf<BuildId, FrontendBuildInfo>()
private val viewMap = mutableMapOf<FrontendBuildInfo, BuildView>()
private val viewMap = mutableMapOf<FrontendBuildInfo, FrontendBuildView>()
private val toolbarActions = DefaultActionGroup()
private val threeComponentsSplitter = createSplitter()
private val buildListModel = DefaultListModel<FrontendBuildInfo>()
private val buildList = createList()
private val content: Content = createContent(id, buildContent.isPinned)
private val focusWatcher = FocusWatcher()
private var activeView: BuildView? = null
private var activeView: FrontendBuildView? = null
private var locked = false
init {
@@ -165,7 +164,7 @@ internal class FrontendMultipleBuildsView(
buildList.setFixedCellHeight(JBUI.scale(UIUtil.LIST_FIXED_CELL_HEIGHT * 2))
}
private fun setActiveView(view: BuildView?) {
private fun setActiveView(view: FrontendBuildView?) {
if (activeView === view) {
return
}
@@ -176,20 +175,20 @@ internal class FrontendMultipleBuildsView(
content.preferredFocusableComponent = null
}
else {
threeComponentsSplitter.secondComponent = view.component
content.setPreferredFocusedComponent(Computable { view.preferredFocusableComponent })
threeComponentsSplitter.secondComponent = view
content.preferredFocusableComponent = view
if (focusWatcher.focused) {
view.preferredFocusableComponent?.requestFocusInWindow() // todo check IJPL-172285
view.requestFocusInWindow() // todo check IJPL-172285
}
}
}
private fun configureToolbar(view: BuildView?) {
private fun configureToolbar(view: FrontendBuildView?) {
toolbarActions.removeAll()
if (view != null) {
toolbarActions.addAll(*view.createConsoleActions())
// todo view-dependent actions
toolbarActions.add(PinBuildViewAction())
toolbarActions.add(createFilteringActionsGroup(WeakFilterableSupplier(view)))
toolbarActions.add(view.createFilteringActionGroup())
}
}
@@ -199,7 +198,7 @@ internal class FrontendMultipleBuildsView(
val info = FrontendBuildInfo(event)
buildMap[event.buildId] = info
buildListModel.addElement(info)
val buildView = BuildView(project, info, "build.toolwindow.${name}.selection.state", MyViewManager)
val buildView = FrontendBuildView(project, scope, event.treeViewId, event.consoleComponent)
viewMap[info] = buildView
Disposer.register(this, buildView)
if (activeView == null) {
@@ -243,10 +242,14 @@ internal class FrontendMultipleBuildsView(
is BuildViewEvent.BuildRemoved -> {
val info = buildMap.remove(event.buildId) ?: return
buildListModel.removeElement(info)
Disposer.dispose(viewMap.remove(info)!!)
val buildView = viewMap.remove(info)!!
Disposer.dispose(buildView)
if (buildListModel.size == 1) {
setBuildListVisible(false)
}
if (buildView === activeView) {
setActiveView(null)
}
}
}
}
@@ -304,10 +307,10 @@ internal class FrontendMultipleBuildsView(
val range = if (next) index..<buildListModel.size() else index downTo 0
return range.firstNotNullOfOrNull { i ->
val buildInfo = buildListModel.getElementAt(i)
val buildView: BuildView? = viewMap[buildInfo]
val buildView: FrontendBuildView? = viewMap[buildInfo]
if (buildView == null) return@firstNotNullOfOrNull null
if (i != index) {
val eventView = buildView.eventView
val eventView = buildView.treeView
if (eventView == null) return@firstNotNullOfOrNull null
eventView.clearTreeSelection()
}
@@ -429,9 +432,4 @@ internal class FrontendMultipleBuildsView(
override fun toString() = "Build[$id/$title]"
}
private object MyViewManager : ViewManager {
override fun isConsoleEnabledByDefault() = false
override fun isBuildContentView() = true
}
}
@@ -2,6 +2,7 @@
package com.intellij.build
import com.intellij.build.events.*
import com.intellij.ide.rpc.setupTransfer
import com.intellij.openapi.diagnostic.debug
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.diagnostic.trace
@@ -121,9 +122,13 @@ class BackendMultipleBuildsView(
buildView.onEvent(buildId, event)
val treeViewId = buildView.eventView?.buildViewId
val consoleComponent = buildView.consoleComponent.setupTransfer(buildView)
viewManager.onBuildStart(buildInfo)
viewModel.onBuildStarted(this, id, buildInfo.title, buildInfo.startTime, event.message,
buildInfo.isAutoFocusContent, buildInfo.isActivateToolWindowWhenAdded)
buildInfo.isAutoFocusContent, buildInfo.isActivateToolWindowWhenAdded,
treeViewId, consoleComponent)
}
else {
if (!isFirstErrorShown &&
@@ -101,7 +101,10 @@ public final class BuildTreeConsoleView implements ConsoleView, UiDataProvider,
public static final DataKey<BuildTreeConsoleView> COMPONENT_KEY = DataKey.create("BuildTreeConsoleView");
private static final @NonNls String TREE = "tree";
private static final @NonNls String SPLITTER_PROPERTY = "BuildView.Splitter.Proportion";
@ApiStatus.Internal
public static final @NonNls String SPLITTER_PROPERTY = "BuildView.Splitter.Proportion";
@ApiStatus.Internal
public static final float SPLITTER_DEFAULT_PROPORTION = 0.33f;
@Service(Service.Level.PROJECT)
private static final class ScopeHolder {
@@ -140,7 +143,8 @@ public final class BuildTreeConsoleView implements ConsoleView, UiDataProvider,
private final OccurenceNavigator myOccurrenceNavigatorSupport;
private final Set<BuildEvent> myDeferredEvents = ConcurrentCollectionFactory.createConcurrentSet();
private final boolean mySplitImplementation = Registry.is("build.toolwindow.split.tree", false);
private final boolean mySplitImplementation = Registry.is("build.toolwindow.split.tree", false) ||
Registry.is("build.toolwindow.split", false);
public BuildTreeConsoleView(@NotNull Project project,
@NotNull BuildDescriptor buildDescriptor,
@@ -204,7 +208,7 @@ public final class BuildTreeConsoleView implements ConsoleView, UiDataProvider,
}
myPanel.setLayout(new BorderLayout());
OnePixelSplitter myThreeComponentsSplitter = new OnePixelSplitter(SPLITTER_PROPERTY, 0.33f);
OnePixelSplitter myThreeComponentsSplitter = new OnePixelSplitter(SPLITTER_PROPERTY, SPLITTER_DEFAULT_PROPORTION);
myThreeComponentsSplitter.setFirstComponent(treeComponent);
List<Filter> filters = myBuildDescriptor.getExecutionFilters();
myConsoleViewHandler = new ConsoleViewHandler(myProject, myTree, myBuildProgressRootNode, this,
@@ -1119,6 +1123,15 @@ public final class BuildTreeConsoleView implements ConsoleView, UiDataProvider,
myConsoleViewHandler.setNodeIfChanged(node);
}
BuildViewId getBuildViewId() {
return mySplitImplementation ? myTreeVm.getId() : null;
}
@NotNull
JComponent getConsoleComponent() {
return myConsoleViewHandler.getComponent();
}
private static final class ConsoleViewHandler implements Disposable {
private static final String EMPTY_CONSOLE_NAME = "empty";
private final Project myProject;
@@ -245,6 +245,19 @@ public class BuildView extends CompositeView<ExecutionConsole>
return getView(BuildTreeConsoleView.class.getName(), BuildTreeConsoleView.class);
}
// returns a non-null value after the view is initialized (after build start event is processed)
JComponent getConsoleComponent() {
BuildTreeConsoleView eventView = getEventView();
if (eventView != null) {
return eventView.getConsoleComponent();
}
ExecutionConsole console = getView(CONSOLE_VIEW_NAME);
if (console != null) {
return console.getComponent();
}
return null;
}
@Override
public void addChangeListener(@NotNull ChangeListener listener, @NotNull Disposable parent) {
ExecutionConsole console = getConsoleView();
@@ -2,6 +2,7 @@
package com.intellij.build
import com.intellij.build.events.BuildEventsNls
import com.intellij.ide.rpc.ComponentDirectTransferId
import com.intellij.ide.ui.icons.IconId
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.rpc.Id
@@ -23,6 +24,8 @@ sealed interface BuildViewEvent {
val message: @BuildEventsNls.Message String,
val requestFocus: Boolean,
val activateToolWindow: Boolean,
val treeViewId: BuildViewId?,
val consoleComponent: ComponentDirectTransferId,
) : BuildViewEvent
@Serializable
@@ -2,6 +2,7 @@
package com.intellij.build
import com.intellij.build.events.BuildEventsNls
import com.intellij.ide.rpc.ComponentDirectTransferId
import com.intellij.ide.ui.icons.rpcId
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
@@ -27,10 +28,13 @@ class BuildViewViewModel(scope: CoroutineScope) : FlowWithHistory<BuildViewEvent
message: @BuildEventsNls.Message String,
requestFocus: Boolean,
activateToolWindow: Boolean,
treeViewId: BuildViewId?,
consoleComponent: ComponentDirectTransferId,
) {
updateHistoryAndEmit {
val viewState = viewStates.computeIfAbsent(view) { ViewState() }
BuildViewEvent.BuildStarted(view.toDto(), buildId, title, startTime, message, requestFocus, activateToolWindow).also {
BuildViewEvent.BuildStarted(view.toDto(), buildId, title, startTime, message, requestFocus, activateToolWindow,
treeViewId, consoleComponent).also {
viewState.buildMap[buildId] = BuildState(it.copy(requestFocus = false, activateToolWindow = false))
}
}
@@ -48,7 +48,8 @@ internal fun install(filterable: Filterable<ExecutionNode>) {
WarningsToggleAction.install(filterable)
}
internal open class FilterToggleAction(
@ApiStatus.Internal
open class FilterToggleAction(
@NlsContexts.Command text: String,
private val stateKey: String?,
private val filterableRef: Supplier<Filterable<ExecutionNode>?>,
@@ -100,26 +101,36 @@ private fun install(
}
}
internal class SuccessfulStepsToggleAction(filterable: Supplier<Filterable<ExecutionNode>?>) :
FilterToggleAction(LangBundle.message("build.tree.filters.show.successful"), STATE_KEY, filterable, SUCCESSFUL_STEPS_FILTER, false), DumbAware {
@ApiStatus.Internal
class SuccessfulStepsToggleAction(filterable: Supplier<Filterable<ExecutionNode>?>) :
FilterToggleAction(LangBundle.message("build.tree.filters.show.successful"),
STATE_KEY, filterable, SUCCESSFUL_STEPS_FILTER, DEFAULT_STATE), DumbAware {
companion object {
private const val STATE_KEY = "build.toolwindow.show.successful.steps.selection.state"
@ApiStatus.Internal
const val STATE_KEY: String = "build.toolwindow.show.successful.steps.selection.state"
@ApiStatus.Internal
const val DEFAULT_STATE: Boolean = false
fun install(filterable: Filterable<ExecutionNode>) {
install(filterable, SUCCESSFUL_STEPS_FILTER, STATE_KEY, false)
install(filterable, SUCCESSFUL_STEPS_FILTER, STATE_KEY, DEFAULT_STATE)
}
}
}
internal class WarningsToggleAction(filterable: Supplier<Filterable<ExecutionNode>?>) :
FilterToggleAction(LangBundle.message("build.tree.filters.show.warnings"), STATE_KEY, filterable, WARNINGS_FILTER, true), DumbAware {
@ApiStatus.Internal
class WarningsToggleAction(filterable: Supplier<Filterable<ExecutionNode>?>) :
FilterToggleAction(LangBundle.message("build.tree.filters.show.warnings"),
STATE_KEY, filterable, WARNINGS_FILTER, DEFAULT_STATE), DumbAware {
companion object {
private const val STATE_KEY = "build.toolwindow.show.warnings.selection.state"
@ApiStatus.Internal
const val STATE_KEY: String = "build.toolwindow.show.warnings.selection.state"
@ApiStatus.Internal
const val DEFAULT_STATE: Boolean = true
fun install(filterable: Filterable<ExecutionNode>) {
install(filterable, WARNINGS_FILTER, STATE_KEY, true)
install(filterable, WARNINGS_FILTER, STATE_KEY, DEFAULT_STATE)
}
}
}