Skip to content

Commit

Permalink
feat: add capability to display hover (#605)
Browse files Browse the repository at this point in the history
* feat: add capability to display hover

This is disabled by default and can be enabled in the IntelliJ registry with the key snyk.documentationHoversEnabled.

* fix: minimize read lock during code vision calculation, improve annotations
  • Loading branch information
bastiandoetsch committed Sep 9, 2024
1 parent 85db49f commit fc50f9b
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- add color and highlighting setting for Snyk issues
- add dialog to choose reference branch when net new scanning
- always display info nodes
- add option in IntelliJ registry to display tooltips with issue information

### Fixes
- add name to code vision provider
Expand Down
26 changes: 2 additions & 24 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@ import java.io.File
import java.io.FileNotFoundException
import java.net.URI
import java.nio.file.Path
import java.security.KeyStore
import java.util.Objects.nonNull
import java.util.SortedSet
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import javax.swing.JComponent

private val logger = Logger.getInstance("#io.snyk.plugin.UtilsKt")
Expand Down Expand Up @@ -223,6 +218,8 @@ fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled
fun isSnykIaCLSEnabled(): Boolean = false


fun isDocumentationHoverEnabled(): Boolean = Registry.get("snyk.isDocumentationHoverEnabled").asBoolean()

fun getWaitForResultsTimeout(): Long =
Registry.intValue(
"snyk.timeout.results.waiting",
Expand All @@ -233,25 +230,6 @@ const val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN = 12L
val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MS =
TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN, TimeUnit.MINUTES).toInt()

fun getSSLContext(): SSLContext {
val trustManager = getX509TrustManager()
val sslContext = SSLContext.getInstance("TLSv1.2")
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
return sslContext
}

fun getX509TrustManager(): X509TrustManager {
val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
val trustManagers: Array<TrustManager> = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
("Unexpected default trust managers:${trustManagers.contentToString()}")
}
return trustManagers[0] as X509TrustManager
}

fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? {
return if (!virtualFile.isValid || project.isDisposed) {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@ import java.util.concurrent.TimeUnit
*/
@Service(Service.Level.APP, Service.Level.PROJECT)
class SnykPluginDisposable : Disposable, AppLifecycleListener {
private var disposed = false
get() {
return ApplicationManager.getApplication().isDisposed || field
}

fun isDisposed() = disposed

override fun dispose() {
disposed = true
}

companion object {
@NotNull
fun getInstance(): Disposable {
fun getInstance(): SnykPluginDisposable {
return ApplicationManager.getApplication().getService(SnykPluginDisposable::class.java)
}

@NotNull
fun getInstance(@NotNull project: Project): Disposable {
fun getInstance(@NotNull project: Project): SnykPluginDisposable {
return project.getService(SnykPluginDisposable::class.java)
}
}
Expand All @@ -46,7 +57,4 @@ class SnykPluginDisposable : Disposable, AppLifecycleListener {
// do nothing
}
}

override fun dispose() = Unit

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ class SnykToolWindowSnykScanListenerLS(
// TODO implement
}
}
refreshAnnotationsForOpenFiles(project)
}

override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List<ScanIssue>) {
}
override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List<ScanIssue>) {}

fun displaySnykCodeResults(snykResults: Map<SnykFile, List<ScanIssue>>) {
if (disposed) return
Expand Down
11 changes: 5 additions & 6 deletions src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import java.util.concurrent.TimeoutException
import javax.swing.Icon


private const val CODEACTION_TIMEOUT = 5000L
private const val CODEACTION_TIMEOUT = 10L

typealias SnykAnnotationInput = Pair<PsiFile, Map<Range, List<ScanIssue>>>
typealias SnykAnnotationList = List<SnykAnnotation>
Expand Down Expand Up @@ -78,8 +78,9 @@ abstract class SnykAnnotator(private val product: ProductType) :
var gutterIconRenderer: GutterIconRenderer? = null
)

