From e67d5ca8456a4edf3dfe02caddf01c8a4a8dd882 Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Thu, 27 Sep 2018 17:52:36 +0300 Subject: [PATCH] [github] Rework pull request data loaders Use single thread to load everything and publish data via CompletableFuture's --- .../GithubPullRequestsComponentFactory.kt | 27 ++-- .../GithubPullRequestCreateBranchAction.kt | 6 +- .../action/GithubPullRequestKeys.kt | 12 +- .../data/GithubPullRequestsBranchesFetcher.kt | 66 --------- .../data/GithubPullRequestsChangesLoader.kt | 62 -------- .../data/GithubPullRequestsDataLoader.kt | 135 ++++++++++++++++++ .../data/GithubPullRequestsDetailsLoader.kt | 85 ----------- .../ui/GithubPullRequestChangesComponent.kt | 63 ++++---- .../ui/GithubPullRequestDetailsComponent.kt | 70 +++++---- .../plugins/github/util/GithubAsyncUtil.kt | 27 ++++ 10 files changed, 257 insertions(+), 296 deletions(-) delete mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsBranchesFetcher.kt delete mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsChangesLoader.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDataLoader.kt delete mode 100644 plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDetailsLoader.kt create mode 100644 plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt index 3a2b60e73cde..5dc5e867d737 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/GithubPullRequestsComponentFactory.kt @@ -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 } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestCreateBranchAction.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestCreateBranchAction.kt index 1347cc61b0f0..834a47f80547 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestCreateBranchAction.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestCreateBranchAction.kt @@ -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 diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt index a5a2a330f301..e36e127eb684 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/action/GithubPullRequestKeys.kt @@ -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("org.jetbrains.plugins.github.pullrequest.loader") @JvmStatic - val PULL_REQUESTS_DETAILS_LOADER = - DataKey.create("org.jetbrains.plugins.github.pullrequest.details.loader") - @JvmStatic - val PULL_REQUESTS_BRANCHES_FETCHER = - DataKey.create("org.jetbrains.plugins.github.pullrequest.branch.fetcher") - @JvmStatic val SELECTED_PULL_REQUEST = DataKey.create("org.jetbrains.plugins.github.pullrequest.selected") @JvmStatic + val SELECTED_PULL_REQUEST_DATA_PROVIDER = + DataKey.create("org.jetbrains.plugins.github.pullrequest.selected.dataprovider") + @JvmStatic val REPOSITORY = DataKey.create("org.jetbrains.plugins.github.pullrequest.repository") @JvmStatic val REMOTE = DataKey.create("org.jetbrains.plugins.github.pullrequest.remote") diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsBranchesFetcher.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsBranchesFetcher.kt deleted file mode 100644 index 28b64d49b44b..000000000000 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsBranchesFetcher.kt +++ /dev/null @@ -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>? by Delegates.observable>?>(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 { - 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() - } -} diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsChangesLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsChangesLoader.kt deleted file mode 100644 index f196c67def09..000000000000 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsChangesLoader.kt +++ /dev/null @@ -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) {} - fun errorOccurred(error: Throwable) {} - fun loaderCleared() {} - } -} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDataLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDataLoader.kt new file mode 100644 index 000000000000..efd3737c57ed --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDataLoader.kt @@ -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() + .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() + override val branchFetchRequest = CompletableFuture>() + override val logCommitsRequest = CompletableFuture>() + override val changesRequest = CompletableFuture>() + + 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 runPartialTask(resultFuture: CompletableFuture, 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 getOrHandle(future: CompletableFuture): 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 + val branchFetchRequest: CompletableFuture> + val logCommitsRequest: CompletableFuture> + val changesRequest: CompletableFuture> + } +} \ No newline at end of file diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDetailsLoader.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDetailsLoader.kt deleted file mode 100644 index ff98d7b41a86..000000000000 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/data/GithubPullRequestsDetailsLoader.kt +++ /dev/null @@ -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? - by Delegates.observable?>(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() - } -} diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestChangesComponent.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestChangesComponent.kt index 448ffce3b10c..8a495ace0e38 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestChangesComponent.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestChangesComponent.kt @@ -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? = 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) { - 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() } diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestDetailsComponent.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestDetailsComponent.kt index 67d59c3a0a55..79ccd56a33c6 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestDetailsComponent.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/GithubPullRequestDetailsComponent.kt @@ -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? = 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() {} diff --git a/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt b/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt new file mode 100644 index 000000000000..5f4c69adfb3b --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/util/GithubAsyncUtil.kt @@ -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 CompletableFuture.handleOnEdt(handler: (T?, Throwable?) -> Unit): CompletableFuture = + handleAsync(BiFunction { result: T?, error: Throwable? -> + handler(result, error) + }, EDT_EXECUTOR) + +val EDT_EXECUTOR = Executor { runnable -> runInEdt { runnable.run() } } \ No newline at end of file