From fdddb2e2f2ad749f91eb19edc0eb711b4a762977 Mon Sep 17 00:00:00 2001 From: Ilia Permiashkin Date: Tue, 13 May 2025 22:23:49 +0300 Subject: [PATCH] IJPL-89611 Download Languages Asynchronously GitOrigin-RevId: 8a4943caa71ee165eced9bc13c0c8011258cd61f --- .../quickfix/DownloadLanguageQuickFix.kt | 5 +- .../intellij/grazie/remote/GrazieRemote.kt | 9 +- .../intellij/grazie/remote/LangDownloader.kt | 104 ++++++++++++------ .../messages/GrazieBundle.properties | 2 +- 4 files changed, 78 insertions(+), 42 deletions(-) diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/inspection/detection/quickfix/DownloadLanguageQuickFix.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/inspection/detection/quickfix/DownloadLanguageQuickFix.kt index a588099f863a..2ff549431aff 100644 --- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/inspection/detection/quickfix/DownloadLanguageQuickFix.kt +++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/ide/inspection/detection/quickfix/DownloadLanguageQuickFix.kt @@ -32,10 +32,7 @@ class DownloadLanguageQuickFix(private val languages: Set) : LocalQuic override fun startInWriteAction() = false override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - for (lang in langs) { - GrazieRemote.download(lang) - } - + GrazieRemote.downloadAsync(langs, project) GrazieConfig.update { state -> state.copy(enabledLanguages = state.enabledLanguages + langs) } GrazieFUSCounter.languagesSuggested(languages, isEnabled = true) } diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/GrazieRemote.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/GrazieRemote.kt index aef099292705..f77723d12b36 100644 --- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/GrazieRemote.kt +++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/GrazieRemote.kt @@ -21,14 +21,13 @@ object GrazieRemote { fun allAvailableLocally() = Lang.values().filter { isAvailableLocally(it) } /** Downloads [lang] to local storage */ - fun download(lang: Lang, project: Project? = null): Boolean { - if (isAvailableLocally(lang)) return true + fun download(lang: Lang, project: Project? = null): Boolean = LangDownloader.download(lang, project) - return LangDownloader.download(lang, project) - } + /** Downloads [languages] asynchronously to local storage */ + fun downloadAsync(languages: Collection, project: Project): Unit = LangDownloader.downloadAsync(languages, project) /** Downloads all missing languages to local storage*/ - fun downloadMissing(project: Project?) = GrazieConfig.get().missedLanguages.forEach { LangDownloader.download(it, project) } + fun downloadMissing(project: Project): Unit = LangDownloader.downloadAsync(GrazieConfig.get().missedLanguages, project) fun isValidBundleForLanguage(language: Lang, file: Path): Boolean { val actualChecksum = checksum(file) diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/LangDownloader.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/LangDownloader.kt index 2ab13813b6e5..701460b1458b 100644 --- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/LangDownloader.kt +++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/remote/LangDownloader.kt @@ -6,55 +6,94 @@ import com.intellij.grazie.GrazieDynamic import com.intellij.grazie.GraziePlugin import com.intellij.grazie.ide.ui.components.dsl.msg import com.intellij.grazie.jlanguage.Lang +import com.intellij.grazie.remote.GrazieRemote.isAvailableLocally import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.NioFiles import com.intellij.util.download.DownloadableFileService import com.intellij.util.lang.UrlClassLoader -import org.jetbrains.annotations.Nls import java.nio.file.Path import kotlin.io.path.copyTo +@Suppress("DialogTitleCapitalization") internal object LangDownloader { fun download(lang: Lang, project: Project?): Boolean { // check if language lib already loaded - if (GrazieRemote.isAvailableLocally(lang)) return true + if (isAvailableLocally(lang)) return true - val result = runDownload(lang, project) ?: return false - check(GrazieRemote.isValidBundleForLanguage(lang, result)) { "Language bundle checksum became invalid right before loading it" } - val classLoader = UrlClassLoader.build().parent(GraziePlugin.classLoader).files(listOf(result)).get() - GrazieDynamic.addDynClassLoader(classLoader) - // force reloading available language classes - GrazieConfig.update { it.copy() } - // drop caches, restart highlighting - GrazieConfig.stateChanged(GrazieConfig.get(), GrazieConfig.get()) + val path = runDownload(lang, project) ?: return false + performGrazieUpdate(listOf(lang to path)) return true } + fun downloadAsync(languages: Collection, project: Project) { + // check if language lib already loaded + val notAvailableLocallyLanguages = languages + .filter { !isAvailableLocally(it) } + if (notAvailableLocallyLanguages.isEmpty()) return + + val task = object : Task.Backgroundable( + project, + msg("grazie.settings.proofreading.languages.download"), + true, + ALWAYS_BACKGROUND + ) { + override fun run(indicator: ProgressIndicator) { + performGrazieUpdate( + performDownload(languages) + ) + } + } + ProgressManager.getInstance().runProcessWithProgressAsynchronously( + task, BackgroundableProcessIndicator(task) + ) + } + private fun runDownload(language: Lang, project: Project?): Path? { try { - val presentableName = msg("grazie.settings.proofreading.languages.download.name", language.nativeName) - return ProgressManager.getInstance().runProcessWithProgressSynchronously( - { performDownload(language, presentableName) }, - presentableName, + return ProgressManager.getInstance().runProcessWithProgressSynchronously>, Exception>( + { performDownload(listOf(language)) }, + msg("grazie.settings.proofreading.languages.download"), false, project - ) - } catch (exception: Throwable) { + ).single().second + } + catch (exception: Throwable) { thisLogger().warn(exception) return promptToSelectLanguageBundleManually(project, language) } } - @Throws(IllegalStateException::class) - private fun performDownload(language: Lang, presentableName: @Nls String): Path? { - val bundle = doDownload(language, presentableName) - if (!GrazieRemote.isValidBundleForLanguage(language, bundle)) { - FileUtil.delete(bundle) - throw IllegalStateException("Failed to verify integrity of downloaded language bundle for language ${language.nativeName}.") + private fun performGrazieUpdate(bundles: List>) { + bundles.forEach { (lang, path) -> + check(GrazieRemote.isValidBundleForLanguage(lang, path)) { "Language bundle checksum became invalid right before loading it: $lang" } } - return bundle + val classLoader = UrlClassLoader.build() + .parent(GraziePlugin.classLoader) + .files(bundles.map { it.second }) + .get() + GrazieDynamic.addDynClassLoader(classLoader) + // force reloading available language classes + GrazieConfig.update { it.copy() } + // drop caches, restart highlighting + GrazieConfig.stateChanged(GrazieConfig.get(), GrazieConfig.get()) + } + + @Throws(IllegalStateException::class) + private fun performDownload(languages: Collection): List> { + val bundles = doDownload(languages) + val invalidBundles = bundles + .filter { !GrazieRemote.isValidBundleForLanguage(it.first, it.second) } + .map { it.second } + if (invalidBundles.isNotEmpty()) { + bundles.forEach { NioFiles.deleteRecursively(it.second) } + throw IllegalStateException("Failed to verify integrity of downloaded language bundle for languages ${invalidBundles}.") + } + return bundles } private fun promptToSelectLanguageBundleManually(project: Project?, language: Lang): Path? { @@ -64,13 +103,14 @@ internal object LangDownloader { return targetPath } - private fun doDownload(lang: Lang, presentableName: @Nls String): Path { + private fun doDownload(languages: Collection): List> { val downloaderService = DownloadableFileService.getInstance() - val downloader = downloaderService.createDownloader( - listOf(downloaderService.createFileDescription(lang.remote.url, lang.remote.fileName)), - presentableName - ) - val result = downloader.download(GrazieDynamic.dynamicFolder.toFile()).map { it.first.toPath() } - return result.single() + val descriptors = languages + .map { downloaderService.createFileDescription(it.remote.url, it.remote.fileName) } + val paths = downloaderService + .createDownloader(descriptors, msg("grazie.settings.proofreading.languages.download")) + .download(GrazieDynamic.dynamicFolder.toFile()) + .map { it.first.toPath() } + return languages.zip(paths) } -} +} \ No newline at end of file diff --git a/plugins/grazie/src/main/resources/messages/GrazieBundle.properties b/plugins/grazie/src/main/resources/messages/GrazieBundle.properties index a3ef9e7ae996..4b29d57a44f5 100644 --- a/plugins/grazie/src/main/resources/messages/GrazieBundle.properties +++ b/plugins/grazie/src/main/resources/messages/GrazieBundle.properties @@ -67,7 +67,7 @@ grazie.settings.proofreading.languages.text=Languages: grazie.settings.proofreading.languages.empty.text=No languages added. grazie.settings.proofreading.languages.empty.action=Add language grazie.settings.proofreading.languages.popup.title=Available Languages -grazie.settings.proofreading.languages.download.name={0} language +grazie.settings.proofreading.languages.download=Downloading languages grazie.settings.proofreading.link-to-inspection=Configure 'Proofreading' inspections\u2026 grazie.settings.grammar.scope.file-types.text=File types: