Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ build/
gen/
deps
.intellijPlatform
.kotlin

# MacOS files
.DS_Store
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Opa IntelliJ plugin
A plugin for [IntelliJ](https://www.jetbrains.com/idea/) that provides support for [Open Policy Agent](https://www.openpolicyagent.org/)
# OPA IntelliJ Plugin
A plugin for [IntelliJ](https://www.jetbrains.com/idea/) that provides support for [Open Policy Agent](https://www.openpolicyagent.org/).

Main features are:
* highlighting
* Highlighting
* `opa eval` run configuration
* `opa test` run configuration
* Regal linter and language server support (diagnostics, code completions, code folding, signature help, document symbols, debugging)

# Compatibility

Expand All @@ -17,18 +18,21 @@ The plugin is compatible with all IntelliJ-based IDEs starting from the version
| Other features | + | + |


Plugin has been tested against OPA `0.28.0`, but should work with more recent versions.
Plugin has been tested against OPA `1.6.0`, but should work with more recent versions.


# Installation
OPA binary must be in the path.
Installation instructions for OPA can be found [here](https://www.openpolicyagent.org/docs/latest/#running-opa).

## from Jetbrains repository
For enhanced IDE features (diagnostics, code completions, etc.), Regal must also be installed.
Regal releases are available [here](https://github.com/StyraInc/regal/releases).

## From JetBrains Repository
Go to `Settings / Preferences / Plugins` menu. Then, search `opa` in the `Marketplace` tab and install the plugin.

![Step 3](docs/user/img/3_install_opa_plugin.png)
## from source
## From Source
You can build the project from source and then install it. Build instructions are available [here](docs/devel/setup_development_env.md).

# Documentation
Expand Down
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ allprojects {
intellijPlatform {
create(ideType, ideVersion)
val pluginList = mutableListOf(
"PsiViewer:$psiViewerPluginVersion"
"PsiViewer:$psiViewerPluginVersion",
"com.redhat.devtools.lsp4ij:0.14.2"
)
plugins(pluginList)

Expand Down
3 changes: 2 additions & 1 deletion plugin/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
on how to target different products -->
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.lang</depends>
<depends>com.redhat.devtools.lsp4ij</depends>

<xi:include href="/META-INF/opa-core.xml" xpointer="xpointer(/idea-plugin/*)"/>

Expand All @@ -35,4 +36,4 @@
<moduleType id="REGO_MODULE" implementationClass="org.openpolicyagent.ideaplugin.ide.extensions.RegoModuleType"/>
</extensions>

</idea-plugin>
</idea-plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.openpolicyagent.ideaplugin.dap

import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.RunConfigurationOptions
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessHandlerFactory
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.dap.DebugMode
import com.redhat.devtools.lsp4ij.dap.definitions.DebugAdapterServerDefinition
import com.redhat.devtools.lsp4ij.dap.descriptors.DebugAdapterDescriptor
import com.redhat.devtools.lsp4ij.dap.descriptors.ServerReadyConfig
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
import org.openpolicyagent.ideaplugin.lang.RegoFileType
import org.openpolicyagent.ideaplugin.opa.project.settings.OpaProjectSettings
import org.openpolicyagent.ideaplugin.util.RegalExecutableUtil
import java.io.File

class RegalDebugAdapterDescriptor(
@NotNull options: RunConfigurationOptions,
@NotNull environment: ExecutionEnvironment,
@Nullable serverDefinition: DebugAdapterServerDefinition?
) : DebugAdapterDescriptor(options, environment, serverDefinition) {

private val project: Project = environment.project

companion object {
private val LOG = Logger.getInstance(RegalDebugAdapterDescriptor::class.java)
}

@Throws(ExecutionException::class)
override fun startServer(): ProcessHandler {
val commandLine = GeneralCommandLine()
val regalPath = RegalExecutableUtil.findRegalExecutable(project)

if (regalPath == null) {
throw ExecutionException("Regal executable not found. Please install Regal or configure the path in Settings → Languages & Frameworks → Open Policy Agent")
}

commandLine.exePath = regalPath
commandLine.addParameter("debug")

val settings = OpaProjectSettings.getInstance(project)
if (settings.regalVerboseLogging) {
commandLine.addParameter("--verbose")
}

commandLine.workDirectory = project.basePath?.let { File(it) }

LOG.info("Starting Regal debug adapter with command: ${commandLine.commandLineString}")

return ProcessHandlerFactory.getInstance().createProcessHandler(commandLine)
}

override fun getDapParameters(): Map<String, Any> {
// TODO: Set input and inputPath from project input.json
return mapOf(
"command" to "eval",
"query" to "data",
"bundlePaths" to listOf(project.basePath ?: "."),
"input" to emptyMap<String, Any>(),
"inputPath" to "",
"stopOnEntry" to true,
"enablePrint" to true
)
}

override fun getServerReadyConfig(@NotNull debugMode: DebugMode): ServerReadyConfig {
return ServerReadyConfig(5000) // 5 second timeout
}

@Nullable
override fun getFileType(): FileType? {
return RegoFileType
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.openpolicyagent.ideaplugin.dap

import com.intellij.openapi.project.Project
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.vfs.VirtualFile
import com.redhat.devtools.lsp4ij.dap.descriptors.DebugAdapterDescriptor
import com.redhat.devtools.lsp4ij.dap.descriptors.DebugAdapterDescriptorFactory
import com.redhat.devtools.lsp4ij.dap.definitions.DebugAdapterServerDefinition
import com.redhat.devtools.lsp4ij.dap.configurations.DAPRunConfigurationOptions
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable

class RegalDebugAdapterDescriptorFactory : DebugAdapterDescriptorFactory() {

override fun getServerDefinition(): DebugAdapterServerDefinition {
// This will be set by LSP4IJ framework when the factory is registered
return super.getServerDefinition()
}

override fun isDebuggableFile(file: VirtualFile, project: Project): Boolean {
// Check if file is a Rego file that can be debugged
return file.extension == "rego"
}

override fun createDebugAdapterDescriptor(
@NotNull options: DAPRunConfigurationOptions,
@NotNull environment: ExecutionEnvironment
): DebugAdapterDescriptor {
return RegalDebugAdapterDescriptor(options, environment, getServerDefinition())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.openpolicyagent.ideaplugin.lsp

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl

class RegalLanguageClient(project: Project) : LanguageClientImpl(project) {
companion object {
private val LOG = Logger.getInstance(RegalLanguageClient::class.java)
}

/**
* Custom client message handling can be added here for future Regal-specific features.
*
* To add custom message handling:
* 1. Create an interface extending JsonSegment:
* @JsonSegment("regal")
* interface RegalLanguageClientExtensions {
* @JsonRequest("customMessage")
* fun customMessage(params: JsonObject): CompletableFuture<JsonObject>
* }
* 2. Make this class implement the interface:
* class RegalLanguageClient(project: Project) : LanguageClientImpl(project), RegalLanguageClientExtensions
* 3. Implement the custom message handler methods:
* override fun customMessage(params: JsonObject): CompletableFuture<JsonObject> {
* // Handle custom message
* return CompletableFuture.completedFuture(JsonObject())
* }
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

// Implementation based on LSP4IJ Developer Guide:
// https://github.com/redhat-developer/lsp4ij/blob/035c4fd82cc7603ef3a346be0d9585d4e41564b0/docs/dap/DeveloperGuide.md

package org.openpolicyagent.ideaplugin.lsp

import com.intellij.openapi.project.Project
import com.intellij.openapi.diagnostic.Logger
import com.redhat.devtools.lsp4ij.LanguageServerFactory
import com.redhat.devtools.lsp4ij.client.LanguageClientImpl
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
import org.jetbrains.annotations.NotNull

class RegalLanguageServerFactory : LanguageServerFactory {
override fun createConnectionProvider(@NotNull project: Project): StreamConnectionProvider {
return RegalStreamConnectionProvider(project)
}

override fun createLanguageClient(@NotNull project: Project): LanguageClientImpl {
return RegalLanguageClient(project)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

// Implementation based on LSP4IJ Developer Guide:
// https://github.com/redhat-developer/lsp4ij/blob/035c4fd82cc7603ef3a346be0d9585d4e41564b0/docs/dap/DeveloperGuide.md

package org.openpolicyagent.ideaplugin.lsp

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider
import org.openpolicyagent.ideaplugin.opa.project.settings.OpaProjectSettings

class RegalStreamConnectionProvider(private val project: Project) : OSProcessStreamConnectionProvider() {
init {
val commandLine = GeneralCommandLine()
val regalPath = findRegalExecutable()
if (regalPath == null) {
showRegalNotFoundNotification()
commandLine.exePath = "regal"
} else {
commandLine.exePath = regalPath
}
val settings = OpaProjectSettings.getInstance(project)
commandLine.addParameter("language-server")
if (settings.regalVerboseLogging) {
commandLine.addParameter("--verbose")
}
commandLine.workDirectory = project.basePath?.let { java.io.File(it) }

super.setCommandLine(commandLine)
}

private fun findRegalExecutable(): String? {
val settings = OpaProjectSettings.getInstance(project)
val configuredPath = settings.regalPath.trim()
if (configuredPath.isNotEmpty()) {
val file = java.io.File(configuredPath)
if (file.exists() && file.canExecute()) {
return configuredPath
}
}

val pathExecutable = findExecutableInPath("regal")
if (pathExecutable != null) {
return pathExecutable
}

return null
}

private fun findExecutableInPath(executable: String): String? {
val pathEnv = System.getenv("PATH") ?: return null
val pathSeparator = System.getProperty("path.separator")

for (dir in pathEnv.split(pathSeparator)) {
val file = java.io.File(dir, executable)
if (file.exists() && file.canExecute()) {
return file.absolutePath
}
}

return null
}

private fun showRegalNotFoundNotification() {
val notificationGroup = NotificationGroupManager.getInstance()
.getNotificationGroup("OPA Plugin")

if (notificationGroup != null) {
val notification = notificationGroup.createNotification(
"Regal Language Server Not Found",
"Regal binary not found. Install Regal to enable advanced IDE features like code completion and diagnostics.<br/>" +
"<a href=\"https://github.com/StyraInc/regal/releases\">Install Regal</a> or configure the path in Settings → Languages & Frameworks → Open Policy Agent",
NotificationType.WARNING
).setImportant(true)

notification.notify(project)
}
}

override fun getInitializationOptions(rootUri: VirtualFile?): Any? {
// Placeholder for future client feature customization (e.g. inline eval, etc.)
return emptyMap<String, Any>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.bindText

import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.bindSelected

/**
* UI for the opa setting options.
Expand All @@ -29,6 +30,16 @@ class OpaOptionsConfigurable(private val project: Project) :
.bindText(settings::opaCheckOptions)
.align(AlignX.FILL)
}
row("Regal path:") {
textField()
.bindText(settings::regalPath)
.align(AlignX.FILL)
.comment("Path to Regal binary (leave empty to use PATH)")
}
row {
checkBox("Enable Regal verbose logging")
.bindSelected(settings::regalVerboseLogging)
}
}
}
}
Loading
Loading