// overrides needed for the Annotator to invoke apply(). We don't do anything here
override fun collectInformation(file: PsiFile): SnykAnnotationInput? {
if (disposed) return null
if (!LanguageServerWrapper.getInstance().isInitialized) return null
val map = getIssuesForFile(file)
.filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) }
.sortedByDescending { it.getSeverityAsEnum() }
Expand Down Expand Up @@ -111,9 +112,7 @@ abstract class SnykAnnotator(private val product: ProductType) :
logger.warn("Invalid range for range: $textRange")
return@forEach
}
annotations.addAll(
doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions)
)
annotations.addAll(doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions))
}
return annotations.sortedByDescending { it.issue.getSeverityAsEnum() }
}
Expand Down Expand Up @@ -218,7 +217,7 @@ abstract class SnykAnnotator(private val product: ProductType) :
val codeActions =
try {
languageServer.textDocumentService
.codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList()
.codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.SECONDS) ?: emptyList()
} catch (ignored: TimeoutException) {
logger.info("Timeout fetching code actions for range: $range")
emptyList()
Expand Down
67 changes: 33 additions & 34 deletions src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.intellij.openapi.progress.Task.Backgroundable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import icons.SnykIcons
import io.snyk.plugin.toLanguageServerURL
import org.eclipse.lsp4j.CodeLens
Expand All @@ -26,7 +27,7 @@ import java.awt.event.MouseEvent
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

private const val CODELENS_FETCH_TIMEOUT = 2L
private const val CODELENS_FETCH_TIMEOUT = 10L

@Suppress("UnstableApiUsage")
class LSCodeVisionProvider : CodeVisionProvider<Unit>, CodeVisionGroupSettingProvider {
Expand All @@ -47,44 +48,42 @@ class LSCodeVisionProvider : CodeVisionProvider<Unit>, CodeVisionGroupSettingPro
}

override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState {
if (editor.project == null) return CodeVisionState.READY_EMPTY
if (LanguageServerWrapper.getInstance().isDisposed()) return CodeVisionState.READY_EMPTY
if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY
val project = editor.project ?: return CodeVisionState.READY_EMPTY

return ReadAction.compute<CodeVisionState, RuntimeException> {
val project = editor.project ?: return@compute CodeVisionState.READY_EMPTY
val document = editor.document
val file = PsiDocumentManager.getInstance(project).getPsiFile(document)
?: return@compute CodeVisionState.READY_EMPTY
val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL()))
val lenses = mutableListOf<Pair<TextRange, CodeVisionEntry>>()
val codeLenses = try {
LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params)
.get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS)
} catch (ignored: TimeoutException) {
logger.info("Timeout fetching code lenses for : $file")
emptyList()
}
val document = editor.document

if (codeLenses == null) {
return@compute CodeVisionState.READY_EMPTY
}
codeLenses.forEach { codeLens ->
val range = TextRange(
document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character,
document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character
)
val file = ReadAction.compute<PsiFile, RuntimeException> {
PsiDocumentManager.getInstance(project).getPsiFile(document)
} ?: return CodeVisionState.READY_EMPTY

val entry = ClickableTextCodeVisionEntry(
text = codeLens.command.title,
providerId = id,
onClick = LSCommandExecutionHandler(codeLens),
extraActions = emptyList(),
icon = SnykIcons.TOOL_WINDOW
)
lenses.add(range to entry)
}
return@compute CodeVisionState.Ready(lenses)
val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL()))
val lenses = mutableListOf<Pair<TextRange, CodeVisionEntry>>()
val codeLenses = try {
LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params)
.get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) ?: return CodeVisionState.READY_EMPTY
} catch (ignored: TimeoutException) {
logger.info("Timeout fetching code lenses for : $file")
return CodeVisionState.READY_EMPTY
}

codeLenses.forEach { codeLens ->
val range = TextRange(
document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character,
document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character
)

val entry = ClickableTextCodeVisionEntry(
text = codeLens.command.title,
providerId = id,
onClick = LSCommandExecutionHandler(codeLens),
extraActions = emptyList(),
icon = SnykIcons.TOOL_WINDOW
)
lenses.add(range to entry)
}
return CodeVisionState.Ready(lenses)
}

