Skip to content

Commit

Permalink
Merge pull request #4 from returntocorp/austin/nudge-and-install
Browse files Browse the repository at this point in the history
feat: nudge and install
  • Loading branch information
ajbt200128 authored Aug 28, 2023
2 parents 21f90cb + cb37560 commit 7dc3875
Show file tree
Hide file tree
Showing 22 changed files with 349 additions and 58 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ org.gradle.configuration-cache=true
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
org.gradle.caching=true
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
systemProp.org.gradle.unsafe.kotlin.assignment=true
systemProp.org.gradle.unsafe.kotlin.assignment=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.semgrep.idea.actions

import com.intellij.notification.Notification
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.semgrep.idea.settings.AppState

class DismissLoginNudgeAction(private val notification: Notification) : AnAction("Don't ask again") {
override fun actionPerformed(e: AnActionEvent) {
AppState.getInstance().pluginState.dismissedLoginNudge = true
notification.expire()
}
}
6 changes: 4 additions & 2 deletions src/main/kotlin/com/semgrep/idea/actions/LoginAction.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.semgrep.idea.actions

import com.intellij.ide.BrowserUtil
import com.intellij.notification.Notification
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.platform.lsp.api.LspServer
import com.semgrep.idea.lsp.custom_notifications.LoginFinishRequest
import com.semgrep.idea.lsp.custom_requests.LoginRequest

