diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt index 3dd2a57b5582..774d0e5c44e3 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/ReworkedTerminalView.kt @@ -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, @@ -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` diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventsHandler.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventsHandler.kt index 7a9c305bb17c..8aaab10856a5 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventsHandler.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalEventsHandler.kt @@ -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) {} diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalTypeAhead.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalTypeAhead.kt index b375be961cc5..e638373cce45 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalTypeAhead.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TerminalTypeAhead.kt @@ -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. diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TimedKeyEvent.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TimedKeyEvent.kt index ffe874768539..355576284d25 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TimedKeyEvent.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/TimedKeyEvent.kt @@ -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(), ) \ No newline at end of file diff --git a/plugins/terminal/tests/BUILD.bazel b/plugins/terminal/tests/BUILD.bazel index 541f51851c4f..caa40d5bd030 100644 --- a/plugins/terminal/tests/BUILD.bazel +++ b/plugins/terminal/tests/BUILD.bazel @@ -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", diff --git a/plugins/terminal/tests/intellij.terminal.tests.iml b/plugins/terminal/tests/intellij.terminal.tests.iml index 25674788bb31..3cf0a68a6f19 100644 --- a/plugins/terminal/tests/intellij.terminal.tests.iml +++ b/plugins/terminal/tests/intellij.terminal.tests.iml @@ -28,6 +28,7 @@ + diff --git a/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionFixture.kt b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionFixture.kt new file mode 100644 index 000000000000..4d68af8c4a2c --- /dev/null +++ b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionFixture.kt @@ -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 = CompletableFuture() + 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 { + val lookupManager = LookupManager.getInstance(project) + val activeLookup = lookupManager.activeLookup + return activeLookup?.items ?: emptyList() + } +} \ No newline at end of file diff --git a/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionTest.kt b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionTest.kt new file mode 100644 index 000000000000..1f0170951c23 --- /dev/null +++ b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/TerminalCompletionTest.kt @@ -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) + } + +} \ No newline at end of file