[terminal] Initial commit for creating test for popUp completion.

(cherry picked from commit 0134493ca5207aad0c4b586f88398507f9b30940)

IJ-MR-170869

GitOrigin-RevId: 491b2ee14a59a784efb9bc56aa4e46f134135a04
This commit is contained in:
Yuliya Karalenka
2025-08-07 16:16:56 +00:00
committed by intellij-monorepo-bot
parent c70eded9cb
commit 2c2d95bd92
8 changed files with 143 additions and 13 deletions
@@ -31,6 +31,11 @@ import com.intellij.util.ui.components.BorderLayoutPanel
import com.jediterm.core.util.TermSize
import com.jediterm.terminal.TtyConnector
import kotlinx.coroutines.*
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.plugins.terminal.TerminalFontSettingsListener
import org.jetbrains.plugins.terminal.TerminalFontSettingsService
import org.jetbrains.plugins.terminal.TerminalFontSizeProviderImpl
import org.jetbrains.plugins.terminal.TerminalPanelMarker
import org.jetbrains.plugins.terminal.block.TerminalContentView
import org.jetbrains.plugins.terminal.block.output.TerminalOutputEditorInputMethodSupport
@@ -54,7 +59,8 @@ import java.util.concurrent.CompletableFuture
import javax.swing.JComponent
import kotlin.math.min
internal class ReworkedTerminalView(
@ApiStatus.Internal
class ReworkedTerminalView(
private val project: Project,
settings: JBTerminalSystemSettingsProviderBase,
private val sessionFuture: CompletableFuture<TerminalSession>,
@@ -63,22 +69,29 @@ internal class ReworkedTerminalView(
private val coroutineScope = terminalProjectScope(project).childScope("ReworkedTerminalView")
private val sessionModel: TerminalSessionModel
private val blocksModel: TerminalBlocksModel
@VisibleForTesting
val blocksModel: TerminalBlocksModel
private val encodingManager: TerminalKeyEncodingManager
private val controller: TerminalSessionController
private val terminalInput: TerminalInput
private val terminalSearchController: TerminalSearchController
private val outputEditor: EditorEx
@VisibleForTesting
val outputEditor: EditorEx
private val outputHyperlinkFacade: FrontendTerminalHyperlinkFacade?
private val alternateBufferEditor: EditorEx
@VisibleForTesting
val outputModel: TerminalOutputModelImpl
private val alternateBufferHyperlinkFacade: FrontendTerminalHyperlinkFacade?
private val outputModel: TerminalOutputModelImpl
private val scrollingModel: TerminalOutputScrollingModel
private var isAlternateScreenBuffer = false
private val terminalPanel: TerminalPanel
@VisibleForTesting
val outputEditorEventsHandler: TerminalEventsHandler
override val component: JComponent
get() = terminalPanel
@@ -105,18 +118,18 @@ internal class ReworkedTerminalView(
alternateBufferEditor = TerminalEditorFactory.createAlternateBufferEditor(project, settings, parentDisposable = this)
val alternateBufferModel = TerminalOutputModelImpl(alternateBufferEditor.document, maxOutputLength = 0)
val alternateBufferEventsHandler = TerminalEventsHandlerImpl(sessionModel, alternateBufferEditor, encodingManager, terminalInput, settings, null, alternateBufferModel)
configureOutputEditor(
project,
editor = alternateBufferEditor,
model = alternateBufferModel,
settings,
sessionModel,
encodingManager,
terminalInput,
coroutineScope.childScope("TerminalAlternateBufferModel"),
scrollingModel = null,
fusCursorPaintingListener,
fusFirstOutputListener,
alternateBufferEventsHandler,
)
alternateBufferHyperlinkFacade = if (isSplitHyperlinksSupportEnabled()) {
FrontendTerminalHyperlinkFacade(
@@ -138,6 +151,7 @@ internal class ReworkedTerminalView(
scrollingModel = TerminalOutputScrollingModelImpl(outputEditor, outputModel, sessionModel, coroutineScope.childScope("TerminalOutputScrollingModel"))
outputEditor.putUserData(TerminalOutputScrollingModel.KEY, scrollingModel)
outputEditorEventsHandler = TerminalEventsHandlerImpl(sessionModel, outputEditor, encodingManager, terminalInput, settings, scrollingModel, outputModel)
configureOutputEditor(
project,
@@ -145,12 +159,11 @@ internal class ReworkedTerminalView(
model = outputModel,
settings,
sessionModel,
encodingManager,
terminalInput,
coroutineScope.childScope("TerminalOutputModel"),
scrollingModel,
fusCursorPaintingListener,
fusFirstOutputListener,
outputEditorEventsHandler,
)
outputEditor.putUserData(TerminalSessionModel.KEY, sessionModel)
@@ -318,12 +331,11 @@ internal class ReworkedTerminalView(
model: TerminalOutputModel,
settings: JBTerminalSystemSettingsProviderBase,
sessionModel: TerminalSessionModel,
encodingManager: TerminalKeyEncodingManager,
terminalInput: TerminalInput,
coroutineScope: CoroutineScope,
scrollingModel: TerminalOutputScrollingModel?,
fusCursorPainterListener: TerminalFusCursorPainterListener?,
fusFirstOutputListener: TerminalFusFirstOutputListener?,
eventsHandler: TerminalEventsHandlerImpl,
) {
val parentDisposable = coroutineScope.asDisposable() // same lifecycle as `this@ReworkedTerminalView`
@@ -1,9 +1,11 @@
package com.intellij.terminal.frontend
import org.jetbrains.annotations.ApiStatus
import java.awt.event.MouseEvent
import java.awt.event.MouseWheelEvent
internal interface TerminalEventsHandler {
@ApiStatus.Internal
interface TerminalEventsHandler {
fun keyTyped(e: TimedKeyEvent) {}
fun keyPressed(e: TimedKeyEvent) {}
@@ -53,7 +53,7 @@ internal class TerminalTypeAhead(
fun isDisabled() = PlatformUtils.isJetBrainsClient() || isDisabledInRegistry() || !isTypingCommand()
private fun isDisabledInRegistry(): Boolean = !Registry.`is`("terminal.type.ahead", false)
private fun isTypingCommand(): Boolean = blocksModel.blocks.lastOrNull()?.let { lastBlock ->
// The command start offset is where the prompt ends. If it's not there yet, it means the user can't type a command yet.
// The output start offset is -1 until the command starts executing. Once that happens, it means the user can't type anymore.
@@ -1,5 +1,6 @@
package com.intellij.terminal.frontend
import org.jetbrains.annotations.ApiStatus
import java.awt.event.KeyEvent
import kotlin.time.TimeMark
import kotlin.time.TimeSource
@@ -9,7 +10,8 @@ import kotlin.time.TimeSource
* Original [java.awt.event.KeyEvent] already has the [KeyEvent.`when`] property, but this value is not monotonic
* and can't be used for reliable measurements. So, we use [kotlin.time.TimeMark] instead.
*/
internal data class TimedKeyEvent(
@ApiStatus.Internal
data class TimedKeyEvent(
val original: KeyEvent,
val initTime: TimeMark = TimeSource.Monotonic.markNow(),
)
+1
View File
@@ -42,6 +42,7 @@ jvm_library(
"//platform/projectModel-api:projectModel",
"//platform/util/coroutines",
"//platform/platform-impl/internal",
"//plugins/sh",
"//platform/code-style-api:codeStyle",
"@lib//:assert_j",
"@lib//:jediterm-core",
@@ -28,6 +28,7 @@
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.util.coroutines" />
<orderEntry type="module" module-name="intellij.platform.ide.internal" />
<orderEntry type="module" module-name="intellij.sh" />
<orderEntry type="module" module-name="intellij.platform.codeStyle" />
<orderEntry type="library" name="assertJ" level="project" />
<orderEntry type="library" name="jediterm-core" level="project" />
@@ -0,0 +1,85 @@
package com.intellij.terminal.tests.reworked.frontend
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionUiKind
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.terminal.frontend.ReworkedTerminalView
import com.intellij.terminal.frontend.TimedKeyEvent
import com.intellij.terminal.session.TerminalBlocksModelState
import com.intellij.terminal.session.TerminalOutputBlock
import com.intellij.terminal.session.TerminalSession
import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider
import org.jetbrains.plugins.terminal.LocalBlockTerminalRunner.Companion.REWORKED_TERMINAL_COMPLETION_POPUP
import org.jetbrains.plugins.terminal.block.reworked.TerminalOutputModel
import java.awt.event.KeyEvent
import java.awt.event.KeyEvent.VK_UNDEFINED
import java.util.concurrent.CompletableFuture
import kotlin.time.TimeSource
class TerminalCompletionFixture(val project: Project, testRootDisposable: Disposable) {
private val view: ReworkedTerminalView
init {
val sessionFuture: CompletableFuture<TerminalSession> = CompletableFuture<TerminalSession>()
view = ReworkedTerminalView(project, JBTerminalSystemSettingsProvider(), sessionFuture, null)
Disposer.register(testRootDisposable, view)
val terminalOutputBlock = TerminalOutputBlock(0, 0, 0, -1, 0, null)
val blocksModelState = TerminalBlocksModelState(listOf(terminalOutputBlock), 0)
view.blocksModel.restoreFromState(blocksModelState)
Registry.get(REWORKED_TERMINAL_COMPLETION_POPUP).setValue(true, testRootDisposable)
Registry.get("terminal.type.ahead").setValue(true, testRootDisposable)
}
fun type(text: String) {
for (c in text) {
typeChar(c)
}
}
fun typeChar(keyChar: Char) {
val keyEvent = KeyEvent(view.outputEditor.component, KeyEvent.KEY_TYPED, 1, 0,
VK_UNDEFINED, keyChar, KeyEvent.KEY_LOCATION_UNKNOWN)
val timedKeyEvent = TimedKeyEvent(keyEvent, TimeSource.Monotonic.markNow())
view.outputEditorEventsHandler.keyTyped(timedKeyEvent)
}
fun callCompletionPopup() {
val actionId = "Terminal.CommandCompletion.Gen2"
val action = ActionManager.getInstance().getAction(actionId)
val context = SimpleDataContext.builder()
.add(CommonDataKeys.PROJECT, project)
.add(CommonDataKeys.EDITOR, view.outputEditor)
.add(TerminalOutputModel.KEY, view.outputModel)
.build()
val event = AnActionEvent.createEvent(action, context, null,
"", ActionUiKind.NONE, null)
ActionUtil.updateAction(action, event)
if (event.presentation.isEnabledAndVisible) {
ActionUtil.performAction(action, event)
}
}
fun isLookupActive(): Boolean {
val lookupManager = LookupManager.getInstance(project)
return lookupManager.activeLookup != null
}
fun getLookupElements(): List<LookupElement> {
val lookupManager = LookupManager.getInstance(project)
val activeLookup = lookupManager.activeLookup
return activeLookup?.items ?: emptyList()
}
}
@@ -0,0 +1,27 @@
package com.intellij.terminal.tests.reworked.frontend
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class TerminalCompletionTest : BasePlatformTestCase() {
@Test
fun `test completions list filtered on typing`() {
val terminalCompletionFixture = TerminalCompletionFixture(project, testRootDisposable)
terminalCompletionFixture.type("git s")
terminalCompletionFixture.callCompletionPopup()
assertEquals(true, terminalCompletionFixture.isLookupActive())
val beforeResult = terminalCompletionFixture.getLookupElements()
val beforeResultStrings = beforeResult.map { it.lookupString }
assertEquals(listOf("show", "stage", "stash", "status", "submodule", "switch"), beforeResultStrings)
terminalCompletionFixture.type("t")
val result = terminalCompletionFixture.getLookupElements()
val resultStrings = result.map { it.lookupString }
assertEquals(listOf("stage", "stash", "status"), resultStrings)
}
}