Skip to content

Commit 10861f2

Browse files
authored
Merge pull request #6702 from SoLoHiC/feature/jetbrainPlugins/terminalContent
feat: Support @Terminal Context Provider and RunInTerminal Button for Jetbrains Plugin
2 parents 2b04352 + ebd5154 commit 10861f2

File tree

6 files changed

+122
-26
lines changed

6 files changed

+122
-26
lines changed

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IdeProtocolClient.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@ import com.github.continuedev.continueintellijextension.editor.DiffStreamService
88
import com.github.continuedev.continueintellijextension.editor.EditorUtils
99
import com.github.continuedev.continueintellijextension.error.ContinueErrorService
1010
import com.github.continuedev.continueintellijextension.protocol.*
11-
import com.github.continuedev.continueintellijextension.services.*
12-
import com.github.continuedev.continueintellijextension.utils.*
11+
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
12+
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
13+
import com.github.continuedev.continueintellijextension.utils.getMachineUniqueID
14+
import com.github.continuedev.continueintellijextension.utils.uuid
1315
import com.google.gson.Gson
1416
import com.intellij.openapi.application.ApplicationManager
1517
import com.intellij.openapi.command.WriteCommandAction
16-
import com.intellij.openapi.components.ServiceManager
1718
import com.intellij.openapi.components.service
1819
import com.intellij.openapi.editor.SelectionModel
1920
import com.intellij.openapi.fileEditor.FileEditorManager
2021
import com.intellij.openapi.project.DumbAware
2122
import com.intellij.openapi.project.Project
2223
import com.intellij.openapi.vfs.VirtualFileManager
2324
import com.intellij.openapi.wm.ToolWindowManager
24-
import kotlinx.coroutines.*
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.ExperimentalCoroutinesApi
28+
import kotlinx.coroutines.launch
2529
import java.awt.Toolkit
2630
import java.awt.datatransfer.StringSelection
2731

@@ -323,7 +327,11 @@ class IdeProtocolClient(
323327
}
324328

325329
"runCommand" -> {
326-
// Running commands not yet supported in JetBrains
330+
val params = Gson().fromJson(
331+
dataElement.toString(),
332+
RunCommandParams::class.java
333+
)
334+
ide.runCommand(params.command, params.options)
327335
respond(null)
328336
}
329337

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IntelliJIde.kt

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
package com.github.continuedev.continueintellijextension.`continue`
32

