From 248ad1fa69271062f19ac251c97567e6d165633a Mon Sep 17 00:00:00 2001 From: Bradley Dwyer Date: Thu, 22 Aug 2024 11:00:38 +1000 Subject: [PATCH 1/2] Initial basic support for the FTL LSP Server in IntelliJ Ultimate as a plugin. - Basic completion of go directives - start/stop/restart of the LSP - lsp terminal output (auto scroll/clear buffer/soft wrap) - settings panel for LSP server command configuration and project working directory --- extensions/intellij/build.gradle.kts | 47 ++++ extensions/intellij/gradle.properties | 8 + extensions/intellij/gradlew | 234 ++++++++++++++++++ extensions/intellij/gradlew.bat | 89 +++++++ extensions/intellij/settings.gradle.kts | 8 + .../block/ftl/intellij/CustomLsp4jClient.kt | 10 + .../main/kotlin/xyz/block/ftl/intellij/FTL.kt | 9 + .../ftl/intellij/FTLLspServerDescriptor.kt | 49 ++++ .../block/ftl/intellij/FTLLspServerService.kt | 15 ++ .../intellij/FTLLspServerSupportProvider.kt | 123 +++++++++ .../xyz/block/ftl/intellij/FTLSettings.kt | 39 +++ .../ftl/intellij/FTLSettingsComponent.kt | 57 +++++ .../ftl/intellij/FTLSettingsConfigurable.kt | 49 ++++ .../intellij/toolWindow/FTLMessagesPanel.kt | 33 +++ .../toolWindow/FTLToolWindowFactory.kt | 220 ++++++++++++++++ .../src/main/resources/META-INF/plugin.xml | 48 ++++ .../main/resources/META-INF/pluginIcon.svg | 10 + 17 files changed, 1048 insertions(+) create mode 100644 extensions/intellij/build.gradle.kts create mode 100644 extensions/intellij/gradle.properties create mode 100755 extensions/intellij/gradlew create mode 100644 extensions/intellij/gradlew.bat create mode 100644 extensions/intellij/settings.gradle.kts create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/CustomLsp4jClient.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTL.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerService.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerSupportProvider.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettings.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsComponent.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLMessagesPanel.kt create mode 100644 extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/toolWindow/FTLToolWindowFactory.kt create mode 100644 extensions/intellij/src/main/resources/META-INF/plugin.xml create mode 100644 extensions/intellij/src/main/resources/META-INF/pluginIcon.svg diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts new file mode 100644 index 000000000..e290aa577 --- /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("241.*") + } + + 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 000000000..6c35a1de3 --- /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/gradlew b/extensions/intellij/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/extensions/intellij/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/extensions/intellij/gradlew.bat b/extensions/intellij/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/extensions/intellij/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/extensions/intellij/settings.gradle.kts b/extensions/intellij/settings.gradle.kts new file mode 100644 index 000000000..b34041241 --- /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 000000000..7dcd70b3c --- /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 000000000..33171d8c0 --- /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 000000000..3d31f8951 --- /dev/null +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt @@ -0,0 +1,49 @@ +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.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.lsp.api.LspServerNotificationsHandler +import com.intellij.platform.lsp.api.ProjectWideLspServerDescriptor +import xyz.block.ftl.intellij.toolWindow.FTLMessagesToolWindowFactory +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) + return generalCommandLine + } + + override fun startServerProcess(): OSProcessHandler { + displayMessageInToolWindow("Starting FTL LSP Server") + val processHandler = super.startServerProcess() + processHandler.addProcessListener(object : ProcessAdapter() { + 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 000000000..f381f2b7c --- /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 000000000..51e49aec9 --- /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 000000000..9f59376af --- /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 000000000..9fcda2db4 --- /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 000000000..689c367ee --- /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() ?: "--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 000000000..3c5ab18cc --- /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 000000000..4241fa9e4 --- /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 000000000..3ae75ceb7 --- /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 000000000..ad741c90d --- /dev/null +++ b/extensions/intellij/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From 0d7726d4a783e022f76679d0302214cebc53ad73 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 30 Aug 2024 11:52:58 -0400 Subject: [PATCH 2/2] feat: Hermit support This piggy backs on the Hermit plugin to get the correct FTL version. --- .github/workflows/ci.yml | 17 ++ Justfile | 3 + extensions/intellij/build.gradle.kts | 2 +- extensions/intellij/gradlew | 234 ------------------ extensions/intellij/gradlew.bat | 89 ------- .../ftl/intellij/FTLLspServerDescriptor.kt | 37 +++ .../ftl/intellij/FTLSettingsConfigurable.kt | 2 +- 7 files changed, 59 insertions(+), 325 deletions(-) delete mode 100755 extensions/intellij/gradlew delete mode 100644 extensions/intellij/gradlew.bat diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6588616ed..94cd7fe7a 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 a33fbd397..c1bcd4642 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 index e290aa577..88b2bf362 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -32,7 +32,7 @@ tasks { patchPluginXml { sinceBuild.set("241") - untilBuild.set("241.*") + untilBuild.set("242.*") } signPlugin { diff --git a/extensions/intellij/gradlew b/extensions/intellij/gradlew deleted file mode 100755 index 1b6c78733..000000000 --- a/extensions/intellij/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# 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. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/extensions/intellij/gradlew.bat b/extensions/intellij/gradlew.bat deleted file mode 100644 index 107acd32c..000000000 --- a/extensions/intellij/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega 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 index 3d31f8951..4193ed400 100644 --- a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLLspServerDescriptor.kt @@ -4,12 +4,16 @@ 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") { @@ -26,6 +30,23 @@ class FTLLspServerDescriptor(project: Project) : ProjectWideLspServerDescriptor( 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 } @@ -33,6 +54,22 @@ class FTLLspServerDescriptor(project: Project) : ProjectWideLspServerDescriptor( 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()) { 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 index 689c367ee..1e2c84481 100644 --- a/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt +++ b/extensions/intellij/src/main/kotlin/xyz/block/ftl/intellij/FTLSettingsConfigurable.kt @@ -32,7 +32,7 @@ class FTLSettingsConfigurable : Configurable { override fun apply() { val state = AppSettings.getInstance().state state.lspServerPath = mySettingsComponent?.getLspServerPath() ?: "ftl" - state.lspServerArguments = mySettingsComponent?.getLspServerArguments() ?: "--recreate --lsp" + state.lspServerArguments = mySettingsComponent?.getLspServerArguments() ?: " dev --recreate --lsp" state.lspServerStopArguments = mySettingsComponent?.getLspServerStopArguments() ?: "serve --stop" }