diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3fba5ba..7eecef1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -168,7 +168,7 @@ jobs:
# Run Qodana inspections
- name: Qodana - Code Inspection
- uses: JetBrains/qodana-action@v2024.1.2
+ uses: JetBrains/qodana-action@v2024.1
with:
cache-default-branch-only: true
diff --git a/.gitignore b/.gitignore
index 654ce68..c5db87b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
-.gradle
-.idea
+.gradle/
+.idea/*
+!.idea/icon.svg
.qodana
assets
-build
+build/
.DS_Store
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..092b0bc
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ceb97b3..e725df3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,20 @@
## [Unreleased]
+### Added
+
+- Introduce a setting to customize the maximum amount of simultaneous scans
+
+### Changed
+
+- Slightly improve performance on scans startup
+- Logic improvements for scanners
+
+### Fixed
+
+- Fix a rare crash with the status bar when navigating between projects (#99)
+- Fix cached updates not being used when they should
+
## [2.2.1] - 2024-04-26
### Changed
diff --git a/gradle.properties b/gradle.properties
index 983f802..cae3d05 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,7 +4,7 @@ pluginGroup = com.github.warningimhack3r.npmupdatedependencies
pluginName = npm-update-dependencies
pluginRepositoryUrl = https://github.com/WarningImHack3r/npm-update-dependencies
# SemVer format -> https://semver.org
-pluginVersion = 2.2.1
+pluginVersion = 2.3.0
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 221
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e720888..bd24c92 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,7 +7,7 @@ semver4j = "5.3.0"
kotlin = "1.9.23"
changelog = "2.2.0"
gradleIntelliJPlugin = "1.17.3"
-qodana = "2024.1.2"
+qodana = "2024.1.3"
kover = "0.7.6"
[libraries]
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt
index ebe320e..414407a 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt
@@ -10,7 +10,6 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.jsonObject
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
@@ -79,12 +78,7 @@ class NPMJSClient(private val project: Project) {
log.warn("Error while getting response body from $uri", e)
return null
}
- return try {
- Json.parseToJsonElement(responseBody).jsonObject
- } catch (e: Exception) {
- log.warn("Error while parsing response body from $uri", e)
- null
- }
+ return Json.parseToJsonElement(responseBody).asJsonObject
}
fun getLatestVersion(packageName: String): String? {
@@ -112,7 +106,8 @@ class NPMJSClient(private val project: Project) {
val json = getBodyAsJSON("${registry}/$packageName")
return json?.get("versions")?.asJsonObject?.keys?.toList().also {
if (it != null) {
- log.info("All versions for package $packageName found in cache: $it")
+ log.info("All versions for package $packageName found in cache (${it.size} versions)")
+ log.debug("Versions in cache for $packageName: $it")
}
} ?: ShellRunner.execute(
arrayOf("npm", "v", packageName, "versions", "--json", "--registry=$registry")
@@ -132,7 +127,8 @@ class NPMJSClient(private val project: Project) {
}
}.also { versions ->
if (versions != null) {
- log.info("All versions for package $packageName found: $versions")
+ log.info("All versions for package $packageName found (${versions.size} versions)")
+ log.debug("Versions for $packageName: $versions")
}
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt
index 2cf3c5d..82bd460 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt
@@ -41,13 +41,10 @@ class PackageUpdateChecker(private val project: Project) {
}
private fun areVersionsMatchingComparatorNeeds(versions: Versions, comparator: String): Boolean {
- return if (versions.latest.satisfies(comparator)) {
- versions.satisfies == null
- } else {
- versions.satisfies != null
- && versions.satisfies.satisfies(comparator)
- && isVersionMoreRecentThanComparator(versions.satisfies, comparator)
- } && isVersionMoreRecentThanComparator(versions.latest, comparator)
+ return isVersionMoreRecentThanComparator(versions.latest, comparator) && versions.satisfies?.let { satisfying ->
+ satisfying.satisfies(comparator)
+ && isVersionMoreRecentThanComparator(satisfying, comparator)
+ } ?: true
}
private fun getVersionExcludingFilter(packageName: String, version: Semver): String? {
@@ -81,6 +78,9 @@ class PackageUpdateChecker(private val project: Project) {
if (areVersionsMatchingComparatorNeeds(cachedVersions.versions, comparator)) {
log.info("Cached versions for $packageName are still valid, returning them")
return cachedVersions
+ } else {
+ log.debug("Cached versions for $packageName are outdated, removing them")
+ availableUpdates.remove(packageName)
}
}
@@ -94,7 +94,7 @@ class PackageUpdateChecker(private val project: Project) {
var satisfyingVersion: Semver? = null
val updateAvailable = isVersionMoreRecentThanComparator(newestVersion, comparator)
if (!updateAvailable) {
- if (availableUpdates.containsKey(packageName)) {
+ availableUpdates[packageName]?.let {
availableUpdates.remove(packageName)
}
log.info("No update available for $packageName, removing cached versions")
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt
index 7950381..f2b34dd 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt
@@ -6,7 +6,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
@Service(Service.Level.PROJECT)
-class RegistriesScanner {
+class RegistriesScanner(private val project: Project) {
companion object {
private val log = logger()
@@ -17,7 +17,9 @@ class RegistriesScanner {
var registries: List = emptyList()
fun scan() {
+ val state = NUDState.getInstance(project)
log.info("Starting to scan registries")
+ state.isScanningForRegistries = true
// Run `npm config ls` to get the list of registries
val config = ShellRunner.execute(arrayOf("npm", "config", "ls")) ?: return
registries = config.lines().asSequence().filter { line ->
@@ -36,5 +38,6 @@ class RegistriesScanner {
}
}.map { it.removeSuffix("/") }.distinct().toList()
log.info("Found registries: $registries")
+ state.isScanningForRegistries = false
}
}
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt
index 49b8208..2760f56 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt
@@ -6,6 +6,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NUDState
import com.github.warningimhack3r.npmupdatedependencies.ui.statusbar.StatusBarMode
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.ide.DataManager
+import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileEditorManager
@@ -122,7 +123,7 @@ class NUDSettingsComponent {
setCancelOperation {
dialogWrapper.close(DialogWrapper.CANCEL_EXIT_CODE)
}
- dialogWrapper.setSize(400, 300) // Width is not really respected, text width takes over
+ dialogWrapper.setSize(400, 300) // Width is not respected, text width takes over
}
val panel = panel {
@@ -149,6 +150,19 @@ class NUDSettingsComponent {
.bindSelected(settings::autoReorderDependencies)
}
}
+ group("Parallelism") {
+ row("Maximum parallel processes:") {
+ spinner(1..100)
+ .comment(
+ "Control the maximum number of parallel scans that can be run at the same time. Higher values can speed up the scan but might cause performance issues or out of memory issues. Make sure to bump ${
+ ApplicationNamesInfo.getInstance().fullProductName.substringBefore(
+ " "
+ )
+ }'s memory as needed.
100 means no limit."
+ )
+ .bindIntValue(settings::maxParallelism)
+ }
+ }
group("Status Bar") {
lateinit var statusBarEnabled: Cell
row {
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt
index 9d16b18..f5781dc 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt
@@ -45,6 +45,11 @@ class NUDSettingsState : PersistentStateComponent {
set(value) {
settings.autoReorderDependencies = value
}
+ var maxParallelism: Int
+ get() = settings.maxParallelism
+ set(value) {
+ settings.maxParallelism = value
+ }
var showStatusBarWidget: Boolean
get() = settings.showStatusBarWidget
set(value) {
@@ -71,6 +76,7 @@ class NUDSettingsState : PersistentStateComponent {
var defaultDeprecationAction: Deprecation.Action = Deprecation.Action.REPLACE,
var showDeprecationBanner: Boolean = true,
var autoReorderDependencies: Boolean = true,
+ var maxParallelism: Int = 100,
var showStatusBarWidget: Boolean = true,
var statusBarMode: StatusBarMode = StatusBarMode.FULL,
var autoFixOnSave: Boolean = false,
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt
index 0d9294a..2f0ce4b 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt
@@ -6,6 +6,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NPMJSClie
import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NUDState
import com.github.warningimhack3r.npmupdatedependencies.backend.engine.RegistriesScanner
import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.parallelMap
+import com.github.warningimhack3r.npmupdatedependencies.settings.NUDSettingsState
import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.AnnotatorsCommon
import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.DeprecatedDependencyFix
import com.intellij.codeInspection.ProblemHighlightType
@@ -19,6 +20,7 @@ import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.ui.EditorNotifications
import com.intellij.util.applyIf
+import kotlinx.coroutines.delay
class DeprecationAnnotator : DumbAware, ExternalAnnotator<
Pair>,
@@ -37,21 +39,24 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator<
var state = NUDState.getInstance(project)
if (!state.isScanningForRegistries && state.packageRegistries.isEmpty()) {
- state.isScanningForRegistries = true
log.debug("No registries found, scanning for registries...")
RegistriesScanner.getInstance(project).scan()
log.debug("Registries scanned")
- state.isScanningForRegistries = false
}
- while (state.isScanningForRegistries || state.isScanningForDeprecations) {
- // Wait for the registries to be scanned and avoid multiple scans at the same time
- log.debug("Waiting for registries to be scanned...")
+ if (state.isScanningForRegistries || state.isScanningForUpdates) {
+ log.debug("Waiting for registries and/or updates to be scanned...")
+ while (state.isScanningForRegistries || state.isScanningForUpdates) {
+ // Wait for the registries to be scanned and avoid multiple scans at the same time
+ }
}
log.debug("Scanning for deprecations...")
state = NUDState.getInstance(project)
+ val maxParallelism = NUDSettingsState.instance.maxParallelism
+ var activeTasks = 0
val npmjsClient = NPMJSClient.getInstance(project)
+
return info
.also {
// Remove from the cache all deprecations that are no longer in the file
@@ -62,8 +67,18 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator<
state.scannedDeprecations = 0
state.isScanningForDeprecations = true
}.parallelMap { property ->
+ if (maxParallelism < 100) {
+ while (activeTasks >= maxParallelism) {
+ // Wait for the active tasks count to decrease
+ delay(50)
+ }
+ activeTasks++
+ log.debug("Task $activeTasks/$maxParallelism started: ${property.name}")
+ }
state.deprecations[property.name]?.let { deprecation ->
+ log.debug("Deprecation found in cache: ${property.name}")
state.scannedDeprecations++
+ activeTasks--
// If the deprecation is already in the cache, we don't need to check the NPM registry
Pair(property.jsonProperty, deprecation)
} ?: npmjsClient.getPackageDeprecation(property.name)?.let { reason ->
@@ -72,45 +87,44 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator<
// Remove punctuation at the end of the word
word.replace(Regex("[,;.]$"), "")
}.filter { word ->
- // Try to find a word that looks like a package name
- if (word.startsWith("@")) {
- // Scoped package
- return@filter word.split("/").size == 2
- }
- if (word.contains("/")) {
- // If it contains a slash without being a scoped package, it's likely an URL
- return@filter false
+ with(word) {
+ // Try to find a word that looks like a package name
+ when {
+ // Scoped package
+ startsWith("@") -> split("/").size == 2
+ // If it contains a slash without being a scoped package, it's likely an URL
+ contains("/") -> false
+ // Other potential matches
+ contains("-") -> lowercase() == this
+ // Else if we're unsure, we don't consider it as a package name
+ else -> false
+ }
}
- // Other potential matches
- if (word.contains("-")) {
- return@filter word.lowercase() == word
- }
- // Else if we're unsure, we don't consider it as a package name
- false
}.parallelMap innerMap@{ potentialPackage ->
// Confirm that the word is a package name by trying to get its latest version
npmjsClient.getLatestVersion(potentialPackage)?.let {
Pair(potentialPackage, it)
}
- }.filterNotNull().also {
- state.scannedDeprecations++
- }.firstOrNull()?.let { (name, version) ->
+ }.filterNotNull().firstOrNull()?.let { (name, version) ->
// We found a package name and its latest version, so we can create a replacement
Pair(property.jsonProperty, Deprecation(reason, Deprecation.Replacement(name, version)))
} ?: Pair(
property.jsonProperty,
Deprecation(reason, null)
) // No replacement found in the deprecation reason
- }.also { pair ->
- pair?.let {
+ }.also { result ->
+ result?.let { (property, deprecation) ->
// Add the deprecation to the cache if any
- state.deprecations[property.name] = it.second
- } ?: run {
+ state.deprecations[property.name] = deprecation
+ } ?: state.deprecations[property.name]?.let { _ ->
// Remove the deprecation from the cache if no deprecation is found
- if (state.deprecations.containsKey(property.name)) {
- state.deprecations.remove(property.name)
- }
+ state.deprecations.remove(property.name)
}
+
+ log.debug("Finished task for ${property.name}, deprecation found: ${result != null}")
+ // Manage counters
+ state.scannedDeprecations++
+ activeTasks--
}
}.filterNotNull().toMap().also {
log.debug("Deprecations scanned, ${it.size} found")
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt
index 3bdcbc4..2581578 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt
@@ -8,6 +8,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.PackageUp
import com.github.warningimhack3r.npmupdatedependencies.backend.engine.RegistriesScanner
import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.parallelMap
import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.stringValue
+import com.github.warningimhack3r.npmupdatedependencies.settings.NUDSettingsState
import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.AnnotatorsCommon
import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.BlacklistVersionFix
import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.UpdateDependencyFix
@@ -21,6 +22,7 @@ import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.util.applyIf
+import kotlinx.coroutines.delay
import org.semver4j.Semver
class UpdatesAnnotator : DumbAware, ExternalAnnotator<
@@ -40,21 +42,24 @@ class UpdatesAnnotator : DumbAware, ExternalAnnotator<
var state = NUDState.getInstance(project)
if (!state.isScanningForRegistries && state.packageRegistries.isEmpty()) {
- state.isScanningForRegistries = true
log.debug("No registries found, scanning for registries...")
RegistriesScanner.getInstance(project).scan()
log.debug("Registries scanned")
- state.isScanningForRegistries = false
}
- while (state.isScanningForRegistries || state.isScanningForUpdates) {
- // Wait for the registries to be scanned and avoid multiple scans at the same time
- log.debug("Waiting for registries to be scanned...")
+ if (state.isScanningForRegistries || state.isScanningForDeprecations) {
+ log.debug("Waiting for registries and/or deprecations to be scanned...")
+ while (state.isScanningForRegistries || state.isScanningForDeprecations) {
+ // Wait for the registries to be scanned and avoid multiple scans at the same time
+ }
}
log.debug("Scanning for updates...")
state = NUDState.getInstance(project)
+ val maxParallelism = NUDSettingsState.instance.maxParallelism
+ var activeTasks = 0
val updateChecker = PackageUpdateChecker.getInstance(project)
+
return info
.also {
// Remove from the cache all properties that are no longer in the file
@@ -65,11 +70,24 @@ class UpdatesAnnotator : DumbAware, ExternalAnnotator<
state.scannedUpdates = 0
state.isScanningForUpdates = true
}.parallelMap { property ->
- val value = property.comparator ?: return@parallelMap null
+ if (maxParallelism < 100) {
+ while (activeTasks >= maxParallelism) {
+ // Wait for the active tasks count to decrease
+ delay(50)
+ }
+ activeTasks++
+ log.debug("Task $activeTasks/$maxParallelism started: ${property.name}")
+ }
+ val value = property.comparator ?: return@parallelMap null.also {
+ log.debug("Empty comparator for ${property.name}, skipping")
+ activeTasks--
+ }
val scanResult = updateChecker.areUpdatesAvailable(property.name, value)
state.scannedUpdates++
val coerced = Semver.coerce(value)
+ log.debug("Task finished for ${property.name}")
+ activeTasks--
if (scanResult != null && coerced != null && !scanResult.versions.isEqualToAny(coerced)) {
Pair(property.jsonProperty, scanResult)
} else null
diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt
index f1784bd..4957d0a 100644
--- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt
+++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt
@@ -13,7 +13,7 @@ object StatusBarHelper {
for (project in projectManager.openProjects) {
log.debug("Updating widget for project ${project.name}")
val widgetBar =
- WindowManager.getInstance().getStatusBar(project).getWidget(WidgetBar.ID) as? WidgetBar ?: continue
+ WindowManager.getInstance().getStatusBar(project)?.getWidget(WidgetBar.ID) as? WidgetBar ?: continue
widgetBar.update()
log.debug("Widget updated for project ${project.name}")
}