mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
[github] Add an action to generate title and description with AI
GitOrigin-RevId: f1fedd5c53cd42bc51b67426f148e7855e73121c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
aa2c2605c5
commit
c792d53f08
@@ -6,6 +6,12 @@
|
||||
|
||||
<resource-bundle>messages.GithubBundle</resource-bundle>
|
||||
|
||||
<extensionPoints>
|
||||
<extensionPoint qualifiedName="intellij.vcs.github.titleAndDescriptionGenerator"
|
||||
interface="org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRTitleAndDescriptionGeneratorExtension"
|
||||
dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<httpRequestHandler implementation="org.jetbrains.plugins.github.authentication.GHOAuthCallbackHandler"/>
|
||||
|
||||
@@ -212,5 +218,7 @@
|
||||
</action>
|
||||
<add-to-group group-id="Git.Experimental.Branch.Popup.Actions"/>
|
||||
</group>
|
||||
|
||||
<group id="GitHub.Pull.Request.Create.Title.Actions"/>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create
|
||||
|
||||
import com.intellij.collaboration.async.awaitCancelling
|
||||
import com.intellij.collaboration.async.collectScoped
|
||||
import com.intellij.collaboration.async.extensionListFlow
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.ui.*
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil.defaultButton
|
||||
@@ -21,10 +22,7 @@ import com.intellij.collaboration.ui.util.*
|
||||
import com.intellij.collaboration.util.*
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.DataSink
|
||||
import com.intellij.openapi.actionSystem.EdtNoGetDataProvider
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
@@ -36,16 +34,14 @@ import com.intellij.ui.*
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.components.JBOptionButton
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.ui.util.preferredWidth
|
||||
import com.intellij.util.ui.*
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import com.intellij.vcs.log.util.VcsUserUtil
|
||||
import com.intellij.vcsUtil.showAbove
|
||||
import git4idea.ui.branch.MergeDirectionComponentFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import net.miginfocom.layout.CC
|
||||
import net.miginfocom.layout.LC
|
||||
import net.miginfocom.swing.MigLayout
|
||||
@@ -83,6 +79,7 @@ private const val TEXT_LINK_GAP = 8
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
internal object GHPRCreateComponentFactory {
|
||||
|
||||
fun createIn(cs: CoroutineScope, vm: GHPRCreateViewModel): JComponent = cs.create(vm)
|
||||
|
||||
private fun CoroutineScope.create(vm: GHPRCreateViewModel): JComponent {
|
||||
@@ -151,7 +148,10 @@ internal object GHPRCreateComponentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.textPanel(vm: GHPRCreateViewModel): JPanel {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun CoroutineScope.textPanel(vm: GHPRCreateViewModel): JComponent {
|
||||
val cs = this
|
||||
|
||||
val titleField = CodeReviewCreateReviewUIUtil.createTitleEditor(vm.project, message("pull.request.create.title")).apply {
|
||||
margins = JBUI.insets(EDITOR_MARGINS, EDITOR_MARGINS, 0, EDITOR_MARGINS)
|
||||
launchNow {
|
||||
@@ -215,7 +215,50 @@ internal object GHPRCreateComponentFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
return textPanel
|
||||
|
||||
return Wrapper().apply {
|
||||
bindContentIn(cs, GHPRTitleAndDescriptionGeneratorExtension.EP_NAME.extensionListFlow()) { extensions ->
|
||||
if (extensions.isEmpty()) {
|
||||
return@bindContentIn textPanel
|
||||
}
|
||||
|
||||
val extension = extensions.first()
|
||||
|
||||
val wrappedTextPanel = UiDataProvider.wrapComponent(textPanel) {
|
||||
it[GHPRCreateTitleAndDescriptionGenerationViewModel.DATA_KEY] = vm.titleAndDescriptionGenerationVm.value
|
||||
}
|
||||
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionGroup = actionManager.getAction("GitHub.Pull.Request.Create.Title.Actions") as ActionGroup
|
||||
val toolbar = actionManager.createActionToolbar("GHCreationTitle", actionGroup, true).apply {
|
||||
targetComponent = wrappedTextPanel
|
||||
}
|
||||
|
||||
// Force an action's update with every new value for commits and template
|
||||
cs.launchNow {
|
||||
vm.titleAndDescriptionGenerationVm.collect {
|
||||
if (it == null) {
|
||||
toolbar.updateActionsAsync()
|
||||
return@collect
|
||||
}
|
||||
|
||||
it.isGenerating.collect {
|
||||
toolbar.updateActionsAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cs.launchNow {
|
||||
vm.titleAndDescriptionGenerationVm.flatMapLatest { it?.generationFeedbackActivity ?: flowOf() }.collect {
|
||||
extension.onGenerationDone(vm.project, descriptionField, it)
|
||||
}
|
||||
}
|
||||
|
||||
CodeReviewCreateReviewUIUtil.createGenerationToolbarOverlay(wrappedTextPanel, toolbar) {
|
||||
(descriptionField as EditorEx).scrollPane.verticalScrollBar.preferredWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.directionSelector(vm: GHPRCreateViewModel): JComponent {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create
|
||||
|
||||
import com.intellij.collaboration.util.SingleCoroutineLauncher
|
||||
import com.intellij.openapi.actionSystem.DataKey
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.toolwindow.create.GHPRTitleAndDescriptionGeneratorExtension.*
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface GHPRTitleAndDescriptionGeneratorExtension {
|
||||
companion object {
|
||||
val EP_NAME = ExtensionPointName<GHPRTitleAndDescriptionGeneratorExtension>("intellij.vcs.github.titleAndDescriptionGenerator")
|
||||
}
|
||||
|
||||
sealed interface GenerationState
|
||||
data class GenerationError(val e: Exception) : GenerationState
|
||||
data class GenerationStep(val title: String, val description: String?) : GenerationState
|
||||
data class GenerationDone(val score: (helpfulYesOrNo: Boolean) -> Unit) : GenerationState
|
||||
|
||||
fun generate(project: Project, commits: List<VcsCommitMetadata>, template: String?): Flow<GenerationState>
|
||||
suspend fun onGenerationDone(project: Project, editor: Editor, score: (Boolean) -> Unit)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface GHPRCreateTitleAndDescriptionGenerationViewModel {
|
||||
companion object {
|
||||
val DATA_KEY = DataKey.create<GHPRCreateTitleAndDescriptionGenerationViewModel>("GHPRCreateTitleAndDescriptionGenerationViewModel")
|
||||
}
|
||||
|
||||
val isGenerating: StateFlow<Boolean>
|
||||
val generationFeedbackActivity: SharedFlow<(helpfulYesOrNo: Boolean) -> Unit>
|
||||
|
||||
fun stopGeneration()
|
||||
fun startGeneration()
|
||||
}
|
||||
|
||||
internal class GHPRCreateTitleAndDescriptionGenerationViewModelImpl(
|
||||
parentCs: CoroutineScope,
|
||||
private val project: Project,
|
||||
private val extension: GHPRTitleAndDescriptionGeneratorExtension,
|
||||
private val commits: List<VcsCommitMetadata>,
|
||||
private val template: String?,
|
||||
private val setTitle: (String) -> Unit,
|
||||
private val setDescription: (String) -> Unit,
|
||||
) : GHPRCreateTitleAndDescriptionGenerationViewModel {
|
||||
private val taskLauncher = SingleCoroutineLauncher(parentCs.childScope("Generate Title and Description"))
|
||||
override val isGenerating: StateFlow<Boolean> = taskLauncher.busy
|
||||
|
||||
private val _generationFeedbackActivity = MutableSharedFlow<(helpfulYesOrNo: Boolean) -> Unit>()
|
||||
override val generationFeedbackActivity: SharedFlow<(helpfulYesOrNo: Boolean) -> Unit> = _generationFeedbackActivity
|
||||
|
||||
override fun stopGeneration() {
|
||||
taskLauncher.cancel()
|
||||
}
|
||||
|
||||
override fun startGeneration() {
|
||||
taskLauncher.launch {
|
||||
extension.generate(project, commits, template).collect {
|
||||
when (it) {
|
||||
is GenerationError -> throw it.e
|
||||
is GenerationStep -> {
|
||||
setTitle(it.title)
|
||||
if (it.description != null) setDescription(it.description)
|
||||
}
|
||||
is GenerationDone -> {
|
||||
_generationFeedbackActivity.emit(it.score)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,8 @@ internal interface GHPRCreateViewModel {
|
||||
val descriptionText: StateFlow<String>
|
||||
val templateLoadingState: StateFlow<ComputedResult<String?>>
|
||||
|
||||
val titleAndDescriptionGenerationVm: StateFlow<GHPRCreateTitleAndDescriptionGenerationViewModel?>
|
||||
|
||||
val assigneesVm: LabeledListPanelViewModel<GHUser>
|
||||
val reviewersVm: LabeledListPanelViewModel<GHPullRequestRequestedReviewer>
|
||||
val labelsVm: LabeledListPanelViewModel<GHLabel>
|
||||
@@ -88,7 +90,7 @@ internal interface GHPRCreateViewModel {
|
||||
val baseRepo: GHGitRepositoryMapping,
|
||||
val baseBranch: GitRemoteBranch?,
|
||||
val headRepo: GHGitRepositoryMapping?,
|
||||
val headBranch: GitBranch?
|
||||
val headBranch: GitBranch?,
|
||||
)
|
||||
|
||||
sealed interface BranchesCheckResult {
|
||||
@@ -108,13 +110,14 @@ internal interface GHPRCreateViewModel {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class GHPRCreateViewModelImpl(override val project: Project,
|
||||
parentCs: CoroutineScope,
|
||||
private val repositoryManager: GHHostedRepositoriesManager,
|
||||
private val settings: GithubPullRequestsProjectUISettings,
|
||||
private val dataContext: GHPRDataContext,
|
||||
private val projectVm: GHPRToolWindowProjectViewModel)
|
||||
: GHPRCreateViewModel, Disposable {
|
||||
internal class GHPRCreateViewModelImpl(
|
||||
override val project: Project,
|
||||
parentCs: CoroutineScope,
|
||||
private val repositoryManager: GHHostedRepositoriesManager,
|
||||
private val settings: GithubPullRequestsProjectUISettings,
|
||||
private val dataContext: GHPRDataContext,
|
||||
private val projectVm: GHPRToolWindowProjectViewModel,
|
||||
) : GHPRCreateViewModel, Disposable {
|
||||
private val cs = parentCs.childScope(classAsCoroutineName())
|
||||
override val avatarIconsProvider: GHAvatarIconsProvider = dataContext.avatarIconsProvider
|
||||
private val repoData = dataContext.repositoryDataService
|
||||
@@ -171,6 +174,24 @@ internal class GHPRCreateViewModelImpl(override val project: Project,
|
||||
dataContext.repositoryDataService.loadTemplate()
|
||||
}.stateIn(cs, SharingStarted.Lazily, ComputedResult.loading())
|
||||
|
||||
override val titleAndDescriptionGenerationVm =
|
||||
GHPRTitleAndDescriptionGeneratorExtension.EP_NAME.extensionListFlow()
|
||||
.mapNotNull { it.firstOrNull() }
|
||||
.flatMapLatest { extension ->
|
||||
changesVm.flatMapLatest { it?.getOrNull()?.reviewCommits ?: flowOf(null) }
|
||||
.combine(templateLoadingState) { commits, templateResult ->
|
||||
if (commits == null || templateResult.isInProgress) {
|
||||
return@combine null
|
||||
}
|
||||
|
||||
commits to templateResult.getOrNull()
|
||||
}
|
||||
.mapNullableScoped { (commits, templateResult) ->
|
||||
GHPRCreateTitleAndDescriptionGenerationViewModelImpl(this, project, extension, commits, templateResult, ::setTitle, ::setDescription)
|
||||
}
|
||||
}
|
||||
.stateIn(cs, SharingStarted.Eagerly, null)
|
||||
|
||||
override val assigneesVm: LabeledListPanelViewModel<GHUser> = MetadataListViewModel(cs) {
|
||||
dataContext.repositoryDataService.loadIssuesAssignees()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user