class LoginAction : LspAction("Sign In to Semgrep Code") {
class LoginAction(private val notification: Notification? = null) : LspAction("Sign In to Semgrep Code") {
override fun actionPerformed(e: AnActionEvent, servers: List<com.semgrep.idea.lsp.SemgrepLspServer>) {
val loginRequest = LoginRequest(servers.first())
val response = (servers.first() as LspServer).requestExecutor.sendRequestSync(loginRequest) ?: return
BrowserUtil.browse(response.url)
servers.forEach {
it.requestExecutor.sendNotification(LoginFinishRequest(it, response))
LoginFinishRequest(it, response).sendNotification()
}
notification?.expire()
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/com/semgrep/idea/actions/LogoutAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.semgrep.idea.lsp.custom_notifications.LogoutNotifcation
class LogoutAction : LspAction("Sign out of Semgrep Code") {
override fun actionPerformed(e: AnActionEvent, servers: List<com.semgrep.idea.lsp.SemgrepLspServer>) {
servers.map {
it.requestExecutor.sendNotification(LogoutNotifcation(it))
LogoutNotifcation(it).sendNotification()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package com.semgrep.idea.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.semgrep.idea.lsp.custom_notifications.RefreshRulesNotification
import org.eclipse.lsp4j.DidChangeTextDocumentParams

class RefreshRulesAction : LspAction("Update Semgrep Rules") {
override fun actionPerformed(e: AnActionEvent, servers: List<com.semgrep.idea.lsp.SemgrepLspServer>) {
servers.map {
it.requestExecutor.sendNotification(RefreshRulesNotification(it))
it.lsp4jServer.textDocumentService.didChange(DidChangeTextDocumentParams())
RefreshRulesNotification(it).sendNotification()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.semgrep.idea.lsp.custom_notifications.ScanWorkspaceParams
class ScanWorkspaceAction : LspAction("Scan Workspace with Semgrep") {
override fun actionPerformed(e: AnActionEvent, servers: List<com.semgrep.idea.lsp.SemgrepLspServer>) {
val params = ScanWorkspaceParams(full = false)
servers.map { it.requestExecutor.sendNotification(ScanWorkspaceNotification(it, params)) }
servers.map { ScanWorkspaceNotification(it, params).sendNotification() }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.semgrep.idea.lsp.custom_notifications.ScanWorkspaceParams
class ScanWorkspaceFullAction : LspAction("Scan Workspace with Semgrep (Including Unmodified Files)") {
override fun actionPerformed(e: AnActionEvent, servers: List<com.semgrep.idea.lsp.SemgrepLspServer>) {
val params = ScanWorkspaceParams(full = true)
servers.map { it.requestExecutor.sendNotification(ScanWorkspaceNotification(it, params)) }
servers.map { ScanWorkspaceNotification(it, params).sendNotification() }
}

}
80 changes: 80 additions & 0 deletions src/main/kotlin/com/semgrep/idea/lsp/SemgrepInstaller.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.semgrep.idea.lsp

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project
import com.intellij.util.text.SemVer
import com.semgrep.idea.settings.AppState
import com.semgrep.idea.settings.SemgrepPluginSettings
import com.semgrep.idea.ui.SemgrepNotifier
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

object SemgrepInstaller {
enum class InstallOption(val binary: String, val installCommand: String) {
BREW("brew", "brew install semgrep"),
PIP("pip3", "pip3 install semgrep");

fun isInstalled(): Boolean {
return which(binary) != null
}

fun install(project: Project) {
val cmd = GeneralCommandLine("sh", "-c", installCommand)
val process = cmd.createProcess()
val ret = process.waitFor()
val out = process.inputStream.bufferedReader().readText()
val semgrepNotifier = SemgrepNotifier(project)
if (ret == 0) {
semgrepNotifier.notifyInstallSuccess()
SemgrepLspServer.startServersIfNeeded(project)
} else {
semgrepNotifier.notifyInstallFailure(out, ret)
}
}
}

fun semgrepInstalled(): Boolean {
val defaultPath = SemgrepPluginSettings().path
val state = AppState.getInstance().appSettings
return state.path != defaultPath && which(defaultPath) != null
}

fun getCliVersion(): SemVer? {
val cmd = GeneralCommandLine("semgrep", "--version")
val process = cmd.createProcess()
process.waitFor()
val out = process.inputStream.bufferedReader().readText().trim()
return SemVer.parseFromText(out)
}

fun getMostUpToDateCliVersion(): SemVer? {
val client = HttpClient.newBuilder().build()
val request = HttpRequest.newBuilder()
.uri(URI.create("https://semgrep.dev/api/check-version"))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString()).body()
// Can't figure out how to actually parse this to a string, not a quoted string. Oh well
val version = Json.parseToJsonElement(response).jsonObject.get("latest").toString().replace("\"", "")
return SemVer.parseFromText(version)
}

fun which(binary: String): String? {
val cmd = GeneralCommandLine("which", binary)

val process = cmd.createProcess()
process.waitFor()
val result = process.inputStream.bufferedReader().readLine()

return if (result == "") null else result
}

fun getInstallOptions(): List<InstallOption> {
return InstallOption.values().filter { it.isInstalled() }
}


}
16 changes: 14 additions & 2 deletions src/main/kotlin/com/semgrep/idea/lsp/SemgrepLspServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ import com.intellij.platform.lsp.api.LspServerManager
import com.intellij.platform.lsp.api.LspServerNotificationsHandler
import com.intellij.platform.lsp.api.requests.LspRequestExecutor

class SemgrepLspServer(private val server: LspServer) : LspServer {

class SemgrepLspServer(private val server: LspServer) : LspServer {
companion object {
val MIN_SEMGREP_VERSION = "1.21.0"

// These should probably be split off into SemgrepLspManager or something
private fun getManager(project: Project): LspServerManager {
return LspServerManager.getInstance(project)
}

fun startServersIfNeeded(project: Project) {
val manager = getManager(project)
manager.stopAndRestartIfNeeded(SemgrepLspServerSupportProvider::class.java)
}

fun getInstances(project: Project): List<SemgrepLspServer> {
val manager = LspServerManager.getInstance(project)
val manager = getManager(project)
val servers = manager.getServersForProvider(SemgrepLspServerSupportProvider::class.java)
// There's prolly an easier way to do this but oh well
return servers.filter { it.lsp4jServer is SemgrepLanguageServer }.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package com.semgrep.idea.lsp
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.lsp.api.LspServerListener
import com.intellij.platform.lsp.api.ProjectWideLspServerDescriptor
import com.semgrep.idea.settings.AppSettingsState
import com.semgrep.idea.settings.AppState
import org.eclipse.lsp4j.services.LanguageServer

class SemgrepLspServerDescriptor(project: Project) : ProjectWideLspServerDescriptor(project, "Semgrep") {
override val lsp4jServerClass: Class<out LanguageServer> = SemgrepLanguageServer::class.java

override fun createCommandLine(): GeneralCommandLine {
val settingState = AppSettingsState.getInstance().settings
val settingState = AppState.getInstance().appSettings
return GeneralCommandLine(settingState.path).apply {
withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
withCharset(Charsets.UTF_8)
Expand All @@ -24,6 +24,8 @@ class SemgrepLspServerDescriptor(project: Project) : ProjectWideLspServerDescrip
}

override fun createInitializationOptions(): Any {
return AppSettingsState.getInstance().settings
return AppState.getInstance().appSettings
}

override val lspServerListener: LspServerListener = SemgrepLspServerListener(project)
}
36 changes: 36 additions & 0 deletions src/main/kotlin/com/semgrep/idea/lsp/SemgrepLspServerListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.semgrep.idea.lsp

import com.intellij.openapi.project.Project
import com.intellij.platform.lsp.api.LspServerListener
import com.intellij.util.text.SemVer
import com.semgrep.idea.lsp.custom_requests.LoginStatusRequest
import com.semgrep.idea.settings.AppState
import com.semgrep.idea.ui.SemgrepNotifier
import org.eclipse.lsp4j.InitializeResult

class SemgrepLspServerListener(val project: Project) : LspServerListener {
override fun serverInitialized(params: InitializeResult) {
super.serverInitialized(params)
val settings = AppState.getInstance()
val servers = SemgrepLspServer.getInstances(project)
val first = servers.firstOrNull()
if (first != null && !settings.pluginState.dismissedLoginNudge) {
val loginStatusRequest = LoginStatusRequest(first)
loginStatusRequest.sendRequest().handle({ it, _ ->
if (!it.loggedIn) {
SemgrepNotifier(project).notifyLoginNudge()
}
})
val current = SemgrepInstaller.getCliVersion()
val needed = SemVer.parseFromText(SemgrepLspServer.MIN_SEMGREP_VERSION)
val latest = SemgrepInstaller.getMostUpToDateCliVersion()
if (current != null) {
if (needed != null && current < needed) {
SemgrepNotifier(project).notifyUpdateNeeded(needed, current)
} else if (latest != null && current < latest) {
SemgrepNotifier(project).notifyUpdateAvailable(current, latest)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package com.semgrep.idea.lsp
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.lsp.api.LspServerSupportProvider
import com.semgrep.idea.settings.AppState

class SemgrepLspServerSupportProvider : LspServerSupportProvider {
override fun fileOpened(
project: Project,
file: VirtualFile,
serverStarter: LspServerSupportProvider.LspServerStarter
) {
serverStarter.ensureServerStarted(SemgrepLspServerDescriptor(project))

val installed = SemgrepInstaller.semgrepInstalled()
if (installed || AppState.getInstance().pluginState.handledInstallBanner) {
serverStarter.ensureServerStarted(SemgrepLspServerDescriptor(project))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import javax.swing.JPanel

class AppSettingsComponent(settings: SemgrepLspServerSettings) {
class AppSettingsComponent(settings: SemgrepPluginSettings) {
private var panel: JPanel? = null

// Really we should use reflection to generate this, but I'm lazy
Expand All @@ -25,13 +25,13 @@ class AppSettingsComponent(settings: SemgrepLspServerSettings) {
return panel
}

fun getSettings(): SemgrepLspServerSettings {
fun getSettings(): SemgrepPluginSettings {
val traceLevelStr = traceChooser.selectedItem as String
val traceLevel = TraceLevel.valueOf(traceLevelStr.uppercase())
return SemgrepLspServerSettings(trace = SemgrepLspServerSettings.Trace(traceLevel), path = pathTextField.text)
return SemgrepPluginSettings(trace = SemgrepPluginSettings.Trace(traceLevel), path = pathTextField.text)
}

fun setFieldValues(settings: SemgrepLspServerSettings) {
fun setFieldValues(settings: SemgrepPluginSettings) {
traceChooser.selectedItem = settings.trace.server.name.lowercase()
pathTextField.text = settings.path
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,24 @@ class AppSettingsConfigurable : Configurable {
private var settingsComponent: AppSettingsComponent? = null

override fun createComponent(): JComponent? {
val settings = AppSettingsState.getInstance().settings
val settings = AppState.getInstance().appSettings
settingsComponent = AppSettingsComponent(settings)
return settingsComponent?.getPanel()
}

override fun isModified(): Boolean {
return settingsComponent?.getSettings() != AppSettingsState.getInstance().settings
return settingsComponent?.getSettings() != AppState.getInstance().appSettings
}

override fun apply() {
val appSettingsState = AppSettingsState.getInstance()
val appSettingsState = AppState.getInstance()
val newSettings = settingsComponent?.getSettings() ?: return
appSettingsState.settings = newSettings
settingsComponent?.setFieldValues(appSettingsState.settings)
appSettingsState.appSettings = newSettings
settingsComponent?.setFieldValues(appSettingsState.appSettings)
}

override fun reset() {
val settings = AppSettingsState.getInstance().settings
val settings = AppState.getInstance().appSettings
settingsComponent?.setFieldValues(settings)
}

Expand Down
29 changes: 0 additions & 29 deletions src/main/kotlin/com/semgrep/idea/settings/AppSettingsState.kt

This file was deleted.

Loading

0 comments on commit 7dc3875

Please sign in to comment.