private class LSCommandExecutionHandler(private val codeLens: CodeLens) : (MouseEvent?, Editor) -> Unit {
Expand Down
85 changes: 85 additions & 0 deletions src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@file:Suppress("UnstableApiUsage")

package snyk.common.lsp

import com.intellij.markdown.utils.convertMarkdownToHtml
import com.intellij.model.Pointer
import com.intellij.openapi.Disposable
import com.intellij.platform.backend.documentation.DocumentationResult
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.DocumentationTargetProvider
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.psi.PsiFile
import icons.SnykIcons
import io.snyk.plugin.isDocumentationHoverEnabled
import io.snyk.plugin.toLanguageServerURL
import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable
import org.eclipse.lsp4j.Hover
import org.eclipse.lsp4j.HoverParams
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.TextDocumentIdentifier
import java.util.concurrent.TimeUnit

class SnykDocumentationTargetPointer(private val documentationTarget: DocumentationTarget) :
Pointer<DocumentationTarget> {

override fun dereference(): DocumentationTarget {
return documentationTarget
}
}

class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable {
private var disposed = false
get() {
return SnykPluginDisposable.getInstance().isDisposed() || field
}

fun isDisposed() = disposed

override fun documentationTargets(file: PsiFile, offset: Int): MutableList<out DocumentationTarget> {
val languageServerWrapper = LanguageServerWrapper.getInstance()
if (disposed || !languageServerWrapper.isInitialized || !isDocumentationHoverEnabled()) return mutableListOf()

val lineNumber = file.viewProvider.document.getLineNumber(offset)
val lineStartOffset = file.viewProvider.document.getLineStartOffset(lineNumber)
val hoverParams = HoverParams(
TextDocumentIdentifier(file.virtualFile.toLanguageServerURL()),
Position(lineNumber, offset - lineStartOffset)
)
val hover =
languageServerWrapper.languageServer.textDocumentService.hover(hoverParams).get(2000, TimeUnit.MILLISECONDS)
if (hover == null || hover.contents.right.value.isEmpty()) return mutableListOf()
return mutableListOf(SnykDocumentationTarget(hover))
}

inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget {
override fun computeDocumentationHint(): String? {
val htmlText = convertMarkdownToHtml(hover.contents.right.value)
if (htmlText.isEmpty()) {
return null
}
return htmlText.split("\n")[0]
}

override fun computeDocumentation(): DocumentationResult? {
val htmlText = convertMarkdownToHtml(hover.contents.right.value)
if (htmlText.isEmpty()) {
return null
}
return DocumentationResult.documentation(htmlText)
}

override fun computePresentation(): TargetPresentation {
return TargetPresentation.builder("Snyk Security").icon(SnykIcons.TOOL_WINDOW).presentation()
}

override fun createPointer(): Pointer<out DocumentationTarget> {
return SnykDocumentationTargetPointer(this)
}
}


override fun dispose() {
disposed = true
}
}
12 changes: 6 additions & 6 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,13 @@ class LanguageServerWrapper(
return ""
}

companion object {
override fun dispose() {
disposed = true
shutdown()
}


companion object {
private var instance: LanguageServerWrapper? = null
fun getInstance() =
instance ?: LanguageServerWrapper().also {
Expand All @@ -571,10 +576,5 @@ class LanguageServerWrapper(
}
}

override fun dispose() {
disposed = true
shutdown()
}

}

18 changes: 12 additions & 6 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<depends optional="true" config-file="optional/withXML.xml">com.intellij.modules.xml</depends>

<extensions defaultExtensionNs="com.intellij">
<codeInsight.lineMarkerProvider
language=""
implementationClass="snyk.common.annotator.SnykLineMarkerProvider"/>
<codeInsight.lineMarkerProvider
language=""
implementationClass="snyk.common.annotator.SnykLineMarkerProvider"/>
<colorSettingsPage implementation="snyk.common.annotator.SnykAnnotationColorSettingsPage"/>
<toolWindow id="Snyk"
anchor="bottom"
Expand All @@ -42,12 +42,18 @@
<registryKey key="snyk.timeout.results.waiting"
defaultValue="720000"
description="Snyk timeout (milliseconds) to wait for results during scan"/>
<registryKey key="snyk.isDocumentationHoverEnabled"
defaultValue="false"
description="Show Snyk issue details additionally as documentation popup (hover)"
restartRequired="false"
/>

<notificationGroup id="Snyk" displayType="BALLOON" toolWindowId="Snyk"/>

<codeInsight.codeVisionProvider implementation="snyk.common.lsp.LSCodeVisionProvider" id="snyk.common.lsp.LSCodeVisionProvider"/>
<config.codeVisionGroupSettingProvider implementation="snyk.common.lsp.LSCodeVisionProvider" />

<codeInsight.codeVisionProvider implementation="snyk.common.lsp.LSCodeVisionProvider"
id="snyk.common.lsp.LSCodeVisionProvider"/>
<config.codeVisionGroupSettingProvider implementation="snyk.common.lsp.LSCodeVisionProvider"/>
<platform.backend.documentation.targetProvider implementation="snyk.common.lsp.LSDocumentationTargetProvider" />
<externalAnnotator language="" implementationClass="snyk.common.annotator.SnykCodeAnnotator"/>
<externalAnnotator language="" implementationClass="snyk.common.annotator.SnykOSSAnnotator"/>
</extensions>
Expand Down

0 comments on commit fc50f9b

Please sign in to comment.