43
import com.github.continuedev.continueintellijextension.*
@@ -27,12 +26,14 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
2726
import com.intellij.openapi.fileEditor.FileEditorManager
2827
import com.intellij.openapi.project.Project
2928
import com.intellij.openapi.project.guessProjectDir
30-
import com.intellij.openapi.util.IconLoader
3129
import com.intellij.openapi.vfs.LocalFileSystem
3230
import com.intellij.openapi.vfs.VirtualFileManager
31+
import com.intellij.openapi.wm.ToolWindowManager
3332
import com.intellij.psi.PsiDocumentManager
3433
import com.intellij.testFramework.LightVirtualFile
3534
import kotlinx.coroutines.*
35+
import org.jetbrains.plugins.terminal.ShellTerminalWidget
36+
import org.jetbrains.plugins.terminal.TerminalView
3637
import java.awt.Toolkit
3738
import java.awt.datatransfer.DataFlavor
3839
import java.io.BufferedReader
@@ -54,7 +55,7 @@ class IntelliJIDE(
5455
init {
5556
try {
5657
val os = getOS()
57-
58+
5859
if (os == OS.LINUX || os == OS.MAC) {
5960
val file = File(ripgrep)
6061
if (!file.canExecute()) {
@@ -135,7 +136,33 @@ class IntelliJIDE(
135136
}
136137

137138
override suspend fun getTerminalContents(): String {
138-
return ""
139+
return withContext(Dispatchers.EDT) {
140+
try {
141+
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Terminal")
142+
143+
val terminalView = TerminalView.getInstance(project)
144+
// Find the first terminal widget selected, whatever its state, running command or not.
145+
val widget = terminalView.getWidgets().filterIsInstance<ShellTerminalWidget>().firstOrNull {
146+
toolWindow?.getContentManager()?.getContent(it)?.isSelected ?: false
147+
}
148+
149+
if (widget != null) {
150+
val textBuffer = widget.terminalTextBuffer
151+
val stringBuilder = StringBuilder()
152+
// Iterate through all lines in the buffer (history + screen)
153+
for (i in 0 until textBuffer.historyLinesCount + textBuffer.screenLinesCount) {
154+
stringBuilder.append(textBuffer.getLine(i).text).append('\n')
155+
}
156+
stringBuilder.toString()
157+
} else {
158+
"" // Return empty if no terminal is available
159+
}
160+
} catch (e: Exception) {
161+
println("Error getting terminal contents: ${e.message}")
162+
e.printStackTrace()
163+
"" // Return empty on error
164+
}
165+
}
139166
}
140167

141168
override suspend fun getDebugLocals(threadIndex: Int): String {
@@ -220,8 +247,65 @@ class IntelliJIDE(
220247
}
221248
}
222249

223-
override suspend fun runCommand(command: String) {
224-
throw NotImplementedError("runCommand not implemented in IntelliJ")
250+
override suspend fun runCommand(command: String, options: TerminalOptions?) {
251+
val terminalOptions =
252+
options ?: TerminalOptions(reuseTerminal = true, terminalName = null, waitForCompletion = false)
253+
254+
ApplicationManager.getApplication().invokeLater {
255+
try {
256+
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Terminal")
257+
toolWindow?.activate({
258+
try {
259+
val terminalView = TerminalView.getInstance(project)
260+
var widget: ShellTerminalWidget? = null
261+
262+
// 1. Handle reuseTerminal option
263+
if (terminalOptions.reuseTerminal == true && terminalView.getWidgets().isNotEmpty()) {
264+
// 2. Find by terminalName if provided
265+
if (terminalOptions.terminalName != null) {
266+
widget = terminalView.getWidgets().filterIsInstance<ShellTerminalWidget>()
267+
.firstOrNull {
268+
toolWindow.contentManager.getContent(it).tabName == terminalOptions.terminalName
269+
&& !it.hasRunningCommands()
270+
}
271+
} else {
272+
// 3. Find active terminal, or fall back to the first one
273+
widget = terminalView.getWidgets().filterIsInstance<ShellTerminalWidget>()
274+
.firstOrNull { toolWindow.contentManager.getContent(it).isSelected }
275+
?: terminalView.getWidgets().filterIsInstance<ShellTerminalWidget>().firstOrNull {
276+
!it.hasRunningCommands()
277+
}
278+
}
279+
}
280+
281+
// 4. Create a new terminal if needed
282+
if (widget == null) {
283+
widget = terminalView.createLocalShellWidget(
284+
project.basePath,
285+
terminalOptions.terminalName,
286+
true
287+
)
288+
} else {
289+
// Ensure the found widget is visible
290+
val content = toolWindow.contentManager.getContent(widget)
291+
if (content != null) {
292+
toolWindow.contentManager.setSelectedContent(content, true)
293+
}
294+
}
295+
296+
// 5. Show and send text
297+
widget.ttyConnector?.write(command)
298+
299+
} catch (e: Exception) {
300+
println("Error during terminal widget handling: ${e.message}")
301+
e.printStackTrace()
302+
}
303+
}, true)
304+
} catch (e: Exception) {
305+
println("Error activating terminal tool window: ${e.message}")
306+
e.printStackTrace()
307+
}
308+
}
225309
}
226310

227311
override suspend fun saveFile(filepath: String) {
@@ -343,7 +427,7 @@ class IntelliJIDE(
343427
}
344428

345429
val command = GeneralCommandLine(commandArgs)
346-
430+
347431
command.setWorkDirectory(project.basePath)
348432
val results = ExecUtil.execAndGetOutput(command).stdout
349433
return results.split("\n")
@@ -357,11 +441,12 @@ class IntelliJIDE(
357441
throw NotImplementedError("Ripgrep not supported, this workspace is remote")
358442
}
359443
}
444+
360445
override suspend fun getSearchResults(query: String, maxResults: Int?): String {
361446
val ideInfo = this.getIdeInfo()
362447
if (ideInfo.remoteName == "local") {
363448
try {
364-
val commandArgs = mutableListOf(
449+
val commandArgs = mutableListOf(
365450
ripgrep,
366451
"-i",
367452
"--ignore-file",
@@ -372,20 +457,20 @@ class IntelliJIDE(
372457
"2",
373458
"--heading"
374459
)
375-
460+
376461
// Conditionally add maxResults flag
377462
if (maxResults != null) {
378463
commandArgs.add("-m")
379464
commandArgs.add(maxResults.toString())
380465
}
381-
466+
382467
// Add the search query and path
383468
commandArgs.add("-e")
384469
commandArgs.add(query)
385470
commandArgs.add(".")
386471

387472
val command = GeneralCommandLine(commandArgs)
388-
473+
389474
command.setWorkDirectory(project.basePath)
390475
return ExecUtil.execAndGetOutput(command).stdout
391476
} catch (exception: Exception) {

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/protocol/ide.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.github.continuedev.continueintellijextension.protocol
22

3-
import com.github.continuedev.continueintellijextension.*
3+
import com.github.continuedev.continueintellijextension.Range
4+
import com.github.continuedev.continueintellijextension.TerminalOptions
45

56
data class GetControlPlaneSessionInfoParams(val silent: Boolean, val useOnboarding: Boolean)
67

@@ -59,4 +60,6 @@ data class GetGitRootPathParams(val dir: String)
5960

6061
data class ListDirParams(val dir: String)
6162

62-
data class GetFileStatsParams(val files: List<String>)
63+
data class GetFileStatsParams(val files: List<String>)
64+
65+
data class RunCommandParams(val command: String, val options: TerminalOptions?)

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/types.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ data class ContinueRcJson(
153153
val mergeBehavior: ConfigMergeType
154154
)
155155

156+
data class TerminalOptions(
157+
val reuseTerminal: Boolean?,
158+
val terminalName: String?,
159+
val waitForCompletion: Boolean?
160+
)
156161

157162
interface IDE {
158163
suspend fun getIdeInfo(): IdeInfo
@@ -196,7 +201,7 @@ interface IDE {
196201

197202
suspend fun openUrl(url: String)
198203

199-
suspend fun runCommand(command: String)
204+
suspend fun runCommand(command: String, options: TerminalOptions?)
200205

201206
suspend fun saveFile(filepath: String)
202207

@@ -317,4 +322,4 @@ data class ShowFilePayload(
317322
sealed class FimResult {
318323
data class FimEdit(val fimText: String) : FimResult()
319324
object NotFimEdit : FimResult()
320-
}
325+
}

extensions/intellij/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<![CDATA[View the latest release notes on <a href="https://github.com/continuedev/continue/releases">GitHub</a>]]></change-notes>
88

99
<depends>com.intellij.modules.platform</depends>
10+
<depends>org.jetbrains.plugins.terminal</depends>
1011

1112
<!-- See here for why this is optional: https://github.com/continuedev/continue/issues/2775#issuecomment-2535620877-->
1213
<depends optional="true" config-file="continueintellijextension-withJSON.xml">

gui/src/components/StyledMarkdownPreview/StepContainerPreToolbar/RunInTerminalButton.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { CommandLineIcon } from "@heroicons/react/24/outline";
22
import { useContext } from "react";
33
import { lightGray, vscForeground } from "../..";
44
import { IdeMessengerContext } from "../../../context/IdeMessenger";
5-
import { isJetBrains } from "../../../util";
65
import { extractCommand } from "../utils/commandExtractor";
76

87
interface RunInTerminalButtonProps {
@@ -12,11 +11,6 @@ interface RunInTerminalButtonProps {
1211
export function RunInTerminalButton({ command }: RunInTerminalButtonProps) {
1312
const ideMessenger = useContext(IdeMessengerContext);
1413

15-
if (isJetBrains()) {
16-
// JetBrains plugin doesn't currently have a way to run the command in the terminal for the user
17-
return null;
18-
}
19-
2014
function runInTerminal() {
2115
// Extract just the command line
2216
const extractedCommand = extractCommand(command);

0 commit comments

Comments
 (0)