[github] Rework pull request data loaders

Use single thread to load everything and publish data via CompletableFuture's
This commit is contained in:
Ivan Semenov
2018-10-02 18:45:38 +03:00
parent db0913d1a6
commit e67d5ca845
10 changed files with 257 additions and 296 deletions
@@ -49,12 +49,10 @@ class GithubPullRequestsComponentFactory(private val project: Project,
CachingGithubAvatarIconsProvider.Factory(
avatarLoader, imageResizer, requestExecutor))
val detailsLoader = GithubPullRequestsDetailsLoader(progressManager, requestExecutor, selectionModel)
val branchFetcher = GithubPullRequestsBranchesFetcher(progressManager, git, detailsLoader, repository, remote)
val changesLoader = GithubPullRequestsChangesLoader(project, progressManager, branchFetcher, repository)
val dataLoader = GithubPullRequestsDataLoader(project, progressManager, git, requestExecutor, repository, remote)
val changes = GithubPullRequestChangesComponent(project, changesLoader, actionManager)
val details = GithubPullRequestDetailsComponent(project, detailsLoader)
val changes = GithubPullRequestChangesComponent(project, selectionModel, dataLoader, actionManager)
val details = GithubPullRequestDetailsComponent(project, selectionModel, dataLoader)
val preview = GithubPullRequestPreviewComponent(uiSettings, changes, details)
list.setToolbarHeightReferent(preview.toolbarComponent)
@@ -65,19 +63,15 @@ class GithubPullRequestsComponentFactory(private val project: Project,
// disposed by content manager when tab is closed
val wrapper = WrappingComponent(splitter,
repository,
remote, repoPath,
account,
listLoader,
detailsLoader, branchFetcher)
repository, remote, repoPath, account,
selectionModel, listLoader,
dataLoader)
Disposer.register(wrapper, Disposable {
Disposer.dispose(list)
Disposer.dispose(preview)
Disposer.dispose(listLoader)
Disposer.dispose(changesLoader)
Disposer.dispose(branchFetcher)
Disposer.dispose(detailsLoader)
Disposer.dispose(dataLoader)
})
changes.diffAction.registerCustomShortcutSet(wrapper, wrapper)
return wrapper
@@ -89,9 +83,9 @@ class GithubPullRequestsComponentFactory(private val project: Project,
private val remote: GitRemote,
private val repoPath: GithubFullPath,
private val account: GithubAccount,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val listLoader: GithubPullRequestsLoader,
private val detailsLoader: GithubPullRequestsDetailsLoader,
private val branchesFetcher: GithubPullRequestsBranchesFetcher)
private val dataLoader: GithubPullRequestsDataLoader)
: Wrapper(wrapped), Disposable, DataProvider {
init {
isFocusCycleRoot = true
@@ -104,8 +98,7 @@ class GithubPullRequestsComponentFactory(private val project: Project,
GithubPullRequestKeys.FULL_PATH.`is`(dataId) -> repoPath
GithubPullRequestKeys.SERVER_PATH.`is`(dataId) -> account.server
GithubPullRequestKeys.PULL_REQUESTS_LOADER.`is`(dataId) -> listLoader
GithubPullRequestKeys.PULL_REQUESTS_DETAILS_LOADER.`is`(dataId) -> detailsLoader
GithubPullRequestKeys.PULL_REQUESTS_BRANCHES_FETCHER.`is`(dataId) -> branchesFetcher
GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER.`is`(dataId) -> selectionModel.current?.let(dataLoader::getDataProvider)
else -> null
}
}
@@ -20,17 +20,17 @@ class GithubPullRequestCreateBranchAction : DumbAwareAction("Create New Local Br
override fun update(e: AnActionEvent) {
val project = e.getData(CommonDataKeys.PROJECT)
val pullRequest = e.getData(GithubPullRequestKeys.SELECTED_PULL_REQUEST)
e.presentation.isEnabled = project != null && !project.isDefault && pullRequest != null
val dataProvider = e.getData(GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER)
e.presentation.isEnabled = project != null && !project.isDefault && pullRequest != null && dataProvider != null
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.getRequiredData(CommonDataKeys.PROJECT)
val pullRequest = e.getRequiredData(GithubPullRequestKeys.SELECTED_PULL_REQUEST)
val repository = e.getRequiredData(GithubPullRequestKeys.REPOSITORY)
val branchFetcher = e.getRequiredData(GithubPullRequestKeys.PULL_REQUESTS_BRANCHES_FETCHER)
val repositoryList = listOf(repository)
val hashesFuture = branchFetcher.request ?: return
val hashesFuture = e.getRequiredData(GithubPullRequestKeys.SELECTED_PULL_REQUEST_DATA_PROVIDER).branchFetchRequest
val options = GitBranchUtil.getNewBranchNameFromUser(project, repositoryList,
"Checkout New Branch From Pull Request #${pullRequest.number}",
"pull/${pullRequest.number}") ?: return
@@ -7,8 +7,7 @@ import git4idea.repo.GitRepository
import org.jetbrains.plugins.github.api.GithubFullPath
import org.jetbrains.plugins.github.api.GithubServerPath
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBranchesFetcher
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDetailsLoader
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsLoader
object GithubPullRequestKeys {
@@ -16,14 +15,11 @@ object GithubPullRequestKeys {
val PULL_REQUESTS_LOADER =
DataKey.create<GithubPullRequestsLoader>("org.jetbrains.plugins.github.pullrequest.loader")
@JvmStatic
val PULL_REQUESTS_DETAILS_LOADER =
DataKey.create<GithubPullRequestsDetailsLoader>("org.jetbrains.plugins.github.pullrequest.details.loader")
@JvmStatic
val PULL_REQUESTS_BRANCHES_FETCHER =
DataKey.create<GithubPullRequestsBranchesFetcher>("org.jetbrains.plugins.github.pullrequest.branch.fetcher")
@JvmStatic
val SELECTED_PULL_REQUEST = DataKey.create<GithubSearchedIssue>("org.jetbrains.plugins.github.pullrequest.selected")
@JvmStatic
val SELECTED_PULL_REQUEST_DATA_PROVIDER =
DataKey.create<GithubPullRequestsDataLoader.DataProvider>("org.jetbrains.plugins.github.pullrequest.selected.dataprovider")
@JvmStatic
val REPOSITORY = DataKey.create<GitRepository>("org.jetbrains.plugins.github.pullrequest.repository")
@JvmStatic
val REMOTE = DataKey.create<GitRemote>("org.jetbrains.plugins.github.pullrequest.remote")
@@ -1,66 +0,0 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Couple
import com.intellij.util.EventDispatcher
import git4idea.commands.Git
import git4idea.repo.GitRemote
import git4idea.repo.GitRepository
import org.jetbrains.annotations.CalledInAwt
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed
import java.util.*
import java.util.concurrent.Future
import kotlin.properties.Delegates
class GithubPullRequestsBranchesFetcher(progressManager: ProgressManager,
private val git: Git,
private val detailsLoader: GithubPullRequestsDetailsLoader,
private val repository: GitRepository,
private val remote: GitRemote)
: SingleWorkerProcessExecutor(progressManager, "GitHub PR branch fetching breaker"),
GithubPullRequestsDetailsLoader.RequestChangedListener {
@set:CalledInAwt
var request: Future<Couple<String>>? by Delegates.observable<Future<Couple<String>>?>(null) { _, _, _ ->
changeEventDispatcher.multicaster.requestChanged()
}
private set
private val changeEventDispatcher = EventDispatcher.create(RequestChangedListener::class.java)
init {
detailsLoader.addRequestChangeListener(this, this)
}
override fun requestChanged() {
cancelCurrentTasks()
request = detailsLoader.request?.let { details ->
submit {
val pullRequest = details.get()
fetchBranch(pullRequest)
}
}
}
private fun fetchBranch(details: GithubPullRequestDetailed): Couple<String> {
if (!isCommitFetched(details.head.sha)) {
git.fetch(repository, remote, emptyList(), "refs/pull/${details.number}/head:").throwOnError()
}
if (!isCommitFetched(details.head.sha)) throw IllegalStateException("Pull request head is not available after fetch")
return Couple.of(details.base.sha, details.head.sha)
}
private fun isCommitFetched(commitHash: String): Boolean {
val result = git.getObjectType(repository, commitHash)
return result.success() && result.outputAsJoinedString == "commit"
}
fun addBranchChangeListener(listener: RequestChangedListener, disposable: Disposable) =
changeEventDispatcher.addListener(listener, disposable)
interface RequestChangedListener : EventListener {
fun requestChanged()
}
}
@@ -1,62 +0,0 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser
import com.intellij.util.EventDispatcher
import git4idea.history.GitLogUtil
import git4idea.repo.GitRepository
import java.util.*
class GithubPullRequestsChangesLoader(private val project: Project,
progressManager: ProgressManager,
private val branchesFetcher: GithubPullRequestsBranchesFetcher,
private val repository: GitRepository)
: SingleWorkerProcessExecutor(progressManager, "GitHub PR changes loading breaker"),
GithubPullRequestsBranchesFetcher.RequestChangedListener {
private val loadingEventDispatcher = EventDispatcher.create(ChangesLoadingListener::class.java)
init {
branchesFetcher.addBranchChangeListener(this, this)
}
override fun requestChanged() {
cancelCurrentTasks()
val fetchFuture = branchesFetcher.request
if (fetchFuture == null) {
loadingEventDispatcher.multicaster.loaderCleared()
}
else submit { indicator ->
try {
val commits = fetchFuture.get()
val details = GitLogUtil.collectFullDetails(project, repository.root, "${commits.first}..${commits.second}")
val changes = CommittedChangesTreeBrowser.zipChanges(details.reversed().flatMap { it.changes })
runInEdt { if (!indicator.isCanceled) loadingEventDispatcher.multicaster.changesLoaded(changes) }
}
catch (pce: ProcessCanceledException) {
// ignore
}
catch (e: Exception) {
runInEdt { if (!indicator.isCanceled) loadingEventDispatcher.multicaster.errorOccurred(e) }
}
}
}
fun addLoadingListener(listener: ChangesLoadingListener, disposable: Disposable) =
loadingEventDispatcher.addListener(listener, disposable)
interface ChangesLoadingListener : EventListener {
fun changesLoaded(changes: List<Change>) {}
fun errorOccurred(error: Throwable) {}
fun loaderCleared() {}
}
}
@@ -0,0 +1,135 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.google.common.cache.CacheBuilder
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Couple
import com.intellij.openapi.util.LowMemoryWatcher
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vcs.changes.committed.CommittedChangesTreeBrowser
import git4idea.GitCommit
import git4idea.commands.Git
import git4idea.history.GitLogUtil
import git4idea.repo.GitRemote
import git4idea.repo.GitRepository
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
import org.jetbrains.plugins.github.api.GithubApiRequests
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.util.NonReusableEmptyProgressIndicator
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
//TODO: cancel loading in removed providers
class GithubPullRequestsDataLoader(private val project: Project,
private val progressManager: ProgressManager,
private val git: Git,
private val requestExecutor: GithubApiRequestExecutor,
private val repository: GitRepository,
private val remote: GitRemote) : Disposable {
private val progressIndicator = NonReusableEmptyProgressIndicator()
private val cache = CacheBuilder.newBuilder()
.maximumSize(5)
.build<Long, DataProvider>()
.asMap()
init {
LowMemoryWatcher.register(Runnable { cache.clear() }, this)
}
fun getDataProvider(githubSearchedIssue: GithubSearchedIssue): DataProvider {
return cache.getOrPut(githubSearchedIssue.number) {
val task = DataTask(githubSearchedIssue.pullRequestLinks!!.url)
progressManager.runProcessWithProgressAsynchronously(task, progressIndicator)
task
}
}
private inner class DataTask(private val url: String)
: Task.Backgroundable(project, "Load Pull Request Data", true), DataProvider {
override val detailsRequest = CompletableFuture<GithubPullRequestDetailedWithHtml>()
override val branchFetchRequest = CompletableFuture<Couple<String>>()
override val logCommitsRequest = CompletableFuture<List<GitCommit>>()
override val changesRequest = CompletableFuture<List<Change>>()
override fun run(indicator: ProgressIndicator) {
runPartialTask(detailsRequest, indicator) {
requestExecutor.execute(progressIndicator, GithubApiRequests.Repos.PullRequests.getHtml(url))
}
runPartialTask(branchFetchRequest, indicator) {
val details = getOrHandle(detailsRequest)
if (!isCommitFetched(details.head.sha)) {
git.fetch(repository, remote, emptyList(), "refs/pull/${details.number}/head:").throwOnError()
}
if (!isCommitFetched(details.head.sha)) throw IllegalStateException("Pull request head is not available after fetch")
Couple.of(details.base.sha, details.head.sha)
}
runPartialTask(logCommitsRequest, indicator) {
val hashes = getOrHandle(branchFetchRequest)
GitLogUtil.collectFullDetails(project, repository.root, "${hashes.first}..${hashes.second}")
}
runPartialTask(changesRequest, indicator) {
val commits = getOrHandle(logCommitsRequest)
CommittedChangesTreeBrowser.zipChanges(commits.reversed().flatMap { it.changes })
}
}
private inline fun <T> runPartialTask(resultFuture: CompletableFuture<T>, indicator: ProgressIndicator, crossinline task: () -> T) {
try {
if (resultFuture.isCancelled) return
indicator.checkCanceled()
val result = task()
resultFuture.complete(result)
}
catch (pce: ProcessCanceledException) {
resultFuture.cancel(true)
}
catch (e: Exception) {
resultFuture.completeExceptionally(e)
}
}
@Throws(ProcessCanceledException::class)
private fun <T> getOrHandle(future: CompletableFuture<T>): T {
try {
return future.get()
}
catch (e: CancellationException) {
throw ProcessCanceledException(e)
}
catch (e: InterruptedException) {
throw ProcessCanceledException(e)
}
catch (e: ExecutionException) {
throw e.cause ?: e
}
}
private fun isCommitFetched(commitHash: String): Boolean {
val result = git.getObjectType(repository, commitHash)
return result.success() && result.outputAsJoinedString == "commit"
}
}
override fun dispose() {
progressIndicator.cancel()
}
interface DataProvider {
val detailsRequest: CompletableFuture<GithubPullRequestDetailedWithHtml>
val branchFetchRequest: CompletableFuture<Couple<String>>
val logCommitsRequest: CompletableFuture<List<GitCommit>>
val changesRequest: CompletableFuture<List<Change>>
}
}
@@ -1,85 +0,0 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.pullrequest.data
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.util.EventDispatcher
import org.jetbrains.annotations.CalledInAwt
import org.jetbrains.annotations.CalledInBackground
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
import org.jetbrains.plugins.github.api.GithubApiRequests
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestsListSelectionModel
import java.util.*
import java.util.concurrent.Future
import kotlin.properties.Delegates
class GithubPullRequestsDetailsLoader(progressManager: ProgressManager,
private val requestExecutor: GithubApiRequestExecutor,
private val selectionModel: GithubPullRequestsListSelectionModel)
: SingleWorkerProcessExecutor(progressManager, "GitHub PR info loading breaker"),
GithubPullRequestsListSelectionModel.SelectionChangedListener {
@set:CalledInAwt
var request: Future<GithubPullRequestDetailedWithHtml>?
by Delegates.observable<Future<GithubPullRequestDetailedWithHtml>?>(null) { _, _, _ ->
changeEventDispatcher.multicaster.requestChanged()
}
private set
private val changeEventDispatcher = EventDispatcher.create(RequestChangedListener::class.java)
private val loadingEventDispatcher = EventDispatcher.create(LoadingListener::class.java)
init {
selectionModel.addChangesListener(this, this)
}
override fun selectionChanged() {
cancelCurrentTasks()
val selection = selectionModel.current
if (selection == null) {
request = null
loadingEventDispatcher.multicaster.loaderCleared()
}
else {
request = submit { indicator -> loadDetails(indicator, requestExecutor, selection) }
}
}
@CalledInBackground
private fun loadDetails(indicator: ProgressIndicator, requestExecutor: GithubApiRequestExecutor, searchedIssue: GithubSearchedIssue)
: GithubPullRequestDetailedWithHtml {
try {
val links = searchedIssue.pullRequestLinks ?: throw IllegalStateException("Missing pull request links")
val details = requestExecutor.execute(indicator, GithubApiRequests.Repos.PullRequests.getHtml(links.url))
loadingEventDispatcher.multicaster.detailsLoaded(details)
return details
}
catch (pce: ProcessCanceledException) {
throw pce
}
catch (e: Exception) {
loadingEventDispatcher.multicaster.errorOccurred(e)
throw e
}
}
fun addRequestChangeListener(listener: RequestChangedListener, disposable: Disposable) =
changeEventDispatcher.addListener(listener, disposable)
fun addLoadingListener(listener: LoadingListener, disposable: Disposable) =
loadingEventDispatcher.addListener(listener, disposable)
interface RequestChangedListener : EventListener {
fun requestChanged()
}
interface LoadingListener : EventListener {
fun detailsLoaded(details: GithubPullRequestDetailedWithHtml)
fun errorOccurred(error: Throwable)
fun loaderCleared()
}
}
@@ -17,54 +17,67 @@ import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.ComponentWithEmptyText
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsChangesLoader
import org.jetbrains.plugins.github.pullrequest.data.SingleWorkerProcessExecutor
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.util.GithubAsyncUtil
import org.jetbrains.plugins.github.util.handleOnEdt
import java.awt.BorderLayout
import java.util.concurrent.CompletableFuture
import javax.swing.JComponent
import javax.swing.border.Border
import kotlin.properties.Delegates
class GithubPullRequestChangesComponent(project: Project,
loader: GithubPullRequestsChangesLoader,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val dataLoader: GithubPullRequestsDataLoader,
actionManager: ActionManager)
: Wrapper(), Disposable, GithubPullRequestsChangesLoader.ChangesLoadingListener, SingleWorkerProcessExecutor.ProcessStateListener {
: Wrapper(), Disposable, GithubPullRequestsListSelectionModel.SelectionChangedListener {
private val changesBrowser = PullRequestChangesBrowserWithError(project, actionManager)
val toolbarComponent: JComponent = changesBrowser.toolbar.component
val diffAction = changesBrowser.diffAction
private val changesLoadingPanel = JBLoadingPanel(BorderLayout(), this,
ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
private var updateFuture: CompletableFuture<Unit>? = null
init {
loader.addProcessListener(this, this)
loader.addLoadingListener(this, this)
selectionModel.addChangesListener(this, this)
changesLoadingPanel.add(changesBrowser, BorderLayout.CENTER)
setContent(changesLoadingPanel)
changesBrowser.emptyText.text = DEFAULT_EMPTY_TEXT
}
override fun processStarted() {
changesLoadingPanel.startLoading()
changesBrowser.emptyText.clear()
changesBrowser.changes = emptyList()
override fun selectionChanged() {
reset()
updateFuture = updateChanges(selectionModel.current)
}
override fun processFinished() {
changesLoadingPanel.stopLoading()
}
private fun updateChanges(item: GithubSearchedIssue?) =
item?.let { selection ->
changesBrowser.emptyText.clear()
changesLoadingPanel.startLoading()
override fun changesLoaded(changes: List<Change>) {
changesBrowser.emptyText.text = "Pull request does not contain any changes"
changesBrowser.changes = changes
}
dataLoader.getDataProvider(selection).changesRequest
.handleOnEdt { changes, error ->
when {
error != null && !GithubAsyncUtil.isCancellation(error) -> {
changesBrowser.emptyText
.appendText("Cannot load changes", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
changes != null -> {
changesBrowser.emptyText.text = "Pull request does not contain any changes"
changesBrowser.changes = changes
}
}
changesLoadingPanel.stopLoading()
}
}
override fun errorOccurred(error: Throwable) {
changesBrowser.emptyText
.appendText("Cannot load changes", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
override fun loaderCleared() {
private fun reset() {
updateFuture?.cancel(true)
changesBrowser.emptyText.text = DEFAULT_EMPTY_TEXT
changesBrowser.changes = emptyList()
}
@@ -7,50 +7,60 @@ import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.panels.Wrapper
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDetailsLoader
import org.jetbrains.plugins.github.pullrequest.data.SingleWorkerProcessExecutor
import org.jetbrains.plugins.github.api.data.GithubSearchedIssue
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
import org.jetbrains.plugins.github.util.GithubAsyncUtil
import org.jetbrains.plugins.github.util.handleOnEdt
import java.awt.BorderLayout
import java.util.concurrent.CompletableFuture
class GithubPullRequestDetailsComponent(project: Project,
private val selectionModel: GithubPullRequestsListSelectionModel,
private val dataLoader: GithubPullRequestsDataLoader)
: Wrapper(), Disposable, GithubPullRequestsListSelectionModel.SelectionChangedListener {
class GithubPullRequestDetailsComponent(project: Project, loader: GithubPullRequestsDetailsLoader)
: Wrapper(), Disposable,
SingleWorkerProcessExecutor.ProcessStateListener,
GithubPullRequestsDetailsLoader.LoadingListener {
private val detailsPanel = GithubPullRequestDetailsPanel(project)
private val loadingPanel = JBLoadingPanel(BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS)
private var updateFuture: CompletableFuture<Unit>? = null
init {
loader.addProcessListener(this, this)
loader.addLoadingListener(this, this)
selectionModel.addChangesListener(this, this)
loadingPanel.add(detailsPanel)
setContent(loadingPanel)
}
override fun processStarted() {
loadingPanel.startLoading()
detailsPanel.details = null
override fun selectionChanged() {
reset()
updateFuture = updateDetails(selectionModel.current)
}
private fun updateDetails(item: GithubSearchedIssue?) =
item?.let { selection ->
loadingPanel.startLoading()
dataLoader.getDataProvider(selection).detailsRequest
.handleOnEdt { details, error ->
when {
error != null && !GithubAsyncUtil.isCancellation(error) -> {
detailsPanel.emptyText
.appendText("Cannot load details", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
details != null -> {
detailsPanel.details = details
}
}
loadingPanel.stopLoading()
}
}
private fun reset() {
updateFuture?.cancel(true)
detailsPanel.emptyText.clear()
}
override fun processFinished() {
loadingPanel.stopLoading()
}
override fun detailsLoaded(details: GithubPullRequestDetailedWithHtml) {
detailsPanel.details = details
}
override fun errorOccurred(error: Throwable) {
detailsPanel.details = null
detailsPanel.emptyText.appendText("Cannot load details", SimpleTextAttributes.ERROR_ATTRIBUTES)
.appendSecondaryText(error.message ?: "Unknown error", SimpleTextAttributes.ERROR_ATTRIBUTES, null)
}
override fun loaderCleared() {
detailsPanel.details = null
detailsPanel.emptyText.clear()
}
override fun dispose() {}
@@ -0,0 +1,27 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.github.util
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.ProcessCanceledException
import java.util.concurrent.*
import java.util.function.BiFunction
object GithubAsyncUtil {
fun isCancellation(error: Throwable): Boolean {
return error is ProcessCanceledException
|| error is CancellationException
|| error is InterruptedException
|| error.cause?.let(::isCancellation) ?: false
}
}
/**
* Handle on EDT if [disposable] is not disposed and [condition] is satisfied
*/
fun <T> CompletableFuture<T>.handleOnEdt(handler: (T?, Throwable?) -> Unit): CompletableFuture<Unit> =
handleAsync(BiFunction<T?, Throwable?, Unit> { result: T?, error: Throwable? ->
handler(result, error)
}, EDT_EXECUTOR)
val EDT_EXECUTOR = Executor { runnable -> runInEdt { runnable.run() } }