diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6588616ed0..94cd7fe7a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,6 +156,23 @@ jobs: run: pnpm run lint - name: VSCode extension pnpm build run: just build-extension + plugin: + name: Intellij Plugin + if: github.event_name != 'pull_request' || github.event.action == 'enqueued' || contains( github.event.pull_request.labels.*.name, 'run-all') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Init Hermit + uses: cashapp/activate-hermit@v1 + with: + cache: true + - name: Build Cache + uses: ./.github/actions/build-cache + - name: Install Java + run: java -version + - name: Build Intellij Plugin + run: just build-intellij-plugin build-all: name: Rebuild All if: github.event_name != 'pull_request' || github.event.action == 'enqueued' || contains( github.event.pull_request.labels.*.name, 'run-all') diff --git a/Justfile b/Justfile index a33fbd3970..c1bcd4642d 100644 --- a/Justfile +++ b/Justfile @@ -101,6 +101,9 @@ package-extension: build-extension publish-extension: package-extension @cd extensions/vscode && vsce publish +build-intellij-plugin: + @cd extensions/intellij && gradle buildPlugin + # Kotlin runtime is temporarily disabled; these instructions create a dummy zip in place of the kotlin runtime jar for # the runner. build-kt-runtime: diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts new file mode 100644 index 0000000000..88b2bf362a --- /dev/null +++ b/extensions/intellij/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("java") + id("org.jetbrains.kotlin.jvm") version "1.9.24" + id("org.jetbrains.intellij") version "1.17.3" +} + +group = "xyz.block.ftl" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +// Configure Gradle IntelliJ Plugin +// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html +intellij { + version.set("2024.1.3") + type.set("IU") // Target IDE Platform + + plugins.set(listOf(/* Plugin Dependencies */)) +} + +tasks { + // Set the JVM compatibility versions + withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + withType { + kotlinOptions.jvmTarget = "17" + } + + patchPluginXml { + sinceBuild.set("241") + untilBuild.set("242.*") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } +} diff --git a/extensions/intellij/gradle.properties b/extensions/intellij/gradle.properties new file mode 100644 index 0000000000..6c35a1de39 --- /dev/null +++ b/extensions/intellij/gradle.properties @@ -0,0 +1,8 @@ +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency = false + +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching = true diff --git a/extensions/intellij/settings.gradle.kts b/extensions/intellij/settings.gradle.kts new file mode 100644 index 0000000000..b34041241a --- /dev/null +++ b/extensions/intellij/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "intellij" \ No newline at end of file diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/CustomLsp4jClient.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/CustomLsp4jClient.kt new file mode 100644 index 0000000000..7dcd70b3c4 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/CustomLsp4jClient.kt @@ -0,0 +1,10 @@ +package xyz.block.ftl.intellij + +import com.intellij.platform.lsp.api.Lsp4jClient +import com.intellij.platform.lsp.api.LspServerNotificationsHandler + +class CustomLsp4jClient(handler: LspServerNotificationsHandler) : Lsp4jClient(handler) { + override fun telemetryEvent(`object`: Any) { + super.telemetryEvent(`object`) + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTL.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTL.kt new file mode 100644 index 0000000000..33171d8c06 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTL.kt @@ -0,0 +1,9 @@ +package xyz.block.ftl.intellij + +import com.intellij.openapi.application.ApplicationManager + +fun runOnEDT(runnable: () -> Unit) { + ApplicationManager.getApplication().invokeLater { + runnable() + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt new file mode 100644 index 0000000000..4193ed4000 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt @@ -0,0 +1,86 @@ +package xyz.block.ftl.intellij + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.ProcessAdapter +import com.intellij.execution.process.ProcessEvent +import com.intellij.ide.DataManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.platform.lsp.api.LspServerNotificationsHandler +import com.intellij.platform.lsp.api.ProjectWideLspServerDescriptor +import com.intellij.tools.ToolsCustomizer +import xyz.block.ftl.intellij.toolWindow.FTLMessagesToolWindowFactory +import java.util.concurrent.CompletableFuture +import java.util.regex.Pattern + +class FTLLspServerDescriptor(project: Project) : ProjectWideLspServerDescriptor(project, "FTL") { + override fun isSupportedFile(file: VirtualFile) = file.extension == "go" + + override fun createLsp4jClient(handler: LspServerNotificationsHandler): CustomLsp4jClient { + return CustomLsp4jClient(handler) + } + + override fun createCommandLine(): GeneralCommandLine { + val settings = AppSettings.getInstance().state + val generalCommandLine = + GeneralCommandLine(listOf(settings.lspServerPath) + settings.lspServerArguments.split(Pattern.compile("\\s+"))) + generalCommandLine.setWorkDirectory(project.basePath) + displayMessageInToolWindow("LSP Server Command: " + generalCommandLine.commandLineString) + displayMessageInToolWindow("Working Directory: " + generalCommandLine.workDirectory) + try { + // Hermit support, we need to get the environment variables so we use the correct FTL + val result = CompletableFuture() + runOnEDT { + val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("FTL") + if (toolWindow != null) { + val dataContext = DataManager.getInstance().getDataContext(toolWindow.component) + val customizeCommandLine = + ToolsCustomizer.customizeCommandLine(generalCommandLine, dataContext) + result.complete(customizeCommandLine) + } + } + val res = result.get() + return if (res != null) res else generalCommandLine + } catch (e: Exception) { + displayMessageInToolWindow("Failed to customize LSP Server Command: " + e.message) + } + return generalCommandLine + } + + override fun startServerProcess(): OSProcessHandler { + displayMessageInToolWindow("Starting FTL LSP Server") + val processHandler = super.startServerProcess() + processHandler.addProcessListener(object : ProcessAdapter() { + + override fun startNotified(event: ProcessEvent) { + super.startNotified(event) + displayMessageInToolWindow("LSP Started") + } + + override fun processTerminated(event: ProcessEvent) { + super.processTerminated(event) + displayMessageInToolWindow("LSP Terminated") + } + + override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) { + super.processWillTerminate(event, willBeDestroyed) + displayMessageInToolWindow("LSP Will Terminate") + } + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + val message = event.text.trim() + if (message.isNotBlank()) { + displayMessageInToolWindow(message) + } + } + }) + return processHandler + } + + private fun displayMessageInToolWindow(message: String) { + FTLMessagesToolWindowFactory.Util.displayMessageInToolWindow(project, message) + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerService.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerService.kt new file mode 100644 index 0000000000..f381f2b7c6 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerService.kt @@ -0,0 +1,15 @@ +package xyz.block.ftl.intellij + +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project + +@Service(Service.Level.PROJECT) +class FTLLspServerService(val project: Project) { + val lspServerSupportProvider = FTLLspServerSupportProvider() + + companion object { + fun getInstance(project: Project): FTLLspServerService { + return project.getService(FTLLspServerService::class.java) + } + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerSupportProvider.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerSupportProvider.kt new file mode 100644 index 0000000000..51e49aec90 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerSupportProvider.kt @@ -0,0 +1,123 @@ +package xyz.block.ftl.intellij + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.OSProcessHandler +import com.intellij.icons.AllIcons.Icons +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.lsp.api.LspServer +import com.intellij.platform.lsp.api.LspServerDescriptor.Companion.LOG +import com.intellij.platform.lsp.api.LspServerManager +import com.intellij.platform.lsp.api.LspServerManagerListener +import com.intellij.platform.lsp.api.LspServerState +import com.intellij.platform.lsp.api.LspServerSupportProvider +import com.intellij.platform.lsp.api.lsWidget.LspServerWidgetItem +import com.intellij.util.io.BaseOutputReader +import com.intellij.util.messages.Topic +import xyz.block.ftl.intellij.toolWindow.FTLMessagesToolWindowFactory.Util.displayMessageInToolWindow +import java.util.regex.Pattern + +interface FTLLSPNotifier { + fun lspServerStateChange(state: LspServerState) + + companion object { + @Topic.ProjectLevel + val SERVER_STATE_CHANGE_TOPIC: Topic = Topic.create( + "FTL Server State Changed", + FTLLSPNotifier::class.java + ) + } +} + +class FTLLspServerSupportProvider : LspServerSupportProvider { + private var listenerAdded: Boolean = false + + override fun createLspServerWidgetItem(lspServer: LspServer, currentFile: VirtualFile?): LspServerWidgetItem = + LspServerWidgetItem( + lspServer = lspServer, + currentFile = currentFile, + settingsPageClass = FTLSettingsConfigurable::class.java, + widgetMainActionBaseIcon = Icons.Ide.MenuArrow + ) + + override fun fileOpened( + project: Project, + file: VirtualFile, + serverStarter: LspServerSupportProvider.LspServerStarter + ) { + if (!listenerAdded) { + try { + listenerAdded = true + val lspServerManager = LspServerManager.getInstance(project) + lspServerManager.addLspServerManagerListener(listener = object : LspServerManagerListener { + override fun serverStateChanged(lspServer: LspServer) { + val publisher = project.messageBus.syncPublisher(FTLLSPNotifier.SERVER_STATE_CHANGE_TOPIC) + publisher.lspServerStateChange(lspServer.state) + } + }, parentDisposable = { }, sendEventsForExistingServers = true) + } catch (e: Exception) { + listenerAdded = false + } + } + + val isFtlSupportLanguage = file.extension == "go" || file.extension == "kt" || file.extension == "java" + if (isFtlSupportLanguage && hasFtlProjectFile(project)) { + serverStarter.ensureServerStarted(FTLLspServerDescriptor(project)) + } + } + + private fun hasFtlProjectFile(project: Project): Boolean { + val projectBaseDir = project.baseDir ?: return false + val ftlProjectFile = projectBaseDir.findChild("ftl-project.toml") + return ftlProjectFile != null && ftlProjectFile.exists() + } + + fun startLspServer(project: Project) { + val lspServerManager = LspServerManager.getInstance(project) + lspServerManager.startServersIfNeeded(FTLLspServerSupportProvider::class.java) + } + + fun stopLspServer(project: Project): OSProcessHandler? { + return when (getLspServerStatus(project)) { + LspServerState.ShutdownUnexpectedly -> { + stopViaCommand(project) + } + + else -> { + val lspServerManager = LspServerManager.getInstance(project) + lspServerManager.stopServers(FTLLspServerSupportProvider::class.java) + null + } + } + } + + private fun stopViaCommand(project: Project): OSProcessHandler { + val settings = AppSettings.getInstance().state + val generalCommandLine = + GeneralCommandLine(listOf(settings.lspServerPath) + settings.lspServerStopArguments.split(Pattern.compile("\\s+"))).withCharset( + Charsets.UTF_8 + ) + generalCommandLine.setWorkDirectory(project.basePath) + displayMessageInToolWindow(project, "LSP Server Command: " + generalCommandLine.commandLineString) + displayMessageInToolWindow(project, "Working Directory: " + generalCommandLine.workDirectory) + + LOG.info("$this: stopping LSP server: $generalCommandLine") + val process: OSProcessHandler = object : OSProcessHandler(generalCommandLine) { + override fun readerOptions(): BaseOutputReader.Options = BaseOutputReader.Options.forMostlySilentProcess() + } + + return process + } + + fun restartLspServer(project: Project) { + val lspServerManager = LspServerManager.getInstance(project) + lspServerManager.stopAndRestartIfNeeded(FTLLspServerSupportProvider::class.java) + } + + fun getLspServerStatus(project: Project): LspServerState { + val lspServerManager = LspServerManager.getInstance(project) + val server = lspServerManager.getServersForProvider(FTLLspServerSupportProvider::class.java).firstOrNull() + + return server?.state ?: LspServerState.ShutdownNormally + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettings.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettings.kt new file mode 100644 index 0000000000..9f59376afc --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettings.kt @@ -0,0 +1,39 @@ +package xyz.block.ftl.intellij + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import org.jetbrains.annotations.NonNls + +@State( + name = "org.intellij.sdk.settings.AppSettings", + storages = [Storage("SdkSettingsPlugin.xml")] +) +@Service +class AppSettings : PersistentStateComponent { + + data class State( + @NonNls var lspServerPath: String = "ftl", + var lspServerArguments: String = "--recreate --lsp", + var lspServerStopArguments: String = "serve --stop", + var autoRestartLspServer: Boolean = false, + ) + + private var myState = State() + + companion object { + fun getInstance(): AppSettings { + return ApplicationManager.getApplication().getService(AppSettings::class.java) + } + } + + override fun getState(): State { + return myState + } + + override fun loadState(state: State) { + myState = state + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsComponent.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsComponent.kt new file mode 100644 index 0000000000..9fcda2db49 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsComponent.kt @@ -0,0 +1,57 @@ +package xyz.block.ftl.intellij + +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import javax.swing.JComponent +import javax.swing.JPanel + +class FTLSettingsComponent { + + private val settingsPanel: JPanel + private val lspServerPath = TextFieldWithBrowseButton() + private val lspServerArguments = JBTextField() + private val lspServerStopArguments = JBTextField() + + init { + settingsPanel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel("LSP Server Path:"), lspServerPath, 1, false) + .addLabeledComponent(JBLabel("LSP Server Start Arguments:"), lspServerArguments, 1, false) + .addLabeledComponent(JBLabel("LSP Server Stop Arguments:"), lspServerStopArguments, 1, false) + .addComponentFillVertically(JPanel(), 0) + .panel + } + + fun getPanel(): JPanel { + return settingsPanel + } + + fun getPreferredFocusedComponent(): JComponent { + return lspServerPath + } + + fun getLspServerPath(): String { + return lspServerPath.text + } + + fun setLspServerPath(newPath: String) { + lspServerPath.text = newPath + } + + fun getLspServerArguments(): String { + return lspServerArguments.text + } + + fun getLspServerStopArguments(): String { + return lspServerStopArguments.text + } + + fun setLspServerArguments(newArguments: String) { + lspServerArguments.text = newArguments + } + + fun setLspServerStopArguments(newArguments: String) { + lspServerStopArguments.text = newArguments + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt new file mode 100644 index 0000000000..1e2c844810 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt @@ -0,0 +1,49 @@ +package xyz.block.ftl.intellij + +import com.intellij.openapi.options.Configurable +import org.jetbrains.annotations.Nls +import javax.swing.JComponent + +class FTLSettingsConfigurable : Configurable { + + private var mySettingsComponent: FTLSettingsComponent? = null + + @Nls(capitalization = Nls.Capitalization.Title) + override fun getDisplayName(): String { + return "FTL" + } + + override fun getPreferredFocusedComponent(): JComponent? { + return mySettingsComponent?.getPreferredFocusedComponent() + } + + override fun createComponent(): JComponent? { + mySettingsComponent = FTLSettingsComponent() + return mySettingsComponent?.getPanel() + } + + override fun isModified(): Boolean { + val state = AppSettings.getInstance().state + return mySettingsComponent?.getLspServerPath() != state.lspServerPath || + mySettingsComponent?.getLspServerArguments() != state.lspServerArguments || + mySettingsComponent?.getLspServerStopArguments() != state.lspServerStopArguments + } + + override fun apply() { + val state = AppSettings.getInstance().state + state.lspServerPath = mySettingsComponent?.getLspServerPath() ?: "ftl" + state.lspServerArguments = mySettingsComponent?.getLspServerArguments() ?: " dev --recreate --lsp" + state.lspServerStopArguments = mySettingsComponent?.getLspServerStopArguments() ?: "serve --stop" + } + + override fun reset() { + val state = AppSettings.getInstance().state + mySettingsComponent?.setLspServerPath(state.lspServerPath) + mySettingsComponent?.setLspServerArguments(state.lspServerArguments) + mySettingsComponent?.setLspServerStopArguments(state.lspServerStopArguments) + } + + override fun disposeUIResources() { + mySettingsComponent = null + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLMessagesPanel.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLMessagesPanel.kt new file mode 100644 index 0000000000..3c5ab18cc5 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLMessagesPanel.kt @@ -0,0 +1,33 @@ +package xyz.block.ftl.intellij.toolWindow + +import com.intellij.execution.filters.TextConsoleBuilderFactory +import com.intellij.execution.ui.ConsoleView +import com.intellij.execution.ui.ConsoleViewContentType +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.SimpleToolWindowPanel +import xyz.block.ftl.intellij.runOnEDT +import java.awt.BorderLayout +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class FTLMessagesPanel(project: Project) : SimpleToolWindowPanel(false, false) { + val consoleView: ConsoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).console + var autoScrollEnabled = true + + init { + layout = BorderLayout() + add(consoleView.component, BorderLayout.CENTER) + } + + fun addMessage(message: String) { + runOnEDT { + val timestamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val messageWithTimestamp = "[$timestamp] $message\n" + consoleView.print(messageWithTimestamp, ConsoleViewContentType.NORMAL_OUTPUT) + + if (autoScrollEnabled) { + consoleView.requestScrollingToEnd() + } + } + } +} diff --git a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLToolWindowFactory.kt b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLToolWindowFactory.kt new file mode 100644 index 0000000000..4241fa9e4a --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLToolWindowFactory.kt @@ -0,0 +1,220 @@ +package xyz.block.ftl.intellij.toolWindow + +import com.intellij.execution.process.ProcessAdapter +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.ui.ConsoleView +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionToolbar +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.ToggleAction +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.platform.lsp.api.LspServerState +import com.intellij.ui.content.ContentFactory +import com.intellij.ui.content.ContentManagerEvent +import com.intellij.ui.content.ContentManagerListener +import xyz.block.ftl.intellij.FTLLSPNotifier +import xyz.block.ftl.intellij.FTLLspServerService +import xyz.block.ftl.intellij.FTLSettingsConfigurable +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService + +class FTLMessagesToolWindowFactory() : ToolWindowFactory, DumbAware { + private var currentLspState: LspServerState = LspServerState.Initializing + val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + + private lateinit var startAction: AnAction + private lateinit var stopAction: AnAction + + private lateinit var panel: FTLMessagesPanel + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + panel = FTLMessagesPanel(project) + + val contentFactory = ContentFactory.getInstance() + val actionManager = ActionManager.getInstance() + val content = contentFactory.createContent(panel, "", false) + + val actionGroup = DefaultActionGroup().apply { + + startAction = object : DumbAwareAction("Start", "Start the process", AllIcons.Actions.Execute) { + override fun actionPerformed(e: AnActionEvent) { + panel.addMessage("Start action triggered") + + val service = FTLLspServerService.getInstance(project) + panel.addMessage("Status is: ${service.lspServerSupportProvider.getLspServerStatus(project)}") + service.lspServerSupportProvider.startLspServer(project) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = + LspServerState.Running != currentLspState && LspServerState.Initializing != currentLspState + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + } + add(startAction) + + stopAction = object : DumbAwareAction("Stop", "Stop the process", AllIcons.Actions.Suspend) { + override fun actionPerformed(e: AnActionEvent) { + panel.addMessage("Stop action triggered") + val service = FTLLspServerService.getInstance(project) + panel.addMessage("Status is: ${service.lspServerSupportProvider.getLspServerStatus(project)}") + val processHandler = service.lspServerSupportProvider.stopLspServer(project) + processHandler?.addProcessListener(object : ProcessAdapter() { + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + val message = event.text.trim() + if (message.isNotBlank()) { + Util.displayMessageInToolWindow(project, message) + } + } + }) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = LspServerState.ShutdownNormally != currentLspState + } + } + add(stopAction) + + add(object : DumbAwareAction("Restart", "Restart the process", AllIcons.Actions.Restart) { + override fun actionPerformed(e: AnActionEvent) { + panel.addMessage("Restart action triggered") + val service = FTLLspServerService.getInstance(project) + panel.addMessage("Status is: ${service.lspServerSupportProvider.getLspServerStatus(project)}") + service.lspServerSupportProvider.restartLspServer(project) + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + }) + + add(object : AnAction("Toggle Soft Wrap", "Toggle soft wrap in console view", AllIcons.Actions.ToggleSoftWrap) { + override fun actionPerformed(e: AnActionEvent) { + val editor = getEditorFromConsoleView(panel.consoleView) + if (editor != null) { + val settings = editor.settings + settings.isUseSoftWraps = !settings.isUseSoftWraps + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + private fun getEditorFromConsoleView(consoleView: ConsoleView): EditorEx? { + return try { + val method = consoleView.javaClass.getMethod("getEditor") + method.isAccessible = true + method.invoke(consoleView) as? EditorEx + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + override fun update(e: AnActionEvent) { + val editor = getEditorFromConsoleView(panel.consoleView) + if (editor != null) { + val isSoftWrapEnabled = editor.settings.isUseSoftWraps + e.presentation.isEnabled = true + e.presentation.putClientProperty("selected", isSoftWrapEnabled) + } else { + e.presentation.isEnabled = false + } + } + }) + + add(object : AnAction("Clear", "Clear the console", AllIcons.Actions.GC) { + override fun actionPerformed(e: AnActionEvent) { + panel.consoleView.clear() + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + }) + + add(object : ToggleAction("Auto Scroll", "Toggle auto scroll", AllIcons.RunConfigurations.Scroll_down) { + override fun isSelected(e: AnActionEvent): Boolean { + return panel.autoScrollEnabled + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + override fun setSelected(e: AnActionEvent, state: Boolean) { + panel.autoScrollEnabled = state + } + }) + + add(object : AnAction("Settings", "Open Settings", AllIcons.General.Settings) { + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + override fun actionPerformed(e: AnActionEvent) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, FTLSettingsConfigurable::class.java) + } + }) + } + + val actionToolbar: ActionToolbar = actionManager.createActionToolbar("FTLToolbar", actionGroup, true) + + actionToolbar.targetComponent = panel.toolbar + panel.toolbar = actionToolbar.component + toolWindow.contentManager.addContent(content) + + project.messageBus.connect().subscribe( + FTLLSPNotifier.SERVER_STATE_CHANGE_TOPIC, + object : FTLLSPNotifier { + override fun lspServerStateChange(state: LspServerState) { + currentLspState = state + panel.addMessage("State changed: ${state}") + } + }) + } + + override fun init(toolWindow: ToolWindow) { + toolWindow.contentManager.addContentManagerListener(object : ContentManagerListener { + override fun contentRemoveQuery(event: ContentManagerEvent) { + scheduler.shutdown() + super.contentRemoveQuery(event) + } + }) + } + + object Util { + fun displayMessageInToolWindow(project: Project, message: String) { + ApplicationManager.getApplication().invokeLater { + val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("FTL") + if (toolWindow != null) { + val content = toolWindow.contentManager.getContent(0) + val panel = content?.component as? FTLMessagesPanel + panel?.addMessage(message) + } + } + } + } +} + diff --git a/extensions/intellij/src/main/resources/META-INF/plugin.xml b/extensions/intellij/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000000..3ae75ceb71 --- /dev/null +++ b/extensions/intellij/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,48 @@ + + + + xyz.block.ftl.intellij + + + FTL + + + FTL + + + + + + com.intellij.modules.platform + com.intellij.modules.ultimate + + + + + + + + + + + + + diff --git a/extensions/intellij/src/main/resources/META-INF/pluginIcon.svg b/extensions/intellij/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000000..ad741c90d7 --- /dev/null +++ b/extensions/intellij/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file