mirror of
https://github.com/apple/pkl.git
synced 2026-01-11 22:30:54 +01:00
Add internal intellij plugin (#1248)
This introduces an IntelliJ plugin that's meant to assist with development of the Pkl codebase itself. The plugin adds a file editor that opens snippet tests in a split editor pane, showing the input on the left side and output on the right side.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ testgenerated/
|
|||||||
!.idea/runConfigurations/
|
!.idea/runConfigurations/
|
||||||
!.idea/scopes/
|
!.idea/scopes/
|
||||||
!.idea/vcs.xml
|
!.idea/vcs.xml
|
||||||
|
.intellijPlatform/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ This will listen on port 5005.
|
|||||||
|
|
||||||
Example: `./gradlew test -Djvmdebug=true`
|
Example: `./gradlew test -Djvmdebug=true`
|
||||||
|
|
||||||
|
== Snippet Test Plugin
|
||||||
|
|
||||||
|
There is an IntelliJ plugin meant for development on the Pkl project itself.
|
||||||
|
This plugin provides a split pane window when viewing snippet tests such as LanguageSnippetTests and FormatterSnippetTests.
|
||||||
|
|
||||||
|
To install:
|
||||||
|
|
||||||
|
1. Run `./gradlew pkl-internal-intellij-plugin:buildPlugin`.
|
||||||
|
2. Within IntelliJ, run the action "Install Plugin From Disk...".
|
||||||
|
3. Select the zip file within `pkl-internal-intellij-plugin/build/distributions`.
|
||||||
|
|
||||||
== Resources
|
== Resources
|
||||||
For automated build setup examples see our https://github.com/apple/pkl/blob/main/.circleci/[CircleCI] jobs like our https://github.com/apple/pkl/blob/main/.circleci/jobs/BuildNativeJob.pkl[BuildNativeJob.pkl], where we build Pkl automatically.
|
For automated build setup examples see our https://github.com/apple/pkl/blob/main/.circleci/[CircleCI] jobs like our https://github.com/apple/pkl/blob/main/.circleci/jobs/BuildNativeJob.pkl[BuildNativeJob.pkl], where we build Pkl automatically.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ graalVmSha256-linux-x64 = "1862f2ce97387a303cae4c512cb21baf36fafd2457c3cbbc10d87
|
|||||||
graalVmSha256-linux-aarch64 = "6c3c8b7617006c5d174d9cf7d357ccfb4bae77a4df1294ee28084fcb6eea8921"
|
graalVmSha256-linux-aarch64 = "6c3c8b7617006c5d174d9cf7d357ccfb4bae77a4df1294ee28084fcb6eea8921"
|
||||||
graalVmSha256-windows-x64 = "33ef1d186b5c1e95465fcc97e637bc26e72d5f2250a8615b9c5d667ed5c17fd0"
|
graalVmSha256-windows-x64 = "33ef1d186b5c1e95465fcc97e637bc26e72d5f2250a8615b9c5d667ed5c17fd0"
|
||||||
ideaExtPlugin = "1.1.9"
|
ideaExtPlugin = "1.1.9"
|
||||||
|
intellijPlugin = "2.10.1"
|
||||||
|
intellij = "2025.2.3"
|
||||||
javaPoet = "0.+"
|
javaPoet = "0.+"
|
||||||
javaxInject = "1"
|
javaxInject = "1"
|
||||||
jansi = "2.+"
|
jansi = "2.+"
|
||||||
@@ -60,6 +62,7 @@ geantyref = { group = "io.leangen.geantyref", name = "geantyref", version.ref =
|
|||||||
graalCompiler = { group = "org.graalvm.compiler", name = "compiler", version.ref = "graalVm" }
|
graalCompiler = { group = "org.graalvm.compiler", name = "compiler", version.ref = "graalVm" }
|
||||||
graalSdk = { group = "org.graalvm.sdk", name = "graal-sdk", version.ref = "graalVm" }
|
graalSdk = { group = "org.graalvm.sdk", name = "graal-sdk", version.ref = "graalVm" }
|
||||||
graalJs = { group = "org.graalvm.js", name = "js", version.ref = "graalVm" }
|
graalJs = { group = "org.graalvm.js", name = "js", version.ref = "graalVm" }
|
||||||
|
intellij = { group = "com.jetbrains.intellij.idea", name = "ideaIC", version.ref = "intellij" }
|
||||||
javaPoet = { group = "com.palantir.javapoet", name = "javapoet", version.ref = "javaPoet" }
|
javaPoet = { group = "com.palantir.javapoet", name = "javapoet", version.ref = "javaPoet" }
|
||||||
javaxInject = { group = "javax.inject", name = "javax.inject", version.ref = "javaxInject" }
|
javaxInject = { group = "javax.inject", name = "javax.inject", version.ref = "javaxInject" }
|
||||||
jansi = { group = "org.fusesource.jansi", name = "jansi", version.ref = "jansi" }
|
jansi = { group = "org.fusesource.jansi", name = "jansi", version.ref = "jansi" }
|
||||||
@@ -106,3 +109,4 @@ jmh = { id = "me.champeau.jmh", version.ref = "jmhPlugin" }
|
|||||||
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" }
|
nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" }
|
||||||
shadow = { id = "com.gradleup.shadow", version.ref = "shadowPlugin" }
|
shadow = { id = "com.gradleup.shadow", version.ref = "shadowPlugin" }
|
||||||
|
intellij = { id = "org.jetbrains.intellij.platform", version.ref = "intellijPlugin" }
|
||||||
|
|||||||
4
pkl-internal-intellij-plugin/README.adoc
Normal file
4
pkl-internal-intellij-plugin/README.adoc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Internal IntelliJ plugin for developing the Pkl codebase.
|
||||||
|
|
||||||
|
This plugin is intentionally not published anywhere.
|
||||||
|
For usage, see the docs in link:../DEVELOPMENT.adoc[DEVELOPMENT.adoc].
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
pklAllProjects
|
||||||
|
kotlin("jvm")
|
||||||
|
alias(libs.plugins.intellij)
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
intellijPlatform { defaultRepositories() }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies { intellijPlatform { create("IC", libs.versions.intellij.get()) } }
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
kotlin {
|
||||||
|
ktfmt(libs.versions.ktfmt.get()).googleStyle()
|
||||||
|
target("src/*/kotlin/**/*.kt")
|
||||||
|
licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.internal.intellij
|
||||||
|
|
||||||
|
import com.intellij.openapi.fileEditor.*
|
||||||
|
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
|
||||||
|
import com.intellij.openapi.project.DumbAware
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.openapi.vfs.VirtualFileManager
|
||||||
|
|
||||||
|
class SnippetTestEditorProvider : FileEditorProvider, DumbAware {
|
||||||
|
|
||||||
|
private val hiddenExtensionRegex = Regex(".*[.]([^.]*)[.]pkl")
|
||||||
|
|
||||||
|
override fun accept(project: Project, file: VirtualFile): Boolean {
|
||||||
|
return isSnippetTestInputFile(file) && findOutputFile(file) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createEditor(project: Project, file: VirtualFile): FileEditor {
|
||||||
|
val textEditorProvider = TextEditorProvider.getInstance()
|
||||||
|
val outputFile = findOutputFile(file) ?: return textEditorProvider.createEditor(project, file)
|
||||||
|
|
||||||
|
val inputEditor = textEditorProvider.createEditor(project, file) as TextEditor
|
||||||
|
val outputEditor = textEditorProvider.createEditor(project, outputFile) as TextEditor
|
||||||
|
|
||||||
|
return SnippetTestSplitEditor(inputEditor, outputEditor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEditorTypeId(): String = "snippet-test-split-editor"
|
||||||
|
|
||||||
|
override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
|
||||||
|
|
||||||
|
private fun isSnippetTestInputFile(file: VirtualFile): Boolean {
|
||||||
|
val path = file.path
|
||||||
|
return path.contains("/src/test/files/") && path.contains("/input/") && file.extension == "pkl"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun possibleOutputPaths(testType: String, relativePath: String): String? {
|
||||||
|
return when (testType) {
|
||||||
|
"LanguageSnippetTests" ->
|
||||||
|
if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4)
|
||||||
|
else relativePath.dropLast(3) + "pcf"
|
||||||
|
"FormatterSnippetTests" -> relativePath
|
||||||
|
"SnippetTests" -> relativePath.replaceAfterLast('.', "yaml")
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findOutputFile(inputFile: VirtualFile): VirtualFile? {
|
||||||
|
val path = inputFile.path
|
||||||
|
val inputPattern = Regex(".*/src/test/files/(\\w+)/input/(.+)$")
|
||||||
|
val match = inputPattern.find(path) ?: return null
|
||||||
|
|
||||||
|
val testType = match.groupValues[1]
|
||||||
|
val relativePath = match.groupValues[2]
|
||||||
|
val relativeOutputPath = possibleOutputPaths(testType, relativePath) ?: return null
|
||||||
|
val outputPath = path.replace("/input/$relativePath", "/output/$relativeOutputPath")
|
||||||
|
val fileManager = VirtualFileManager.getInstance()
|
||||||
|
return fileManager.findFileByUrl("file://$outputPath")
|
||||||
|
?: fileManager.findFileByUrl("file://${outputPath.replaceAfterLast('.', "err")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.pkl.internal.intellij
|
||||||
|
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.openapi.actionSystem.*
|
||||||
|
import com.intellij.openapi.fileEditor.FileEditor
|
||||||
|
import com.intellij.openapi.fileEditor.FileEditorLocation
|
||||||
|
import com.intellij.openapi.fileEditor.FileEditorState
|
||||||
|
import com.intellij.openapi.fileEditor.FileEditorStateLevel
|
||||||
|
import com.intellij.openapi.fileEditor.TextEditor
|
||||||
|
import com.intellij.openapi.util.UserDataHolderBase
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
|
import com.intellij.ui.components.JBPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.beans.PropertyChangeListener
|
||||||
|
import javax.swing.JComponent
|
||||||
|
import javax.swing.JSplitPane
|
||||||
|
|
||||||
|
class SnippetTestSplitEditor(
|
||||||
|
private val inputEditor: TextEditor,
|
||||||
|
private val outputEditor: TextEditor,
|
||||||
|
) : UserDataHolderBase(), FileEditor {
|
||||||
|
|
||||||
|
private var currentViewMode = ViewMode.SPLIT
|
||||||
|
|
||||||
|
private val splitPane: JSplitPane =
|
||||||
|
JSplitPane(JSplitPane.HORIZONTAL_SPLIT, inputEditor.component, outputEditor.component).apply {
|
||||||
|
resizeWeight = 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mainPanel =
|
||||||
|
JBPanel<JBPanel<*>>(BorderLayout()).apply {
|
||||||
|
add(createToolbar(), BorderLayout.NORTH)
|
||||||
|
add(splitPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createToolbar(): JComponent {
|
||||||
|
val actionGroup =
|
||||||
|
DefaultActionGroup().apply {
|
||||||
|
add(ShowInputOnlyAction())
|
||||||
|
add(ShowSplitAction())
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbar =
|
||||||
|
ActionManager.getInstance().createActionToolbar("SnippetTestEditor", actionGroup, true)
|
||||||
|
toolbar.targetComponent = mainPanel
|
||||||
|
return toolbar.component
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setViewMode(mode: ViewMode) {
|
||||||
|
if (currentViewMode == mode) return
|
||||||
|
currentViewMode = mode
|
||||||
|
|
||||||
|
// Remove the current center component
|
||||||
|
val layout = mainPanel.layout as BorderLayout
|
||||||
|
layout.getLayoutComponent(BorderLayout.CENTER)?.let { mainPanel.remove(it) }
|
||||||
|
|
||||||
|
when (mode) {
|
||||||
|
ViewMode.INPUT_ONLY -> {
|
||||||
|
mainPanel.add(inputEditor.component, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
ViewMode.SPLIT -> {
|
||||||
|
// Re-add components to splitPane in case they were removed
|
||||||
|
splitPane.leftComponent = inputEditor.component
|
||||||
|
splitPane.rightComponent = outputEditor.component
|
||||||
|
mainPanel.add(splitPane, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainPanel.revalidate()
|
||||||
|
mainPanel.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComponent(): JComponent = mainPanel
|
||||||
|
|
||||||
|
override fun getPreferredFocusedComponent(): JComponent? = inputEditor.preferredFocusedComponent
|
||||||
|
|
||||||
|
override fun getName(): String = "Snippet Test"
|
||||||
|
|
||||||
|
override fun getFile(): VirtualFile? = inputEditor.file
|
||||||
|
|
||||||
|
override fun setState(state: FileEditorState) {
|
||||||
|
if (state is SnippetTestSplitEditorState) {
|
||||||
|
inputEditor.setState(state.inputState)
|
||||||
|
outputEditor.setState(state.outputState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getState(level: FileEditorStateLevel): FileEditorState {
|
||||||
|
return SnippetTestSplitEditorState(inputEditor.getState(level), outputEditor.getState(level))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isModified(): Boolean = inputEditor.isModified || outputEditor.isModified
|
||||||
|
|
||||||
|
override fun isValid(): Boolean = inputEditor.isValid && outputEditor.isValid
|
||||||
|
|
||||||
|
override fun addPropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
|
inputEditor.addPropertyChangeListener(listener)
|
||||||
|
outputEditor.addPropertyChangeListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removePropertyChangeListener(listener: PropertyChangeListener) {
|
||||||
|
inputEditor.removePropertyChangeListener(listener)
|
||||||
|
outputEditor.removePropertyChangeListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentLocation(): FileEditorLocation? = inputEditor.currentLocation
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
inputEditor.dispose()
|
||||||
|
outputEditor.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ViewMode {
|
||||||
|
INPUT_ONLY,
|
||||||
|
SPLIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ShowInputOnlyAction :
|
||||||
|
ToggleAction("Show Input Only", "Show only the input file", AllIcons.General.LayoutEditorOnly) {
|
||||||
|
override fun isSelected(e: AnActionEvent): Boolean = currentViewMode == ViewMode.INPUT_ONLY
|
||||||
|
|
||||||
|
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||||
|
if (state) setViewMode(ViewMode.INPUT_ONLY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ShowSplitAction :
|
||||||
|
ToggleAction(
|
||||||
|
"Show Split",
|
||||||
|
"Show input and output side by side",
|
||||||
|
AllIcons.General.LayoutEditorPreview,
|
||||||
|
) {
|
||||||
|
override fun isSelected(e: AnActionEvent): Boolean = currentViewMode == ViewMode.SPLIT
|
||||||
|
|
||||||
|
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||||
|
if (state) setViewMode(ViewMode.SPLIT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SnippetTestSplitEditorState(
|
||||||
|
val inputState: FileEditorState,
|
||||||
|
val outputState: FileEditorState,
|
||||||
|
) : FileEditorState {
|
||||||
|
override fun canBeMergedWith(otherState: FileEditorState, level: FileEditorStateLevel): Boolean {
|
||||||
|
return otherState is SnippetTestSplitEditorState &&
|
||||||
|
inputState.canBeMergedWith(otherState.inputState, level) &&
|
||||||
|
outputState.canBeMergedWith(otherState.outputState, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<idea-plugin>
|
||||||
|
<id>org.pkl.intellij.snippet-test-helper</id>
|
||||||
|
<name>Pkl Snippet Test Helper</name>
|
||||||
|
<vendor>Apple Inc. and the Pkl project authors</vendor>
|
||||||
|
|
||||||
|
<description><![CDATA[
|
||||||
|
Automatically opens snippet test output files when input files are opened.
|
||||||
|
|
||||||
|
This is an internal plugin that is meant for development on the Pkl project itself.
|
||||||
|
]]></description>
|
||||||
|
|
||||||
|
<depends>com.intellij.modules.platform</depends>
|
||||||
|
|
||||||
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
|
<fileEditorProvider implementation="org.pkl.intellij.SnippetTestEditorProvider"/>
|
||||||
|
</extensions>
|
||||||
|
</idea-plugin>
|
||||||
@@ -45,6 +45,8 @@ include("pkl-formatter")
|
|||||||
|
|
||||||
include("pkl-gradle")
|
include("pkl-gradle")
|
||||||
|
|
||||||
|
include("pkl-internal-intellij-plugin")
|
||||||
|
|
||||||
include("pkl-parser")
|
include("pkl-parser")
|
||||||
|
|
||||||
include("pkl-server")
|
include("pkl-server")
|
||||||
|
|||||||
Reference in New Issue
Block a user