diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index 14520a216..e23170707 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -20,7 +20,7 @@ import io.snyk.plugin.isUrlValid import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.SnykSettingsDialog -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import javax.swing.JComponent diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt index 042d21376..961199fb1 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -9,7 +9,7 @@ import com.intellij.util.ui.GridBag import com.intellij.util.ui.JBUI import org.jetbrains.concurrency.runAsync import snyk.common.lsp.FolderConfig -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import java.awt.GridBagConstraints import java.awt.GridBagLayout diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index f21c6ec99..c6d5ff450 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -48,7 +48,7 @@ import io.snyk.plugin.ui.settings.ScanTypesPanel import io.snyk.plugin.ui.settings.SeveritiesEnablementPanel import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.SnykBundle -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import java.awt.GridBagConstraints import java.awt.GridBagLayout import java.awt.Insets diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index 46a828ada..9544cf91f 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -67,7 +67,7 @@ import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly import snyk.common.ProductType import snyk.common.SnykError -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import snyk.container.ContainerIssuesForImage diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 19aba3824..4d6dfbc7e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -2,7 +2,6 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.TextRange @@ -29,13 +28,10 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode import snyk.common.ProductType import snyk.common.SnykFileIssueComparator -import snyk.common.lsp.FolderConfig -import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import javax.swing.JTree diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 016060ab0..e1b79c4cd 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -58,6 +58,8 @@ import snyk.common.lsp.commands.COMMAND_LOGOUT import snyk.common.lsp.commands.COMMAND_REPORT_ANALYTICS import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN import snyk.common.lsp.commands.ScanDoneEvent +import snyk.common.lsp.settings.LanguageServerSettings +import snyk.common.lsp.settings.SeverityFilter import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index b76bea64a..ef9863f12 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -40,6 +40,8 @@ import org.eclipse.lsp4j.services.LanguageClient import org.jetbrains.concurrency.runAsync import snyk.common.ProductType import snyk.common.editor.DocumentChanger +import snyk.common.lsp.progress.ProgressManager +import snyk.common.lsp.settings.FolderConfigSettings import snyk.trust.WorkspaceTrustService import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CompletableFuture @@ -53,7 +55,7 @@ class SnykLanguageClient : Disposable { val logger = Logger.getInstance("Snyk Language Server") val gson = Gson() - private val progressManager = LSPProgressManager() + private val progressManager = ProgressManager() private var disposed = false get() { diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt similarity index 97% rename from src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt rename to src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt index d19f12cb9..df8e5eccd 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt @@ -1,8 +1,7 @@ @file:Suppress("UnstableApiUsage") -package snyk.common.lsp +package snyk.common.lsp.hovers -import com.intellij.markdown.utils.convertMarkdownToHtml import com.intellij.model.Pointer import com.intellij.openapi.Disposable import com.intellij.platform.backend.documentation.DocumentationResult @@ -18,6 +17,7 @@ import org.eclipse.lsp4j.Hover import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.TextDocumentIdentifier +import snyk.common.lsp.LanguageServerWrapper import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException diff --git a/src/main/kotlin/snyk/common/lsp/progress/Progress.kt b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt new file mode 100644 index 000000000..21082e48d --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Copyright (c) 2024 Snyk Ltd + * + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * Snyk Ltd - adjustments for use in Snyk IntelliJ Plugin + *******************************************************************************/ +package snyk.common.lsp.progress + +import org.eclipse.lsp4j.WorkDoneProgressNotification +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.TimeUnit + +internal class Progress(val token: String) { + var cancellable: Boolean = false + var done: Boolean = false + + private val progressNotifications = LinkedBlockingDeque() + + var cancelled: Boolean = false + private set + + var title: String? = null + get() = if (field != null) field else token + + fun add(progressNotification: WorkDoneProgressNotification) { + progressNotifications.add(progressNotification) + } + + @get:Throws(InterruptedException::class) + val nextProgressNotification: WorkDoneProgressNotification? + get() = progressNotifications.pollFirst(200, TimeUnit.MILLISECONDS) + + fun cancel() { + this.cancelled = true + } +} diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt new file mode 100644 index 000000000..1fffb99b2 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -0,0 +1,220 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Copyright (c) 2024 Snyk Ltd + * + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * Snyk Ltd - adjustments for use in Snyk IntelliJ Plugin + *******************************************************************************/ +package snyk.common.lsp.progress + + +import com.intellij.ide.impl.ProjectUtil +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 io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.ProgressParams +import org.eclipse.lsp4j.WorkDoneProgressBegin +import org.eclipse.lsp4j.WorkDoneProgressCancelParams +import org.eclipse.lsp4j.WorkDoneProgressCreateParams +import org.eclipse.lsp4j.WorkDoneProgressKind +import org.eclipse.lsp4j.WorkDoneProgressNotification +import org.eclipse.lsp4j.WorkDoneProgressReport +import org.eclipse.lsp4j.jsonrpc.messages.Either +import snyk.common.lsp.LanguageServerWrapper +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Function + + +class ProgressManager() : Disposable { + private val progresses: MutableMap = ConcurrentHashMap() + private var disposed = false + get() { + return SnykPluginDisposable.getInstance().isDisposed() || field + } + + fun isDisposed() = disposed + + fun createProgress(params: WorkDoneProgressCreateParams): CompletableFuture { + if (!disposed) { + val token = getToken(params.token) + getProgress(token) + } + return CompletableFuture.completedFuture(null) + } + + private fun createProgressIndicator(progress: Progress) { + val token: String = progress.token + if (isDone(progress)) { + progresses.remove(token) + return + } + val title = "Snyk: " + progress.title + val cancellable: Boolean = progress.cancellable + ProgressManager.getInstance() + .run(newProgressBackgroundTask(title, cancellable, progress, token)) + } + + private fun newProgressBackgroundTask( + title: String, + cancellable: Boolean, + progress: Progress, + token: String + ) = object : Task.Backgroundable(ProjectUtil.getActiveProject(), title, cancellable) { + override fun run(indicator: ProgressIndicator) { + try { + while (!isDone(progress)) { + if (indicator.isCanceled) { + progresses.remove(token) + val workDoneProgressCancelParams = WorkDoneProgressCancelParams() + workDoneProgressCancelParams.setToken(token) + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.isInitialized) { + val languageServer = languageServerWrapper.languageServer + languageServer.cancelProgress(workDoneProgressCancelParams) + } + throw ProcessCanceledException() + } + + var progressNotification: WorkDoneProgressNotification? + try { + progressNotification = progress.nextProgressNotification + } catch (e: InterruptedException) { + progresses.remove(token) + Thread.currentThread().interrupt() + throw ProcessCanceledException(e) + } + if (progressNotification != null) { + val kind = progressNotification.kind ?: return + when (kind) { + WorkDoneProgressKind.begin -> // 'begin' has been notified + begin(progressNotification as WorkDoneProgressBegin, indicator) + + WorkDoneProgressKind.report -> // 'report' has been notified + report(progressNotification as WorkDoneProgressReport, indicator) + + WorkDoneProgressKind.end -> Unit + } + } + } + } finally { + progresses.remove(token) + } + } + } + + private fun isDone(progress: Progress): Boolean { + return progress.done || progress.cancelled || disposed + } + + private fun begin( + begin: WorkDoneProgressBegin, + progressIndicator: ProgressIndicator + ) { + val percentage = begin.percentage + progressIndicator.isIndeterminate = percentage == null + updateProgressIndicator(begin.message, percentage, progressIndicator) + } + + private fun report( + report: WorkDoneProgressReport, + progressIndicator: ProgressIndicator + ) { + updateProgressIndicator(report.message, report.percentage, progressIndicator) + } + + @Synchronized + private fun getProgress(token: String): Progress { + var progress: Progress? = progresses[token] + if (progress != null) { + return progress + } + progress = Progress(token) + progresses[token] = progress + return progress + } + + private fun updateProgressIndicator( + message: String?, + percentage: Int?, + progressIndicator: ProgressIndicator + ) { + if (!message.isNullOrBlank()) { + progressIndicator.text = message + } + if (percentage != null) { + progressIndicator.fraction = percentage.toDouble() / 100 + } + } + + fun notifyProgress(params: ProgressParams) { + if (params.value == null || params.token == null || disposed) { + return + } + val value = params.value + if (value.isRight) { + // we don't need partial results progress support + return + } + + if (!value.isLeft) { + return + } + + val progressNotification = value.left + val kind = progressNotification.kind ?: return + val token = getToken(params.token) + var progress: Progress? = progresses[token] + if (progress == null) { + // The server is not spec-compliant and reports progress using server-initiated progress but didn't + // call window/workDoneProgress/create beforehand. In that case, we check the 'kind' field of the + // progress data. If the 'kind' field is 'begin', we set up a progress reporter anyway. + if (kind != WorkDoneProgressKind.begin) { + return + } + progress = getProgress(token) + } + + // Add the progress notification + progress.add(progressNotification) + when (progressNotification.kind!!) { + WorkDoneProgressKind.begin -> { + // 'begin' progress + val begin = progressNotification as WorkDoneProgressBegin + progress.title = begin.title + progress.cancellable = begin.cancellable != null && begin.cancellable + // The IJ task is created on 'begin' and not on 'create' to initialize + // the Task with the 'begin' title. + createProgressIndicator(progress) + } + + WorkDoneProgressKind.end -> progress.done = true + WorkDoneProgressKind.report -> Unit + } + } + + override fun dispose() { + this.disposed = true + progresses.values.forEach(Progress::cancel) + progresses.clear() + } + + companion object { + private fun getToken(token: Either): String { + return token.map( + Function.identity() + ) { obj: Int -> obj.toString() } + } + } +} diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt similarity index 97% rename from src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt rename to src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt index bd9d56e1f..77dc553af 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt @@ -1,4 +1,4 @@ -package snyk.common.lsp +package snyk.common.lsp.settings import com.google.gson.Gson import com.intellij.openapi.components.BaseState @@ -10,6 +10,7 @@ import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import com.intellij.util.xmlb.annotations.MapAnnotation import io.snyk.plugin.getContentRootPaths +import snyk.common.lsp.FolderConfig import java.util.stream.Collectors @Service diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt similarity index 98% rename from src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt rename to src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt index 8b57c0b1d..b5c806ac1 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt @@ -1,11 +1,12 @@ @file:Suppress("unused") -package snyk.common.lsp +package snyk.common.lsp.settings import com.google.gson.annotations.SerializedName import com.intellij.openapi.components.service import io.snyk.plugin.pluginSettings import org.apache.commons.lang3.SystemUtils +import snyk.common.lsp.FolderConfig import snyk.pluginInfo data class LanguageServerSettings( diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 318d498f7..f500bfabf 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -55,7 +55,7 @@ id="snyk.common.codevision.LSCodeVisionProvider"/> - + diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt index 9a7b000ff..1975f7ddc 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -2,9 +2,6 @@ package io.snyk.plugin.ui import com.intellij.openapi.components.service import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.VirtualFileSystem import com.intellij.testFramework.LightPlatform4TestCase import com.intellij.testFramework.PlatformTestUtil import io.mockk.CapturingSlot @@ -12,14 +9,13 @@ import io.mockk.mockk import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.getContentRootPaths -import io.snyk.plugin.toVirtualFile import okio.Path.Companion.toPath import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.services.LanguageServer import org.junit.Test import snyk.common.lsp.FolderConfig -import snyk.common.lsp.FolderConfigSettings -import snyk.common.lsp.LanguageServerSettings +import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LanguageServerSettings import snyk.common.lsp.LanguageServerWrapper import snyk.trust.WorkspaceTrustService import snyk.trust.WorkspaceTrustSettings @@ -31,7 +27,7 @@ class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { private lateinit var folderConfig: FolderConfig lateinit var cut: BranchChooserComboBoxDialog - override fun setUp(): Unit { + override fun setUp() { super.setUp() unmockkAll() folderConfig = FolderConfig(project.basePath.toString(), "testBranch") diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 0fa9bc0a7..7f4d5508d 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -20,9 +20,9 @@ import org.eclipse.lsp4j.Range import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.DataFlow import snyk.common.lsp.FolderConfig -import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue +import snyk.common.lsp.settings.FolderConfigSettings import snyk.trust.WorkspaceTrustSettings import java.nio.file.Paths import javax.swing.JTree @@ -123,9 +123,21 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { lesson = null, details = "", ruleId = "", + publicId = "", + documentation = "", + lineNumber = "", + issue = "", + impact = "", + resolve = "", + path = emptyList(), + references = emptyList(), + customUIContent = "", + key = "", ), isIgnored = isIgnored, ignoreDetails = null, + isNew = false, + filterableIssueType = ScanIssue.OPEN_SOURCE, ) return listOf(issue) } diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 338ae9734..cfb322366 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -29,6 +29,7 @@ import org.junit.Before import org.junit.Ignore import org.junit.Test import snyk.common.lsp.commands.ScanDoneEvent +import snyk.common.lsp.settings.FolderConfigSettings import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import java.util.concurrent.CompletableFuture